dabke 0.78.2 → 0.80.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 (129) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +68 -31
  3. package/dist/client.types.d.ts +58 -0
  4. package/dist/client.types.d.ts.map +1 -1
  5. package/dist/client.types.js.map +1 -1
  6. package/dist/cpsat/model-builder.d.ts +13 -2
  7. package/dist/cpsat/model-builder.d.ts.map +1 -1
  8. package/dist/cpsat/model-builder.js.map +1 -1
  9. package/dist/cpsat/response.d.ts +12 -3
  10. package/dist/cpsat/response.d.ts.map +1 -1
  11. package/dist/cpsat/response.js.map +1 -1
  12. package/dist/cpsat/rules/assign-together.d.ts +7 -0
  13. package/dist/cpsat/rules/assign-together.d.ts.map +1 -1
  14. package/dist/cpsat/rules/assign-together.js +1 -0
  15. package/dist/cpsat/rules/assign-together.js.map +1 -1
  16. package/dist/cpsat/rules/employee-assignment-priority.d.ts +11 -37
  17. package/dist/cpsat/rules/employee-assignment-priority.d.ts.map +1 -1
  18. package/dist/cpsat/rules/employee-assignment-priority.js +12 -104
  19. package/dist/cpsat/rules/employee-assignment-priority.js.map +1 -1
  20. package/dist/cpsat/rules/location-preference.d.ts +12 -10
  21. package/dist/cpsat/rules/location-preference.d.ts.map +1 -1
  22. package/dist/cpsat/rules/location-preference.js +16 -14
  23. package/dist/cpsat/rules/location-preference.js.map +1 -1
  24. package/dist/cpsat/rules/max-consecutive-days.d.ts +12 -13
  25. package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
  26. package/dist/cpsat/rules/max-consecutive-days.js +11 -12
  27. package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
  28. package/dist/cpsat/rules/max-hours-day.d.ts +12 -28
  29. package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
  30. package/dist/cpsat/rules/max-hours-day.js +12 -95
  31. package/dist/cpsat/rules/max-hours-day.js.map +1 -1
  32. package/dist/cpsat/rules/max-hours-week.d.ts +14 -34
  33. package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
  34. package/dist/cpsat/rules/max-hours-week.js +12 -103
  35. package/dist/cpsat/rules/max-hours-week.js.map +1 -1
  36. package/dist/cpsat/rules/max-shifts-day.d.ts +14 -39
  37. package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
  38. package/dist/cpsat/rules/max-shifts-day.js +14 -106
  39. package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
  40. package/dist/cpsat/rules/min-consecutive-days.d.ts +12 -13
  41. package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
  42. package/dist/cpsat/rules/min-consecutive-days.js +11 -12
  43. package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
  44. package/dist/cpsat/rules/min-hours-day.d.ts +12 -13
  45. package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
  46. package/dist/cpsat/rules/min-hours-day.js +11 -12
  47. package/dist/cpsat/rules/min-hours-day.js.map +1 -1
  48. package/dist/cpsat/rules/min-hours-week.d.ts +13 -13
  49. package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
  50. package/dist/cpsat/rules/min-hours-week.js +10 -14
  51. package/dist/cpsat/rules/min-hours-week.js.map +1 -1
  52. package/dist/cpsat/rules/min-rest-between-shifts.d.ts +13 -14
  53. package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
  54. package/dist/cpsat/rules/min-rest-between-shifts.js +11 -12
  55. package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
  56. package/dist/cpsat/rules/resolver.d.ts +2 -2
  57. package/dist/cpsat/rules/resolver.d.ts.map +1 -1
  58. package/dist/cpsat/rules/resolver.js +55 -30
  59. package/dist/cpsat/rules/resolver.js.map +1 -1
  60. package/dist/cpsat/rules/scope.types.d.ts +267 -0
  61. package/dist/cpsat/rules/scope.types.d.ts.map +1 -0
  62. package/dist/cpsat/rules/scope.types.js +325 -0
  63. package/dist/cpsat/rules/scope.types.js.map +1 -0
  64. package/dist/cpsat/rules/time-off.d.ts +21 -25
  65. package/dist/cpsat/rules/time-off.d.ts.map +1 -1
  66. package/dist/cpsat/rules/time-off.js +20 -110
  67. package/dist/cpsat/rules/time-off.js.map +1 -1
  68. package/dist/cpsat/semantic-time.d.ts +2 -0
  69. package/dist/cpsat/semantic-time.d.ts.map +1 -1
  70. package/dist/cpsat/semantic-time.js +2 -4
  71. package/dist/cpsat/semantic-time.js.map +1 -1
  72. package/dist/cpsat/types.d.ts +22 -6
  73. package/dist/cpsat/types.d.ts.map +1 -1
  74. package/dist/cpsat/utils.d.ts +1 -1
  75. package/dist/cpsat/utils.js +1 -1
  76. package/dist/cpsat/validation-reporter.js +1 -1
  77. package/dist/cpsat/validation-reporter.js.map +1 -1
  78. package/dist/datetime.utils.d.ts +14 -14
  79. package/dist/datetime.utils.d.ts.map +1 -1
  80. package/dist/datetime.utils.js +26 -27
  81. package/dist/datetime.utils.js.map +1 -1
  82. package/dist/index.d.ts +4 -3
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +2 -2
  85. package/dist/index.js.map +1 -1
  86. package/dist/llms.d.ts +1 -1
  87. package/dist/llms.d.ts.map +1 -1
  88. package/dist/llms.js +1 -1
  89. package/dist/llms.js.map +1 -1
  90. package/dist/testing/index.d.ts +1 -1
  91. package/dist/testing/index.js +1 -1
  92. package/dist/testing/solver-container.js +3 -1
  93. package/dist/testing/solver-container.js.map +1 -1
  94. package/dist/types.d.ts +18 -20
  95. package/dist/types.d.ts.map +1 -1
  96. package/llms.txt +516 -263
  97. package/package.json +25 -25
  98. package/src/client.types.ts +58 -0
  99. package/src/cpsat/model-builder.ts +19 -7
  100. package/src/cpsat/response.ts +12 -3
  101. package/src/cpsat/rules/assign-together.ts +7 -0
  102. package/src/cpsat/rules/employee-assignment-priority.ts +28 -128
  103. package/src/cpsat/rules/location-preference.ts +24 -17
  104. package/src/cpsat/rules/max-consecutive-days.ts +19 -15
  105. package/src/cpsat/rules/max-hours-day.ts +29 -119
  106. package/src/cpsat/rules/max-hours-week.ts +42 -135
  107. package/src/cpsat/rules/max-shifts-day.ts +31 -130
  108. package/src/cpsat/rules/min-consecutive-days.ts +19 -15
  109. package/src/cpsat/rules/min-hours-day.ts +19 -15
  110. package/src/cpsat/rules/min-hours-week.ts +28 -26
  111. package/src/cpsat/rules/min-rest-between-shifts.ts +21 -17
  112. package/src/cpsat/rules/resolver.ts +66 -45
  113. package/src/cpsat/rules/scope.types.ts +534 -0
  114. package/src/cpsat/rules/time-off.ts +48 -145
  115. package/src/cpsat/semantic-time.ts +10 -8
  116. package/src/cpsat/types.ts +22 -6
  117. package/src/cpsat/utils.ts +1 -1
  118. package/src/cpsat/validation-reporter.ts +1 -1
  119. package/src/datetime.utils.ts +27 -29
  120. package/src/index.ts +11 -7
  121. package/src/llms.ts +1 -1
  122. package/src/testing/index.ts +1 -1
  123. package/src/testing/solver-container.ts +3 -3
  124. package/src/types.ts +27 -31
  125. package/dist/cpsat/rules/scoping.d.ts +0 -129
  126. package/dist/cpsat/rules/scoping.d.ts.map +0 -1
  127. package/dist/cpsat/rules/scoping.js +0 -190
  128. package/dist/cpsat/rules/scoping.js.map +0 -1
  129. package/src/cpsat/rules/scoping.ts +0 -340
