dabke 0.78.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 (194) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/LICENSE +21 -0
  3. package/README.md +187 -0
  4. package/dist/client.d.ts +14 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +42 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/client.schemas.d.ts +250 -0
  9. package/dist/client.schemas.d.ts.map +1 -0
  10. package/dist/client.schemas.js +137 -0
  11. package/dist/client.schemas.js.map +1 -0
  12. package/dist/client.types.d.ts +34 -0
  13. package/dist/client.types.d.ts.map +1 -0
  14. package/dist/client.types.js +18 -0
  15. package/dist/client.types.js.map +1 -0
  16. package/dist/cpsat/model-builder.d.ts +128 -0
  17. package/dist/cpsat/model-builder.d.ts.map +1 -0
  18. package/dist/cpsat/model-builder.js +640 -0
  19. package/dist/cpsat/model-builder.js.map +1 -0
  20. package/dist/cpsat/response.d.ts +74 -0
  21. package/dist/cpsat/response.d.ts.map +1 -0
  22. package/dist/cpsat/response.js +92 -0
  23. package/dist/cpsat/response.js.map +1 -0
  24. package/dist/cpsat/rules/assign-together.d.ts +23 -0
  25. package/dist/cpsat/rules/assign-together.d.ts.map +1 -0
  26. package/dist/cpsat/rules/assign-together.js +78 -0
  27. package/dist/cpsat/rules/assign-together.js.map +1 -0
  28. package/dist/cpsat/rules/employee-assignment-priority.d.ts +64 -0
  29. package/dist/cpsat/rules/employee-assignment-priority.d.ts.map +1 -0
  30. package/dist/cpsat/rules/employee-assignment-priority.js +151 -0
  31. package/dist/cpsat/rules/employee-assignment-priority.js.map +1 -0
  32. package/dist/cpsat/rules/index.d.ts +13 -0
  33. package/dist/cpsat/rules/index.d.ts.map +1 -0
  34. package/dist/cpsat/rules/index.js +13 -0
  35. package/dist/cpsat/rules/index.js.map +1 -0
  36. package/dist/cpsat/rules/location-preference.d.ts +29 -0
  37. package/dist/cpsat/rules/location-preference.d.ts.map +1 -0
  38. package/dist/cpsat/rules/location-preference.js +59 -0
  39. package/dist/cpsat/rules/location-preference.js.map +1 -0
  40. package/dist/cpsat/rules/max-consecutive-days.d.ts +28 -0
  41. package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -0
  42. package/dist/cpsat/rules/max-consecutive-days.js +70 -0
  43. package/dist/cpsat/rules/max-consecutive-days.js.map +1 -0
  44. package/dist/cpsat/rules/max-hours-day.d.ts +57 -0
  45. package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -0
  46. package/dist/cpsat/rules/max-hours-day.js +159 -0
  47. package/dist/cpsat/rules/max-hours-day.js.map +1 -0
  48. package/dist/cpsat/rules/max-hours-week.d.ts +62 -0
  49. package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -0
  50. package/dist/cpsat/rules/max-hours-week.js +169 -0
  51. package/dist/cpsat/rules/max-hours-week.js.map +1 -0
  52. package/dist/cpsat/rules/max-shifts-day.d.ts +69 -0
  53. package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -0
  54. package/dist/cpsat/rules/max-shifts-day.js +170 -0
  55. package/dist/cpsat/rules/max-shifts-day.js.map +1 -0
  56. package/dist/cpsat/rules/min-consecutive-days.d.ts +29 -0
  57. package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -0
  58. package/dist/cpsat/rules/min-consecutive-days.js +104 -0
  59. package/dist/cpsat/rules/min-consecutive-days.js.map +1 -0
  60. package/dist/cpsat/rules/min-hours-day.d.ts +28 -0
  61. package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -0
  62. package/dist/cpsat/rules/min-hours-day.js +61 -0
  63. package/dist/cpsat/rules/min-hours-day.js.map +1 -0
  64. package/dist/cpsat/rules/min-hours-week.d.ts +29 -0
  65. package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -0
  66. package/dist/cpsat/rules/min-hours-week.js +68 -0
  67. package/dist/cpsat/rules/min-hours-week.js.map +1 -0
  68. package/dist/cpsat/rules/min-rest-between-shifts.d.ts +28 -0
  69. package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -0
  70. package/dist/cpsat/rules/min-rest-between-shifts.js +95 -0
  71. package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -0
  72. package/dist/cpsat/rules/registry.d.ts +7 -0
  73. package/dist/cpsat/rules/registry.d.ts.map +1 -0
  74. package/dist/cpsat/rules/registry.js +28 -0
  75. package/dist/cpsat/rules/registry.js.map +1 -0
  76. package/dist/cpsat/rules/resolver.d.ts +31 -0
  77. package/dist/cpsat/rules/resolver.d.ts.map +1 -0
  78. package/dist/cpsat/rules/resolver.js +124 -0
  79. package/dist/cpsat/rules/resolver.js.map +1 -0
  80. package/dist/cpsat/rules/rules.types.d.ts +32 -0
  81. package/dist/cpsat/rules/rules.types.d.ts.map +1 -0
  82. package/dist/cpsat/rules/rules.types.js +2 -0
  83. package/dist/cpsat/rules/rules.types.js.map +1 -0
  84. package/dist/cpsat/rules/scoping.d.ts +129 -0
  85. package/dist/cpsat/rules/scoping.d.ts.map +1 -0
  86. package/dist/cpsat/rules/scoping.js +190 -0
  87. package/dist/cpsat/rules/scoping.js.map +1 -0
  88. package/dist/cpsat/rules/time-off.d.ts +78 -0
  89. package/dist/cpsat/rules/time-off.d.ts.map +1 -0
  90. package/dist/cpsat/rules/time-off.js +261 -0
  91. package/dist/cpsat/rules/time-off.js.map +1 -0
  92. package/dist/cpsat/rules.d.ts +5 -0
  93. package/dist/cpsat/rules.d.ts.map +1 -0
  94. package/dist/cpsat/rules.js +4 -0
  95. package/dist/cpsat/rules.js.map +1 -0
  96. package/dist/cpsat/semantic-time.d.ts +198 -0
  97. package/dist/cpsat/semantic-time.d.ts.map +1 -0
  98. package/dist/cpsat/semantic-time.js +222 -0
  99. package/dist/cpsat/semantic-time.js.map +1 -0
  100. package/dist/cpsat/types.d.ts +180 -0
  101. package/dist/cpsat/types.d.ts.map +1 -0
  102. package/dist/cpsat/types.js +2 -0
  103. package/dist/cpsat/types.js.map +1 -0
  104. package/dist/cpsat/utils.d.ts +47 -0
  105. package/dist/cpsat/utils.d.ts.map +1 -0
  106. package/dist/cpsat/utils.js +92 -0
  107. package/dist/cpsat/utils.js.map +1 -0
  108. package/dist/cpsat/validation-reporter.d.ts +54 -0
  109. package/dist/cpsat/validation-reporter.d.ts.map +1 -0
  110. package/dist/cpsat/validation-reporter.js +261 -0
  111. package/dist/cpsat/validation-reporter.js.map +1 -0
  112. package/dist/cpsat/validation.types.d.ts +141 -0
  113. package/dist/cpsat/validation.types.d.ts.map +1 -0
  114. package/dist/cpsat/validation.types.js +14 -0
  115. package/dist/cpsat/validation.types.js.map +1 -0
  116. package/dist/datetime.utils.d.ts +245 -0
  117. package/dist/datetime.utils.d.ts.map +1 -0
  118. package/dist/datetime.utils.js +372 -0
  119. package/dist/datetime.utils.js.map +1 -0
  120. package/dist/errors.d.ts +12 -0
  121. package/dist/errors.d.ts.map +1 -0
  122. package/dist/errors.js +17 -0
  123. package/dist/errors.js.map +1 -0
  124. package/dist/index.d.ts +112 -0
  125. package/dist/index.d.ts.map +1 -0
  126. package/dist/index.js +116 -0
  127. package/dist/index.js.map +1 -0
  128. package/dist/llms.d.ts +5 -0
  129. package/dist/llms.d.ts.map +1 -0
  130. package/dist/llms.js +8 -0
  131. package/dist/llms.js.map +1 -0
  132. package/dist/testing/index.d.ts +12 -0
  133. package/dist/testing/index.d.ts.map +1 -0
  134. package/dist/testing/index.js +11 -0
  135. package/dist/testing/index.js.map +1 -0
  136. package/dist/testing/solver-container.d.ts +49 -0
  137. package/dist/testing/solver-container.d.ts.map +1 -0
  138. package/dist/testing/solver-container.js +127 -0
  139. package/dist/testing/solver-container.js.map +1 -0
  140. package/dist/types.d.ts +155 -0
  141. package/dist/types.d.ts.map +1 -0
  142. package/dist/types.js +20 -0
  143. package/dist/types.js.map +1 -0
  144. package/dist/validation.d.ts +105 -0
  145. package/dist/validation.d.ts.map +1 -0
  146. package/dist/validation.js +130 -0
  147. package/dist/validation.js.map +1 -0
  148. package/llms.txt +2188 -0
  149. package/package.json +76 -0
  150. package/solver/Dockerfile +31 -0
  151. package/solver/README.md +23 -0
  152. package/solver/pyproject.toml +28 -0
  153. package/solver/src/solver/__init__.py +1 -0
  154. package/solver/src/solver/app.py +24 -0
  155. package/solver/src/solver/models.py +120 -0
  156. package/solver/src/solver/solver.py +359 -0
  157. package/solver/tests/test_solver.py +156 -0
  158. package/solver/uv.lock +661 -0
  159. package/src/client.schemas.ts +163 -0
  160. package/src/client.ts +67 -0
  161. package/src/client.types.ts +66 -0
  162. package/src/cpsat/model-builder.ts +858 -0
  163. package/src/cpsat/response.ts +130 -0
  164. package/src/cpsat/rules/assign-together.ts +96 -0
  165. package/src/cpsat/rules/employee-assignment-priority.ts +182 -0
  166. package/src/cpsat/rules/index.ts +12 -0
  167. package/src/cpsat/rules/location-preference.ts +68 -0
  168. package/src/cpsat/rules/max-consecutive-days.ts +98 -0
  169. package/src/cpsat/rules/max-hours-day.ts +187 -0
  170. package/src/cpsat/rules/max-hours-week.ts +197 -0
  171. package/src/cpsat/rules/max-shifts-day.ts +198 -0
  172. package/src/cpsat/rules/min-consecutive-days.ts +140 -0
  173. package/src/cpsat/rules/min-hours-day.ts +69 -0
  174. package/src/cpsat/rules/min-hours-week.ts +77 -0
  175. package/src/cpsat/rules/min-rest-between-shifts.ts +121 -0
  176. package/src/cpsat/rules/registry.ts +49 -0
  177. package/src/cpsat/rules/resolver.ts +181 -0
  178. package/src/cpsat/rules/rules.types.ts +41 -0
  179. package/src/cpsat/rules/scoping.ts +340 -0
  180. package/src/cpsat/rules/time-off.ts +336 -0
  181. package/src/cpsat/rules.ts +27 -0
  182. package/src/cpsat/semantic-time.ts +463 -0
  183. package/src/cpsat/types.ts +194 -0
  184. package/src/cpsat/utils.ts +105 -0
  185. package/src/cpsat/validation-reporter.ts +366 -0
  186. package/src/cpsat/validation.types.ts +185 -0
  187. package/src/datetime.utils.ts +426 -0
  188. package/src/errors.ts +17 -0
  189. package/src/index.ts +289 -0
  190. package/src/llms.ts +9 -0
  191. package/src/testing/index.ts +12 -0
  192. package/src/testing/solver-container.ts +172 -0
  193. package/src/types.ts +191 -0
  194. package/src/validation.ts +188 -0
