dabke 0.81.1 → 0.83.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 (231) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +45 -27
  3. package/dist/client.d.ts +20 -2
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +4 -1
  6. package/dist/client.js.map +1 -1
  7. package/dist/client.types.d.ts +9 -0
  8. package/dist/client.types.d.ts.map +1 -1
  9. package/dist/client.types.js +1 -0
  10. package/dist/client.types.js.map +1 -1
  11. package/dist/cpsat/model-builder.d.ts +9 -0
  12. package/dist/cpsat/model-builder.d.ts.map +1 -1
  13. package/dist/cpsat/model-builder.js +36 -34
  14. package/dist/cpsat/model-builder.js.map +1 -1
  15. package/dist/cpsat/response.d.ts +13 -1
  16. package/dist/cpsat/response.d.ts.map +1 -1
  17. package/dist/cpsat/response.js +4 -0
  18. package/dist/cpsat/response.js.map +1 -1
  19. package/dist/cpsat/rules/cost-utils.d.ts +11 -0
  20. package/dist/cpsat/rules/cost-utils.d.ts.map +1 -0
  21. package/dist/cpsat/rules/cost-utils.js +24 -0
  22. package/dist/cpsat/rules/cost-utils.js.map +1 -0
  23. package/dist/cpsat/rules/day-cost-multiplier.d.ts.map +1 -1
  24. package/dist/cpsat/rules/day-cost-multiplier.js +3 -14
  25. package/dist/cpsat/rules/day-cost-multiplier.js.map +1 -1
  26. package/dist/cpsat/rules/day-cost-surcharge.d.ts.map +1 -1
  27. package/dist/cpsat/rules/day-cost-surcharge.js +3 -7
  28. package/dist/cpsat/rules/day-cost-surcharge.js.map +1 -1
  29. package/dist/cpsat/rules/index.d.ts +3 -0
  30. package/dist/cpsat/rules/index.d.ts.map +1 -1
  31. package/dist/cpsat/rules/index.js +3 -0
  32. package/dist/cpsat/rules/index.js.map +1 -1
  33. package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
  34. package/dist/cpsat/rules/max-consecutive-days.js +16 -2
  35. package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
  36. package/dist/cpsat/rules/max-days-week.d.ts +44 -0
  37. package/dist/cpsat/rules/max-days-week.d.ts.map +1 -0
  38. package/dist/cpsat/rules/max-days-week.js +95 -0
  39. package/dist/cpsat/rules/max-days-week.js.map +1 -0
  40. package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
  41. package/dist/cpsat/rules/max-hours-day.js +15 -2
  42. package/dist/cpsat/rules/max-hours-day.js.map +1 -1
  43. package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
  44. package/dist/cpsat/rules/max-hours-week.js +16 -2
  45. package/dist/cpsat/rules/max-hours-week.js.map +1 -1
  46. package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
  47. package/dist/cpsat/rules/max-shifts-day.js +15 -2
  48. package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
  49. package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
  50. package/dist/cpsat/rules/min-consecutive-days.js +15 -2
  51. package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
  52. package/dist/cpsat/rules/min-days-week.d.ts +34 -0
  53. package/dist/cpsat/rules/min-days-week.d.ts.map +1 -0
  54. package/dist/cpsat/rules/min-days-week.js +84 -0
  55. package/dist/cpsat/rules/min-days-week.js.map +1 -0
  56. package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
  57. package/dist/cpsat/rules/min-hours-day.js +15 -2
  58. package/dist/cpsat/rules/min-hours-day.js.map +1 -1
  59. package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
  60. package/dist/cpsat/rules/min-hours-week.js +16 -2
  61. package/dist/cpsat/rules/min-hours-week.js.map +1 -1
  62. package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
  63. package/dist/cpsat/rules/min-rest-between-shifts.js +72 -2
  64. package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
  65. package/dist/cpsat/rules/minimize-cost.d.ts.map +1 -1
  66. package/dist/cpsat/rules/minimize-cost.js +2 -23
  67. package/dist/cpsat/rules/minimize-cost.js.map +1 -1
  68. package/dist/cpsat/rules/must-assign.d.ts +49 -0
  69. package/dist/cpsat/rules/must-assign.d.ts.map +1 -0
  70. package/dist/cpsat/rules/must-assign.js +86 -0
  71. package/dist/cpsat/rules/must-assign.js.map +1 -0
  72. package/dist/cpsat/rules/overtime-daily-multiplier.d.ts.map +1 -1
  73. package/dist/cpsat/rules/overtime-daily-multiplier.js +1 -12
  74. package/dist/cpsat/rules/overtime-daily-multiplier.js.map +1 -1
  75. package/dist/cpsat/rules/overtime-daily-surcharge.d.ts.map +1 -1
  76. package/dist/cpsat/rules/overtime-daily-surcharge.js +1 -5
  77. package/dist/cpsat/rules/overtime-daily-surcharge.js.map +1 -1
  78. package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts +5 -1
  79. package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts.map +1 -1
  80. package/dist/cpsat/rules/overtime-tiered-multiplier.js +1 -12
  81. package/dist/cpsat/rules/overtime-tiered-multiplier.js.map +1 -1
  82. package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts.map +1 -1
  83. package/dist/cpsat/rules/overtime-weekly-multiplier.js +1 -12
  84. package/dist/cpsat/rules/overtime-weekly-multiplier.js.map +1 -1
  85. package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts.map +1 -1
  86. package/dist/cpsat/rules/overtime-weekly-surcharge.js +1 -5
  87. package/dist/cpsat/rules/overtime-weekly-surcharge.js.map +1 -1
  88. package/dist/cpsat/rules/registry.d.ts +28 -2
  89. package/dist/cpsat/rules/registry.d.ts.map +1 -1
  90. package/dist/cpsat/rules/registry.js +4 -1
  91. package/dist/cpsat/rules/registry.js.map +1 -1
  92. package/dist/cpsat/rules/resolver.js +2 -2
  93. package/dist/cpsat/rules/resolver.js.map +1 -1
  94. package/dist/cpsat/rules/rules.types.d.ts +3 -0
  95. package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
  96. package/dist/cpsat/rules/scope.types.d.ts +18 -1
  97. package/dist/cpsat/rules/scope.types.d.ts.map +1 -1
  98. package/dist/cpsat/rules/scope.types.js +59 -16
  99. package/dist/cpsat/rules/scope.types.js.map +1 -1
  100. package/dist/cpsat/rules/time-cost-surcharge.d.ts.map +1 -1
  101. package/dist/cpsat/rules/time-cost-surcharge.js +2 -1
  102. package/dist/cpsat/rules/time-cost-surcharge.js.map +1 -1
  103. package/dist/cpsat/rules/time-off.d.ts.map +1 -1
  104. package/dist/cpsat/rules/time-off.js +6 -3
  105. package/dist/cpsat/rules/time-off.js.map +1 -1
  106. package/dist/cpsat/semantic-time.d.ts +44 -42
  107. package/dist/cpsat/semantic-time.d.ts.map +1 -1
  108. package/dist/cpsat/semantic-time.js +64 -46
  109. package/dist/cpsat/semantic-time.js.map +1 -1
  110. package/dist/cpsat/types.d.ts +37 -27
  111. package/dist/cpsat/types.d.ts.map +1 -1
  112. package/dist/cpsat/utils.d.ts.map +1 -1
  113. package/dist/cpsat/utils.js +7 -12
  114. package/dist/cpsat/utils.js.map +1 -1
  115. package/dist/cpsat/validation-reporter.d.ts +10 -7
  116. package/dist/cpsat/validation-reporter.d.ts.map +1 -1
  117. package/dist/cpsat/validation-reporter.js +44 -72
  118. package/dist/cpsat/validation-reporter.js.map +1 -1
  119. package/dist/cpsat/validation.types.d.ts +54 -44
  120. package/dist/cpsat/validation.types.d.ts.map +1 -1
  121. package/dist/cpsat/validation.types.js +15 -10
  122. package/dist/cpsat/validation.types.js.map +1 -1
  123. package/dist/datetime.utils.d.ts +3 -203
  124. package/dist/datetime.utils.d.ts.map +1 -1
  125. package/dist/datetime.utils.js +1 -288
  126. package/dist/datetime.utils.js.map +1 -1
  127. package/dist/index.d.ts +14 -83
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +11 -83
  130. package/dist/index.js.map +1 -1
  131. package/dist/schedule/cost.d.ts +204 -0
  132. package/dist/schedule/cost.d.ts.map +1 -0
  133. package/dist/schedule/cost.js +187 -0
  134. package/dist/schedule/cost.js.map +1 -0
  135. package/dist/schedule/coverage.d.ts +85 -0
  136. package/dist/schedule/coverage.d.ts.map +1 -0
  137. package/dist/schedule/coverage.js +33 -0
  138. package/dist/schedule/coverage.js.map +1 -0
  139. package/dist/schedule/definition.d.ts +227 -0
  140. package/dist/schedule/definition.d.ts.map +1 -0
  141. package/dist/schedule/definition.js +659 -0
  142. package/dist/schedule/definition.js.map +1 -0
  143. package/dist/schedule/index.d.ts +67 -0
  144. package/dist/schedule/index.d.ts.map +1 -0
  145. package/dist/schedule/index.js +69 -0
  146. package/dist/schedule/index.js.map +1 -0
  147. package/dist/schedule/rules.d.ts +353 -0
  148. package/dist/schedule/rules.d.ts.map +1 -0
  149. package/dist/schedule/rules.js +352 -0
  150. package/dist/schedule/rules.js.map +1 -0
  151. package/dist/schedule/shift-patterns.d.ts +34 -0
  152. package/dist/schedule/shift-patterns.d.ts.map +1 -0
  153. package/dist/schedule/shift-patterns.js +41 -0
  154. package/dist/schedule/shift-patterns.js.map +1 -0
  155. package/dist/schedule/time-periods.d.ts +69 -0
  156. package/dist/schedule/time-periods.d.ts.map +1 -0
  157. package/dist/schedule/time-periods.js +91 -0
  158. package/dist/schedule/time-periods.js.map +1 -0
  159. package/dist/types.d.ts +14 -78
  160. package/dist/types.d.ts.map +1 -1
  161. package/dist/types.js.map +1 -1
  162. package/package.json +4 -9
  163. package/solver/src/solver/app.py +1 -1
  164. package/solver/src/solver/solver.py +7 -4
  165. package/src/client.ts +6 -8
  166. package/src/client.types.ts +9 -0
  167. package/src/cpsat/model-builder.ts +44 -35
  168. package/src/cpsat/response.ts +13 -1
  169. package/src/cpsat/rules/cost-utils.ts +25 -0
  170. package/src/cpsat/rules/day-cost-multiplier.ts +3 -14
  171. package/src/cpsat/rules/day-cost-surcharge.ts +3 -8
  172. package/src/cpsat/rules/index.ts +3 -0
  173. package/src/cpsat/rules/max-consecutive-days.ts +17 -0
  174. package/src/cpsat/rules/max-days-week.ts +143 -0
  175. package/src/cpsat/rules/max-hours-day.ts +21 -1
  176. package/src/cpsat/rules/max-hours-week.ts +22 -1
  177. package/src/cpsat/rules/max-shifts-day.ts +21 -1
  178. package/src/cpsat/rules/min-consecutive-days.ts +16 -1
  179. package/src/cpsat/rules/min-days-week.ts +120 -0
  180. package/src/cpsat/rules/min-hours-day.ts +16 -1
  181. package/src/cpsat/rules/min-hours-week.ts +17 -1
  182. package/src/cpsat/rules/min-rest-between-shifts.ts +92 -2
  183. package/src/cpsat/rules/minimize-cost.ts +2 -29
  184. package/src/cpsat/rules/must-assign.ts +108 -0
  185. package/src/cpsat/rules/overtime-daily-multiplier.ts +1 -12
  186. package/src/cpsat/rules/overtime-daily-surcharge.ts +1 -6
  187. package/src/cpsat/rules/overtime-tiered-multiplier.ts +6 -13
  188. package/src/cpsat/rules/overtime-weekly-multiplier.ts +1 -12
  189. package/src/cpsat/rules/overtime-weekly-surcharge.ts +1 -6
  190. package/src/cpsat/rules/registry.ts +8 -2
  191. package/src/cpsat/rules/resolver.ts +2 -2
  192. package/src/cpsat/rules/rules.types.ts +3 -0
  193. package/src/cpsat/rules/scope.types.ts +73 -20
  194. package/src/cpsat/rules/time-cost-surcharge.ts +2 -1
  195. package/src/cpsat/rules/time-off.ts +6 -2
  196. package/src/cpsat/semantic-time.ts +115 -91
  197. package/src/cpsat/types.ts +37 -27
  198. package/src/cpsat/utils.ts +8 -12
  199. package/src/cpsat/validation-reporter.ts +51 -82
  200. package/src/cpsat/validation.types.ts +72 -47
  201. package/src/datetime.utils.ts +3 -334
  202. package/src/index.ts +35 -107
  203. package/src/schedule/cost.ts +242 -0
  204. package/src/schedule/coverage.ts +135 -0
  205. package/src/schedule/definition.ts +958 -0
  206. package/src/schedule/index.ts +112 -0
  207. package/src/schedule/rules.ts +529 -0
  208. package/src/schedule/shift-patterns.ts +46 -0
  209. package/src/schedule/time-periods.ts +110 -0
  210. package/src/types.ts +14 -88
  211. package/dist/errors.d.ts +0 -12
  212. package/dist/errors.d.ts.map +0 -1
  213. package/dist/errors.js +0 -17
  214. package/dist/errors.js.map +0 -1
  215. package/dist/llms.d.ts +0 -2
  216. package/dist/llms.d.ts.map +0 -1
  217. package/dist/llms.js +0 -3
  218. package/dist/llms.js.map +0 -1
  219. package/dist/schedule.d.ts +0 -724
  220. package/dist/schedule.d.ts.map +0 -1
  221. package/dist/schedule.js +0 -899
  222. package/dist/schedule.js.map +0 -1
  223. package/dist/validation.d.ts +0 -105
  224. package/dist/validation.d.ts.map +0 -1
  225. package/dist/validation.js +0 -130
  226. package/dist/validation.js.map +0 -1
  227. package/llms.txt +0 -925
  228. package/src/errors.ts +0 -17
  229. package/src/llms.ts +0 -3
  230. package/src/schedule.ts +0 -1419
  231. package/src/validation.ts +0 -188