package/package.json CHANGED
@@ -1,41 +1,38 @@
1
1
  {
2
2
  "name": "dabke",
3
- "version": "0.78.2",
4
- "license": "MIT",
3
+ "version": "0.80.0",
5
4
  "description": "Scheduling library powered by constraint programming (CP-SAT)",
6
- "author": "Christian Klotz <hello@christianklotz.co.uk>",
7
- "homepage": "https://github.com/christianklotz/dabke#readme",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/christianklotz/dabke.git"
11
- },
12
- "bugs": {
13
- "url": "https://github.com/christianklotz/dabke/issues"
14
- },
15
5
  "keywords": [
16
- "scheduling",
17
- "staff-scheduling",
18
6
  "constraint-programming",
19
7
  "cp-sat",
20
- "workforce-management",
21
- "optimization",
22
8
  "operations-research",
23
- "or-tools"
9
+ "optimization",
10
+ "or-tools",
11
+ "scheduling",
12
+ "staff-scheduling",
13
+ "workforce-management"
24
14
  ],
25
- "type": "module",
26
- "sideEffects": false,
27
- "engines": {
28
- "node": ">=20"
15
+ "homepage": "https://github.com/christianklotz/dabke#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/christianklotz/dabke/issues"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Christian Klotz <hello@christianklotz.co.uk>",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/christianklotz/dabke.git"
29
24
  },
