dabke 0.83.0 → 0.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +22 -27
- package/dist/client.d.ts +39 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/client.schemas.d.ts +118 -18
- package/dist/client.schemas.d.ts.map +1 -1
- package/dist/client.schemas.js +110 -6
- package/dist/client.schemas.js.map +1 -1
- package/dist/client.types.d.ts +63 -7
- package/dist/client.types.d.ts.map +1 -1
- package/dist/client.types.js.map +1 -1
- package/dist/cpsat/cost.d.ts +5 -2
- package/dist/cpsat/cost.d.ts.map +1 -1
- package/dist/cpsat/cost.js +13 -6
- package/dist/cpsat/cost.js.map +1 -1
- package/dist/cpsat/model-builder.d.ts +42 -61
- package/dist/cpsat/model-builder.d.ts.map +1 -1
- package/dist/cpsat/model-builder.js +451 -56
- package/dist/cpsat/model-builder.js.map +1 -1
- package/dist/cpsat/response.d.ts +19 -7
- package/dist/cpsat/response.d.ts.map +1 -1
- package/dist/cpsat/response.js +48 -9
- package/dist/cpsat/response.js.map +1 -1
- package/dist/cpsat/rule-descriptor.d.ts +250 -0
- package/dist/cpsat/rule-descriptor.d.ts.map +1 -0
- package/dist/cpsat/rule-descriptor.js +51 -0
- package/dist/cpsat/rule-descriptor.js.map +1 -0
- package/dist/cpsat/rules/artifacts.d.ts +9 -0
- package/dist/cpsat/rules/artifacts.d.ts.map +1 -0
- package/dist/cpsat/rules/artifacts.js +31 -0
- package/dist/cpsat/rules/artifacts.js.map +1 -0
- package/dist/cpsat/rules/assign-together.d.ts +18 -20
- package/dist/cpsat/rules/assign-together.d.ts.map +1 -1
- package/dist/cpsat/rules/assign-together.js +90 -54
- package/dist/cpsat/rules/assign-together.js.map +1 -1
- package/dist/cpsat/rules/assigned-day.d.ts +12 -0
- package/dist/cpsat/rules/assigned-day.d.ts.map +1 -0
- package/dist/cpsat/rules/assigned-day.js +43 -0
- package/dist/cpsat/rules/assigned-day.js.map +1 -0
- package/dist/cpsat/rules/assignment-priority.d.ts +15 -29
- package/dist/cpsat/rules/assignment-priority.d.ts.map +1 -1
- package/dist/cpsat/rules/assignment-priority.js +47 -50
- package/dist/cpsat/rules/assignment-priority.js.map +1 -1
- package/dist/cpsat/rules/assignment-terms.d.ts +6 -0
- package/dist/cpsat/rules/assignment-terms.d.ts.map +1 -0
- package/dist/cpsat/rules/assignment-terms.js +19 -0
- package/dist/cpsat/rules/assignment-terms.js.map +1 -0
- package/dist/cpsat/rules/concurrent-intervals.d.ts +19 -0
- package/dist/cpsat/rules/concurrent-intervals.d.ts.map +1 -0
- package/dist/cpsat/rules/concurrent-intervals.js +57 -0
- package/dist/cpsat/rules/concurrent-intervals.js.map +1 -0
- package/dist/cpsat/rules/cost-utils.d.ts +1 -3
- package/dist/cpsat/rules/cost-utils.d.ts.map +1 -1
- package/dist/cpsat/rules/cost-utils.js +0 -7
- package/dist/cpsat/rules/cost-utils.js.map +1 -1
- package/dist/cpsat/rules/day-cost-multiplier.d.ts +10 -10
- package/dist/cpsat/rules/day-cost-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/day-cost-multiplier.js +88 -77
- package/dist/cpsat/rules/day-cost-multiplier.js.map +1 -1
- package/dist/cpsat/rules/day-cost-surcharge.d.ts +10 -10
- package/dist/cpsat/rules/day-cost-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/day-cost-surcharge.js +76 -65
- package/dist/cpsat/rules/day-cost-surcharge.js.map +1 -1
- package/dist/cpsat/rules/index.d.ts +30 -24
- package/dist/cpsat/rules/index.d.ts.map +1 -1
- package/dist/cpsat/rules/index.js +30 -24
- package/dist/cpsat/rules/index.js.map +1 -1
- package/dist/cpsat/rules/location-preference.d.ts +14 -21
- package/dist/cpsat/rules/location-preference.d.ts.map +1 -1
- package/dist/cpsat/rules/location-preference.js +45 -45
- package/dist/cpsat/rules/location-preference.js.map +1 -1
- package/dist/cpsat/rules/max-concurrent-assignments.d.ts +46 -0
- package/dist/cpsat/rules/max-concurrent-assignments.d.ts.map +1 -0
- package/dist/cpsat/rules/max-concurrent-assignments.js +121 -0
- package/dist/cpsat/rules/max-concurrent-assignments.js.map +1 -0
- package/dist/cpsat/rules/max-consecutive-days.d.ts +18 -17
- package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/max-consecutive-days.js +67 -66
- package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/max-days-of-week-per-period.d.ts +45 -0
- package/dist/cpsat/rules/max-days-of-week-per-period.d.ts.map +1 -0
- package/dist/cpsat/rules/max-days-of-week-per-period.js +95 -0
- package/dist/cpsat/rules/max-days-of-week-per-period.js.map +1 -0
- package/dist/cpsat/rules/max-days-week.d.ts +17 -31
- package/dist/cpsat/rules/max-days-week.d.ts.map +1 -1
- package/dist/cpsat/rules/max-days-week.js +69 -81
- package/dist/cpsat/rules/max-days-week.js.map +1 -1
- package/dist/cpsat/rules/max-hours-day.d.ts +14 -31
- package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-day.js +56 -72
- package/dist/cpsat/rules/max-hours-day.js.map +1 -1
- package/dist/cpsat/rules/max-hours-week.d.ts +15 -31
- package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-week.js +60 -75
- package/dist/cpsat/rules/max-hours-week.js.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.d.ts +17 -33
- package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.js +57 -73
- package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.d.ts +18 -18
- package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.js +123 -94
- package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/min-days-of-week-per-period.d.ts +35 -0
- package/dist/cpsat/rules/min-days-of-week-per-period.d.ts.map +1 -0
- package/dist/cpsat/rules/min-days-of-week-per-period.js +108 -0
- package/dist/cpsat/rules/min-days-of-week-per-period.js.map +1 -0
- package/dist/cpsat/rules/min-days-week.d.ts +13 -26
- package/dist/cpsat/rules/min-days-week.d.ts.map +1 -1
- package/dist/cpsat/rules/min-days-week.js +85 -74
- package/dist/cpsat/rules/min-days-week.js.map +1 -1
- package/dist/cpsat/rules/min-hours-day.d.ts +14 -17
- package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-day.js +72 -54
- package/dist/cpsat/rules/min-hours-day.js.map +1 -1
- package/dist/cpsat/rules/min-hours-week.d.ts +15 -18
- package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-week.js +75 -57
- package/dist/cpsat/rules/min-hours-week.js.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.d.ts +14 -17
- package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.js +148 -138
- package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
- package/dist/cpsat/rules/minimize-cost.d.ts +2 -18
- package/dist/cpsat/rules/minimize-cost.d.ts.map +1 -1
- package/dist/cpsat/rules/minimize-cost.js +151 -163
- package/dist/cpsat/rules/minimize-cost.js.map +1 -1
- package/dist/cpsat/rules/must-assign.d.ts +5 -42
- package/dist/cpsat/rules/must-assign.d.ts.map +1 -1
- package/dist/cpsat/rules/must-assign.js +43 -75
- package/dist/cpsat/rules/must-assign.js.map +1 -1
- package/dist/cpsat/rules/overtime-daily-multiplier.d.ts +6 -10
- package/dist/cpsat/rules/overtime-daily-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-daily-multiplier.js +118 -112
- package/dist/cpsat/rules/overtime-daily-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-daily-surcharge.d.ts +6 -11
- package/dist/cpsat/rules/overtime-daily-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-daily-surcharge.js +104 -99
- package/dist/cpsat/rules/overtime-daily-surcharge.js.map +1 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts +9 -21
- package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.js +181 -199
- package/dist/cpsat/rules/overtime-tiered-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts +13 -12
- package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-multiplier.js +143 -143
- package/dist/cpsat/rules/overtime-weekly-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts +7 -11
- package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-surcharge.js +124 -119
- package/dist/cpsat/rules/overtime-weekly-surcharge.js.map +1 -1
- package/dist/cpsat/rules/pattern-eligibility.d.ts +6 -0
- package/dist/cpsat/rules/pattern-eligibility.d.ts.map +1 -0
- package/dist/cpsat/rules/pattern-eligibility.js +16 -0
- package/dist/cpsat/rules/pattern-eligibility.js.map +1 -0
- package/dist/cpsat/rules/pattern-time.d.ts +5 -0
- package/dist/cpsat/rules/pattern-time.d.ts.map +1 -0
- package/dist/cpsat/rules/pattern-time.js +20 -0
- package/dist/cpsat/rules/pattern-time.js.map +1 -0
- package/dist/cpsat/rules/registry.d.ts +217 -30
- package/dist/cpsat/rules/registry.d.ts.map +1 -1
- package/dist/cpsat/rules/registry.js +98 -34
- package/dist/cpsat/rules/registry.js.map +1 -1
- package/dist/cpsat/rules/resolver.d.ts +9 -7
- package/dist/cpsat/rules/resolver.d.ts.map +1 -1
- package/dist/cpsat/rules/resolver.js +19 -18
- package/dist/cpsat/rules/resolver.js.map +1 -1
- package/dist/cpsat/rules/role-preference.d.ts +24 -0
- package/dist/cpsat/rules/role-preference.d.ts.map +1 -0
- package/dist/cpsat/rules/role-preference.js +72 -0
- package/dist/cpsat/rules/role-preference.js.map +1 -0
- package/dist/cpsat/rules/rules.types.d.ts +90 -17
- package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
- package/dist/cpsat/rules/scope.types.d.ts +24 -11
- package/dist/cpsat/rules/scope.types.d.ts.map +1 -1
- package/dist/cpsat/rules/scope.types.js +14 -19
- package/dist/cpsat/rules/scope.types.js.map +1 -1
- package/dist/cpsat/rules/target-days-week.d.ts +31 -0
- package/dist/cpsat/rules/target-days-week.d.ts.map +1 -0
- package/dist/cpsat/rules/target-days-week.js +80 -0
- package/dist/cpsat/rules/target-days-week.js.map +1 -0
- package/dist/cpsat/rules/target-peak-concurrent-assignments.d.ts +33 -0
- package/dist/cpsat/rules/target-peak-concurrent-assignments.d.ts.map +1 -0
- package/dist/cpsat/rules/target-peak-concurrent-assignments.js +146 -0
- package/dist/cpsat/rules/target-peak-concurrent-assignments.js.map +1 -0
- package/dist/cpsat/rules/time-cost-surcharge.d.ts +15 -14
- package/dist/cpsat/rules/time-cost-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/time-cost-surcharge.js +82 -96
- package/dist/cpsat/rules/time-cost-surcharge.js.map +1 -1
- package/dist/cpsat/rules/time-off.d.ts +21 -57
- package/dist/cpsat/rules/time-off.d.ts.map +1 -1
- package/dist/cpsat/rules/time-off.js +114 -127
- package/dist/cpsat/rules/time-off.js.map +1 -1
- package/dist/cpsat/rules/variables.d.ts +4 -0
- package/dist/cpsat/rules/variables.d.ts.map +1 -0
- package/dist/cpsat/rules/variables.js +10 -0
- package/dist/cpsat/rules/variables.js.map +1 -0
- package/dist/cpsat/rules.d.ts +20 -3
- package/dist/cpsat/rules.d.ts.map +1 -1
- package/dist/cpsat/rules.js +19 -2
- package/dist/cpsat/rules.js.map +1 -1
- package/dist/cpsat/semantic-time.d.ts +7 -6
- package/dist/cpsat/semantic-time.d.ts.map +1 -1
- package/dist/cpsat/semantic-time.js +15 -28
- package/dist/cpsat/semantic-time.js.map +1 -1
- package/dist/cpsat/types.d.ts +47 -4
- package/dist/cpsat/types.d.ts.map +1 -1
- package/dist/cpsat/types.js +24 -1
- package/dist/cpsat/types.js.map +1 -1
- package/dist/cpsat/utils.d.ts +23 -6
- package/dist/cpsat/utils.d.ts.map +1 -1
- package/dist/cpsat/utils.js +76 -15
- package/dist/cpsat/utils.js.map +1 -1
- package/dist/cpsat/validation-reporter.d.ts +6 -2
- package/dist/cpsat/validation-reporter.d.ts.map +1 -1
- package/dist/cpsat/validation-reporter.js +122 -10
- package/dist/cpsat/validation-reporter.js.map +1 -1
- package/dist/cpsat/validation.types.d.ts +21 -10
- package/dist/cpsat/validation.types.d.ts.map +1 -1
- package/dist/datetime.utils.d.ts +7 -13
- package/dist/datetime.utils.d.ts.map +1 -1
- package/dist/datetime.utils.js +29 -36
- package/dist/datetime.utils.js.map +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/schedule/cost.d.ts +48 -60
- package/dist/schedule/cost.d.ts.map +1 -1
- package/dist/schedule/cost.js +15 -51
- package/dist/schedule/cost.js.map +1 -1
- package/dist/schedule/coverage.d.ts +57 -28
- package/dist/schedule/coverage.d.ts.map +1 -1
- package/dist/schedule/coverage.js +24 -1
- package/dist/schedule/coverage.js.map +1 -1
- package/dist/schedule/definition.d.ts +138 -19
- package/dist/schedule/definition.d.ts.map +1 -1
- package/dist/schedule/definition.js +99 -90
- package/dist/schedule/definition.js.map +1 -1
- package/dist/schedule/index.d.ts +4 -4
- package/dist/schedule/index.d.ts.map +1 -1
- package/dist/schedule/index.js +2 -2
- package/dist/schedule/index.js.map +1 -1
- package/dist/schedule/rules.d.ts +438 -113
- package/dist/schedule/rules.d.ts.map +1 -1
- package/dist/schedule/rules.js +310 -108
- package/dist/schedule/rules.js.map +1 -1
- package/dist/schedule/shift-patterns.d.ts +0 -17
- package/dist/schedule/shift-patterns.d.ts.map +1 -1
- package/dist/schedule/shift-patterns.js +0 -17
- package/dist/schedule/shift-patterns.js.map +1 -1
- package/dist/schedule/time-periods.d.ts +37 -27
- package/dist/schedule/time-periods.d.ts.map +1 -1
- package/dist/schedule/time-periods.js +37 -27
- package/dist/schedule/time-periods.js.map +1 -1
- package/dist/types.d.ts +67 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +64 -0
- package/dist/types.js.map +1 -1
- package/package.json +8 -9
- package/solver/src/solver/models.py +41 -4
- package/solver/src/solver/solver.py +546 -157
- package/solver/tests/test_solver.py +363 -7
- package/src/client.schemas.ts +127 -10
- package/src/client.ts +3 -0
- package/src/client.types.ts +73 -7
- package/src/cpsat/cost.ts +17 -7
- package/src/cpsat/model-builder.ts +614 -129
- package/src/cpsat/response.ts +57 -13
- package/src/cpsat/rule-descriptor.ts +311 -0
- package/src/cpsat/rules/artifacts.ts +56 -0
- package/src/cpsat/rules/assign-together.ts +104 -69
- package/src/cpsat/rules/assigned-day.ts +70 -0
- package/src/cpsat/rules/assignment-priority.ts +54 -58
- package/src/cpsat/rules/assignment-terms.ts +43 -0
- package/src/cpsat/rules/concurrent-intervals.ts +93 -0
- package/src/cpsat/rules/cost-utils.ts +1 -9
- package/src/cpsat/rules/day-cost-multiplier.ts +101 -81
- package/src/cpsat/rules/day-cost-surcharge.ts +91 -70
- package/src/cpsat/rules/index.ts +30 -24
- package/src/cpsat/rules/location-preference.ts +47 -52
- package/src/cpsat/rules/max-concurrent-assignments.ts +170 -0
- package/src/cpsat/rules/max-consecutive-days.ts +74 -91
- package/src/cpsat/rules/max-days-of-week-per-period.ts +127 -0
- package/src/cpsat/rules/max-days-week.ts +89 -115
- package/src/cpsat/rules/max-hours-day.ts +64 -84
- package/src/cpsat/rules/max-hours-week.ts +74 -90
- package/src/cpsat/rules/max-shifts-day.ts +65 -85
- package/src/cpsat/rules/min-consecutive-days.ts +161 -127
- package/src/cpsat/rules/min-days-of-week-per-period.ts +144 -0
- package/src/cpsat/rules/min-days-week.ts +93 -91
- package/src/cpsat/rules/min-hours-day.ts +79 -58
- package/src/cpsat/rules/min-hours-week.ts +87 -62
- package/src/cpsat/rules/min-rest-between-shifts.ts +191 -178
- package/src/cpsat/rules/minimize-cost.ts +175 -188
- package/src/cpsat/rules/must-assign.ts +44 -81
- package/src/cpsat/rules/overtime-daily-multiplier.ts +158 -140
- package/src/cpsat/rules/overtime-daily-surcharge.ts +141 -126
- package/src/cpsat/rules/overtime-tiered-multiplier.ts +234 -253
- package/src/cpsat/rules/overtime-weekly-multiplier.ts +183 -175
- package/src/cpsat/rules/overtime-weekly-surcharge.ts +161 -150
- package/src/cpsat/rules/pattern-eligibility.ts +26 -0
- package/src/cpsat/rules/pattern-time.ts +30 -0
- package/src/cpsat/rules/registry.ts +140 -62
- package/src/cpsat/rules/resolver.ts +58 -30
- package/src/cpsat/rules/role-preference.ts +92 -0
- package/src/cpsat/rules/rules.types.ts +89 -17
- package/src/cpsat/rules/scope.types.ts +31 -26
- package/src/cpsat/rules/target-days-week.ts +105 -0
- package/src/cpsat/rules/target-peak-concurrent-assignments.ts +212 -0
- package/src/cpsat/rules/time-cost-surcharge.ts +116 -122
- package/src/cpsat/rules/time-off.ts +154 -163
- package/src/cpsat/rules/variables.ts +11 -0
- package/src/cpsat/rules.ts +43 -17
- package/src/cpsat/semantic-time.ts +29 -43
- package/src/cpsat/types.ts +53 -4
- package/src/cpsat/utils.ts +92 -20
- package/src/cpsat/validation-reporter.ts +170 -19
- package/src/cpsat/validation.types.ts +22 -11
- package/src/datetime.utils.ts +45 -44
- package/src/index.ts +64 -12
- package/src/schedule/cost.ts +40 -61
- package/src/schedule/coverage.ts +57 -28
- package/src/schedule/definition.ts +565 -76
- package/src/schedule/index.ts +23 -3
- package/src/schedule/rules.ts +631 -132
- package/src/schedule/shift-patterns.ts +0 -17
- package/src/schedule/time-periods.ts +37 -27
- package/src/types.ts +114 -2
package/src/cpsat/response.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SolverResponse } from "../client.types.js";
|
|
2
2
|
import type { ShiftPattern } from "./types.js";
|
|
3
|
-
import type
|
|
3
|
+
import { isDateString, type DateString, type TimeOfDay } from "../types.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* A raw assignment from the solver: which member works which shift on which day.
|
|
@@ -12,8 +12,10 @@ export interface ShiftAssignment {
|
|
|
12
12
|
memberId: string;
|
|
13
13
|
/** The shift pattern this member is assigned to. */
|
|
14
14
|
shiftPatternId: string;
|
|
15
|
+
/** The concrete role this member is filling for the assignment. */
|
|
16
|
+
roleId?: string;
|
|
15
17
|
/** The date of the assignment (YYYY-MM-DD). */
|
|
16
|
-
day:
|
|
18
|
+
day: DateString;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -24,8 +26,10 @@ export interface ShiftAssignment {
|
|
|
24
26
|
export interface ResolvedShiftAssignment {
|
|
25
27
|
/** The assigned member's ID. */
|
|
26
28
|
memberId: string;
|
|
29
|
+
/** The concrete role this member is filling for the assignment. */
|
|
30
|
+
roleId?: string;
|
|
27
31
|
/** The date of the assignment (YYYY-MM-DD). */
|
|
28
|
-
day:
|
|
32
|
+
day: DateString;
|
|
29
33
|
/** When the shift starts. */
|
|
30
34
|
startTime: TimeOfDay;
|
|
31
35
|
/** When the shift ends. */
|
|
@@ -51,12 +55,20 @@ export interface SolverResult {
|
|
|
51
55
|
/**
|
|
52
56
|
* Extracts shift assignments from solver response.
|
|
53
57
|
*
|
|
54
|
-
* Parses variable names matching the pattern
|
|
55
|
-
* and
|
|
58
|
+
* Parses role-specific variable names matching the pattern
|
|
59
|
+
* `assign_role:${memberId}:${patternId}:${roleId}:${day}` and aggregate
|
|
60
|
+
* assignment variable names matching `assign:${memberId}:${patternId}:${day}`.
|
|
56
61
|
*
|
|
57
62
|
* @remarks
|
|
58
|
-
*
|
|
59
|
-
*
|
|
63
|
+
* The model uses aggregate assignment variables as the canonical shift presence
|
|
64
|
+
* literals for intervals, objectives, shift-level rules, and skill-only coverage.
|
|
65
|
+
* Role-specific variables are emitted when the solver also chooses a concrete
|
|
66
|
+
* role for that shift. When both variable shapes exist for the same member,
|
|
67
|
+
* pattern, and day, the role-specific assignment is returned so callers can see
|
|
68
|
+
* the selected role without receiving a duplicate aggregate assignment.
|
|
69
|
+
*
|
|
70
|
+
* IDs are validated by ModelBuilder to not contain colons, ensuring unambiguous
|
|
71
|
+
* parsing.
|
|
60
72
|
*
|
|
61
73
|
* @param response - The solver response containing variable values
|
|
62
74
|
* @returns Parsed schedule result with assignments
|
|
@@ -85,22 +97,49 @@ export function parseSolverResponse(response: SolverResponse): SolverResult {
|
|
|
85
97
|
};
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
const
|
|
100
|
+
const roleAssignments = new Map<string, ShiftAssignment>();
|
|
101
|
+
const aggregateAssignments = new Map<string, ShiftAssignment>();
|
|
89
102
|
|
|
90
103
|
for (const [varName, value] of Object.entries(response.values ?? {})) {
|
|
91
104
|
if (value !== 1) continue;
|
|
105
|
+
|
|
106
|
+
if (varName.startsWith("assign_role:")) {
|
|
107
|
+
const parts = varName.split(":");
|
|
108
|
+
if (parts.length !== 5) continue;
|
|
109
|
+
|
|
110
|
+
const [, memberId, shiftPatternId, roleId, day] = parts;
|
|
111
|
+
if (!memberId || !shiftPatternId || !roleId || !day) continue;
|
|
112
|
+
if (!isDateString(day)) continue;
|
|
113
|
+
|
|
114
|
+
roleAssignments.set(assignmentKey(memberId, shiftPatternId, day), {
|
|
115
|
+
memberId,
|
|
116
|
+
shiftPatternId,
|
|
117
|
+
roleId,
|
|
118
|
+
day,
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
92
123
|
if (!varName.startsWith("assign:")) continue;
|
|
93
124
|
|
|
94
|
-
// Pattern: assign:${memberId}:${patternId}:${day}
|
|
95
|
-
// IDs are validated to not contain colons, so splitting is unambiguous.
|
|
96
125
|
const parts = varName.split(":");
|
|
97
126
|
if (parts.length !== 4) continue;
|
|
98
|
-
|
|
99
127
|
const [, memberId, shiftPatternId, day] = parts;
|
|
100
128
|
if (!memberId || !shiftPatternId || !day) continue;
|
|
101
|
-
if (
|
|
129
|
+
if (!isDateString(day)) continue;
|
|
130
|
+
|
|
131
|
+
aggregateAssignments.set(assignmentKey(memberId, shiftPatternId, day), {
|
|
132
|
+
memberId,
|
|
133
|
+
shiftPatternId,
|
|
134
|
+
day,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
102
137
|
|
|
103
|
-
|
|
138
|
+
const assignments = [...roleAssignments.values()];
|
|
139
|
+
for (const [key, assignment] of aggregateAssignments) {
|
|
140
|
+
if (!roleAssignments.has(key)) {
|
|
141
|
+
assignments.push(assignment);
|
|
142
|
+
}
|
|
104
143
|
}
|
|
105
144
|
|
|
106
145
|
return {
|
|
@@ -142,6 +181,7 @@ export function resolveAssignments(
|
|
|
142
181
|
|
|
143
182
|
return {
|
|
144
183
|
memberId: a.memberId,
|
|
184
|
+
...(a.roleId ? { roleId: a.roleId } : {}),
|
|
145
185
|
day: a.day,
|
|
146
186
|
startTime: pattern.startTime,
|
|
147
187
|
endTime: pattern.endTime,
|
|
@@ -149,3 +189,7 @@ export function resolveAssignments(
|
|
|
149
189
|
})
|
|
150
190
|
.filter((a): a is ResolvedShiftAssignment => a !== null);
|
|
151
191
|
}
|
|
192
|
+
|
|
193
|
+
function assignmentKey(memberId: string, shiftPatternId: string, day: DateString): string {
|
|
194
|
+
return `${memberId}:${shiftPatternId}:${day}`;
|
|
195
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule descriptor contracts for CP-SAT compilation.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Rules compile into declarative artifacts instead of mutating the solver
|
|
6
|
+
* builder directly. This keeps validation handling and backend lowering
|
|
7
|
+
* centralized. The user-facing feedback unit is the rule or validation group,
|
|
8
|
+
* not an individual solver constraint.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { z } from "zod";
|
|
14
|
+
import type { DateString, DayOfWeek, SchedulingDay, TimeOfDay } from "../types.js";
|
|
15
|
+
import type { ModelBuilder } from "./model-builder.js";
|
|
16
|
+
import type { ResolvedShiftAssignment, ShiftAssignment } from "./response.js";
|
|
17
|
+
import type { SchedulingMember, ShiftPattern, Term } from "./types.js";
|
|
18
|
+
import type { ValidationContext, ValidationGroup } from "./validation.types.js";
|
|
19
|
+
import type { ValidationReporter } from "./validation-reporter.js";
|
|
20
|
+
|
|
21
|
+
export type RuleMode =
|
|
22
|
+
| "hard-constraint"
|
|
23
|
+
| "soft-constraint"
|
|
24
|
+
| "objective"
|
|
25
|
+
| "coverage-exclusion"
|
|
26
|
+
| "pre-solve-feedback"
|
|
27
|
+
| "post-solve-feedback"
|
|
28
|
+
| "cost"
|
|
29
|
+
| "variable";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Inputs available while compiling a rule descriptor.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* Descriptor compilation happens before the model builder applies artifacts, so
|
|
36
|
+
* rules receive normalized scheduling inputs and return declarative artifacts
|
|
37
|
+
* instead of mutating the builder directly.
|
|
38
|
+
*
|
|
39
|
+
* @category Rules
|
|
40
|
+
*/
|
|
41
|
+
export interface RuleCompileContext {
|
|
42
|
+
readonly members: readonly SchedulingMember[];
|
|
43
|
+
readonly shiftPatterns: readonly ShiftPattern[];
|
|
44
|
+
readonly days: readonly SchedulingDay[];
|
|
45
|
+
readonly weekStartsOn: DayOfWeek;
|
|
46
|
+
readonly coverageBucketMinutes?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type RulePreSolveFeedbackContext = RuleCompileContext;
|
|
50
|
+
export type RulePostSolveFeedbackContext = RuleCompileContext;
|
|
51
|
+
export type CostCalculationContext = RuleCompileContext;
|
|
52
|
+
|
|
53
|
+
export type PreSolveFeedbackFn = (
|
|
54
|
+
ctx: RulePreSolveFeedbackContext,
|
|
55
|
+
reporter: ValidationReporter,
|
|
56
|
+
) => void;
|
|
57
|
+
|
|
58
|
+
export type PostSolveFeedbackFn = (
|
|
59
|
+
assignments: readonly ResolvedShiftAssignment[],
|
|
60
|
+
reporter: ValidationReporter,
|
|
61
|
+
ctx: RulePostSolveFeedbackContext,
|
|
62
|
+
) => void;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Structured opt-out categories for artifact-level validation reporting.
|
|
66
|
+
*
|
|
67
|
+
* @category Rules
|
|
68
|
+
*/
|
|
69
|
+
export type ValidationSkipCategory = "scaffolding" | "no-meaningful-feedback";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validation strategy that reports a hard constraint into the validation pipeline.
|
|
73
|
+
*
|
|
74
|
+
* @category Rules
|
|
75
|
+
*/
|
|
76
|
+
export interface ReportHardConstraintValidationStrategy {
|
|
77
|
+
readonly strategy: "report";
|
|
78
|
+
readonly id: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validation strategy that reports a soft constraint into the validation pipeline.
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* Soft constraints report using their artifact `constraintId`.
|
|
86
|
+
*
|
|
87
|
+
* @category Rules
|
|
88
|
+
*/
|
|
89
|
+
export interface ReportSoftConstraintValidationStrategy {
|
|
90
|
+
readonly strategy: "report";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validation strategy that explicitly skips validation reporting for an artifact.
|
|
95
|
+
*
|
|
96
|
+
* @category Rules
|
|
97
|
+
*/
|
|
98
|
+
export interface SkipValidationStrategy {
|
|
99
|
+
readonly strategy: "skip";
|
|
100
|
+
readonly category: ValidationSkipCategory;
|
|
101
|
+
readonly rationale: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validation strategy attached to hard constraints.
|
|
106
|
+
*
|
|
107
|
+
* @category Rules
|
|
108
|
+
*/
|
|
109
|
+
export type HardConstraintValidationStrategy =
|
|
110
|
+
| ReportHardConstraintValidationStrategy
|
|
111
|
+
| SkipValidationStrategy;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validation strategy attached to soft constraints.
|
|
115
|
+
*
|
|
116
|
+
* @category Rules
|
|
117
|
+
*/
|
|
118
|
+
export type SoftConstraintValidationStrategy =
|
|
119
|
+
| ReportSoftConstraintValidationStrategy
|
|
120
|
+
| SkipValidationStrategy;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validation strategy attached to objective artifacts.
|
|
124
|
+
*
|
|
125
|
+
* @category Rules
|
|
126
|
+
*/
|
|
127
|
+
export type ObjectiveValidationStrategy = SkipValidationStrategy;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Validation strategy attached to cost artifacts.
|
|
131
|
+
*
|
|
132
|
+
* @category Rules
|
|
133
|
+
*/
|
|
134
|
+
export type CostValidationStrategy = SkipValidationStrategy;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A compiled hard constraint emitted by a rule descriptor.
|
|
138
|
+
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* Hard constraints are still implementation details. End-user feedback should
|
|
141
|
+
* normally be summarized at the rule or validation-group level. Artifact rule
|
|
142
|
+
* identity is inherited from the containing `CompiledRule`. Solver-stage
|
|
143
|
+
* reporting uses artifact-level validation strategies. Pre-solve feedback
|
|
144
|
+
* should be emitted via dedicated feedback artifacts.
|
|
145
|
+
*
|
|
146
|
+
* @category Rules
|
|
147
|
+
*/
|
|
148
|
+
export interface HardConstraintArtifact {
|
|
149
|
+
readonly kind: "hard-constraint";
|
|
150
|
+
readonly group?: ValidationGroup;
|
|
151
|
+
readonly validation: HardConstraintValidationStrategy;
|
|
152
|
+
readonly description: string;
|
|
153
|
+
readonly context: ValidationContext;
|
|
154
|
+
readonly terms: readonly Term[];
|
|
155
|
+
readonly comparator: "<=" | ">=" | "==";
|
|
156
|
+
readonly targetValue: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface SoftConstraintArtifact {
|
|
160
|
+
readonly kind: "soft-constraint";
|
|
161
|
+
readonly group?: ValidationGroup;
|
|
162
|
+
readonly validation: SoftConstraintValidationStrategy;
|
|
163
|
+
readonly description: string;
|
|
164
|
+
readonly context: ValidationContext;
|
|
165
|
+
readonly terms: readonly Term[];
|
|
166
|
+
readonly comparator: "<=" | ">=";
|
|
167
|
+
readonly targetValue: number;
|
|
168
|
+
readonly penalty: number;
|
|
169
|
+
readonly constraintId: string;
|
|
170
|
+
/** Optional objective stage ID used for staged solver requests. */
|
|
171
|
+
readonly stage?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface ObjectiveArtifact {
|
|
175
|
+
readonly kind: "objective";
|
|
176
|
+
readonly group?: ValidationGroup;
|
|
177
|
+
readonly terms: readonly Term[];
|
|
178
|
+
readonly validation: ObjectiveValidationStrategy;
|
|
179
|
+
/** Optional objective stage ID used for staged solver requests. */
|
|
180
|
+
readonly stage?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface CoverageExclusionArtifact {
|
|
184
|
+
readonly kind: "coverage-exclusion";
|
|
185
|
+
readonly group?: ValidationGroup;
|
|
186
|
+
readonly memberId: string;
|
|
187
|
+
readonly day: DateString;
|
|
188
|
+
readonly startTime?: TimeOfDay;
|
|
189
|
+
readonly endTime?: TimeOfDay;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface PreSolveFeedbackArtifact {
|
|
193
|
+
readonly kind: "pre-solve-feedback";
|
|
194
|
+
readonly run: PreSolveFeedbackFn;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface PostSolveFeedbackArtifact {
|
|
198
|
+
readonly kind: "post-solve-feedback";
|
|
199
|
+
readonly run: PostSolveFeedbackFn;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface CostEntry {
|
|
203
|
+
readonly memberId: string;
|
|
204
|
+
readonly day: string;
|
|
205
|
+
readonly category: string;
|
|
206
|
+
readonly amount: number;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface CostContribution {
|
|
210
|
+
readonly entries: readonly CostEntry[];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface CostArtifact {
|
|
214
|
+
readonly kind: "cost";
|
|
215
|
+
readonly compileObjective?: (builder: ModelBuilder) => void;
|
|
216
|
+
readonly calculateCost?: (
|
|
217
|
+
assignments: readonly ShiftAssignment[],
|
|
218
|
+
ctx: CostCalculationContext,
|
|
219
|
+
) => CostContribution;
|
|
220
|
+
readonly validation: CostValidationStrategy;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export type VariableArtifact =
|
|
224
|
+
| {
|
|
225
|
+
readonly kind: "variable";
|
|
226
|
+
readonly variable: { type: "bool"; name: string };
|
|
227
|
+
}
|
|
228
|
+
| {
|
|
229
|
+
readonly kind: "variable";
|
|
230
|
+
readonly variable: { type: "int"; name: string; min: number; max: number };
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export type RuleArtifact =
|
|
234
|
+
| VariableArtifact
|
|
235
|
+
| HardConstraintArtifact
|
|
236
|
+
| SoftConstraintArtifact
|
|
237
|
+
| ObjectiveArtifact
|
|
238
|
+
| CoverageExclusionArtifact
|
|
239
|
+
| PreSolveFeedbackArtifact
|
|
240
|
+
| PostSolveFeedbackArtifact
|
|
241
|
+
| CostArtifact;
|
|
242
|
+
|
|
243
|
+
export interface CompiledRule {
|
|
244
|
+
readonly rule: string;
|
|
245
|
+
readonly artifacts: readonly RuleArtifact[];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* A low-level CP-SAT rule descriptor.
|
|
250
|
+
*
|
|
251
|
+
* @remarks
|
|
252
|
+
* Advanced consumers can register custom descriptors via
|
|
253
|
+
* {@link createCpsatRuleRegistry}. Each descriptor owns runtime validation via
|
|
254
|
+
* its Zod schema and compiles a parsed config into declarative rule artifacts.
|
|
255
|
+
*
|
|
256
|
+
* @category Rules
|
|
257
|
+
*/
|
|
258
|
+
export interface RuleDescriptor<Name extends string = string, Config = unknown> {
|
|
259
|
+
readonly name: Name;
|
|
260
|
+
readonly schema: z.ZodType<Config>;
|
|
261
|
+
compile(config: Config, ctx: RuleCompileContext): CompiledRule;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Defines a typed low-level CP-SAT rule descriptor.
|
|
266
|
+
*
|
|
267
|
+
* @remarks
|
|
268
|
+
* Use this for custom rules that should participate in the same descriptor and
|
|
269
|
+
* artifact pipeline as the built-in rules.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* const debugRuleDescriptor = defineRuleDescriptor({
|
|
274
|
+
* name: "debug",
|
|
275
|
+
* schema: z.object({ flag: z.boolean() }),
|
|
276
|
+
* compile(config) {
|
|
277
|
+
* return {
|
|
278
|
+
* rule: "debug",
|
|
279
|
+
* artifacts: config.flag ? [] : [],
|
|
280
|
+
* };
|
|
281
|
+
* },
|
|
282
|
+
* });
|
|
283
|
+
* ```
|
|
284
|
+
*
|
|
285
|
+
* @category Rules
|
|
286
|
+
*/
|
|
287
|
+
export function defineRuleDescriptor<Name extends string, Schema extends z.ZodTypeAny>(descriptor: {
|
|
288
|
+
readonly name: Name;
|
|
289
|
+
readonly schema: Schema;
|
|
290
|
+
compile(config: z.infer<Schema>, ctx: RuleCompileContext): CompiledRule;
|
|
291
|
+
}): RuleDescriptor<Name, z.infer<Schema>> {
|
|
292
|
+
return descriptor as RuleDescriptor<Name, z.infer<Schema>>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Parses raw config through a descriptor schema, then compiles it into artifacts.
|
|
297
|
+
*
|
|
298
|
+
* @remarks
|
|
299
|
+
* Schema parsing fails loudly. This helper does not recover from invalid JSON or
|
|
300
|
+
* invalid rule config.
|
|
301
|
+
*
|
|
302
|
+
* @category Rules
|
|
303
|
+
*/
|
|
304
|
+
export function compileRuleDescriptor<Name extends string, Config>(
|
|
305
|
+
descriptor: RuleDescriptor<Name, Config>,
|
|
306
|
+
rawConfig: unknown,
|
|
307
|
+
ctx: RuleCompileContext,
|
|
308
|
+
): CompiledRule {
|
|
309
|
+
const config = descriptor.schema.parse(rawConfig);
|
|
310
|
+
return descriptor.compile(config, ctx);
|
|
311
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HardConstraintArtifact,
|
|
3
|
+
ReportHardConstraintValidationStrategy,
|
|
4
|
+
ReportSoftConstraintValidationStrategy,
|
|
5
|
+
SkipValidationStrategy,
|
|
6
|
+
SoftConstraintArtifact,
|
|
7
|
+
ValidationSkipCategory,
|
|
8
|
+
VariableArtifact,
|
|
9
|
+
} from "../rule-descriptor.js";
|
|
10
|
+
|
|
11
|
+
export function boolVariable(name: string): VariableArtifact {
|
|
12
|
+
return {
|
|
13
|
+
kind: "variable",
|
|
14
|
+
variable: { type: "bool", name },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function intVariable(name: string, min: number, max: number): VariableArtifact {
|
|
19
|
+
return {
|
|
20
|
+
kind: "variable",
|
|
21
|
+
variable: { type: "int", name, min, max },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function reportValidation(id: string): ReportHardConstraintValidationStrategy;
|
|
26
|
+
export function reportValidation(): ReportSoftConstraintValidationStrategy;
|
|
27
|
+
export function reportValidation(
|
|
28
|
+
id?: string,
|
|
29
|
+
): ReportHardConstraintValidationStrategy | ReportSoftConstraintValidationStrategy {
|
|
30
|
+
return id ? { strategy: "report", id } : { strategy: "report" };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function skipValidation(
|
|
34
|
+
category: ValidationSkipCategory,
|
|
35
|
+
rationale: string,
|
|
36
|
+
): SkipValidationStrategy {
|
|
37
|
+
return { strategy: "skip", category, rationale };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hardConstraint(
|
|
41
|
+
params: Omit<HardConstraintArtifact, "kind">,
|
|
42
|
+
): HardConstraintArtifact {
|
|
43
|
+
return {
|
|
44
|
+
...params,
|
|
45
|
+
kind: "hard-constraint",
|
|
46
|
+
} satisfies HardConstraintArtifact;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function softConstraint(
|
|
50
|
+
params: Omit<SoftConstraintArtifact, "kind">,
|
|
51
|
+
): SoftConstraintArtifact {
|
|
52
|
+
return {
|
|
53
|
+
...params,
|
|
54
|
+
kind: "soft-constraint",
|
|
55
|
+
} satisfies SoftConstraintArtifact;
|
|
56
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
import
|
|
2
|
+
import { defineRuleDescriptor } from "../rule-descriptor.js";
|
|
3
3
|
import { priorityToPenalty } from "../utils.js";
|
|
4
4
|
import { PrioritySchema } from "./scope.types.js";
|
|
5
|
+
import { assignmentVar } from "./variables.js";
|
|
6
|
+
import { boolVariable, hardConstraint, reportValidation, skipValidation } from "./artifacts.js";
|
|
7
|
+
import { canAssignMemberToPattern, isPatternAvailableOnDay } from "./pattern-eligibility.js";
|
|
5
8
|
|
|
6
|
-
const AssignTogetherSchema = z.object({
|
|
9
|
+
export const AssignTogetherSchema = z.object({
|
|
7
10
|
groupMemberIds: z
|
|
8
11
|
.tuple([z.string(), z.string()])
|
|
9
12
|
.rest(z.string())
|
|
@@ -14,86 +17,118 @@ const AssignTogetherSchema = z.object({
|
|
|
14
17
|
});
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
|
-
* Configuration for {@link
|
|
18
|
-
*
|
|
19
|
-
* - `groupMemberIds` (required): member IDs to assign together (at least two, must be unique)
|
|
20
|
-
* - `priority` (required): how strictly the solver enforces this rule
|
|
20
|
+
* Configuration for {@link assignTogetherRuleDescriptor}.
|
|
21
21
|
*/
|
|
22
22
|
export type AssignTogetherConfig = z.infer<typeof AssignTogetherSchema>;
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* Low-level descriptor for the `assign-together` rule.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* Register this in a custom rule registry when you need the descriptor API
|
|
29
|
+
* directly instead of the high-level {@link assignTogether} helper.
|
|
27
30
|
*
|
|
28
|
-
* @
|
|
29
|
-
* @example
|
|
30
|
-
* ```ts
|
|
31
|
-
* const rule = createAssignTogetherRule({
|
|
32
|
-
* groupMemberIds: ["alice", "bob", "charlie"],
|
|
33
|
-
* priority: "HIGH",
|
|
34
|
-
* });
|
|
35
|
-
* builder = new ModelBuilder({ ...config, rules: [rule] });
|
|
36
|
-
* ```
|
|
31
|
+
* @category Rules
|
|
37
32
|
*/
|
|
38
|
-
export
|
|
39
|
-
|
|
33
|
+
export const assignTogetherRuleDescriptor = defineRuleDescriptor({
|
|
34
|
+
name: "assign-together",
|
|
35
|
+
schema: AssignTogetherSchema,
|
|
36
|
+
compile(config, ctx) {
|
|
37
|
+
const { groupMemberIds, priority } = config;
|
|
38
|
+
const targetMembers = groupMemberIds
|
|
39
|
+
.map((id: string) => ctx.members.find((member) => member.id === id))
|
|
40
|
+
.filter((member): member is NonNullable<typeof member> => member !== undefined);
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.map((id) => b.members.find((m) => m.id === id))
|
|
45
|
-
.filter((m): m is NonNullable<typeof m> => m !== undefined);
|
|
42
|
+
if (targetMembers.length < 2) {
|
|
43
|
+
return { rule: "assign-together", artifacts: [] };
|
|
44
|
+
}
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
const artifacts = targetMembers.flatMap((member, index) => {
|
|
47
|
+
const next = targetMembers[index + 1];
|
|
48
|
+
if (!next) return [];
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
return ctx.shiftPatterns.flatMap((pattern) => {
|
|
51
|
+
if (
|
|
52
|
+
!canAssignMemberToPattern(member, pattern) ||
|
|
53
|
+
!canAssignMemberToPattern(next, pattern)
|
|
54
|
+
) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const canAssign2 = b.canAssign(m2, pattern);
|
|
56
|
-
if (!canAssign1 || !canAssign2) continue;
|
|
58
|
+
return ctx.days.flatMap((day) => {
|
|
59
|
+
if (!isPatternAvailableOnDay(pattern, day)) return [];
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
const var1 = assignmentVar(member.id, pattern.id, day.iso);
|
|
62
|
+
const var2 = assignmentVar(next.id, pattern.id, day.iso);
|
|
63
|
+
const description = `${member.id} and ${next.id} stay together on ${pattern.id} for ${day.iso}`;
|
|
64
|
+
const context = { memberIds: [member.id, next.id], days: [day.iso] };
|
|
65
|
+
const constraintId = `assign-together:${member.id}:${next.id}:${pattern.id}:${day.iso}`;
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
if (priority === "MANDATORY") {
|
|
68
|
+
return [
|
|
69
|
+
hardConstraint({
|
|
70
|
+
description,
|
|
71
|
+
context,
|
|
72
|
+
validation: reportValidation(constraintId),
|
|
73
|
+
terms: [
|
|
66
74
|
{ var: var1, coeff: 1 },
|
|
67
75
|
{ var: var2, coeff: -1 },
|
|
68
76
|
],
|
|
69
|
-
"==",
|
|
70
|
-
0,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
const diffVar = b.boolVar(`together_diff_${m1.id}_${m2.id}_${pattern.id}_${day}`);
|
|
74
|
-
b.addLinear(
|
|
75
|
-
[
|
|
76
|
-
{ var: diffVar, coeff: 1 },
|
|
77
|
-
{ var: var1, coeff: -1 },
|
|
78
|
-
{ var: var2, coeff: 1 },
|
|
79
|
-
],
|
|
80
|
-
">=",
|
|
81
|
-
0,
|
|
82
|
-
);
|
|
83
|
-
b.addLinear(
|
|
84
|
-
[
|
|
85
|
-
{ var: diffVar, coeff: 1 },
|
|
86
|
-
{ var: var1, coeff: 1 },
|
|
87
|
-
{ var: var2, coeff: -1 },
|
|
88
|
-
],
|
|
89
|
-
">=",
|
|
90
|
-
0,
|
|
91
|
-
);
|
|
92
|
-
b.addPenalty(diffVar, priorityToPenalty(priority));
|
|
93
|
-
}
|
|
77
|
+
comparator: "==",
|
|
78
|
+
targetValue: 0,
|
|
79
|
+
}),
|
|
80
|
+
];
|
|
94
81
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
|
|
83
|
+
const diffVar = `together_diff_${member.id}_${next.id}_${pattern.id}_${day.iso}`;
|
|
84
|
+
return [
|
|
85
|
+
boolVariable(diffVar),
|
|
86
|
+
hardConstraint({
|
|
87
|
+
description: `${diffVar} captures ${var1} without ${var2}`,
|
|
88
|
+
validation: skipValidation(
|
|
89
|
+
"scaffolding",
|
|
90
|
+
"This helper variable measures pair mismatch so a soft pairing preference can be optimized without surfacing helper-level feedback.",
|
|
91
|
+
),
|
|
92
|
+
context,
|
|
93
|
+
terms: [
|
|
94
|
+
{ var: diffVar, coeff: 1 },
|
|
95
|
+
{ var: var1, coeff: -1 },
|
|
96
|
+
{ var: var2, coeff: 1 },
|
|
97
|
+
],
|
|
98
|
+
comparator: ">=",
|
|
99
|
+
targetValue: 0,
|
|
100
|
+
}),
|
|
101
|
+
hardConstraint({
|
|
102
|
+
description: `${diffVar} captures ${var2} without ${var1}`,
|
|
103
|
+
validation: skipValidation(
|
|
104
|
+
"scaffolding",
|
|
105
|
+
"This helper variable measures pair mismatch symmetrically so the objective can penalize either member being assigned alone.",
|
|
106
|
+
),
|
|
107
|
+
context,
|
|
108
|
+
terms: [
|
|
109
|
+
{ var: diffVar, coeff: 1 },
|
|
110
|
+
{ var: var1, coeff: 1 },
|
|
111
|
+
{ var: var2, coeff: -1 },
|
|
112
|
+
],
|
|
113
|
+
comparator: ">=",
|
|
114
|
+
targetValue: 0,
|
|
115
|
+
}),
|
|
116
|
+
{
|
|
117
|
+
kind: "objective" as const,
|
|
118
|
+
terms: [{ var: diffVar, coeff: Math.max(1, priorityToPenalty(priority) * 5) }],
|
|
119
|
+
validation: skipValidation(
|
|
120
|
+
"no-meaningful-feedback",
|
|
121
|
+
"Pairing preferences influence the objective but do not emit validation feedback.",
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
rule: "assign-together",
|
|
131
|
+
artifacts,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
});
|