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
|
@@ -1,66 +1,59 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type {
|
|
2
|
+
SolverConstraint,
|
|
3
|
+
SolverObjectiveStage,
|
|
4
|
+
SolverRequest,
|
|
5
|
+
SolverVariable,
|
|
6
|
+
} from "../client.types.js";
|
|
7
|
+
import type { DayOfWeek, SchedulingDay, SchedulingPeriod } from "../types.js";
|
|
8
|
+
import { resolveDaysFromPeriod } from "../datetime.utils.js";
|
|
4
9
|
import {
|
|
5
10
|
MINUTES_PER_DAY,
|
|
6
11
|
normalizeEndMinutes,
|
|
7
12
|
OBJECTIVE_WEIGHTS,
|
|
8
|
-
parseDayString,
|
|
9
13
|
priorityToPenalty,
|
|
10
14
|
timeOfDayToMinutes,
|
|
11
15
|
} from "./utils.js";
|
|
12
16
|
import type {
|
|
13
17
|
CoverageRequirement,
|
|
14
18
|
ModelBuilderOptions,
|
|
19
|
+
ModelSolveStrategy,
|
|
15
20
|
ShiftPattern,
|
|
16
21
|
SchedulingMember,
|
|
17
22
|
Term,
|
|
18
23
|
} from "./types.js";
|
|
19
|
-
import type {
|
|
24
|
+
import type {
|
|
25
|
+
AnyCpsatRuleConfigEntry,
|
|
26
|
+
BuiltInCpsatRuleRegistry,
|
|
27
|
+
CpsatRuleConfigEntry,
|
|
28
|
+
CpsatRuleRegistry,
|
|
29
|
+
} from "./rules/rules.types.js";
|
|
20
30
|
import { buildCpsatRules } from "./rules/resolver.js";
|
|
21
|
-
import {
|
|
31
|
+
import { builtInCpsatRuleRegistry } from "./rules/registry.js";
|
|
32
|
+
import { TARGET_PEAK_CONCURRENT_ASSIGNMENTS_OBJECTIVE_STAGE_ID } from "./rules/target-peak-concurrent-assignments.js";
|
|
22
33
|
import { ValidationReporterImpl } from "./validation-reporter.js";
|
|
23
34
|
import type { ValidationReporter } from "./validation-reporter.js";
|
|
24
35
|
import type { ScheduleValidation, CoverageExclusion } from "./validation.types.js";
|
|
25
36
|
import type { ShiftAssignment, ResolvedShiftAssignment } from "./response.js";
|
|
37
|
+
import type {
|
|
38
|
+
CompiledRule,
|
|
39
|
+
CostArtifact,
|
|
40
|
+
CostContribution,
|
|
41
|
+
PostSolveFeedbackArtifact,
|
|
42
|
+
RuleArtifact,
|
|
43
|
+
RuleCompileContext,
|
|
44
|
+
} from "./rule-descriptor.js";
|
|
45
|
+
|
|
46
|
+
type BoundRuleArtifact = {
|
|
47
|
+
readonly rule: string;
|
|
48
|
+
readonly artifact: RuleArtifact;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const UNSTAGED_OBJECTIVE_STAGE_ID = "__dabke_unstaged__";
|
|
26
52
|
|
|
27
53
|
/**
|
|
28
54
|
* Builds a CP-SAT solver request from high-level scheduling constructs
|
|
29
55
|
* (team, shift patterns, coverage, and rule compilers).
|
|
30
56
|
*/
|
|
31
|
-
/**
|
|
32
|
-
* Context provided to rules during post-solve validation.
|
|
33
|
-
*/
|
|
34
|
-
export interface RuleValidationContext {
|
|
35
|
-
readonly members: SchedulingMember[];
|
|
36
|
-
readonly days: string[];
|
|
37
|
-
readonly shiftPatterns: ShiftPattern[];
|
|
38
|
-
}
|
|
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
|
-
*/
|
|
47
|
-
export interface CompilationRule {
|
|
48
|
-
/** Emit constraints and objectives into the model builder. */
|
|
49
|
-
compile(builder: ModelBuilder): void;
|
|
50
|
-
/** Validate the solved schedule and report violations. */
|
|
51
|
-
validate?(
|
|
52
|
-
assignments: ResolvedShiftAssignment[],
|
|
53
|
-
reporter: ValidationReporter,
|
|
54
|
-
context: RuleValidationContext,
|
|
55
|
-
): void;
|
|
56
|
-
/** Compute cost contribution for a solved schedule. */
|
|
57
|
-
cost?(
|
|
58
|
-
assignments: ShiftAssignment[],
|
|
59
|
-
members: ReadonlyArray<SchedulingMember>,
|
|
60
|
-
shiftPatterns: ReadonlyArray<ShiftPattern>,
|
|
61
|
-
): CostContribution;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
57
|
/**
|
|
65
58
|
* Shared context for cost rules.
|
|
66
59
|
*
|
|
@@ -74,26 +67,6 @@ export interface CostContext {
|
|
|
74
67
|
active: boolean;
|
|
75
68
|
}
|
|
76
69
|
|
|
77
|
-
/**
|
|
78
|
-
* A cost entry produced by a rule's `cost()` method.
|
|
79
|
-
*
|
|
80
|
-
* The `category` is an open-ended string. Built-in rules use well-known
|
|
81
|
-
* categories from {@link COST_CATEGORY}. Custom rules can use any string.
|
|
82
|
-
*/
|
|
83
|
-
export interface CostEntry {
|
|
84
|
-
memberId: string;
|
|
85
|
-
day: string;
|
|
86
|
-
category: string;
|
|
87
|
-
amount: number;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Cost contribution from a single rule.
|
|
92
|
-
*/
|
|
93
|
-
export interface CostContribution {
|
|
94
|
-
entries: CostEntry[];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
70
|
/**
|
|
98
71
|
* Result of {@link ModelBuilder.compile}.
|
|
99
72
|
*
|
|
@@ -123,7 +96,19 @@ export interface CompilationResult {
|
|
|
123
96
|
* };
|
|
124
97
|
* ```
|
|
125
98
|
*/
|
|
126
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Configuration for {@link ModelBuilder}.
|
|
101
|
+
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* The optional `Registry` generic keeps `ruleConfigs` type-safe for custom rule
|
|
104
|
+
* registries. When omitted, the config defaults to the built-in CP-SAT registry.
|
|
105
|
+
*
|
|
106
|
+
* @category Model Builder
|
|
107
|
+
*/
|
|
108
|
+
export interface ModelBuilderConfig<
|
|
109
|
+
Registry extends CpsatRuleRegistry = BuiltInCpsatRuleRegistry,
|
|
110
|
+
RuleConfigEntry extends AnyCpsatRuleConfigEntry = CpsatRuleConfigEntry<Registry>,
|
|
111
|
+
> extends ModelBuilderOptions {
|
|
127
112
|
/** Team members available for scheduling. */
|
|
128
113
|
members: SchedulingMember[];
|
|
129
114
|
/** Available shift patterns (time slots) that members can be assigned to. */
|
|
@@ -137,17 +122,17 @@ export interface ModelBuilderConfig extends ModelBuilderOptions {
|
|
|
137
122
|
/**
|
|
138
123
|
* Pre-compiled rules; use this for custom rules that are not part of the registry.
|
|
139
124
|
*/
|
|
140
|
-
rules?:
|
|
125
|
+
rules?: CompiledRule[];
|
|
141
126
|
/**
|
|
142
|
-
* Named rule configurations that will be compiled using the provided rule
|
|
127
|
+
* Named rule configurations that will be compiled using the provided rule registry.
|
|
143
128
|
*/
|
|
144
|
-
ruleConfigs?:
|
|
129
|
+
ruleConfigs?: RuleConfigEntry[];
|
|
145
130
|
/**
|
|
146
|
-
* Rule
|
|
131
|
+
* Rule registry to use when compiling ruleConfigs. Defaults to the built-in CP-SAT registry.
|
|
147
132
|
*/
|
|
148
|
-
|
|
133
|
+
ruleRegistry?: Registry;
|
|
149
134
|
/**
|
|
150
|
-
* Optional validation reporter for
|
|
135
|
+
* Optional validation reporter for validation feedback.
|
|
151
136
|
*/
|
|
152
137
|
reporter?: ValidationReporter;
|
|
153
138
|
}
|
|
@@ -161,11 +146,12 @@ export interface ModelBuilderConfig extends ModelBuilderOptions {
|
|
|
161
146
|
export class ModelBuilder {
|
|
162
147
|
readonly members: SchedulingMember[];
|
|
163
148
|
readonly shiftPatterns: ShiftPattern[];
|
|
164
|
-
readonly days:
|
|
149
|
+
readonly days: SchedulingDay[];
|
|
165
150
|
readonly coverage: CoverageRequirement[];
|
|
166
|
-
readonly rules:
|
|
151
|
+
readonly rules: CompiledRule[];
|
|
167
152
|
readonly weekStartsOn: DayOfWeek;
|
|
168
153
|
readonly options: SolverRequest["options"] | undefined;
|
|
154
|
+
readonly strategy: ModelSolveStrategy;
|
|
169
155
|
readonly coverageBucketMinutes: number;
|
|
170
156
|
readonly reporter: ValidationReporter;
|
|
171
157
|
readonly fairDistribution: boolean;
|
|
@@ -176,22 +162,39 @@ export class ModelBuilder {
|
|
|
176
162
|
#variables = new Map<string, SolverVariable>();
|
|
177
163
|
#constraints: SolverConstraint[] = [];
|
|
178
164
|
#objective: Term[] = [];
|
|
165
|
+
#objectiveStageTerms = new Map<string, Term[]>();
|
|
166
|
+
#referencedObjectiveStages = new Set<string>();
|
|
167
|
+
#objectiveStageOrder: readonly string[] | undefined;
|
|
179
168
|
#dayIndex = new Map<string, number>();
|
|
169
|
+
#dayByIso = new Map<string, SchedulingDay>();
|
|
180
170
|
#shiftPatternMap = new Map<string, ShiftPattern>();
|
|
181
171
|
#builtRequest: SolverRequest | undefined;
|
|
182
172
|
#builtCompilation: CompilationResult | undefined;
|
|
173
|
+
#compiledArtifacts: BoundRuleArtifact[] = [];
|
|
174
|
+
#postSolveValidators: PostSolveFeedbackArtifact[] = [];
|
|
175
|
+
#costArtifacts: CostArtifact[] = [];
|
|
183
176
|
|
|
184
|
-
constructor(config: ModelBuilderConfig) {
|
|
177
|
+
constructor(config: ModelBuilderConfig<CpsatRuleRegistry, AnyCpsatRuleConfigEntry>) {
|
|
185
178
|
// Validate IDs don't contain the separator character
|
|
186
179
|
for (const member of config.members) {
|
|
187
180
|
if (member.id.includes(":")) {
|
|
188
181
|
throw new Error(`Member ID "${member.id}" cannot contain colons`);
|
|
189
182
|
}
|
|
183
|
+
for (const roleId of member.roleIds) {
|
|
184
|
+
if (roleId.includes(":")) {
|
|
185
|
+
throw new Error(`Role ID "${roleId}" cannot contain colons`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
190
188
|
}
|
|
191
189
|
for (const pattern of config.shiftPatterns) {
|
|
192
190
|
if (pattern.id.includes(":")) {
|
|
193
191
|
throw new Error(`Shift pattern ID "${pattern.id}" cannot contain colons`);
|
|
194
192
|
}
|
|
193
|
+
for (const roleId of pattern.roleIds ?? []) {
|
|
194
|
+
if (roleId.includes(":")) {
|
|
195
|
+
throw new Error(`Role ID "${roleId}" cannot contain colons`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
// Validate coverage requirements have at least roles or skills
|
|
@@ -203,28 +206,50 @@ export class ModelBuilder {
|
|
|
203
206
|
`Coverage requirement for day "${cov.day}" must have at least one of roles or skills`,
|
|
204
207
|
);
|
|
205
208
|
}
|
|
209
|
+
for (const roleId of cov.roleIds ?? []) {
|
|
210
|
+
if (roleId.includes(":")) {
|
|
211
|
+
throw new Error(`Role ID "${roleId}" cannot contain colons`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
206
214
|
}
|
|
207
215
|
|
|
208
216
|
this.members = config.members;
|
|
209
217
|
this.shiftPatterns = config.shiftPatterns;
|
|
210
218
|
this.days = resolveDaysFromPeriod(config.schedulingPeriod);
|
|
211
219
|
this.coverage = config.coverage;
|
|
220
|
+
const ruleCompileContext: RuleCompileContext = {
|
|
221
|
+
members: this.members,
|
|
222
|
+
shiftPatterns: this.shiftPatterns,
|
|
223
|
+
days: this.days,
|
|
224
|
+
weekStartsOn: config.weekStartsOn ?? "monday",
|
|
225
|
+
coverageBucketMinutes: config.coverageBucketMinutes ?? 15,
|
|
226
|
+
};
|
|
212
227
|
const compiledRuleConfigs = config.ruleConfigs
|
|
213
228
|
? buildCpsatRules(
|
|
214
229
|
config.ruleConfigs,
|
|
215
230
|
this.members,
|
|
216
|
-
|
|
231
|
+
ruleCompileContext,
|
|
232
|
+
config.ruleRegistry ?? builtInCpsatRuleRegistry,
|
|
217
233
|
)
|
|
218
234
|
: [];
|
|
219
235
|
this.rules = [...compiledRuleConfigs, ...(config.rules ?? [])];
|
|
220
236
|
this.weekStartsOn = config.weekStartsOn ?? "monday";
|
|
221
237
|
this.options = config.solverOptions;
|
|
238
|
+
this.strategy = resolveModelSolveStrategy(config.strategy);
|
|
239
|
+
this.#objectiveStageOrder =
|
|
240
|
+
config.objectiveStageOrder === undefined ? undefined : [...config.objectiveStageOrder];
|
|
222
241
|
this.coverageBucketMinutes = config.coverageBucketMinutes ?? 15;
|
|
223
242
|
this.reporter = config.reporter ?? new ValidationReporterImpl();
|
|
224
243
|
this.fairDistribution = config.fairDistribution ?? true;
|
|
225
244
|
|
|
226
|
-
this.days.forEach((day, idx) =>
|
|
245
|
+
this.days.forEach((day, idx) => {
|
|
246
|
+
this.#dayIndex.set(day.iso, idx);
|
|
247
|
+
this.#dayByIso.set(day.iso, day);
|
|
248
|
+
});
|
|
227
249
|
this.shiftPatterns.forEach((pattern) => this.#shiftPatternMap.set(pattern.id, pattern));
|
|
250
|
+
this.#compiledArtifacts = this.rules.flatMap((rule) =>
|
|
251
|
+
rule.artifacts.map((artifact) => ({ rule: rule.rule, artifact })),
|
|
252
|
+
);
|
|
228
253
|
}
|
|
229
254
|
|
|
230
255
|
boolVar(name: string): string {
|
|
@@ -253,20 +278,54 @@ export class ModelBuilder {
|
|
|
253
278
|
return name;
|
|
254
279
|
}
|
|
255
280
|
|
|
256
|
-
shiftActive(patternId: string, day:
|
|
257
|
-
return this.boolVar(`shift:${patternId}:${day}`);
|
|
281
|
+
shiftActive(patternId: string, day: SchedulingDay): string {
|
|
282
|
+
return this.boolVar(`shift:${patternId}:${day.iso}`);
|
|
258
283
|
}
|
|
259
284
|
|
|
260
|
-
|
|
261
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Returns the aggregate shift assignment variable for a member, pattern, and day.
|
|
287
|
+
*
|
|
288
|
+
* @remarks
|
|
289
|
+
* This variable means "the member works this shift pattern on this day". It is
|
|
290
|
+
* the presence literal for optional interval variables and the common variable
|
|
291
|
+
* used by shift-level rules, objectives, and skill-only coverage. Role-specific
|
|
292
|
+
* assignments are modeled separately and linked to this aggregate variable when
|
|
293
|
+
* a concrete role choice is available.
|
|
294
|
+
*/
|
|
295
|
+
assignment(memberId: string, patternId: string, day: SchedulingDay): string {
|
|
296
|
+
return this.boolVar(`assign:${memberId}:${patternId}:${day.iso}`);
|
|
262
297
|
}
|
|
263
298
|
|
|
264
|
-
|
|
265
|
-
|
|
299
|
+
/** Returns the role-specific assignment variable for a concrete role choice. */
|
|
300
|
+
#roleAssignment(memberId: string, patternId: string, roleId: string, day: SchedulingDay): string {
|
|
301
|
+
return this.boolVar(`assign_role:${memberId}:${patternId}:${roleId}:${day.iso}`);
|
|
266
302
|
}
|
|
267
303
|
|
|
268
|
-
|
|
269
|
-
|
|
304
|
+
addLinear(terms: Term[], op: "<=" | ">=" | "==", rhs: number, id?: string): void {
|
|
305
|
+
const constraint: SolverConstraint =
|
|
306
|
+
id === undefined
|
|
307
|
+
? { type: "linear", terms, op, rhs }
|
|
308
|
+
: { type: "linear", terms, op, rhs, id };
|
|
309
|
+
this.#constraints.push(constraint);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
addSoftLinear(
|
|
313
|
+
terms: Term[],
|
|
314
|
+
op: "<=" | ">=",
|
|
315
|
+
rhs: number,
|
|
316
|
+
penalty: number,
|
|
317
|
+
id?: string,
|
|
318
|
+
stage?: string,
|
|
319
|
+
): void {
|
|
320
|
+
if (!this.#isOptimizing()) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
this.#markObjectiveStageReference(stage);
|
|
324
|
+
const constraint: SolverConstraint =
|
|
325
|
+
stage === undefined
|
|
326
|
+
? { type: "soft_linear", terms, op, rhs, penalty, id }
|
|
327
|
+
: { type: "soft_linear", terms, op, rhs, penalty, id, stage };
|
|
328
|
+
this.#constraints.push(constraint);
|
|
270
329
|
}
|
|
271
330
|
|
|
272
331
|
addExactlyOne(vars: string[]): void {
|
|
@@ -334,10 +393,25 @@ export class ModelBuilder {
|
|
|
334
393
|
}
|
|
335
394
|
|
|
336
395
|
addPenalty(varName: string, weight: number): void {
|
|
396
|
+
this.#addPenalty(varName, weight);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#addPenalty(varName: string, weight: number, stage?: string): void {
|
|
400
|
+
if (!this.#isOptimizing()) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
this.#markObjectiveStageReference(stage);
|
|
337
404
|
// CP-SAT requires integer coefficients. Round to nearest integer.
|
|
338
405
|
const rounded = Math.round(weight);
|
|
339
406
|
if (rounded === 0) return;
|
|
340
|
-
|
|
407
|
+
const term = { var: varName, coeff: rounded };
|
|
408
|
+
if (stage !== undefined) {
|
|
409
|
+
const stageTerms = this.#objectiveStageTerms.get(stage) ?? [];
|
|
410
|
+
stageTerms.push(term);
|
|
411
|
+
this.#objectiveStageTerms.set(stage, stageTerms);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
this.#objective.push(term);
|
|
341
415
|
}
|
|
342
416
|
|
|
343
417
|
membersWithRole(roleId: string): SchedulingMember[] {
|
|
@@ -377,19 +451,60 @@ export class ModelBuilder {
|
|
|
377
451
|
return pattern.roleIds.some((role) => member.roleIds.includes(role));
|
|
378
452
|
}
|
|
379
453
|
|
|
454
|
+
#assignableRoleIds(member: SchedulingMember, pattern: ShiftPattern): string[] {
|
|
455
|
+
const patternRoleIds = pattern.roleIds;
|
|
456
|
+
const roles =
|
|
457
|
+
patternRoleIds && patternRoleIds.length > 0
|
|
458
|
+
? member.roleIds.filter((roleId) => patternRoleIds.includes(roleId))
|
|
459
|
+
: member.roleIds;
|
|
460
|
+
|
|
461
|
+
return [...new Set(roles)];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Returns the variables that can satisfy one coverage bucket for this member and pattern.
|
|
466
|
+
*
|
|
467
|
+
* Role coverage uses role-specific variables so a single shift assignment fills
|
|
468
|
+
* one concrete role. Skill-only coverage has no role to choose, so it uses the
|
|
469
|
+
* aggregate assignment variable directly.
|
|
470
|
+
*/
|
|
471
|
+
#coverageRoleAssignmentVars(
|
|
472
|
+
member: SchedulingMember,
|
|
473
|
+
pattern: ShiftPattern,
|
|
474
|
+
cov: CoverageRequirement,
|
|
475
|
+
day: SchedulingDay,
|
|
476
|
+
): string[] {
|
|
477
|
+
if (!cov.roleIds || cov.roleIds.length === 0) {
|
|
478
|
+
return [this.assignment(member.id, pattern.id, day)];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const assignableRoles = this.#assignableRoleIds(member, pattern);
|
|
482
|
+
const coverageRoles = assignableRoles.filter((roleId) => cov.roleIds.includes(roleId));
|
|
483
|
+
|
|
484
|
+
return coverageRoles.map((roleId) => this.#roleAssignment(member.id, pattern.id, roleId, day));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
#canCoverCoverage(
|
|
488
|
+
member: SchedulingMember,
|
|
489
|
+
pattern: ShiftPattern,
|
|
490
|
+
cov: CoverageRequirement,
|
|
491
|
+
): boolean {
|
|
492
|
+
const assignableRoles = this.#assignableRoleIds(member, pattern);
|
|
493
|
+
if (cov.roleIds && cov.roleIds.length > 0) {
|
|
494
|
+
return assignableRoles.some((roleId) => cov.roleIds.includes(roleId));
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
|
|
380
499
|
/**
|
|
381
500
|
* Checks if a shift pattern can be used on a specific day.
|
|
382
501
|
* Returns false if the pattern has dayOfWeek restrictions that exclude this day.
|
|
383
502
|
*/
|
|
384
|
-
patternAvailableOnDay(pattern: ShiftPattern, day:
|
|
385
|
-
// If pattern has no day restrictions, it's available every day
|
|
503
|
+
patternAvailableOnDay(pattern: ShiftPattern, day: SchedulingDay): boolean {
|
|
386
504
|
if (!pattern.dayOfWeek || pattern.dayOfWeek.length === 0) {
|
|
387
505
|
return true;
|
|
388
506
|
}
|
|
389
|
-
|
|
390
|
-
const date = parseDayString(day);
|
|
391
|
-
const dow = toDayOfWeekUTC(date);
|
|
392
|
-
return pattern.dayOfWeek.includes(dow);
|
|
507
|
+
return pattern.dayOfWeek.includes(day.dayOfWeek);
|
|
393
508
|
}
|
|
394
509
|
|
|
395
510
|
patternDuration(patternId: string): number {
|
|
@@ -401,13 +516,13 @@ export class ModelBuilder {
|
|
|
401
516
|
return end - start;
|
|
402
517
|
}
|
|
403
518
|
|
|
404
|
-
startMinutes(pattern: ShiftPattern, day:
|
|
405
|
-
const base = this.#dayOffset(day);
|
|
519
|
+
startMinutes(pattern: ShiftPattern, day: SchedulingDay): number {
|
|
520
|
+
const base = this.#dayOffset(day.iso);
|
|
406
521
|
return base + timeOfDayToMinutes(pattern.startTime);
|
|
407
522
|
}
|
|
408
523
|
|
|
409
|
-
endMinutes(pattern: ShiftPattern, day:
|
|
410
|
-
const base = this.#dayOffset(day);
|
|
524
|
+
endMinutes(pattern: ShiftPattern, day: SchedulingDay): number {
|
|
525
|
+
const base = this.#dayOffset(day.iso);
|
|
411
526
|
const startOffset = timeOfDayToMinutes(pattern.startTime);
|
|
412
527
|
const endOffset = normalizeEndMinutes(startOffset, timeOfDayToMinutes(pattern.endTime));
|
|
413
528
|
return base + endOffset;
|
|
@@ -418,14 +533,34 @@ export class ModelBuilder {
|
|
|
418
533
|
return this.#builtCompilation;
|
|
419
534
|
}
|
|
420
535
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
rule.compile(this);
|
|
424
|
-
}
|
|
536
|
+
this.#runRulePrechecks();
|
|
537
|
+
this.#applyRuleArtifacts();
|
|
425
538
|
|
|
426
|
-
// Build exclusion lookup from
|
|
539
|
+
// Build exclusion lookup from rule artifacts for coverage feasibility analysis.
|
|
427
540
|
const mandatoryExclusions = buildExclusionLookup(this.reporter.getExclusions());
|
|
428
541
|
|
|
542
|
+
// Link concrete role choices to the aggregate assignment used by intervals and
|
|
543
|
+
// shift-level rules. This is the CP-SAT channeling pattern: exactly one role
|
|
544
|
+
// literal is true iff the aggregate assignment is true. Role-less members on
|
|
545
|
+
// unrestricted shifts deliberately skip this link; skill-only coverage uses
|
|
546
|
+
// the aggregate variable directly because there is no role decision to report.
|
|
547
|
+
for (const emp of this.members) {
|
|
548
|
+
for (const pattern of this.shiftPatterns) {
|
|
549
|
+
if (!this.canAssign(emp, pattern)) continue;
|
|
550
|
+
for (const day of this.days) {
|
|
551
|
+
if (!this.patternAvailableOnDay(pattern, day)) continue;
|
|
552
|
+
const assignmentVar = this.assignment(emp.id, pattern.id, day);
|
|
553
|
+
const roleTerms = this.#assignableRoleIds(emp, pattern).map((roleId) => ({
|
|
554
|
+
var: this.#roleAssignment(emp.id, pattern.id, roleId, day),
|
|
555
|
+
coeff: 1,
|
|
556
|
+
}));
|
|
557
|
+
if (roleTerms.length === 0) continue;
|
|
558
|
+
|
|
559
|
+
this.addLinear([...roleTerms, { var: assignmentVar, coeff: -1 }], "==", 0);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
429
564
|
// 1. Assignment implies shift is active
|
|
430
565
|
for (const emp of this.members) {
|
|
431
566
|
for (const pattern of this.shiftPatterns) {
|
|
@@ -455,7 +590,7 @@ export class ModelBuilder {
|
|
|
455
590
|
const end = this.endMinutes(pattern, day);
|
|
456
591
|
const size = end - start;
|
|
457
592
|
|
|
458
|
-
const intervalName = `interval:${emp.id}:${pattern.id}:${day}`;
|
|
593
|
+
const intervalName = `interval:${emp.id}:${pattern.id}:${day.iso}`;
|
|
459
594
|
this.intervalVar(intervalName, start, end, size, presenceVar);
|
|
460
595
|
empIntervals.push(intervalName);
|
|
461
596
|
}
|
|
@@ -505,6 +640,8 @@ export class ModelBuilder {
|
|
|
505
640
|
}
|
|
506
641
|
|
|
507
642
|
for (const cov of this.coverage) {
|
|
643
|
+
const covDay = this.#dayByIso.get(cov.day);
|
|
644
|
+
if (!covDay) continue;
|
|
508
645
|
const covStart = timeOfDayToMinutes(cov.startTime);
|
|
509
646
|
const covEnd = normalizeEndMinutes(covStart, timeOfDayToMinutes(cov.endTime));
|
|
510
647
|
const covKey = cov.roleIds?.join(",") ?? cov.skillIds?.join(",") ?? "unknown";
|
|
@@ -551,7 +688,7 @@ export class ModelBuilder {
|
|
|
551
688
|
const lookupBucketStart = bucketStart % MINUTES_PER_DAY;
|
|
552
689
|
// Get patterns that overlap this time bucket, then filter by day availability
|
|
553
690
|
const allPatterns = patternsByBucketStart.get(lookupBucketStart) ?? [];
|
|
554
|
-
const patterns = allPatterns.filter((p) => this.patternAvailableOnDay(p,
|
|
691
|
+
const patterns = allPatterns.filter((p) => this.patternAvailableOnDay(p, covDay));
|
|
555
692
|
|
|
556
693
|
if (patterns.length === 0) {
|
|
557
694
|
recordBucketIssue(
|
|
@@ -580,6 +717,7 @@ export class ModelBuilder {
|
|
|
580
717
|
for (const emp of eligibleMembers) {
|
|
581
718
|
for (const pattern of patterns) {
|
|
582
719
|
if (!this.canAssign(emp, pattern)) continue;
|
|
720
|
+
if (!this.#canCoverCoverage(emp, pattern, cov)) continue;
|
|
583
721
|
assignableMembers.add(emp.id);
|
|
584
722
|
break;
|
|
585
723
|
}
|
|
@@ -654,7 +792,14 @@ export class ModelBuilder {
|
|
|
654
792
|
for (const pattern of patterns) {
|
|
655
793
|
for (const emp of eligibleMembers) {
|
|
656
794
|
if (!this.canAssign(emp, pattern)) continue;
|
|
657
|
-
|
|
795
|
+
for (const roleAssignment of this.#coverageRoleAssignmentVars(
|
|
796
|
+
emp,
|
|
797
|
+
pattern,
|
|
798
|
+
cov,
|
|
799
|
+
covDay,
|
|
800
|
+
)) {
|
|
801
|
+
coveringVarsSet.add(roleAssignment);
|
|
802
|
+
}
|
|
658
803
|
}
|
|
659
804
|
}
|
|
660
805
|
|
|
@@ -668,8 +813,8 @@ export class ModelBuilder {
|
|
|
668
813
|
const constraintId = `coverage:${covKey}:${cov.day}:${bucketStart}`;
|
|
669
814
|
|
|
670
815
|
if (cov.priority === "MANDATORY") {
|
|
671
|
-
this.addLinear(terms, ">=", cov.targetCount);
|
|
672
|
-
} else {
|
|
816
|
+
this.addLinear(terms, ">=", cov.targetCount, constraintId);
|
|
817
|
+
} else if (this.#isOptimizing()) {
|
|
673
818
|
this.addSoftLinear(
|
|
674
819
|
terms,
|
|
675
820
|
">=",
|
|
@@ -679,23 +824,26 @@ export class ModelBuilder {
|
|
|
679
824
|
);
|
|
680
825
|
}
|
|
681
826
|
|
|
682
|
-
// Track
|
|
683
|
-
this
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
827
|
+
// Track coverage constraints that can produce post-solve feedback.
|
|
828
|
+
if (cov.priority === "MANDATORY" || this.#isOptimizing()) {
|
|
829
|
+
this.reporter.trackConstraint({
|
|
830
|
+
id: constraintId,
|
|
831
|
+
type: "coverage",
|
|
832
|
+
source: cov.priority === "MANDATORY" ? "hard" : "soft",
|
|
833
|
+
description: `${cov.targetCount}x ${covKey} on ${cov.day} at ${formatMinutes(bucketStart)}`,
|
|
834
|
+
targetValue: cov.targetCount,
|
|
835
|
+
comparator: ">=",
|
|
836
|
+
day: cov.day,
|
|
837
|
+
timeSlot: formatMinutes(bucketStart),
|
|
838
|
+
roleIds: cov.roleIds,
|
|
839
|
+
skillIds: cov.skillIds,
|
|
840
|
+
context: {
|
|
841
|
+
days: [cov.day],
|
|
842
|
+
memberIds: eligibleMembers.map((e) => e.id),
|
|
843
|
+
},
|
|
844
|
+
group: cov.group,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
699
847
|
}
|
|
700
848
|
|
|
701
849
|
for (const issue of bucketIssues.values()) {
|
|
@@ -717,11 +865,25 @@ export class ModelBuilder {
|
|
|
717
865
|
group: cov.group,
|
|
718
866
|
});
|
|
719
867
|
}
|
|
720
|
-
// Note: soft coverage
|
|
868
|
+
// Note: optimized solves report soft coverage post-solve via tracked constraints.
|
|
721
869
|
}
|
|
722
870
|
}
|
|
723
871
|
|
|
724
|
-
|
|
872
|
+
if (this.#isOptimizing()) {
|
|
873
|
+
this.#addDefaultObjective();
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
this.#builtRequest = this.#buildRequest();
|
|
877
|
+
this.#builtCompilation = {
|
|
878
|
+
request: this.#builtRequest,
|
|
879
|
+
validation: this.reporter.getValidation(),
|
|
880
|
+
canSolve: !this.reporter.hasErrors(),
|
|
881
|
+
};
|
|
882
|
+
return this.#builtCompilation;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
#addDefaultObjective(): void {
|
|
886
|
+
// Default objective: shift minimization with optional fair distribution
|
|
725
887
|
//
|
|
726
888
|
// The objective has three components (see OBJECTIVE_WEIGHTS in utils.ts):
|
|
727
889
|
// a) Minimize number of active shift patterns (SHIFT_ACTIVE=1000)
|
|
@@ -785,14 +947,6 @@ export class ModelBuilder {
|
|
|
785
947
|
}
|
|
786
948
|
}
|
|
787
949
|
}
|
|
788
|
-
|
|
789
|
-
this.#builtRequest = this.#buildRequest();
|
|
790
|
-
this.#builtCompilation = {
|
|
791
|
-
request: this.#builtRequest,
|
|
792
|
-
validation: this.reporter.getValidation(),
|
|
793
|
-
canSolve: !this.reporter.hasErrors(),
|
|
794
|
-
};
|
|
795
|
-
return this.#builtCompilation;
|
|
796
950
|
}
|
|
797
951
|
|
|
798
952
|
/**
|
|
@@ -800,18 +954,203 @@ export class ModelBuilder {
|
|
|
800
954
|
* Call this after solving with the resolved assignments.
|
|
801
955
|
*/
|
|
802
956
|
validateSolution(assignments: ResolvedShiftAssignment[]): void {
|
|
803
|
-
const context:
|
|
957
|
+
const context: RuleCompileContext = {
|
|
804
958
|
members: this.members,
|
|
805
959
|
days: this.days,
|
|
806
960
|
shiftPatterns: this.shiftPatterns,
|
|
961
|
+
weekStartsOn: this.weekStartsOn,
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
for (const validator of this.#postSolveValidators) {
|
|
965
|
+
validator.run(assignments, this.reporter, context);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
calculateCost(assignments: readonly ShiftAssignment[]): CostContribution {
|
|
970
|
+
const context: RuleCompileContext = {
|
|
971
|
+
members: this.members,
|
|
972
|
+
days: this.days,
|
|
973
|
+
shiftPatterns: this.shiftPatterns,
|
|
974
|
+
weekStartsOn: this.weekStartsOn,
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
const entries = this.#costArtifacts.flatMap((artifact) =>
|
|
978
|
+
artifact.calculateCost ? [...artifact.calculateCost(assignments, context).entries] : [],
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
return { entries };
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
#runRulePrechecks(): void {
|
|
985
|
+
const context: RuleCompileContext = {
|
|
986
|
+
members: this.members,
|
|
987
|
+
days: this.days,
|
|
988
|
+
shiftPatterns: this.shiftPatterns,
|
|
989
|
+
weekStartsOn: this.weekStartsOn,
|
|
807
990
|
};
|
|
808
991
|
|
|
809
992
|
for (const rule of this.rules) {
|
|
810
|
-
rule.
|
|
993
|
+
const hasHardConstraint = rule.artifacts.some(
|
|
994
|
+
(artifact) => artifact.kind === "hard-constraint",
|
|
995
|
+
);
|
|
996
|
+
const shouldRunPrechecks = this.#isOptimizing() || hasHardConstraint;
|
|
997
|
+
if (!shouldRunPrechecks) continue;
|
|
998
|
+
|
|
999
|
+
for (const artifact of rule.artifacts) {
|
|
1000
|
+
if (artifact.kind === "pre-solve-feedback") {
|
|
1001
|
+
artifact.run(context, this.reporter);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
#applyRuleArtifacts(): void {
|
|
1008
|
+
this.#postSolveValidators = [];
|
|
1009
|
+
this.#costArtifacts = [];
|
|
1010
|
+
|
|
1011
|
+
for (const { rule, artifact } of this.#compiledArtifacts) {
|
|
1012
|
+
switch (artifact.kind) {
|
|
1013
|
+
case "variable": {
|
|
1014
|
+
if (artifact.variable.type === "bool") {
|
|
1015
|
+
this.boolVar(artifact.variable.name);
|
|
1016
|
+
} else {
|
|
1017
|
+
this.intVar(artifact.variable.name, artifact.variable.min, artifact.variable.max);
|
|
1018
|
+
}
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
case "hard-constraint": {
|
|
1022
|
+
const constraintId =
|
|
1023
|
+
artifact.validation.strategy === "report" ? artifact.validation.id : undefined;
|
|
1024
|
+
this.addLinear(
|
|
1025
|
+
[...artifact.terms],
|
|
1026
|
+
artifact.comparator,
|
|
1027
|
+
artifact.targetValue,
|
|
1028
|
+
constraintId,
|
|
1029
|
+
);
|
|
1030
|
+
this.#reportHardConstraint(rule, artifact);
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
case "soft-constraint": {
|
|
1034
|
+
if (!this.#isOptimizing()) {
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
const solverConstraintId =
|
|
1038
|
+
artifact.validation.strategy === "report" ? artifact.constraintId : undefined;
|
|
1039
|
+
this.addSoftLinear(
|
|
1040
|
+
[...artifact.terms],
|
|
1041
|
+
artifact.comparator,
|
|
1042
|
+
artifact.targetValue,
|
|
1043
|
+
artifact.penalty,
|
|
1044
|
+
solverConstraintId,
|
|
1045
|
+
artifact.stage,
|
|
1046
|
+
);
|
|
1047
|
+
this.#reportSoftConstraint(rule, artifact);
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
case "objective": {
|
|
1051
|
+
if (!this.#isOptimizing()) {
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
this.#markObjectiveStageReference(artifact.stage);
|
|
1055
|
+
for (const term of artifact.terms) {
|
|
1056
|
+
this.#addPenalty(term.var, term.coeff, artifact.stage);
|
|
1057
|
+
}
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1060
|
+
case "coverage-exclusion": {
|
|
1061
|
+
this.reporter.excludeFromCoverage({
|
|
1062
|
+
memberId: artifact.memberId,
|
|
1063
|
+
day: artifact.day,
|
|
1064
|
+
startTime: artifact.startTime,
|
|
1065
|
+
endTime: artifact.endTime,
|
|
1066
|
+
});
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
case "pre-solve-feedback": {
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
case "post-solve-feedback": {
|
|
1073
|
+
if (!this.#isOptimizing()) {
|
|
1074
|
+
break;
|
|
1075
|
+
}
|
|
1076
|
+
this.#postSolveValidators.push(artifact);
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
case "cost": {
|
|
1080
|
+
if (!this.#isOptimizing()) {
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
artifact.compileObjective?.(this);
|
|
1084
|
+
this.#costArtifacts.push(artifact);
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
811
1088
|
}
|
|
812
1089
|
}
|
|
813
1090
|
|
|
1091
|
+
#reportHardConstraint(
|
|
1092
|
+
rule: string,
|
|
1093
|
+
artifact: Extract<RuleArtifact, { kind: "hard-constraint" }>,
|
|
1094
|
+
): void {
|
|
1095
|
+
if (artifact.validation.strategy !== "report") {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
this.reporter.trackConstraint({
|
|
1100
|
+
id: artifact.validation.id,
|
|
1101
|
+
type: "rule",
|
|
1102
|
+
source: "hard",
|
|
1103
|
+
rule,
|
|
1104
|
+
description: artifact.description,
|
|
1105
|
+
targetValue: artifact.targetValue,
|
|
1106
|
+
comparator: artifact.comparator,
|
|
1107
|
+
context: artifact.context,
|
|
1108
|
+
group: artifact.group,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
#reportSoftConstraint(
|
|
1113
|
+
rule: string,
|
|
1114
|
+
artifact: Extract<RuleArtifact, { kind: "soft-constraint" }>,
|
|
1115
|
+
): void {
|
|
1116
|
+
if (artifact.validation.strategy !== "report") {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
this.reporter.trackConstraint({
|
|
1121
|
+
id: artifact.constraintId,
|
|
1122
|
+
type: "rule",
|
|
1123
|
+
source: "soft",
|
|
1124
|
+
rule,
|
|
1125
|
+
description: artifact.description,
|
|
1126
|
+
targetValue: artifact.targetValue,
|
|
1127
|
+
comparator: artifact.comparator,
|
|
1128
|
+
context: artifact.context,
|
|
1129
|
+
group: artifact.group,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
814
1133
|
#buildRequest(): SolverRequest {
|
|
1134
|
+
if (!this.#isOptimizing()) {
|
|
1135
|
+
return {
|
|
1136
|
+
variables: Array.from(this.#variables.values()),
|
|
1137
|
+
constraints: this.#constraints,
|
|
1138
|
+
mode: "satisfy",
|
|
1139
|
+
options: this.#satisfyOptions(),
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const objectiveStageOrder = this.#resolveObjectiveStageOrder();
|
|
1144
|
+
|
|
1145
|
+
if (objectiveStageOrder) {
|
|
1146
|
+
return {
|
|
1147
|
+
variables: Array.from(this.#variables.values()),
|
|
1148
|
+
constraints: this.#constraintsForStagedRequest(),
|
|
1149
|
+
objectiveStages: this.#buildObjectiveStages(objectiveStageOrder),
|
|
1150
|
+
options: this.options,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
815
1154
|
return {
|
|
816
1155
|
variables: Array.from(this.#variables.values()),
|
|
817
1156
|
constraints: this.#constraints,
|
|
@@ -821,6 +1160,135 @@ export class ModelBuilder {
|
|
|
821
1160
|
};
|
|
822
1161
|
}
|
|
823
1162
|
|
|
1163
|
+
#markObjectiveStageReference(stage: string | undefined): void {
|
|
1164
|
+
if (stage === undefined) {
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
this.#referencedObjectiveStages.add(stage);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
#isOptimizing(): boolean {
|
|
1171
|
+
return this.strategy.type === "optimize";
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
#satisfyOptions(): NonNullable<SolverRequest["options"]> {
|
|
1175
|
+
return {
|
|
1176
|
+
...this.options,
|
|
1177
|
+
solutionLimit: this.options?.solutionLimit ?? 1,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
#resolveObjectiveStageOrder(): readonly string[] | undefined {
|
|
1182
|
+
const referencedStages = [...this.#referencedObjectiveStages];
|
|
1183
|
+
if (referencedStages.length === 0) {
|
|
1184
|
+
return undefined;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
this.#validateReferencedObjectiveStages(referencedStages);
|
|
1188
|
+
|
|
1189
|
+
if (this.options?.solutionLimit === 1) {
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
"ModelBuilder objectiveStageOrder cannot be used with solverOptions.solutionLimit=1.",
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const stageOrder = this.#objectiveStageOrder;
|
|
1196
|
+
if (stageOrder === undefined) {
|
|
1197
|
+
if (referencedStages.length === 1) {
|
|
1198
|
+
return [referencedStages[0]!];
|
|
1199
|
+
}
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`Multiple objective stages were referenced (${referencedStages.join(", ")}); provide ModelBuilder objectiveStageOrder explicitly.`,
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (referencedStages.includes(TARGET_PEAK_CONCURRENT_ASSIGNMENTS_OBJECTIVE_STAGE_ID)) {
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
"targetPeakConcurrentAssignments cannot be combined with ModelBuilder objectiveStageOrder until the multi-stage objective API is public.",
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (stageOrder.length === 0) {
|
|
1212
|
+
throw new Error("ModelBuilder objectiveStageOrder cannot be empty when provided.");
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const declaredStages = new Set<string>();
|
|
1216
|
+
for (const stage of stageOrder) {
|
|
1217
|
+
if (stage.trim() === "") {
|
|
1218
|
+
throw new Error("ModelBuilder objectiveStageOrder cannot contain an empty stage id.");
|
|
1219
|
+
}
|
|
1220
|
+
if (stage === UNSTAGED_OBJECTIVE_STAGE_ID) {
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
`ModelBuilder objectiveStageOrder cannot contain reserved stage id "${UNSTAGED_OBJECTIVE_STAGE_ID}".`,
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
if (declaredStages.has(stage)) {
|
|
1226
|
+
throw new Error(`Duplicate objective stage id "${stage}" in objectiveStageOrder.`);
|
|
1227
|
+
}
|
|
1228
|
+
declaredStages.add(stage);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const allowedStages = new Set(declaredStages);
|
|
1232
|
+
allowedStages.add(UNSTAGED_OBJECTIVE_STAGE_ID);
|
|
1233
|
+
for (const stage of referencedStages) {
|
|
1234
|
+
if (stage === UNSTAGED_OBJECTIVE_STAGE_ID) {
|
|
1235
|
+
throw new Error(
|
|
1236
|
+
`Rule artifacts cannot use reserved objective stage id "${UNSTAGED_OBJECTIVE_STAGE_ID}".`,
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
if (!allowedStages.has(stage)) {
|
|
1240
|
+
throw new Error(
|
|
1241
|
+
`Objective stage "${stage}" is not declared in ModelBuilder objectiveStageOrder.`,
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return stageOrder;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
#validateReferencedObjectiveStages(referencedStages: readonly string[]): void {
|
|
1250
|
+
for (const stage of referencedStages) {
|
|
1251
|
+
if (stage.trim() === "") {
|
|
1252
|
+
throw new Error("Rule artifacts cannot use an empty objective stage id.");
|
|
1253
|
+
}
|
|
1254
|
+
if (stage === UNSTAGED_OBJECTIVE_STAGE_ID) {
|
|
1255
|
+
throw new Error(
|
|
1256
|
+
`Rule artifacts cannot use reserved objective stage id "${UNSTAGED_OBJECTIVE_STAGE_ID}".`,
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
#constraintsForStagedRequest(): SolverConstraint[] {
|
|
1263
|
+
return this.#constraints.map((constraint) => {
|
|
1264
|
+
if (constraint.type !== "soft_linear" || constraint.stage !== undefined) {
|
|
1265
|
+
return constraint;
|
|
1266
|
+
}
|
|
1267
|
+
return { ...constraint, stage: UNSTAGED_OBJECTIVE_STAGE_ID };
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
#buildObjectiveStages(objectiveStageOrder: readonly string[]): SolverObjectiveStage[] {
|
|
1272
|
+
const stages = objectiveStageOrder.map((id) => ({
|
|
1273
|
+
id,
|
|
1274
|
+
sense: "minimize" as const,
|
|
1275
|
+
terms: this.#objectiveStageTerms.get(id) ?? [],
|
|
1276
|
+
}));
|
|
1277
|
+
const hasTailSoftConstraint = this.#constraints.some(
|
|
1278
|
+
(constraint) => constraint.type === "soft_linear" && constraint.stage === undefined,
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
if (this.#objective.length > 0 || hasTailSoftConstraint) {
|
|
1282
|
+
stages.push({
|
|
1283
|
+
id: UNSTAGED_OBJECTIVE_STAGE_ID,
|
|
1284
|
+
sense: "minimize",
|
|
1285
|
+
terms: this.#objective,
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
return stages;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
824
1292
|
#dayOffset(day: string): number {
|
|
825
1293
|
const idx = this.#dayIndex.get(day);
|
|
826
1294
|
if (idx === undefined) {
|
|
@@ -844,6 +1312,23 @@ type ExclusionWindow = {
|
|
|
844
1312
|
endMinutes: number;
|
|
845
1313
|
};
|
|
846
1314
|
|
|
1315
|
+
function resolveModelSolveStrategy(strategy: unknown): ModelSolveStrategy {
|
|
1316
|
+
if (strategy === undefined) {
|
|
1317
|
+
return { type: "optimize" };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (typeof strategy !== "object" || strategy === null || !("type" in strategy)) {
|
|
1321
|
+
throw new Error("ModelBuilder strategy must be an object with a type field.");
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const strategyType = (strategy as { type: unknown }).type;
|
|
1325
|
+
if (strategyType === "optimize" || strategyType === "feasibility-only") {
|
|
1326
|
+
return { type: strategyType };
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
throw new Error(`Unknown ModelBuilder strategy "${String(strategyType)}".`);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
847
1332
|
function recordBucketIssue(
|
|
848
1333
|
bucketIssues: Map<string, BucketIssueGroup>,
|
|
849
1334
|
issue: Omit<BucketIssueGroup, "bucketStarts">,
|