30
25
  "files": [
26
+ "CHANGELOG.md",
31
27
  "dist",
32
- "src",
33
- "solver",
34
- "llms.txt",
35
28
  "LICENSE",
29
+ "llms.txt",
36
30
  "README.md",
37
- "CHANGELOG.md"
31
+ "solver",
32
+ "src"
38
33
  ],
34
+ "type": "module",
35
+ "sideEffects": false,
39
36
  "main": "dist/index.js",
40
37
  "types": "dist/index.d.ts",
41
38
  "exports": {
@@ -72,5 +69,8 @@
72
69
  "commander": "^14.0.2",
73
70
  "typescript": "^5.9.3",
74
71
  "vitest": "^4.0.18"
72
+ },
73
+ "engines": {
74
+ "node": ">=20"
75
75
  }
76
- }
76
+ }
@@ -22,20 +22,78 @@ import type {
22
22
  // Types derived from Zod schemas
23
23
  // --------------------------------------------------------------------------
24
24
 
25
+ /**
26
+ * A single linear term in a constraint or objective.
27
+ *
28
+ * - `var` (required): variable name
29
+ * - `coeff` (required): integer coefficient
30
+ */
25
31
  export type SolverTerm = z.infer<typeof SolverTermSchema>;
26
32
 
33
+ /**
34
+ * A decision variable in the CP-SAT model.
35
+ *
36
+ * - `name` (required): unique variable identifier
37
+ * - `lb` (required): lower bound
38
+ * - `ub` (required): upper bound
39
+ * - `isBoolean` (optional): whether this is a boolean variable
40
+ * - `isInterval` (optional): whether this is an interval variable
41
+ * - `start`, `end`, `size`, `presenceVar` (optional): interval variable fields
42
+ */
27
43
  export type SolverVariable = z.infer<typeof SolverVariableSchema>;
28
44
 
45
+ /**
46
+ * A constraint in the CP-SAT model.
47
+ *
48
+ * - `name` (required): constraint identifier
49
+ * - `type` (required): constraint kind (e.g. "linear", "bool_and", "no_overlap")
50
+ * - Additional fields vary by constraint type
51
+ */
29
52
  export type SolverConstraint = z.infer<typeof SolverConstraintSchema>;
30
53
 
54
+ /**
55
+ * An optimization objective for the solver.
56
+ *
57
+ * - `terms` (required): linear terms to minimize/maximize
58
+ * - `minimize` (required): whether to minimize (true) or maximize (false)
59
+ */
31
60
  export type SolverObjective = z.infer<typeof SolverObjectiveSchema>;
32
61
 
62
+ /**
63
+ * The full request payload sent to the CP-SAT solver service.
64
+ *
65
+ * - `variables` (required): all decision variables
66
+ * - `constraints` (required): all constraints
67
+ * - `objective` (optional): optimization objective
68
+ * - `timeoutSeconds` (optional): solver time limit
69
+ */
33
70
  export type SolverRequest = z.infer<typeof SolverRequestSchema>;
34
71
 
72
+ /**
73
+ * The response payload returned by the CP-SAT solver service.
74
+ *
75
+ * - `status` (required): solve outcome (see {@link SolverStatus})
76
+ * - `values` (optional): variable assignments when a solution is found
77
+ * - `statistics` (optional): solve time, conflicts, branches
78
+ * - `softViolations` (optional): which soft constraints were violated
79
+ * - `error` (optional): error message on failure
80
+ * - `solutionInfo` (optional): solver diagnostic info
81
+ */
35
82
  export type SolverResponse = z.infer<typeof SolverResponseSchema>;
36
83
 
84
+ /**
85
+ * Solver outcome status.
86
+ *
87
+ * One of `"OPTIMAL"`, `"FEASIBLE"`, `"INFEASIBLE"`, `"TIMEOUT"`, or `"ERROR"`.
88
+ */
37
89
  export type SolverStatus = z.infer<typeof SolverStatusSchema>;
38
90
 
91
+ /**
92
+ * A soft constraint violation reported by the solver.
93
+ *
94
+ * - `constraintId` (required): which soft constraint was violated
95
+ * - `violationAmount` (required): magnitude of the violation
96
+ */
39
97
  export type SoftConstraintViolation = z.infer<typeof SoftConstraintViolationSchema>;
40
98
 
41
99
  // --------------------------------------------------------------------------
@@ -37,8 +37,17 @@ export interface RuleValidationContext {
37
37
  readonly shiftPatterns: ShiftPattern[];
38
38
  }
39
39
 
40
+ /**
41
+ * A rule that adds constraints or objectives to the solver model.
42
+ *
43
+ * Rules implement `compile` to emit solver constraints during model building,
44
+ * and optionally `validate` to check the solution after solving.
45
+ * Use the `create*Rule` functions to create built-in rules.
46
+ */
40
47
  export interface CompilationRule {
48
+ /** Emit constraints and objectives into the model builder. */
41
49
  compile(builder: ModelBuilder): void;
50
+ /** Validate the solved schedule and report violations. */
42
51
  validate?(
43
52
  assignments: ResolvedShiftAssignment[],
44
53
  reporter: ValidationReporter,
@@ -69,11 +78,13 @@ export interface CompilationResult {
69
78
  * ```
70
79
  */
71
80
  export interface ModelBuilderConfig extends ModelBuilderOptions {
81
+ /** Team members available for scheduling. */
72
82
  employees: SchedulingEmployee[];
83
+ /** Available shift patterns (time slots) that employees can be assigned to. */
73
84
  shiftPatterns: ShiftPattern[];
74
85
  /**
75
- * Defines when scheduling should occur. Can specify either a date range
76
- * (with optional day-of-week filtering) or a list of specific dates.
86
+ * Defines when scheduling should occur as a date range with optional
87
+ * `daysOfWeek` and `dates` filters that compose to narrow which days are included.
77
88
  */
78
89
  schedulingPeriod: SchedulingPeriod;
79
90
  coverage: CoverageRequirement[];
@@ -448,11 +459,12 @@ export class ModelBuilder {
448
459
  const covStart = timeOfDayToMinutes(cov.startTime);
449
460
  const covEnd = normalizeEndMinutes(covStart, timeOfDayToMinutes(cov.endTime));
450
461
  const covKey = cov.roleIds?.join(",") ?? cov.skillIds?.join(",") ?? "unknown";
451
- const coverageLabel = cov.roleIds && cov.roleIds.length > 0
452
- ? cov.roleIds.length === 1
453
- ? `role "${cov.roleIds[0]}"`
454
- : `role "${cov.roleIds.join(" or ")}"`
455
- : `skills [${cov.skillIds?.join(", ")}]`;
462
+ const coverageLabel =
463
+ cov.roleIds && cov.roleIds.length > 0
464
+ ? cov.roleIds.length === 1
465
+ ? `role "${cov.roleIds[0]}"`
466
+ : `role "${cov.roleIds.join(" or ")}"`
467
+ : `skills [${cov.skillIds?.join(", ")}]`;
456
468
  const coverageWindow = formatTimeRange(covStart, covEnd);
457
469
 
458
470
  const eligibleEmployees = this.employeesForCoverage(cov);
@@ -2,12 +2,13 @@ import type { SolverResponse } from "../client.types.js";
2
2
  import type { ShiftPattern } from "./types.js";
3
3
  import type { TimeOfDay } from "../types.js";
4
4
 
5
- /**
6
- * A shift assignment extracted from the solver response.
7
- */
5
+ /** A raw assignment from the solver: which employee works which shift on which day. */
8
6
  export interface ShiftAssignment {
7
+ /** The assigned employee's ID. */
9
8
  employeeId: string;
9
+ /** The shift pattern this employee is assigned to. */
10
10
  shiftPatternId: string;
11
+ /** The date of the assignment (YYYY-MM-DD). */
11
12
  day: string;
12
13
  }
13
14
 
@@ -15,9 +16,13 @@ export interface ShiftAssignment {
15
16
  * A shift assignment with resolved times.
16
17
  */
17
18
  export interface ResolvedShiftAssignment {
19
+ /** The assigned employee's ID. */
18
20
  employeeId: string;
21
+ /** The date of the assignment (YYYY-MM-DD). */
19
22
  day: string;
23
+ /** When the shift starts. */
20
24
  startTime: TimeOfDay;
25
+ /** When the shift ends. */
21
26
  endTime: TimeOfDay;
22
27
  }
23
28
 
@@ -25,9 +30,13 @@ export interface ResolvedShiftAssignment {
25
30
  * Parsed solver result with assignments and metadata.
26
31
  */
27
32
  export interface SolverResult {
33
+ /** The solver outcome: OPTIMAL, FEASIBLE, INFEASIBLE, TIMEOUT, or ERROR. */
28
34
  status: SolverResponse["status"];
35
+ /** The shift assignments extracted from the solution. */
29
36
  assignments: ShiftAssignment[];
37
+ /** Solver performance statistics (branches, conflicts, solve time). */
30
38
  statistics?: SolverResponse["statistics"];
39
+ /** Error message if the solver returned an error status. */
31
40
  error?: string;
32
41
  }
33
42
 
@@ -17,12 +17,19 @@ const AssignTogetherSchema = z.object({
17
17
  ]),
18
18
  });
19
19
 
20
+ /**
21
+ * Configuration for {@link createAssignTogetherRule}.
22
+ *
23
+ * - `groupEmployeeIds` (required): employee IDs to assign together (at least two, must be unique)
24
+ * - `priority` (required): how strictly the solver enforces this rule
25
+ */
20
26
  export type AssignTogetherConfig = z.infer<typeof AssignTogetherSchema>;
21
27
 
22
28
  /**
23
29
  * Encourages or enforces that team members in the group work the same shift patterns on a day.
24
30
  * For each pair of team members in the group, ensures they are assigned to the same shifts.
25
31
  *
32
+ * @param config - See {@link AssignTogetherConfig}
26
33
  * @example
27
34
  * ```ts
28
35
  * const rule = createAssignTogetherRule({
@@ -1,27 +1,36 @@
1
1
  import * as z from "zod";
2
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({
3
+ import { OBJECTIVE_WEIGHTS } from "../utils.js";
4
+ import {
5
+ entityScope,
6
+ timeScope,
7
+ parseEntityScope,
8
+ parseTimeScope,
9
+ resolveEmployeesFromScope,
10
+ resolveActiveDaysFromScope,
11
+ } from "./scope.types.js";
12
+
13
+ const EmployeeAssignmentPrioritySchema = z
14
+ .object({
9
15
  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
+ .and(entityScope(["employees", "roles", "skills"]))
18
+ .and(timeScope(["dateRange", "specificDates", "dayOfWeek", "recurring"]));
16
19
 
20
+ /**
21
+ * Configuration for {@link createEmployeeAssignmentPriorityRule}.
22
+ *
23
+ * - `preference` (required): `"high"` to prefer assigning or `"low"` to avoid assigning
24
+ *
25
+ * Entity scoping (at most one): `employeeIds`, `roleIds`, `skillIds`
26
+ * Time scoping (at most one, optional): `dateRange`, `specificDates`, `dayOfWeek`, `recurringPeriods`
27
+ */
17
28
  export type EmployeeAssignmentPriorityConfig = z.infer<typeof EmployeeAssignmentPrioritySchema>;
18
29
 
19
30
  /**
20
31
  * Adds objective weight to prefer or avoid assigning team members.
21
32
  *
22
- * Supports entity scoping (people, roles, skills) and time scoping
23
- * (date ranges, specific dates, days of week, recurring periods).
24
- *
33
+ * @param config - See {@link EmployeeAssignmentPriorityConfig}
25
34
  * @example Prefer specific team members
26
35
  * ```ts
27
36
  * createEmployeeAssignmentPriorityRule({
@@ -38,27 +47,19 @@ export type EmployeeAssignmentPriorityConfig = z.infer<typeof EmployeeAssignment
38
47
  * preference: "low",
39
48
  * });
40
49
  * ```
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
50
  */
51
51
  export function createEmployeeAssignmentPriorityRule(
52
52
  config: EmployeeAssignmentPriorityConfig,
53
53
  ): CompilationRule {
54
54
  const parsed = EmployeeAssignmentPrioritySchema.parse(config);
55
55
  const { preference } = parsed;
56
+ const entityScopeValue = parseEntityScope(parsed);
57
+ const timeScopeValue = parseTimeScope(parsed);
56
58
 
57
59
  return {
58
60
  compile(b) {
59
- const scope = normalizeScope(parsed, b.employees);
60
- const targetEmployees = resolveEmployees(scope, b.employees);
61
- const activeDays = resolveActiveDays(scope, b.days);
61
+ const targetEmployees = resolveEmployeesFromScope(entityScopeValue, b.employees);
62
+ const activeDays = resolveActiveDaysFromScope(timeScopeValue, b.days);
62
63
 
63
64
  if (targetEmployees.length === 0 || activeDays.length === 0) return;
64
65
 
@@ -79,104 +80,3 @@ export function createEmployeeAssignmentPriorityRule(
79
80
  },
80
81
  };
81
82
  }
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
- }
@@ -2,10 +2,10 @@ import * as z from "zod";
2
2
  import type { CompilationRule } from "../model-builder.js";
3
3
  import type { Priority } from "../types.js";
4
4
  import { OBJECTIVE_WEIGHTS } from "../utils.js";
5
- import { withScopes } from "./scoping.js";
5
+ import { entityScope, parseEntityScope, resolveEmployeesFromScope } from "./scope.types.js";
6
6
 
7
- const LocationPreferenceSchema = withScopes(
8
- z.object({
7
+ const LocationPreferenceSchema = z
8
+ .object({
9
9
  locationId: z.string(),
10
10
  priority: z.union([
11
11
  z.literal("LOW"),
@@ -13,43 +13,50 @@ const LocationPreferenceSchema = withScopes(
13
13
  z.literal("HIGH"),
14
14
  z.literal("MANDATORY"),
15
15
  ]),
16
- }),
17
- { entities: ["employees", "roles", "skills"], times: [] },
18
- );
16
+ })
17
+ .and(entityScope(["employees", "roles", "skills"]));
19
18
 
19
+ /**
20
+ * Configuration for {@link createLocationPreferenceRule}.
21
+ *
22
+ * - `locationId` (required): the location ID to prefer for matching shift patterns
23
+ * - `priority` (required): how strongly to prefer this location
24
+ *
25
+ * Entity scoping (at most one): `employeeIds`, `roleIds`, `skillIds`
26
+ */
20
27
  export type LocationPreferenceConfig = z.infer<typeof LocationPreferenceSchema>;
21
28
 
22
29
  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
30
+ LOW: OBJECTIVE_WEIGHTS.FAIRNESS,
31
+ MEDIUM: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE,
32
+ HIGH: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE * 2.5,
33
+ MANDATORY: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE * 5,
27
34
  };
28
35
 
29
36
  /**
30
37
  * Prefers assigning a person to shift patterns matching a specific location.
31
38
  *
39
+ * @param config - See {@link LocationPreferenceConfig}
32
40
  * @example
33
41
  * ```ts
34
- * const rule = createLocationPreferenceRule({
42
+ * createLocationPreferenceRule({
35
43
  * locationId: "terrace",
36
44
  * priority: "HIGH",
37
45
  * employeeIds: ["alice"],
38
46
  * });
39
- * builder = new ModelBuilder({ ...config, rules: [rule] });
40
47
  * ```
41
48
  */
42
49
  export function createLocationPreferenceRule(config: LocationPreferenceConfig): CompilationRule {
43
- const { locationId, priority, employeeIds } = LocationPreferenceSchema.parse(config);
44
- const weight = PRIORITY_WEIGHTS[priority] ?? 0;
50
+ const parsed = LocationPreferenceSchema.parse(config);
51
+ const scope = parseEntityScope(parsed);
52
+ const { locationId } = parsed;
53
+ const weight = PRIORITY_WEIGHTS[parsed.priority] ?? 0;
45
54
 
46
55
  return {
47
56
  compile(b) {
48
57
  if (weight === 0) return;
49
58
 
50
- const employees = employeeIds
51
- ? b.employees.filter((e) => employeeIds.includes(e.id))
52
- : b.employees;
59
+ const employees = resolveEmployeesFromScope(scope, b.employees);
53
60
 
54
61
  for (const emp of employees) {
55
62
  for (const pattern of b.shiftPatterns) {
@@ -1,10 +1,10 @@
1
1
  import * as z from "zod";
2
2
  import type { CompilationRule } from "../model-builder.js";
3
3
  import { priorityToPenalty } from "../utils.js";
4
- import { withScopes } from "./scoping.js";
4
+ import { entityScope, parseEntityScope, resolveEmployeesFromScope } from "./scope.types.js";
5
5
 
6
- const MaxConsecutiveDaysSchema = withScopes(
7
- z.object({
6
+ const MaxConsecutiveDaysSchema = z
7
+ .object({
8
8
  days: z.number().min(0),
9
9
  priority: z.union([
10
10
  z.literal("LOW"),
@@ -12,33 +12,37 @@ const MaxConsecutiveDaysSchema = withScopes(
12
12
  z.literal("HIGH"),
13
13
  z.literal("MANDATORY"),
14
14
  ]),
15
- }),
16
- { entities: ["employees", "roles", "skills"], times: [] },
17
- );
15
+ })
16
+ .and(entityScope(["employees", "roles", "skills"]));
18
17
 
18
+ /**
19
+ * Configuration for {@link createMaxConsecutiveDaysRule}.
20
+ *
21
+ * - `days` (required): maximum consecutive days allowed
22
+ * - `priority` (required): how strictly the solver enforces this rule
23
+ *
24
+ * Entity scoping (at most one): `employeeIds`, `roleIds`, `skillIds`
25
+ */
19
26
  export type MaxConsecutiveDaysConfig = z.infer<typeof MaxConsecutiveDaysSchema>;
20
27
 
21
28
  /**
22
29
  * Limits how many consecutive days a person can be assigned.
23
30
  *
31
+ * @param config - See {@link MaxConsecutiveDaysConfig}
24
32
  * @example
25
33
  * ```ts
26
- * const rule = createMaxConsecutiveDaysRule({
27
- * days: 5,
28
- * priority: "MANDATORY",
29
- * });
30
- * builder = new ModelBuilder({ ...config, rules: [rule] });
34
+ * createMaxConsecutiveDaysRule({ days: 5, priority: "MANDATORY" });
31
35
  * ```
32
36
  */
33
37
  export function createMaxConsecutiveDaysRule(config: MaxConsecutiveDaysConfig): CompilationRule {
34
- const { days, priority, employeeIds } = MaxConsecutiveDaysSchema.parse(config);
38
+ const parsed = MaxConsecutiveDaysSchema.parse(config);
39
+ const scope = parseEntityScope(parsed);
40
+ const { days, priority } = parsed;
35
41
  const windowSize = days + 1;
36
42
 
37
43
  return {
38
44
  compile(b) {
39
- const employees = employeeIds
40
- ? b.employees.filter((e) => employeeIds.includes(e.id))
41
- : b.employees;
45
+ const employees = resolveEmployeesFromScope(scope, b.employees);
42
46
 
43
47
  for (const emp of employees) {
44
48
  for (let i = 0; i <= b.days.length - windowSize; i++) {