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,140 @@
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 MinConsecutiveDaysSchema = 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 MinConsecutiveDaysConfig = z.infer<typeof MinConsecutiveDaysSchema>;
20
+
21
+ /**
22
+ * Requires that once a person starts working, they continue for a minimum
23
+ * number of consecutive days.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const rule = createMinConsecutiveDaysRule({
28
+ * days: 3,
29
+ * priority: "MANDATORY",
30
+ * });
31
+ * builder = new ModelBuilder({ ...config, rules: [rule] });
32
+ * ```
33
+ */
34
+ export function createMinConsecutiveDaysRule(config: MinConsecutiveDaysConfig): CompilationRule {
35
+ const { days, priority, employeeIds } = MinConsecutiveDaysSchema.parse(config);
36
+
37
+ return {
38
+ compile(b) {
39
+ if (days <= 1) return;
40
+
41
+ const employees = employeeIds
42
+ ? b.employees.filter((e) => employeeIds.includes(e.id))
43
+ : b.employees;
44
+
45
+ for (const emp of employees) {
46
+ const worksByDay: string[] = [];
47
+
48
+ for (const day of b.days) {
49
+ const worksVar = b.boolVar(`works_${emp.id}_${day}`);
50
+ worksByDay.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
+ [{ var: worksVar, coeff: 1 }, ...dayAssignments.map((v) => ({ var: v, coeff: -1 }))],
71
+ "<=",
72
+ 0,
73
+ );
74
+ }
75
+ }
76
+
77
+ for (let i = 0; i < b.days.length; i++) {
78
+ const dayLabel = b.days[i];
79
+ if (!dayLabel) continue;
80
+
81
+ const worksToday = worksByDay[i];
82
+ if (!worksToday) continue;
83
+ const worksYesterday = i > 0 ? worksByDay[i - 1] : undefined;
84
+ const startVar = b.boolVar(`work_start_${emp.id}_${dayLabel}`);
85
+
86
+ b.addLinear(
87
+ [
88
+ { var: startVar, coeff: 1 },
89
+ { var: worksToday, coeff: -1 },
90
+ ],
91
+ "<=",
92
+ 0,
93
+ );
94
+ if (worksYesterday) {
95
+ b.addLinear(
96
+ [
97
+ { var: startVar, coeff: 1 },
98
+ { var: worksYesterday, coeff: 1 },
99
+ ],
100
+ "<=",
101
+ 1,
102
+ );
103
+ b.addLinear(
104
+ [
105
+ { var: startVar, coeff: 1 },
106
+ { var: worksYesterday, coeff: 1 },
107
+ { var: worksToday, coeff: -1 },
108
+ ],
109
+ ">=",
110
+ 0,
111
+ );
112
+ } else {
113
+ b.addLinear(
114
+ [
115
+ { var: startVar, coeff: 1 },
116
+ { var: worksToday, coeff: -1 },
117
+ ],
118
+ ">=",
119
+ 0,
120
+ );
121
+ }
122
+
123
+ const window = worksByDay.slice(i, i + days).filter(Boolean) as string[];
124
+ if (window.length === 0) continue;
125
+
126
+ const terms = [
127
+ ...window.map((v) => ({ var: v, coeff: 1 })),
128
+ { var: startVar, coeff: -days },
129
+ ];
130
+
131
+ if (priority === "MANDATORY") {
132
+ b.addLinear(terms, ">=", 0);
133
+ } else {
134
+ b.addSoftLinear(terms, ">=", 0, priorityToPenalty(priority));
135
+ }
136
+ }
137
+ }
138
+ },
139
+ };
140
+ }
@@ -0,0 +1,69 @@
1
+ import * as z from "zod";
2
+ import type { CompilationRule } from "../model-builder.js";
3
+ import type { Term } from "../types.js";
4
+ import { priorityToPenalty } from "../utils.js";
5
+ import { withScopes } from "./scoping.js";
6
+
7
+ const MinHoursDaySchema = withScopes(
8
+ z.object({
9
+ hours: z.number().min(0),
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 MinHoursDayConfig = z.infer<typeof MinHoursDaySchema>;
21
+
22
+ /**
23
+ * Ensures a person works at least a minimum number of hours per day.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const rule = createMinHoursDayRule({
28
+ * hours: 6,
29
+ * priority: "MANDATORY",
30
+ * });
31
+ * builder = new ModelBuilder({ ...config, rules: [rule] });
32
+ * ```
33
+ */
34
+ export function createMinHoursDayRule(config: MinHoursDayConfig): CompilationRule {
35
+ const { hours, priority, employeeIds } = MinHoursDaySchema.parse(config);
36
+ const minMinutes = hours * 60;
37
+
38
+ return {
39
+ compile(b) {
40
+ if (hours <= 0) return;
41
+
42
+ const employees = employeeIds
43
+ ? b.employees.filter((e) => employeeIds.includes(e.id))
44
+ : b.employees;
45
+
46
+ for (const emp of employees) {
47
+ for (const day of b.days) {
48
+ const terms: Term[] = [];
49
+ for (const pattern of b.shiftPatterns) {
50
+ if (!b.canAssign(emp, pattern)) continue;
51
+ if (!b.patternAvailableOnDay(pattern, day)) continue;
52
+ terms.push({
53
+ var: b.assignment(emp.id, pattern.id, day),
54
+ coeff: b.patternDuration(pattern.id),
55
+ });
56
+ }
57
+
58
+ if (terms.length === 0) continue;
59
+
60
+ if (priority === "MANDATORY") {
61
+ b.addLinear(terms, ">=", minMinutes);
62
+ } else {
63
+ b.addSoftLinear(terms, ">=", minMinutes, priorityToPenalty(priority));
64
+ }
65
+ }
66
+ }
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,77 @@
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 { withScopes } from "./scoping.js";
7
+
8
+ const MinHoursWeekSchema = withScopes(
9
+ z.object({
10
+ hours: z.number().min(0),
11
+ priority: z.union([
12
+ z.literal("LOW"),
13
+ z.literal("MEDIUM"),
14
+ z.literal("HIGH"),
15
+ z.literal("MANDATORY"),
16
+ ]),
17
+ // Optional override; defaults to ModelBuilder.weekStartsOn
18
+ weekStartsOn: DayOfWeekSchema.optional(),
19
+ }),
20
+ { entities: ["employees", "roles", "skills"], times: [] },
21
+ );
22
+
23
+ export type MinHoursWeekConfig = z.infer<typeof MinHoursWeekSchema>;
24
+
25
+ /**
26
+ * Enforces a minimum total number of hours per scheduling week.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const rule = createMinHoursWeekRule({
31
+ * hours: 30,
32
+ * priority: "HIGH",
33
+ * });
34
+ * builder = new ModelBuilder({ ...config, rules: [rule] });
35
+ * ```
36
+ */
37
+ export function createMinHoursWeekRule(config: MinHoursWeekConfig): CompilationRule {
38
+ const parsed = MinHoursWeekSchema.parse(config);
39
+ const { hours, priority, employeeIds } = parsed;
40
+ const minMinutes = hours * 60;
41
+
42
+ return {
43
+ compile(b) {
44
+ if (hours <= 0) return;
45
+
46
+ const employees = employeeIds
47
+ ? b.employees.filter((e) => employeeIds.includes(e.id))
48
+ : b.employees;
49
+
50
+ const weeks = splitIntoWeeks(b.days, parsed.weekStartsOn ?? b.weekStartsOn);
51
+
52
+ for (const emp of employees) {
53
+ for (const weekDays of weeks) {
54
+ const terms: Term[] = [];
55
+ for (const day of weekDays) {
56
+ for (const pattern of b.shiftPatterns) {
57
+ if (!b.canAssign(emp, pattern)) continue;
58
+ if (!b.patternAvailableOnDay(pattern, day)) continue;
59
+ terms.push({
60
+ var: b.assignment(emp.id, pattern.id, day),
61
+ coeff: b.patternDuration(pattern.id),
62
+ });
63
+ }
64
+ }
65
+
66
+ if (terms.length === 0) continue;
67
+
68
+ if (priority === "MANDATORY") {
69
+ b.addLinear(terms, ">=", minMinutes);
70
+ } else {
71
+ b.addSoftLinear(terms, ">=", minMinutes, priorityToPenalty(priority));
72
+ }
73
+ }
74
+ }
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,121 @@
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
+ export type MinRestBetweenShiftsConfig = z.infer<typeof MinRestBetweenShiftsSchema>;
7
+
8
+ const MinRestBetweenShiftsSchema = withScopes(
9
+ z.object({
10
+ hours: z.number().min(0),
11
+ priority: z.union([
12
+ z.literal("LOW"),
13
+ z.literal("MEDIUM"),
14
+ z.literal("HIGH"),
15
+ z.literal("MANDATORY"),
16
+ ]),
17
+ }),
18
+ { entities: ["employees", "roles", "skills"], times: [] },
19
+ );
20
+
21
+ /**
22
+ * Enforces a minimum rest period between any two shifts a person works.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const rule = createMinRestBetweenShiftsRule({
27
+ * hours: 10,
28
+ * priority: "MANDATORY",
29
+ * });
30
+ * builder = new ModelBuilder({ ...config, rules: [rule] });
31
+ * ```
32
+ */
33
+ export function createMinRestBetweenShiftsRule(
34
+ config: MinRestBetweenShiftsConfig,
35
+ ): CompilationRule {
36
+ const { hours, priority, employeeIds } = MinRestBetweenShiftsSchema.parse(config);
37
+ const minMinutes = hours * 60;
38
+
39
+ return {
40
+ compile(b) {
41
+ const employees = employeeIds
42
+ ? b.employees.filter((e) => employeeIds.includes(e.id))
43
+ : b.employees;
44
+
45
+ for (const emp of employees) {
46
+ for (let i = 0; i < b.days.length; i++) {
47
+ const day1 = b.days[i];
48
+ if (!day1) continue;
49
+
50
+ const checkDays: string[] = [day1];
51
+ const nextDay = b.days[i + 1];
52
+ if (nextDay) checkDays.push(nextDay);
53
+
54
+ for (const pattern1 of b.shiftPatterns) {
55
+ if (!b.canAssign(emp, pattern1)) continue;
56
+ if (!b.patternAvailableOnDay(pattern1, day1)) continue;
57
+ const end1 = b.endMinutes(pattern1, day1);
58
+
59
+ for (const day2 of checkDays) {
60
+ if (!day2) continue;
61
+ for (const pattern2 of b.shiftPatterns) {
62
+ if (!b.canAssign(emp, pattern2)) continue;
63
+ if (!b.patternAvailableOnDay(pattern2, day2)) continue;
64
+ if (day1 === day2 && pattern1.id === pattern2.id) continue;
65
+
66
+ const start2 = b.startMinutes(pattern2, day2);
67
+ const gap = start2 - end1;
68
+
69
+ if (gap >= 0 && gap < minMinutes) {
70
+ const var1 = b.assignment(emp.id, pattern1.id, day1);
71
+ const var2 = b.assignment(emp.id, pattern2.id, day2);
72
+
73
+ if (priority === "MANDATORY") {
74
+ b.addLinear(
75
+ [
76
+ { var: var1, coeff: 1 },
77
+ { var: var2, coeff: 1 },
78
+ ],
79
+ "<=",
80
+ 1,
81
+ );
82
+ } else {
83
+ const conflictVar = b.boolVar(
84
+ `rest_conflict_${emp.id}_${pattern1.id}_${day1}_${pattern2.id}_${day2}`,
85
+ );
86
+ b.addLinear(
87
+ [
88
+ { var: var1, coeff: 1 },
89
+ { var: var2, coeff: 1 },
90
+ { var: conflictVar, coeff: -1 },
91
+ ],
92
+ "<=",
93
+ 1,
94
+ );
95
+ b.addLinear(
96
+ [
97
+ { var: conflictVar, coeff: 1 },
98
+ { var: var1, coeff: -1 },
99
+ ],
100
+ "<=",
101
+ 0,
102
+ );
103
+ b.addLinear(
104
+ [
105
+ { var: conflictVar, coeff: 1 },
106
+ { var: var2, coeff: -1 },
107
+ ],
108
+ "<=",
109
+ 0,
110
+ );
111
+ b.addPenalty(conflictVar, priorityToPenalty(priority));
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ },
120
+ };
121
+ }
@@ -0,0 +1,49 @@
1
+ import {
2
+ createAssignTogetherRule,
3
+ createEmployeeAssignmentPriorityRule,
4
+ createLocationPreferenceRule,
5
+ createMaxConsecutiveDaysRule,
6
+ createMaxHoursDayRule,
7
+ createMaxHoursWeekRule,
8
+ createMaxShiftsDayRule,
9
+ createMinConsecutiveDaysRule,
10
+ createMinHoursDayRule,
11
+ createMinHoursWeekRule,
12
+ createMinRestBetweenShiftsRule,
13
+ createTimeOffRule,
14
+ } from "./index.js";
15
+ import type {
16
+ BuiltInCpsatRuleFactories,
17
+ CpsatRuleFactories,
18
+ CpsatRuleName,
19
+ } from "./rules.types.js";
20
+
21
+ export const builtInCpsatRuleFactories: BuiltInCpsatRuleFactories = {
22
+ "assign-together": createAssignTogetherRule,
23
+ "employee-assignment-priority": createEmployeeAssignmentPriorityRule,
24
+ "location-preference": createLocationPreferenceRule,
25
+ "max-consecutive-days": createMaxConsecutiveDaysRule,
26
+ "max-hours-day": createMaxHoursDayRule,
27
+ "max-hours-week": createMaxHoursWeekRule,
28
+ "max-shifts-day": createMaxShiftsDayRule,
29
+ "min-consecutive-days": createMinConsecutiveDaysRule,
30
+ "min-hours-day": createMinHoursDayRule,
31
+ "min-hours-week": createMinHoursWeekRule,
32
+ "min-rest-between-shifts": createMinRestBetweenShiftsRule,
33
+ "time-off": createTimeOffRule,
34
+ };
35
+
36
+ /**
37
+ * Creates a rule factory map, preventing overriding built-in rules.
38
+ */
39
+ export function createCpsatRuleFactory<F extends CpsatRuleFactories>(factories: F): F {
40
+ for (const name in factories) {
41
+ if (
42
+ name in builtInCpsatRuleFactories &&
43
+ factories[name] !== builtInCpsatRuleFactories[name as CpsatRuleName]
44
+ ) {
45
+ throw new Error(`Cannot override built-in CP-SAT rule "${name}" in custom factory`);
46
+ }
47
+ }
48
+ return factories;
49
+ }
@@ -0,0 +1,181 @@
1
+ import type { SchedulingEmployee } from "../types.js";
2
+ import {
3
+ normalizeScope,
4
+ specificity,
5
+ subtractIds,
6
+ timeScopeKey,
7
+ type EntityScope,
8
+ type RuleScope,
9
+ } from "./scoping.js";
10
+ import { builtInCpsatRuleFactories } from "./registry.js";
11
+ import type {
12
+ CpsatRuleConfigEntry,
13
+ CpsatRuleFactories,
14
+ CpsatRuleName,
15
+ CpsatRuleRegistry,
16
+ } from "./rules.types.js";
17
+ import type { CompilationRule } from "../model-builder.js";
18
+
19
+ type ResolvedRuleConfigEntry = {
20
+ name: CpsatRuleName;
21
+ config: CpsatRuleRegistry[CpsatRuleName];
22
+ };
23
+
24
+ type InternalEntry = {
25
+ index: number;
26
+ name: CpsatRuleName;
27
+ config: CpsatRuleRegistry[CpsatRuleName];
28
+ scope: RuleScope;
29
+ };
30
+
31
+ /**
32
+ * Gets the IDs that match an entity scope.
33
+ * All scope types are expanded to concrete IDs.
34
+ */
35
+ export function getEmployeeIdsForScope(
36
+ entity: EntityScope,
37
+ employees: SchedulingEmployee[],
38
+ ): string[] {
39
+ switch (entity.type) {
40
+ case "employees":
41
+ // Filter to only IDs that exist in the team
42
+ const validIds = new Set(employees.map((e) => e.id));
43
+ return entity.employeeIds.filter((id) => validIds.has(id));
44
+
45
+ case "roles":
46
+ // Find team members that have any of the specified roles
47
+ return employees
48
+ .filter((e) => e.roleIds.some((r) => entity.roleIds.includes(r)))
49
+ .map((e) => e.id);
50
+
51
+ case "skills":
52
+ // Find team members that have any of the specified skills
53
+ return employees
54
+ .filter((e) => e.skillIds?.some((s) => entity.skillIds.includes(s)))
55
+ .map((e) => e.id);
56
+
57
+ case "global":
58
+ default:
59
+ return employees.map((e) => e.id);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Rules that don't use standard scoping and should pass through unchanged.
65
+ */
66
+ const NON_SCOPED_RULES = new Set<string>(["assign-together"]);
67
+
68
+ /**
69
+ * Resolves overlapping scopes so that more specific scopes override broader ones.
70
+ *
71
+ * Resolution rules:
72
+ * 1. Rules in NON_SCOPED_RULES pass through unchanged (they handle their own logic)
73
+ * 2. Rules with different time scopes coexist (different time windows don't compete)
74
+ * 3. Within the same time scope, rules are sorted by specificity (person > role > skill > global)
75
+ * and then by insertion order (later wins for same specificity, i.e., last insert overrides)
76
+ * 4. More specific rules claim their IDs first; less specific rules get the remainder
77
+ * 5. All scope types (person, role, skill, global) are expanded to explicit IDs
78
+ */
79
+ export function resolveRuleScopes(
80
+ entries: CpsatRuleConfigEntry[],
81
+ employees: SchedulingEmployee[],
82
+ ): ResolvedRuleConfigEntry[] {
83
+ const resolved: ResolvedRuleConfigEntry[] = [];
84
+ const scopedEntries: Array<{ entry: CpsatRuleConfigEntry; index: number }> = [];
85
+
86
+ // Separate rules that use scoping from those that don't
87
+ entries.forEach((entry, index) => {
88
+ if (NON_SCOPED_RULES.has(entry.name)) {
89
+ // Pass through unchanged
90
+ resolved.push({ name: entry.name, config: entry.config });
91
+ } else {
92
+ scopedEntries.push({ entry, index });
93
+ }
94
+ });
95
+
96
+ if (scopedEntries.length === 0) {
97
+ return resolved;
98
+ }
99
+
100
+ // Group scoped entries by (ruleName, timeScopeKey) - different time scopes don't compete
101
+ const grouped = new Map<string, InternalEntry[]>();
102
+ scopedEntries.forEach(({ entry, index }) => {
103
+ const scope = normalizeScope(entry.config as Parameters<typeof normalizeScope>[0], employees);
104
+ const timeKey = timeScopeKey(scope.time);
105
+ const groupKey = `${entry.name}::${timeKey}`;
106
+ const list = grouped.get(groupKey) ?? [];
107
+ list.push({ index, name: entry.name, config: entry.config, scope });
108
+ grouped.set(groupKey, list);
109
+ });
110
+
111
+ for (const group of grouped.values()) {
112
+ // Sort by specificity (descending), then by insertion order (descending, later wins)
113
+ const sorted = group
114
+ .map((g) => ({
115
+ ...g,
116
+ specificity: specificity(g.scope.entity),
117
+ }))
118
+ .toSorted((a, b) => {
119
+ if (b.specificity !== a.specificity) {
120
+ return b.specificity - a.specificity;
121
+ }
122
+ // Later insertion wins (higher index first)
123
+ return b.index - a.index;
124
+ });
125
+
126
+ // Track assigned IDs - all scope types participate in claiming
127
+ const assignedEmployees = new Set<string>();
128
+
129
+ for (const entry of sorted) {
130
+ const { entity } = entry.scope;
131
+
132
+ // Get IDs for this scope (expands role/skill to concrete IDs)
133
+ const targetIds = getEmployeeIdsForScope(entity, employees);
134
+
135
+ // Only claim IDs not already assigned to a more specific rule
136
+ const remaining = subtractIds(targetIds, assignedEmployees);
137
+ if (remaining.length === 0) continue;
138
+
139
+ // Claim these IDs
140
+ remaining.forEach((id) => assignedEmployees.add(id));
141
+
142
+ // Emit resolved config with explicit IDs
143
+ // Remove roleIds/skillIds since we've expanded them to employeeIds
144
+ const {
145
+ roleIds: _roleIds,
146
+ skillIds: _skillIds,
147
+ ...configWithoutEntityScope
148
+ } = entry.config as Record<string, unknown>;
149
+ resolved.push({
150
+ name: entry.name,
151
+ config: {
152
+ ...configWithoutEntityScope,
153
+ employeeIds: remaining,
154
+ } as CpsatRuleRegistry[CpsatRuleName],
155
+ });
156
+ }
157
+ }
158
+
159
+ return resolved;
160
+ }
161
+
162
+ /**
163
+ * Builds CompilationRule instances from named configs, applying scoping resolution.
164
+ */
165
+ export function buildCpsatRules(
166
+ entries: CpsatRuleConfigEntry[],
167
+ employees: SchedulingEmployee[],
168
+ factories: CpsatRuleFactories = builtInCpsatRuleFactories,
169
+ ): CompilationRule[] {
170
+ if (entries.length === 0) return [];
171
+
172
+ const resolved = resolveRuleScopes(entries, employees);
173
+
174
+ return resolved.map((entry) => {
175
+ const factory = factories[entry.name];
176
+ if (!factory) {
177
+ throw new Error(`Unknown CP-SAT rule "${entry.name}"`);
178
+ }
179
+ return factory(entry.config as any);
180
+ });
181
+ }
@@ -0,0 +1,41 @@
1
+ import type { CompilationRule } from "../model-builder.js";
2
+
3
+ export type CreateCpsatRuleFunction<TConfig = any> = (config: TConfig) => CompilationRule;
4
+
5
+ // Registry of built-in rule names to their config types
6
+ export interface CpsatRuleRegistry {
7
+ "assign-together": import("./assign-together.js").AssignTogetherConfig;
8
+ "employee-assignment-priority": import("./employee-assignment-priority.js").EmployeeAssignmentPriorityConfig;
9
+ "location-preference": import("./location-preference.js").LocationPreferenceConfig;
10
+ "max-consecutive-days": import("./max-consecutive-days.js").MaxConsecutiveDaysConfig;
11
+ "max-hours-day": import("./max-hours-day.js").MaxHoursDayConfig;
12
+ "max-hours-week": import("./max-hours-week.js").MaxHoursWeekConfig;
13
+ "max-shifts-day": import("./max-shifts-day.js").MaxShiftsDayConfig;
14
+ "min-consecutive-days": import("./min-consecutive-days.js").MinConsecutiveDaysConfig;
15
+ "min-hours-day": import("./min-hours-day.js").MinHoursDayConfig;
16
+ "min-hours-week": import("./min-hours-week.js").MinHoursWeekConfig;
17
+ "min-rest-between-shifts": import("./min-rest-between-shifts.js").MinRestBetweenShiftsConfig;
18
+ "time-off": import("./time-off.js").TimeOffConfig;
19
+ }
20
+
21
+ export type CpsatRuleName = keyof CpsatRuleRegistry;
22
+
23
+ export type CpsatRuleFactories = {
24
+ [ruleName: string]: CreateCpsatRuleFunction<any>;
25
+ };
26
+
27
+ export type BuiltInCpsatRuleFactories = {
28
+ [K in CpsatRuleName]: CreateCpsatRuleFunction<CpsatRuleRegistry[K]>;
29
+ };
30
+
31
+ export type InferCpsatRuleConfig<T> =
32
+ T extends CreateCpsatRuleFunction<infer Config> ? Config : never;
33
+
34
+ export type CpsatRuleRegistryFromFactories<F extends CpsatRuleFactories> = {
35
+ [K in keyof F]: InferCpsatRuleConfig<F[K]>;
36
+ };
37
+
38
+ export type CpsatRuleConfigEntry<K extends CpsatRuleName = CpsatRuleName> = {
39
+ name: K;
40
+ config: CpsatRuleRegistry[K];
41
+ };