@@ -0,0 +1,130 @@
1
+ import type { SolverResponse } from "../client.types.js";
2
+ import type { ShiftPattern } from "./types.js";
3
+ import type { TimeOfDay } from "../types.js";
4
+
5
+ /**
6
+ * A shift assignment extracted from the solver response.
7
+ */
8
+ export interface ShiftAssignment {
9
+ employeeId: string;
10
+ shiftPatternId: string;
11
+ day: string;
12
+ }
13
+
14
+ /**
15
+ * A shift assignment with resolved times.
16
+ */
17
+ export interface ResolvedShiftAssignment {
18
+ employeeId: string;
19
+ day: string;
20
+ startTime: TimeOfDay;
21
+ endTime: TimeOfDay;
22
+ }
23
+
24
+ /**
25
+ * Parsed solver result with assignments and metadata.
26
+ */
27
+ export interface SolverResult {
28
+ status: SolverResponse["status"];
29
+ assignments: ShiftAssignment[];
30
+ statistics?: SolverResponse["statistics"];
31
+ error?: string;
32
+ }
33
+
34
+ /**
35
+ * Extracts shift assignments from solver response.
36
+ *
37
+ * Parses variable names matching the pattern `assign:${employeeId}:${patternId}:${day}`
38
+ * and returns assignments where the variable value is 1 (true).
39
+ *
40
+ * @remarks
41
+ * IDs are validated by ModelBuilder to not contain colons,
42
+ * ensuring unambiguous parsing.
43
+ *
44
+ * @param response - The solver response containing variable values
45
+ * @returns Parsed schedule result with assignments
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const response = await client.solve(request);
50
+ * const result = parseSolverResponse(response);
51
+ *
52
+ * if (result.status === "OPTIMAL" || result.status === "FEASIBLE") {
53
+ * for (const assignment of result.assignments) {
54
+ * console.log(`${assignment.employeeId} works ${assignment.shiftPatternId} on ${assignment.day}`);
55
+ * }
56
+ * }
57
+ * ```
58
+ */
59
+ export function parseSolverResponse(response: SolverResponse): SolverResult {
60
+ if (response.status === "INFEASIBLE" || response.status === "ERROR") {
61
+ return {
62
+ status: response.status,
63
+ assignments: [],
64
+ statistics: response.statistics,
65
+ error: response.error,
66
+ };
67
+ }
68
+
69
+ const assignments: ShiftAssignment[] = [];
70
+
71
+ for (const [varName, value] of Object.entries(response.values ?? {})) {
72
+ if (value !== 1) continue;
73
+ if (!varName.startsWith("assign:")) continue;
74
+
75
+ // Pattern: assign:${employeeId}:${patternId}:${day}
76
+ // IDs are validated to not contain colons, so splitting is unambiguous.
77
+ const parts = varName.split(":");
78
+ if (parts.length !== 4) continue;
79
+
80
+ const [, employeeId, shiftPatternId, day] = parts;
81
+ if (!employeeId || !shiftPatternId || !day) continue;
82
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) continue;
83
+
84
+ assignments.push({ employeeId, shiftPatternId, day });
85
+ }
86
+
87
+ return {
88
+ status: response.status,
89
+ assignments,
90
+ statistics: response.statistics,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Resolves shift assignments to concrete times using shift patterns.
96
+ *
97
+ * @param assignments - Raw assignments from parseScheduleResult
98
+ * @param shiftPatterns - The shift patterns used in the model
99
+ * @returns Assignments with resolved start/end times
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const result = parseScheduleResult(response);
104
+ * const resolved = resolveAssignments(result.assignments, shiftPatterns);
105
+ *
106
+ * for (const shift of resolved) {
107
+ * console.log(`${shift.employeeId} works ${shift.day} from ${shift.startTime.hours}:${shift.startTime.minutes}`);
108
+ * }
109
+ * ```
110
+ */
111
+ export function resolveAssignments(
112
+ assignments: ShiftAssignment[],
113
+ shiftPatterns: ShiftPattern[],
114
+ ): ResolvedShiftAssignment[] {
115
+ const patternMap = new Map(shiftPatterns.map((p) => [p.id, p]));
116
+
117
+ return assignments
118
+ .map((a) => {
119
+ const pattern = patternMap.get(a.shiftPatternId);
120
+ if (!pattern) return null;
121
+
122
+ return {
123
+ employeeId: a.employeeId,
124
+ day: a.day,
125
+ startTime: pattern.startTime,
126
+ endTime: pattern.endTime,
127
+ };
128
+ })
129
+ .filter((a): a is ResolvedShiftAssignment => a !== null);
130
+ }
@@ -0,0 +1,96 @@
1
+ import * as z from "zod";
2
+ import type { CompilationRule } from "../model-builder.js";
3
+ import { priorityToPenalty } from "../utils.js";
4
+
5
+ const AssignTogetherSchema = z.object({
6
+ groupEmployeeIds: z
7
+ .tuple([z.string(), z.string()])
8
+ .rest(z.string())
9
+ .refine((ids) => new Set(ids).size === ids.length, {
10
+ message: "IDs must be unique",
11
+ }),
12
+ priority: z.union([
13
+ z.literal("LOW"),
14
+ z.literal("MEDIUM"),
15
+ z.literal("HIGH"),
16
+ z.literal("MANDATORY"),
17
+ ]),
18
+ });
19
+
20
+ export type AssignTogetherConfig = z.infer<typeof AssignTogetherSchema>;
21
+
22
+ /**
23
+ * Encourages or enforces that team members in the group work the same shift patterns on a day.
24
+ * For each pair of team members in the group, ensures they are assigned to the same shifts.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const rule = createAssignTogetherRule({
29
+ * groupEmployeeIds: ["alice", "bob", "charlie"],
30
+ * priority: "HIGH",
31
+ * });
32
+ * builder = new ModelBuilder({ ...config, rules: [rule] });
33
+ * ```
34
+ */
35
+ export function createAssignTogetherRule(config: AssignTogetherConfig): CompilationRule {
36
+ const { groupEmployeeIds, priority } = AssignTogetherSchema.parse(config);
37
+
38
+ return {
39
+ compile(b) {
40
+ const employees = groupEmployeeIds
41
+ .map((id) => b.employees.find((e) => e.id === id))
42
+ .filter((e): e is NonNullable<typeof e> => e !== undefined);
43
+
44
+ if (employees.length < 2) return;
45
+
46
+ for (let i = 0; i < employees.length - 1; i++) {
47
+ const emp1 = employees[i]!;
48
+ const emp2 = employees[i + 1]!;
49
+
50
+ for (const pattern of b.shiftPatterns) {
51
+ const canAssign1 = b.canAssign(emp1, pattern);
52
+ const canAssign2 = b.canAssign(emp2, pattern);
53
+ if (!canAssign1 || !canAssign2) continue;
54
+
55
+ for (const day of b.days) {
56
+ if (!b.patternAvailableOnDay(pattern, day)) continue;
57
+ const var1 = b.assignment(emp1.id, pattern.id, day);
58
+ const var2 = b.assignment(emp2.id, pattern.id, day);
59
+
60
+ if (priority === "MANDATORY") {
61
+ b.addLinear(
62
+ [
63
+ { var: var1, coeff: 1 },
64
+ { var: var2, coeff: -1 },
65
+ ],
66
+ "==",
67
+ 0,
68
+ );
69
+ } else {
70
+ const diffVar = b.boolVar(`together_diff_${emp1.id}_${emp2.id}_${pattern.id}_${day}`);
71
+ b.addLinear(
72
+ [
73
+ { var: diffVar, coeff: 1 },
74
+ { var: var1, coeff: -1 },
75
+ { var: var2, coeff: 1 },
76
+ ],
77
+ ">=",
78
+ 0,
79
+ );
80
+ b.addLinear(
81
+ [
82
+ { var: diffVar, coeff: 1 },
83
+ { var: var1, coeff: 1 },
84
+ { var: var2, coeff: -1 },
85
+ ],
86
+ ">=",
87
+ 0,
88
+ );
89
+ b.addPenalty(diffVar, priorityToPenalty(priority));
90
+ }
91
+ }
92
+ }
93
+ }
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,182 @@
1
+ import * as z from "zod";
2
+ import type { CompilationRule } from "../model-builder.js";
3
+ import type { SchedulingEmployee } from "../types.js";
4
+ import { OBJECTIVE_WEIGHTS, parseDayString } from "../utils.js";
5
+ import { normalizeScope, withScopes, type RuleScope } from "./scoping.js";
6
+
7
+ const EmployeeAssignmentPrioritySchema = withScopes(
8
+ z.object({
9
+ preference: z.union([z.literal("high"), z.literal("low")]),
10
+ }),
11
+ {
12
+ entities: ["employees", "roles", "skills"],
13
+ times: ["dateRange", "specificDates", "dayOfWeek", "recurring"],
14
+ },
15
+ );
16
+
17
+ export type EmployeeAssignmentPriorityConfig = z.infer<typeof EmployeeAssignmentPrioritySchema>;
18
+
19
+ /**
20
+ * Adds objective weight to prefer or avoid assigning team members.
21
+ *
22
+ * Supports entity scoping (people, roles, skills) and time scoping
23
+ * (date ranges, specific dates, days of week, recurring periods).
24
+ *
25
+ * @example Prefer specific team members
26
+ * ```ts
27
+ * createEmployeeAssignmentPriorityRule({
28
+ * employeeIds: ["alice", "bob"],
29
+ * preference: "high",
30
+ * });
31
+ * ```
32
+ *
33
+ * @example Avoid assigning students on weekdays
34
+ * ```ts
35
+ * createEmployeeAssignmentPriorityRule({
36
+ * roleIds: ["student"],
37
+ * dayOfWeek: ["monday", "tuesday", "wednesday", "thursday", "friday"],
38
+ * preference: "low",
39
+ * });
40
+ * ```
41
+ *
42
+ * @example Prefer experienced staff on weekends
43
+ * ```ts
44
+ * createEmployeeAssignmentPriorityRule({
45
+ * skillIds: ["senior"],
46
+ * dayOfWeek: ["saturday", "sunday"],
47
+ * preference: "high",
48
+ * });
49
+ * ```
50
+ */
51
+ export function createEmployeeAssignmentPriorityRule(
52
+ config: EmployeeAssignmentPriorityConfig,
53
+ ): CompilationRule {
54
+ const parsed = EmployeeAssignmentPrioritySchema.parse(config);
55
+ const { preference } = parsed;
56
+
57
+ return {
58
+ compile(b) {
59
+ const scope = normalizeScope(parsed, b.employees);
60
+ const targetEmployees = resolveEmployees(scope, b.employees);
61
+ const activeDays = resolveActiveDays(scope, b.days);
62
+
63
+ if (targetEmployees.length === 0 || activeDays.length === 0) return;
64
+
65
+ const weight =
66
+ preference === "high"
67
+ ? -OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE
68
+ : OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE;
69
+
70
+ for (const emp of targetEmployees) {
71
+ for (const pattern of b.shiftPatterns) {
72
+ if (!b.canAssign(emp, pattern)) continue;
73
+ for (const day of activeDays) {
74
+ if (!b.patternAvailableOnDay(pattern, day)) continue;
75
+ b.addPenalty(b.assignment(emp.id, pattern.id, day), weight);
76
+ }
77
+ }
78
+ }
79
+ },
80
+ };
81
+ }
82
+
83
+ function resolveEmployees(scope: RuleScope, employees: SchedulingEmployee[]): SchedulingEmployee[] {
84
+ const entity = scope.entity;
85
+ switch (entity.type) {
86
+ case "employees":
87
+ return employees.filter((e) => entity.employeeIds.includes(e.id));
88
+ case "roles":
89
+ return employees.filter((e) => e.roleIds.some((r) => entity.roleIds.includes(r)));
90
+ case "skills":
91
+ return employees.filter((e) => e.skillIds?.some((s) => entity.skillIds.includes(s)));
92
+ case "global":
93
+ default:
94
+ return employees;
95
+ }
96
+ }
97
+
98
+ type DayName = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";
99
+
100
+ function getDayOfWeekName(dayIndex: number): DayName {
101
+ const names: Record<number, DayName> = {
102
+ 0: "sunday",
103
+ 1: "monday",
104
+ 2: "tuesday",
105
+ 3: "wednesday",
106
+ 4: "thursday",
107
+ 5: "friday",
108
+ 6: "saturday",
109
+ };
110
+ return names[dayIndex % 7] ?? "sunday";
111
+ }
112
+
113
+ function resolveActiveDays(scope: RuleScope, allDays: string[]): string[] {
114
+ const timeScope = scope.time;
115
+
116
+ if (!timeScope) {
117
+ return allDays;
118
+ }
119
+
120
+ switch (timeScope.type) {
121
+ case "always":
122
+ return allDays;
123
+
124
+ case "dateRange": {
125
+ const start = timeScope.start;
126
+ const end = timeScope.end;
127
+ return allDays.filter((day) => day >= start && day <= end);
128
+ }
129
+
130
+ case "specificDates":
131
+ return allDays.filter((day) => timeScope.dates.includes(day));
132
+
133
+ case "dayOfWeek": {
134
+ const targetDays = new Set(timeScope.days);
135
+ return allDays.filter((day) => {
136
+ const date = parseDayString(day);
137
+ const dayName = getDayOfWeekName(date.getUTCDay());
138
+ return targetDays.has(dayName);
139
+ });
140
+ }
141
+
142
+ case "recurring": {
143
+ return allDays.filter((day) => {
144
+ const date = parseDayString(day);
145
+ const month = date.getUTCMonth() + 1;
146
+ const dayOfMonth = date.getUTCDate();
147
+
148
+ return timeScope.periods.some((period) =>
149
+ isDateInRecurringPeriod(month, dayOfMonth, period),
150
+ );
151
+ });
152
+ }
153
+
154
+ default:
155
+ return allDays;
156
+ }
157
+ }
158
+
159
+ function isDateInRecurringPeriod(
160
+ month: number,
161
+ dayOfMonth: number,
162
+ period: {
163
+ startMonth: number;
164
+ startDay: number;
165
+ endMonth: number;
166
+ endDay: number;
167
+ },
168
+ ): boolean {
169
+ const { startMonth, startDay, endMonth, endDay } = period;
170
+
171
+ if (startMonth <= endMonth) {
172
+ if (month < startMonth || month > endMonth) return false;
173
+ if (month === startMonth && dayOfMonth < startDay) return false;
174
+ if (month === endMonth && dayOfMonth > endDay) return false;
175
+ return true;
176
+ } else {
177
+ if (month > endMonth && month < startMonth) return false;
178
+ if (month === startMonth && dayOfMonth < startDay) return false;
179
+ if (month === endMonth && dayOfMonth > endDay) return false;
180
+ return true;
181
+ }
182
+ }
@@ -0,0 +1,12 @@
1
+ export { createAssignTogetherRule } from "./assign-together.js";
2
+ export { createEmployeeAssignmentPriorityRule } from "./employee-assignment-priority.js";
3
+ export { createLocationPreferenceRule } from "./location-preference.js";
4
+ export { createMaxConsecutiveDaysRule } from "./max-consecutive-days.js";
5
+ export { createMaxHoursDayRule } from "./max-hours-day.js";
6
+ export { createMaxHoursWeekRule } from "./max-hours-week.js";
7
+ export { createMaxShiftsDayRule } from "./max-shifts-day.js";
8
+ export { createMinConsecutiveDaysRule } from "./min-consecutive-days.js";
9
+ export { createMinHoursDayRule } from "./min-hours-day.js";
10
+ export { createMinHoursWeekRule } from "./min-hours-week.js";
11
+ export { createMinRestBetweenShiftsRule } from "./min-rest-between-shifts.js";
12
+ export { createTimeOffRule } from "./time-off.js";
@@ -0,0 +1,68 @@
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";
5
+ import { withScopes } from "./scoping.js";
6
+
7
+ const LocationPreferenceSchema = withScopes(
8
+ z.object({
9
+ locationId: z.string(),
10
+ priority: z.union([
11
+ z.literal("LOW"),
12
+ z.literal("MEDIUM"),
13
+ z.literal("HIGH"),
14
+ z.literal("MANDATORY"),
15
+ ]),
16
+ }),
17
+ { entities: ["employees", "roles", "skills"], times: [] },
18
+ );
19
+
20
+ export type LocationPreferenceConfig = z.infer<typeof LocationPreferenceSchema>;
21
+
22
+ const PRIORITY_WEIGHTS: Record<Priority, number> = {
23
+ LOW: OBJECTIVE_WEIGHTS.FAIRNESS, // 5 - same as fairness, weak preference
24
+ MEDIUM: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE, // 10 - standard preference
25
+ HIGH: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE * 2.5, // 25 - strong preference
26
+ MANDATORY: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE * 5, // 50 - very strong
27
+ };
28
+
29
+ /**
30
+ * Prefers assigning a person to shift patterns matching a specific location.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const rule = createLocationPreferenceRule({
35
+ * locationId: "terrace",
36
+ * priority: "HIGH",
37
+ * employeeIds: ["alice"],
38
+ * });
39
+ * builder = new ModelBuilder({ ...config, rules: [rule] });
40
+ * ```
41
+ */
42
+ export function createLocationPreferenceRule(config: LocationPreferenceConfig): CompilationRule {
43
+ const { locationId, priority, employeeIds } = LocationPreferenceSchema.parse(config);
44
+ const weight = PRIORITY_WEIGHTS[priority] ?? 0;
45
+
46
+ return {
47
+ compile(b) {
48
+ if (weight === 0) return;
49
+
50
+ const employees = employeeIds
51
+ ? b.employees.filter((e) => employeeIds.includes(e.id))
52
+ : b.employees;
53
+
54
+ for (const emp of employees) {
55
+ for (const pattern of b.shiftPatterns) {
56
+ if (!b.canAssign(emp, pattern)) continue;
57
+ const isPreferred = pattern.locationId === locationId;
58
+ if (isPreferred) continue;
59
+
60
+ for (const day of b.days) {
61
+ if (!b.patternAvailableOnDay(pattern, day)) continue;
62
+ b.addPenalty(b.assignment(emp.id, pattern.id, day), weight);
63
+ }
64
+ }
65
+ }
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,98 @@
1
+ import * as z from "zod";
2
+ import type { CompilationRule } from "../model-builder.js";
3
+ import { priorityToPenalty } from "../utils.js";
4
+ import { withScopes } from "./scoping.js";
5
+
6
+ const MaxConsecutiveDaysSchema = withScopes(
7
+ z.object({
8
+ days: z.number().min(0),
9
+ priority: z.union([
10
+ z.literal("LOW"),
11
+ z.literal("MEDIUM"),
12
+ z.literal("HIGH"),
13
+ z.literal("MANDATORY"),
14
+ ]),
15
+ }),
16
+ { entities: ["employees", "roles", "skills"], times: [] },
17
+ );
18
+
19
+ export type MaxConsecutiveDaysConfig = z.infer<typeof MaxConsecutiveDaysSchema>;
20
+
21
+ /**
22
+ * Limits how many consecutive days a person can be assigned.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const rule = createMaxConsecutiveDaysRule({
27
+ * days: 5,
28
+ * priority: "MANDATORY",
29
+ * });
30
+ * builder = new ModelBuilder({ ...config, rules: [rule] });
31
+ * ```
32
+ */
33
+ export function createMaxConsecutiveDaysRule(config: MaxConsecutiveDaysConfig): CompilationRule {
34
+ const { days, priority, employeeIds } = MaxConsecutiveDaysSchema.parse(config);
35
+ const windowSize = days + 1;
36
+
37
+ return {
38
+ compile(b) {
39
+ const employees = employeeIds
40
+ ? b.employees.filter((e) => employeeIds.includes(e.id))
41
+ : b.employees;
42
+
43
+ for (const emp of employees) {
44
+ for (let i = 0; i <= b.days.length - windowSize; i++) {
45
+ const windowDays = b.days.slice(i, i + windowSize);
46
+
47
+ const worksDayVars: string[] = [];
48
+ for (const day of windowDays) {
49
+ const worksVar = b.boolVar(`works_${emp.id}_${day}`);
50
+ worksDayVars.push(worksVar);
51
+
52
+ const dayAssignments = b.shiftPatterns
53
+ .filter((p) => b.canAssign(emp, p) && b.patternAvailableOnDay(p, day))
54
+ .map((p) => b.assignment(emp.id, p.id, day));
55
+
56
+ if (dayAssignments.length === 0) {
57
+ b.addLinear([{ var: worksVar, coeff: 1 }], "==", 0);
58
+ } else {
59
+ for (const assignVar of dayAssignments) {
60
+ b.addLinear(
61
+ [
62
+ { var: worksVar, coeff: 1 },
63
+ { var: assignVar, coeff: -1 },
64
+ ],
65
+ ">=",
66
+ 0,
67
+ );
68
+ }
69
+ b.addLinear(
70
+ [
71
+ { var: worksVar, coeff: 1 },
72
+ ...dayAssignments.map((v) => ({ var: v, coeff: -1 })),
73
+ ],
74
+ "<=",
75
+ 0,
76
+ );
77
+ }
78
+ }
79
+
80
+ if (priority === "MANDATORY") {
81
+ b.addLinear(
82
+ worksDayVars.map((v) => ({ var: v, coeff: 1 })),
83
+ "<=",
84
+ days,
85
+ );
86
+ } else {
87
+ b.addSoftLinear(
88
+ worksDayVars.map((v) => ({ var: v, coeff: 1 })),
89
+ "<=",
90
+ days,
91
+ priorityToPenalty(priority),
92
+ );
93
+ }
94
+ }
95
+ }
96
+ },
97
+ };
98
+ }