@@ -2,7 +2,7 @@ import * as z from "zod";
2
2
  import type { CompilationRule, CostContribution } from "../model-builder.js";
3
3
  import type { ShiftPattern, SchedulingMember } from "../types.js";
4
4
  import type { ShiftAssignment } from "../response.js";
5
- import { timeOfDayToMinutes, normalizeEndMinutes } from "../utils.js";
5
+ import { COST_CATEGORY } from "../cost.js";
6
6
  import {
7
7
  entityScope,
8
8
  timeScope,
@@ -11,6 +11,7 @@ import {
11
11
  resolveMembersFromScope,
12
12
  resolveActiveDaysFromScope,
13
13
  } from "./scope.types.js";
14
+ import { patternDurationMinutes } from "./cost-utils.js";
14
15
 
15
16
  const DayCostSurchargeSchema = z
16
17
  .object({
@@ -22,12 +23,6 @@ const DayCostSurchargeSchema = z
22
23
  /** Configuration for {@link createDayCostSurchargeRule}. */
23
24
  export type DayCostSurchargeConfig = z.infer<typeof DayCostSurchargeSchema>;
24
25
 
25
- function patternDurationMinutes(pattern: ShiftPattern): number {
26
- const start = timeOfDayToMinutes(pattern.startTime);
27
- const end = normalizeEndMinutes(start, timeOfDayToMinutes(pattern.endTime));
28
- return end - start;
29
- }
30
-
31
26
  /**
32
27
  * Creates a day-based flat surcharge rule.
33
28
  *
@@ -95,7 +90,7 @@ export function createDayCostSurchargeRule(config: DayCostSurchargeConfig): Comp
95
90
  entries.push({
96
91
  memberId: a.memberId,
97
92
  day: a.day,
98
- category: "premium",
93
+ category: COST_CATEGORY.PREMIUM,
99
94
  amount,
100
95
  });
101
96
  }
@@ -2,13 +2,16 @@ export { createAssignTogetherRule } from "./assign-together.js";
2
2
  export { createAssignmentPriorityRule } from "./assignment-priority.js";
3
3
  export { createLocationPreferenceRule } from "./location-preference.js";
4
4
  export { createMaxConsecutiveDaysRule } from "./max-consecutive-days.js";
5
+ export { createMaxDaysWeekRule } from "./max-days-week.js";
5
6
  export { createMaxHoursDayRule } from "./max-hours-day.js";
6
7
  export { createMaxHoursWeekRule } from "./max-hours-week.js";
7
8
  export { createMaxShiftsDayRule } from "./max-shifts-day.js";
8
9
  export { createMinConsecutiveDaysRule } from "./min-consecutive-days.js";
10
+ export { createMinDaysWeekRule } from "./min-days-week.js";
9
11
  export { createMinHoursDayRule } from "./min-hours-day.js";
10
12
  export { createMinHoursWeekRule } from "./min-hours-week.js";
11
13
  export { createMinRestBetweenShiftsRule } from "./min-rest-between-shifts.js";
14
+ export { createMustAssignRule } from "./must-assign.js";
12
15
  export { createTimeOffRule } from "./time-off.js";
13
16
  export { createMinimizeCostRule } from "./minimize-cost.js";
14
17
  export { createDayCostMultiplierRule } from "./day-cost-multiplier.js";
@@ -6,6 +6,7 @@ import {
6
6
  entityScope,
7
7
  parseEntityScope,
8
8
  resolveMembersFromScope,
9
+ ruleGroup,
9
10
  } from "./scope.types.js";
10
11
 
11
12
  const MaxConsecutiveDaysSchema = z
@@ -39,6 +40,7 @@ export function createMaxConsecutiveDaysRule(config: MaxConsecutiveDaysConfig):
39
40
  const scope = parseEntityScope(parsed);
40
41
  const { days, priority } = parsed;
41
42
  const windowSize = days + 1;
43
+ const group = ruleGroup(`max-consecutive-days:${days}`, `Max ${days} consecutive days`, scope);
42
44
 
43
45
  return {
44
46
  compile(b) {
@@ -81,6 +83,9 @@ export function createMaxConsecutiveDaysRule(config: MaxConsecutiveDaysConfig):
81
83
  }
82
84
  }
83
85
 
86
+ const windowStart = windowDays[0]!;
87
+ const constraintId = `max-consecutive-days:${emp.id}:${windowStart}`;
88
+
84
89
  if (priority === "MANDATORY") {
85
90
  b.addLinear(
86
91
  worksDayVars.map((v) => ({ var: v, coeff: 1 })),
@@ -93,7 +98,19 @@ export function createMaxConsecutiveDaysRule(config: MaxConsecutiveDaysConfig):
93
98
  "<=",
94
99
  days,
95
100
  priorityToPenalty(priority),
101
+ constraintId,
96
102
  );
103
+ b.reporter.trackConstraint({
104
+ id: constraintId,
105
+ type: "rule",
106
+ rule: "max-consecutive-days",
107
+ description: `${emp.id} max ${days} consecutive days from ${windowStart}`,
108
+ targetValue: days,
109
+ comparator: "<=",
110
+ day: windowStart,
111
+ context: { memberIds: [emp.id], days: windowDays },
112
+ group,
113
+ });
97
114
  }
98
115
  }
99
116
  }
@@ -0,0 +1,143 @@
1
+ import * as z from "zod";
2
+ import { DayOfWeekSchema } from "../../types.js";
3
+ import type { CompilationRule } from "../model-builder.js";
4
+ import type { Term } from "../types.js";
5
+ import { priorityToPenalty, splitIntoWeeks } from "../utils.js";
6
+ import {
7
+ PrioritySchema,
8
+ entityScope,
9
+ timeScope,
10
+ parseEntityScope,
11
+ parseTimeScope,
12
+ resolveMembersFromScope,
13
+ resolveActiveDaysFromScope,
14
+ ruleGroup,
15
+ } from "./scope.types.js";
16
+
17
+ const MaxDaysWeekBase = z.object({
18
+ days: z.number().int().min(0),
19
+ priority: PrioritySchema,
20
+ weekStartsOn: DayOfWeekSchema.optional(),
21
+ });
22
+
23
+ const MaxDaysWeekSchema = MaxDaysWeekBase.and(entityScope(["members", "roles", "skills"])).and(
24
+ timeScope(["dateRange", "specificDates", "dayOfWeek", "recurring"]),
25
+ );
26
+
27
+ /**
28
+ * Configuration for {@link createMaxDaysWeekRule}.
29
+ *
30
+ * - `days` (required): maximum number of days allowed per scheduling week
31
+ * - `priority` (required): how strictly the solver enforces this rule
32
+ * - `weekStartsOn` (optional): which day starts the week; defaults to {@link ModelBuilder.weekStartsOn}
33
+ *
34
+ * Entity scoping (at most one): `memberIds`, `roleIds`, `skillIds`
35
+ * Time scoping (at most one, optional): `dateRange`, `specificDates`, `dayOfWeek`, `recurringPeriods`
36
+ */
37
+ export type MaxDaysWeekConfig = z.infer<typeof MaxDaysWeekSchema>;
38
+
39
+ /**
40
+ * Caps total number of days a person can work within each scheduling week.
41
+ *
42
+ * @remarks
43
+ * Creates a binary "works on day" variable for each member and day, then
44
+ * constrains the weekly sum. This counts distinct days, regardless of how
45
+ * many shifts are assigned on a single day.
46
+ *
47
+ * @param config - See {@link MaxDaysWeekConfig}
48
+ * @example Limit everyone to 5 days per week
49
+ * ```ts
50
+ * createMaxDaysWeekRule({ days: 5, priority: "MANDATORY" });
51
+ * ```
52
+ *
53
+ * @example Part-time staff limited to 3 days
54
+ * ```ts
55
+ * createMaxDaysWeekRule({
56
+ * roleIds: ["part-time"],
57
+ * days: 3,
58
+ * priority: "HIGH",
59
+ * });
60
+ * ```
61
+ */
62
+ export function createMaxDaysWeekRule(config: MaxDaysWeekConfig): CompilationRule {
63
+ const parsed = MaxDaysWeekSchema.parse(config);
64
+ const entityScopeValue = parseEntityScope(parsed);
65
+ const timeScopeValue = parseTimeScope(parsed);
66
+ const { days, priority, weekStartsOn } = parsed;
67
+ const group = ruleGroup(
68
+ `max-days-week:${days}`,
69
+ `Max ${days}d per week`,
70
+ entityScopeValue,
71
+ timeScopeValue,
72
+ );
73
+
74
+ return {
75
+ compile(b) {
76
+ const targetMembers = resolveMembersFromScope(entityScopeValue, b.members);
77
+ const activeDays = resolveActiveDaysFromScope(timeScopeValue, b.days);
78
+
79
+ if (targetMembers.length === 0 || activeDays.length === 0) return;
80
+
81
+ const weeks = splitIntoWeeks(activeDays, weekStartsOn ?? b.weekStartsOn);
82
+
83
+ for (const emp of targetMembers) {
84
+ for (const weekDays of weeks) {
85
+ const weekWorkVars: string[] = [];
86
+
87
+ for (const day of weekDays) {
88
+ const dayAssignments = b.shiftPatterns
89
+ .filter((p) => b.canAssign(emp, p) && b.patternAvailableOnDay(p, day))
90
+ .map((p) => b.assignment(emp.id, p.id, day));
91
+
92
+ if (dayAssignments.length === 0) continue;
93
+
94
+ const worksVar = b.boolVar(`works_day_${emp.id}_${day}`);
95
+ weekWorkVars.push(worksVar);
96
+
97
+ // worksVar >= each assignment (if any assignment is 1, worksVar must be 1)
98
+ for (const assignVar of dayAssignments) {
99
+ b.addLinear(
100
+ [
101
+ { var: worksVar, coeff: 1 },
102
+ { var: assignVar, coeff: -1 },
103
+ ],
104
+ ">=",
105
+ 0,
106
+ );
107
+ }
108
+
109
+ // worksVar <= sum(assignments) (if no assignment is 1, worksVar must be 0)
110
+ b.addLinear(
111
+ [{ var: worksVar, coeff: 1 }, ...dayAssignments.map((v) => ({ var: v, coeff: -1 }))],
112
+ "<=",
113
+ 0,
114
+ );
115
+ }
116
+
117
+ if (weekWorkVars.length === 0) continue;
118
+
119
+ const weekLabel = weekDays[0]!;
120
+ const constraintId = `max-days-week:${emp.id}:${weekLabel}`;
121
+ const terms: Term[] = weekWorkVars.map((v) => ({ var: v, coeff: 1 }));
122
+
123
+ if (priority === "MANDATORY") {
124
+ b.addLinear(terms, "<=", days);
125
+ } else {
126
+ b.addSoftLinear(terms, "<=", days, priorityToPenalty(priority), constraintId);
127
+ b.reporter.trackConstraint({
128
+ id: constraintId,
129
+ type: "rule",
130
+ rule: "max-days-week",
131
+ description: `${emp.id} max ${days}d in week starting ${weekLabel}`,
132
+ targetValue: days,
133
+ comparator: "<=",
134
+ day: weekLabel,
135
+ context: { memberIds: [emp.id], days: weekDays },
136
+ group,
137
+ });
138
+ }
139
+ }
140
+ }
141
+ },
142
+ };
143
+ }
@@ -10,6 +10,7 @@ import {
10
10
  parseTimeScope,
11
11
  resolveMembersFromScope,
12
12
  resolveActiveDaysFromScope,
13
+ ruleGroup,
13
14
  } from "./scope.types.js";
14
15
 
15
16
  const MaxHoursDaySchema = z
@@ -59,6 +60,12 @@ export function createMaxHoursDayRule(config: MaxHoursDayConfig): CompilationRul
59
60
  const timeScopeValue = parseTimeScope(parsed);
60
61
  const { hours, priority } = parsed;
61
62
  const maxMinutes = hours * 60;
63
+ const group = ruleGroup(
64
+ `max-hours-day:${hours}`,
65
+ `Max ${hours}h per day`,
66
+ entityScopeValue,
67
+ timeScopeValue,
68
+ );
62
69
 
63
70
  return {
64
71
  compile(b) {
@@ -81,10 +88,23 @@ export function createMaxHoursDayRule(config: MaxHoursDayConfig): CompilationRul
81
88
 
82
89
  if (terms.length === 0) continue;
83
90
 
91
+ const constraintId = `max-hours-day:${emp.id}:${day}`;
92
+
84
93
  if (priority === "MANDATORY") {
85
94
  b.addLinear(terms, "<=", maxMinutes);
86
95
  } else {
87
- b.addSoftLinear(terms, "<=", maxMinutes, priorityToPenalty(priority));
96
+ b.addSoftLinear(terms, "<=", maxMinutes, priorityToPenalty(priority), constraintId);
97
+ b.reporter.trackConstraint({
98
+ id: constraintId,
99
+ type: "rule",
100
+ rule: "max-hours-day",
101
+ description: `${emp.id} max ${hours}h on ${day}`,
102
+ targetValue: maxMinutes,
103
+ comparator: "<=",
104
+ day,
105
+ context: { memberIds: [emp.id], days: [day] },
106
+ group,
107
+ });
88
108
  }
89
109
  }
90
110
  }
@@ -11,6 +11,7 @@ import {
11
11
  parseTimeScope,
12
12
  resolveMembersFromScope,
13
13
  resolveActiveDaysFromScope,
14
+ ruleGroup,
14
15
  } from "./scope.types.js";
15
16
 
16
17
  const MaxHoursWeekBase = z.object({
@@ -62,6 +63,12 @@ export function createMaxHoursWeekRule(config: MaxHoursWeekConfig): CompilationR
62
63
  const timeScopeValue = parseTimeScope(parsed);
63
64
  const { hours, priority, weekStartsOn } = parsed;
64
65
  const maxMinutes = hours * 60;
66
+ const group = ruleGroup(
67
+ `max-hours-week:${hours}`,
68
+ `Max ${hours}h per week`,
69
+ entityScopeValue,
70
+ timeScopeValue,
71
+ );
65
72
 
66
73
  return {
67
74
  compile(b) {
@@ -88,10 +95,24 @@ export function createMaxHoursWeekRule(config: MaxHoursWeekConfig): CompilationR
88
95
 
89
96
  if (terms.length === 0) continue;
90
97
 
98
+ const weekLabel = weekDays[0]!;
99
+ const constraintId = `max-hours-week:${emp.id}:${weekLabel}`;
100
+
91
101
  if (priority === "MANDATORY") {
92
102
  b.addLinear(terms, "<=", maxMinutes);
93
103
  } else {
94
- b.addSoftLinear(terms, "<=", maxMinutes, priorityToPenalty(priority));
104
+ b.addSoftLinear(terms, "<=", maxMinutes, priorityToPenalty(priority), constraintId);
105
+ b.reporter.trackConstraint({
106
+ id: constraintId,
107
+ type: "rule",
108
+ rule: "max-hours-week",
109
+ description: `${emp.id} max ${hours}h in week starting ${weekLabel}`,
110
+ targetValue: maxMinutes,
111
+ comparator: "<=",
112
+ day: weekLabel,
113
+ context: { memberIds: [emp.id], days: weekDays },
114
+ group,
115
+ });
95
116
  }
96
117
  }
97
118
  }
@@ -10,6 +10,7 @@ import {
10
10
  parseTimeScope,
11
11
  resolveMembersFromScope,
12
12
  resolveActiveDaysFromScope,
13
+ ruleGroup,
13
14
  } from "./scope.types.js";
14
15
 
15
16
  const MaxShiftsDaySchema = z
@@ -61,6 +62,12 @@ export function createMaxShiftsDayRule(config: MaxShiftsDayConfig): CompilationR
61
62
  const entityScopeValue = parseEntityScope(parsed);
62
63
  const timeScopeValue = parseTimeScope(parsed);
63
64
  const { shifts, priority } = parsed;
65
+ const group = ruleGroup(
66
+ `max-shifts-day:${shifts}`,
67
+ `Max ${shifts} shift${shifts === 1 ? "" : "s"} per day`,
68
+ entityScopeValue,
69
+ timeScopeValue,
70
+ );
64
71
 
65
72
  return {
66
73
  compile(b) {
@@ -83,10 +90,23 @@ export function createMaxShiftsDayRule(config: MaxShiftsDayConfig): CompilationR
83
90
 
84
91
  if (terms.length === 0) continue;
85
92
 
93
+ const constraintId = `max-shifts-day:${emp.id}:${day}`;
94
+
86
95
  if (priority === "MANDATORY") {
87
96
  b.addLinear(terms, "<=", shifts);
88
97
  } else {
89
- b.addSoftLinear(terms, "<=", shifts, priorityToPenalty(priority));
98
+ b.addSoftLinear(terms, "<=", shifts, priorityToPenalty(priority), constraintId);
99
+ b.reporter.trackConstraint({
100
+ id: constraintId,
101
+ type: "rule",
102
+ rule: "max-shifts-day",
103
+ description: `${emp.id} max ${shifts} shift${shifts === 1 ? "" : "s"} on ${day}`,
104
+ targetValue: shifts,
105
+ comparator: "<=",
106
+ day,
107
+ context: { memberIds: [emp.id], days: [day] },
108
+ group,
109
+ });
90
110
  }
91
111
  }
92
112
  }
@@ -6,6 +6,7 @@ import {
6
6
  entityScope,
7
7
  parseEntityScope,
8
8
  resolveMembersFromScope,
9
+ ruleGroup,
9
10
  } from "./scope.types.js";
10
11
 
11
12
  const MinConsecutiveDaysSchema = z
@@ -39,6 +40,7 @@ export function createMinConsecutiveDaysRule(config: MinConsecutiveDaysConfig):
39
40
  const parsed = MinConsecutiveDaysSchema.parse(config);
40
41
  const scope = parseEntityScope(parsed);
41
42
  const { days, priority } = parsed;
43
+ const group = ruleGroup(`min-consecutive-days:${days}`, `Min ${days} consecutive days`, scope);
42
44
 
43
45
  return {
44
46
  compile(b) {
@@ -132,10 +134,23 @@ export function createMinConsecutiveDaysRule(config: MinConsecutiveDaysConfig):
132
134
  { var: startVar, coeff: -days },
133
135
  ];
134
136
 
137
+ const constraintId = `min-consecutive-days:${emp.id}:${dayLabel}`;
138
+
135
139
  if (priority === "MANDATORY") {
136
140
  b.addLinear(terms, ">=", 0);
137
141
  } else {
138
- b.addSoftLinear(terms, ">=", 0, priorityToPenalty(priority));
142
+ b.addSoftLinear(terms, ">=", 0, priorityToPenalty(priority), constraintId);
143
+ b.reporter.trackConstraint({
144
+ id: constraintId,
145
+ type: "rule",
146
+ rule: "min-consecutive-days",
147
+ description: `${emp.id} min ${days} consecutive days from ${dayLabel}`,
148
+ targetValue: 0,
149
+ comparator: ">=",
150
+ day: dayLabel,
151
+ context: { memberIds: [emp.id], days: b.days.slice(i, i + days) },
152
+ group,
153
+ });
139
154
  }
140
155
  }
141
156
  }
@@ -0,0 +1,120 @@
1
+ import * as z from "zod";
2
+ import { DayOfWeekSchema } from "../../types.js";
3
+ import type { CompilationRule } from "../model-builder.js";
4
+ import type { Term } from "../types.js";
5
+ import { priorityToPenalty, splitIntoWeeks } from "../utils.js";
6
+ import {
7
+ PrioritySchema,
8
+ entityScope,
9
+ parseEntityScope,
10
+ resolveMembersFromScope,
11
+ ruleGroup,
12
+ } from "./scope.types.js";
13
+
14
+ const MinDaysWeekBase = z.object({
15
+ days: z.number().int().min(0),
16
+ priority: PrioritySchema,
17
+ weekStartsOn: DayOfWeekSchema.optional(),
18
+ });
19
+
20
+ const MinDaysWeekSchema = MinDaysWeekBase.and(entityScope(["members", "roles", "skills"]));
21
+
22
+ /**
23
+ * Configuration for {@link createMinDaysWeekRule}.
24
+ *
25
+ * - `days` (required): minimum number of days required per scheduling week
26
+ * - `priority` (required): how strictly the solver enforces this rule
27
+ * - `weekStartsOn` (optional): which day starts the week; defaults to {@link ModelBuilder.weekStartsOn}
28
+ *
29
+ * Entity scoping (at most one): `memberIds`, `roleIds`, `skillIds`
30
+ */
31
+ export type MinDaysWeekConfig = z.infer<typeof MinDaysWeekSchema>;
32
+
33
+ /**
34
+ * Enforces a minimum number of days a person must work per scheduling week.
35
+ *
36
+ * @remarks
37
+ * Creates a binary "works on day" variable for each member and day, then
38
+ * constrains the weekly sum. This counts distinct days, regardless of how
39
+ * many shifts are assigned on a single day.
40
+ *
41
+ * @param config - See {@link MinDaysWeekConfig}
42
+ * @example
43
+ * ```ts
44
+ * createMinDaysWeekRule({ days: 3, priority: "HIGH" });
45
+ * ```
46
+ */
47
+ export function createMinDaysWeekRule(config: MinDaysWeekConfig): CompilationRule {
48
+ const parsed = MinDaysWeekSchema.parse(config);
49
+ const scope = parseEntityScope(parsed);
50
+ const { days, priority, weekStartsOn } = parsed;
51
+ const group = ruleGroup(`min-days-week:${days}`, `Min ${days}d per week`, scope);
52
+
53
+ return {
54
+ compile(b) {
55
+ if (days <= 0) return;
56
+
57
+ const members = resolveMembersFromScope(scope, b.members);
58
+ const weeks = splitIntoWeeks(b.days, weekStartsOn ?? b.weekStartsOn);
59
+
60
+ for (const emp of members) {
61
+ for (const weekDays of weeks) {
62
+ const weekWorkVars: string[] = [];
63
+
64
+ for (const day of weekDays) {
65
+ const dayAssignments = b.shiftPatterns
66
+ .filter((p) => b.canAssign(emp, p) && b.patternAvailableOnDay(p, day))
67
+ .map((p) => b.assignment(emp.id, p.id, day));
68
+
69
+ if (dayAssignments.length === 0) continue;
70
+
71
+ const worksVar = b.boolVar(`works_day_${emp.id}_${day}`);
72
+ weekWorkVars.push(worksVar);
73
+
74
+ // worksVar >= each assignment (if any assignment is 1, worksVar must be 1)
75
+ for (const assignVar of dayAssignments) {
76
+ b.addLinear(
77
+ [
78
+ { var: worksVar, coeff: 1 },
79
+ { var: assignVar, coeff: -1 },
80
+ ],
81
+ ">=",
82
+ 0,
83
+ );
84
+ }
85
+
86
+ // worksVar <= sum(assignments) (if no assignment is 1, worksVar must be 0)
87
+ b.addLinear(
88
+ [{ var: worksVar, coeff: 1 }, ...dayAssignments.map((v) => ({ var: v, coeff: -1 }))],
89
+ "<=",
90
+ 0,
91
+ );
92
+ }
93
+
94
+ if (weekWorkVars.length === 0) continue;
95
+
96
+ const weekLabel = weekDays[0]!;
97
+ const constraintId = `min-days-week:${emp.id}:${weekLabel}`;
98
+ const terms: Term[] = weekWorkVars.map((v) => ({ var: v, coeff: 1 }));
99
+
100
+ if (priority === "MANDATORY") {
101
+ b.addLinear(terms, ">=", days);
102
+ } else {
103
+ b.addSoftLinear(terms, ">=", days, priorityToPenalty(priority), constraintId);
104
+ b.reporter.trackConstraint({
105
+ id: constraintId,
106
+ type: "rule",
107
+ rule: "min-days-week",
108
+ description: `${emp.id} min ${days}d in week starting ${weekLabel}`,
109
+ targetValue: days,
110
+ comparator: ">=",
111
+ day: weekLabel,
112
+ context: { memberIds: [emp.id], days: weekDays },
113
+ group,
114
+ });
115
+ }
116
+ }
117
+ }
118
+ },
119
+ };
120
+ }
@@ -7,6 +7,7 @@ import {
7
7
  entityScope,
8
8
  parseEntityScope,
9
9
  resolveMembersFromScope,
10
+ ruleGroup,
10
11
  } from "./scope.types.js";
11
12
 
12
13
  const MinHoursDaySchema = z
@@ -40,6 +41,7 @@ export function createMinHoursDayRule(config: MinHoursDayConfig): CompilationRul
40
41
  const scope = parseEntityScope(parsed);
41
42
  const { hours, priority } = parsed;
42
43
  const minMinutes = hours * 60;
44
+ const group = ruleGroup(`min-hours-day:${hours}`, `Min ${hours}h per day`, scope);
43
45
 
44
46
  return {
45
47
  compile(b) {
@@ -61,10 +63,23 @@ export function createMinHoursDayRule(config: MinHoursDayConfig): CompilationRul
61
63
 
62
64
  if (terms.length === 0) continue;
63
65
 
66
+ const constraintId = `min-hours-day:${emp.id}:${day}`;
67
+
64
68
  if (priority === "MANDATORY") {
65
69
  b.addLinear(terms, ">=", minMinutes);
66
70
  } else {
67
- b.addSoftLinear(terms, ">=", minMinutes, priorityToPenalty(priority));
71
+ b.addSoftLinear(terms, ">=", minMinutes, priorityToPenalty(priority), constraintId);
72
+ b.reporter.trackConstraint({
73
+ id: constraintId,
74
+ type: "rule",
75
+ rule: "min-hours-day",
76
+ description: `${emp.id} min ${hours}h on ${day}`,
77
+ targetValue: minMinutes,
78
+ comparator: ">=",
79
+ day,
80
+ context: { memberIds: [emp.id], days: [day] },
81
+ group,
82
+ });
68
83
  }
69
84
  }
70
85
  }
@@ -8,6 +8,7 @@ import {
8
8
  entityScope,
9
9
  parseEntityScope,
10
10
  resolveMembersFromScope,
11
+ ruleGroup,
11
12
  } from "./scope.types.js";
12
13
 
13
14
  const MinHoursWeekBase = z.object({
@@ -43,6 +44,7 @@ export function createMinHoursWeekRule(config: MinHoursWeekConfig): CompilationR
43
44
  const scope = parseEntityScope(parsed);
44
45
  const { hours, priority, weekStartsOn } = parsed;
45
46
  const minMinutes = hours * 60;
47
+ const group = ruleGroup(`min-hours-week:${hours}`, `Min ${hours}h per week`, scope);
46
48
 
47
49
  return {
48
50
  compile(b) {
@@ -67,10 +69,24 @@ export function createMinHoursWeekRule(config: MinHoursWeekConfig): CompilationR
67
69
 
68
70
  if (terms.length === 0) continue;
69
71
 
72
+ const weekLabel = weekDays[0]!;
73
+ const constraintId = `min-hours-week:${emp.id}:${weekLabel}`;
74
+
70
75
  if (priority === "MANDATORY") {
71
76
  b.addLinear(terms, ">=", minMinutes);
72
77
  } else {
73
- b.addSoftLinear(terms, ">=", minMinutes, priorityToPenalty(priority));
78
+ b.addSoftLinear(terms, ">=", minMinutes, priorityToPenalty(priority), constraintId);
79
+ b.reporter.trackConstraint({
80
+ id: constraintId,
81
+ type: "rule",
82
+ rule: "min-hours-week",
83
+ description: `${emp.id} min ${hours}h in week starting ${weekLabel}`,
84
+ targetValue: minMinutes,
85
+ comparator: ">=",
86
+ day: weekLabel,
87
+ context: { memberIds: [emp.id], days: weekDays },
88
+ group,
89
+ });
74
90
  }
75
91
  }
76
92
  }