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,8 +1,10 @@
|
|
|
1
|
-
import { resolveDaysFromPeriod
|
|
2
|
-
import { MINUTES_PER_DAY, normalizeEndMinutes, OBJECTIVE_WEIGHTS,
|
|
1
|
+
import { resolveDaysFromPeriod } from "../datetime.utils.js";
|
|
2
|
+
import { MINUTES_PER_DAY, normalizeEndMinutes, OBJECTIVE_WEIGHTS, priorityToPenalty, timeOfDayToMinutes, } from "./utils.js";
|
|
3
3
|
import { buildCpsatRules } from "./rules/resolver.js";
|
|
4
|
-
import {
|
|
4
|
+
import { builtInCpsatRuleRegistry } from "./rules/registry.js";
|
|
5
|
+
import { TARGET_PEAK_CONCURRENT_ASSIGNMENTS_OBJECTIVE_STAGE_ID } from "./rules/target-peak-concurrent-assignments.js";
|
|
5
6
|
import { ValidationReporterImpl } from "./validation-reporter.js";
|
|
7
|
+
const UNSTAGED_OBJECTIVE_STAGE_ID = "__dabke_unstaged__";
|
|
6
8
|
/**
|
|
7
9
|
* Compilation context that creates variables, constraints, and objectives
|
|
8
10
|
* and emits a `SolverRequest` for the Python CP-SAT solver service.
|
|
@@ -17,6 +19,7 @@ export class ModelBuilder {
|
|
|
17
19
|
rules;
|
|
18
20
|
weekStartsOn;
|
|
19
21
|
options;
|
|
22
|
+
strategy;
|
|
20
23
|
coverageBucketMinutes;
|
|
21
24
|
reporter;
|
|
22
25
|
fairDistribution;
|
|
@@ -25,21 +28,38 @@ export class ModelBuilder {
|
|
|
25
28
|
#variables = new Map();
|
|
26
29
|
#constraints = [];
|
|
27
30
|
#objective = [];
|
|
31
|
+
#objectiveStageTerms = new Map();
|
|
32
|
+
#referencedObjectiveStages = new Set();
|
|
33
|
+
#objectiveStageOrder;
|
|
28
34
|
#dayIndex = new Map();
|
|
35
|
+
#dayByIso = new Map();
|
|
29
36
|
#shiftPatternMap = new Map();
|
|
30
37
|
#builtRequest;
|
|
31
38
|
#builtCompilation;
|
|
39
|
+
#compiledArtifacts = [];
|
|
40
|
+
#postSolveValidators = [];
|
|
41
|
+
#costArtifacts = [];
|
|
32
42
|
constructor(config) {
|
|
33
43
|
// Validate IDs don't contain the separator character
|
|
34
44
|
for (const member of config.members) {
|
|
35
45
|
if (member.id.includes(":")) {
|
|
36
46
|
throw new Error(`Member ID "${member.id}" cannot contain colons`);
|
|
37
47
|
}
|
|
48
|
+
for (const roleId of member.roleIds) {
|
|
49
|
+
if (roleId.includes(":")) {
|
|
50
|
+
throw new Error(`Role ID "${roleId}" cannot contain colons`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
38
53
|
}
|
|
39
54
|
for (const pattern of config.shiftPatterns) {
|
|
40
55
|
if (pattern.id.includes(":")) {
|
|
41
56
|
throw new Error(`Shift pattern ID "${pattern.id}" cannot contain colons`);
|
|
42
57
|
}
|
|
58
|
+
for (const roleId of pattern.roleIds ?? []) {
|
|
59
|
+
if (roleId.includes(":")) {
|
|
60
|
+
throw new Error(`Role ID "${roleId}" cannot contain colons`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
43
63
|
}
|
|
44
64
|
// Validate coverage requirements have at least roles or skills
|
|
45
65
|
for (const cov of config.coverage) {
|
|
@@ -48,22 +68,41 @@ export class ModelBuilder {
|
|
|
48
68
|
if (!hasRoles && !hasSkills) {
|
|
49
69
|
throw new Error(`Coverage requirement for day "${cov.day}" must have at least one of roles or skills`);
|
|
50
70
|
}
|
|
71
|
+
for (const roleId of cov.roleIds ?? []) {
|
|
72
|
+
if (roleId.includes(":")) {
|
|
73
|
+
throw new Error(`Role ID "${roleId}" cannot contain colons`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
51
76
|
}
|
|
52
77
|
this.members = config.members;
|
|
53
78
|
this.shiftPatterns = config.shiftPatterns;
|
|
54
79
|
this.days = resolveDaysFromPeriod(config.schedulingPeriod);
|
|
55
80
|
this.coverage = config.coverage;
|
|
81
|
+
const ruleCompileContext = {
|
|
82
|
+
members: this.members,
|
|
83
|
+
shiftPatterns: this.shiftPatterns,
|
|
84
|
+
days: this.days,
|
|
85
|
+
weekStartsOn: config.weekStartsOn ?? "monday",
|
|
86
|
+
coverageBucketMinutes: config.coverageBucketMinutes ?? 15,
|
|
87
|
+
};
|
|
56
88
|
const compiledRuleConfigs = config.ruleConfigs
|
|
57
|
-
? buildCpsatRules(config.ruleConfigs, this.members, config.
|
|
89
|
+
? buildCpsatRules(config.ruleConfigs, this.members, ruleCompileContext, config.ruleRegistry ?? builtInCpsatRuleRegistry)
|
|
58
90
|
: [];
|
|
59
91
|
this.rules = [...compiledRuleConfigs, ...(config.rules ?? [])];
|
|
60
92
|
this.weekStartsOn = config.weekStartsOn ?? "monday";
|
|
61
93
|
this.options = config.solverOptions;
|
|
94
|
+
this.strategy = resolveModelSolveStrategy(config.strategy);
|
|
95
|
+
this.#objectiveStageOrder =
|
|
96
|
+
config.objectiveStageOrder === undefined ? undefined : [...config.objectiveStageOrder];
|
|
62
97
|
this.coverageBucketMinutes = config.coverageBucketMinutes ?? 15;
|
|
63
98
|
this.reporter = config.reporter ?? new ValidationReporterImpl();
|
|
64
99
|
this.fairDistribution = config.fairDistribution ?? true;
|
|
65
|
-
this.days.forEach((day, idx) =>
|
|
100
|
+
this.days.forEach((day, idx) => {
|
|
101
|
+
this.#dayIndex.set(day.iso, idx);
|
|
102
|
+
this.#dayByIso.set(day.iso, day);
|
|
103
|
+
});
|
|
66
104
|
this.shiftPatterns.forEach((pattern) => this.#shiftPatternMap.set(pattern.id, pattern));
|
|
105
|
+
this.#compiledArtifacts = this.rules.flatMap((rule) => rule.artifacts.map((artifact) => ({ rule: rule.rule, artifact })));
|
|
67
106
|
}
|
|
68
107
|
boolVar(name) {
|
|
69
108
|
const existing = this.#variables.get(name);
|
|
@@ -88,16 +127,40 @@ export class ModelBuilder {
|
|
|
88
127
|
return name;
|
|
89
128
|
}
|
|
90
129
|
shiftActive(patternId, day) {
|
|
91
|
-
return this.boolVar(`shift:${patternId}:${day}`);
|
|
130
|
+
return this.boolVar(`shift:${patternId}:${day.iso}`);
|
|
92
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Returns the aggregate shift assignment variable for a member, pattern, and day.
|
|
134
|
+
*
|
|
135
|
+
* @remarks
|
|
136
|
+
* This variable means "the member works this shift pattern on this day". It is
|
|
137
|
+
* the presence literal for optional interval variables and the common variable
|
|
138
|
+
* used by shift-level rules, objectives, and skill-only coverage. Role-specific
|
|
139
|
+
* assignments are modeled separately and linked to this aggregate variable when
|
|
140
|
+
* a concrete role choice is available.
|
|
141
|
+
*/
|
|
93
142
|
assignment(memberId, patternId, day) {
|
|
94
|
-
return this.boolVar(`assign:${memberId}:${patternId}:${day}`);
|
|
143
|
+
return this.boolVar(`assign:${memberId}:${patternId}:${day.iso}`);
|
|
95
144
|
}
|
|
96
|
-
|
|
97
|
-
|
|
145
|
+
/** Returns the role-specific assignment variable for a concrete role choice. */
|
|
146
|
+
#roleAssignment(memberId, patternId, roleId, day) {
|
|
147
|
+
return this.boolVar(`assign_role:${memberId}:${patternId}:${roleId}:${day.iso}`);
|
|
98
148
|
}
|
|
99
|
-
|
|
100
|
-
|
|
149
|
+
addLinear(terms, op, rhs, id) {
|
|
150
|
+
const constraint = id === undefined
|
|
151
|
+
? { type: "linear", terms, op, rhs }
|
|
152
|
+
: { type: "linear", terms, op, rhs, id };
|
|
153
|
+
this.#constraints.push(constraint);
|
|
154
|
+
}
|
|
155
|
+
addSoftLinear(terms, op, rhs, penalty, id, stage) {
|
|
156
|
+
if (!this.#isOptimizing()) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.#markObjectiveStageReference(stage);
|
|
160
|
+
const constraint = stage === undefined
|
|
161
|
+
? { type: "soft_linear", terms, op, rhs, penalty, id }
|
|
162
|
+
: { type: "soft_linear", terms, op, rhs, penalty, id, stage };
|
|
163
|
+
this.#constraints.push(constraint);
|
|
101
164
|
}
|
|
102
165
|
addExactlyOne(vars) {
|
|
103
166
|
if (vars.length === 0)
|
|
@@ -153,11 +216,25 @@ export class ModelBuilder {
|
|
|
153
216
|
this.#constraints.push({ type: "no_overlap", intervals });
|
|
154
217
|
}
|
|
155
218
|
addPenalty(varName, weight) {
|
|
219
|
+
this.#addPenalty(varName, weight);
|
|
220
|
+
}
|
|
221
|
+
#addPenalty(varName, weight, stage) {
|
|
222
|
+
if (!this.#isOptimizing()) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
this.#markObjectiveStageReference(stage);
|
|
156
226
|
// CP-SAT requires integer coefficients. Round to nearest integer.
|
|
157
227
|
const rounded = Math.round(weight);
|
|
158
228
|
if (rounded === 0)
|
|
159
229
|
return;
|
|
160
|
-
|
|
230
|
+
const term = { var: varName, coeff: rounded };
|
|
231
|
+
if (stage !== undefined) {
|
|
232
|
+
const stageTerms = this.#objectiveStageTerms.get(stage) ?? [];
|
|
233
|
+
stageTerms.push(term);
|
|
234
|
+
this.#objectiveStageTerms.set(stage, stageTerms);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.#objective.push(term);
|
|
161
238
|
}
|
|
162
239
|
membersWithRole(roleId) {
|
|
163
240
|
return this.members.filter((m) => m.roleIds.includes(roleId));
|
|
@@ -193,19 +270,44 @@ export class ModelBuilder {
|
|
|
193
270
|
}
|
|
194
271
|
return pattern.roleIds.some((role) => member.roleIds.includes(role));
|
|
195
272
|
}
|
|
273
|
+
#assignableRoleIds(member, pattern) {
|
|
274
|
+
const patternRoleIds = pattern.roleIds;
|
|
275
|
+
const roles = patternRoleIds && patternRoleIds.length > 0
|
|
276
|
+
? member.roleIds.filter((roleId) => patternRoleIds.includes(roleId))
|
|
277
|
+
: member.roleIds;
|
|
278
|
+
return [...new Set(roles)];
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Returns the variables that can satisfy one coverage bucket for this member and pattern.
|
|
282
|
+
*
|
|
283
|
+
* Role coverage uses role-specific variables so a single shift assignment fills
|
|
284
|
+
* one concrete role. Skill-only coverage has no role to choose, so it uses the
|
|
285
|
+
* aggregate assignment variable directly.
|
|
286
|
+
*/
|
|
287
|
+
#coverageRoleAssignmentVars(member, pattern, cov, day) {
|
|
288
|
+
if (!cov.roleIds || cov.roleIds.length === 0) {
|
|
289
|
+
return [this.assignment(member.id, pattern.id, day)];
|
|
290
|
+
}
|
|
291
|
+
const assignableRoles = this.#assignableRoleIds(member, pattern);
|
|
292
|
+
const coverageRoles = assignableRoles.filter((roleId) => cov.roleIds.includes(roleId));
|
|
293
|
+
return coverageRoles.map((roleId) => this.#roleAssignment(member.id, pattern.id, roleId, day));
|
|
294
|
+
}
|
|
295
|
+
#canCoverCoverage(member, pattern, cov) {
|
|
296
|
+
const assignableRoles = this.#assignableRoleIds(member, pattern);
|
|
297
|
+
if (cov.roleIds && cov.roleIds.length > 0) {
|
|
298
|
+
return assignableRoles.some((roleId) => cov.roleIds.includes(roleId));
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
196
302
|
/**
|
|
197
303
|
* Checks if a shift pattern can be used on a specific day.
|
|
198
304
|
* Returns false if the pattern has dayOfWeek restrictions that exclude this day.
|
|
199
305
|
*/
|
|
200
306
|
patternAvailableOnDay(pattern, day) {
|
|
201
|
-
// If pattern has no day restrictions, it's available every day
|
|
202
307
|
if (!pattern.dayOfWeek || pattern.dayOfWeek.length === 0) {
|
|
203
308
|
return true;
|
|
204
309
|
}
|
|
205
|
-
|
|
206
|
-
const date = parseDayString(day);
|
|
207
|
-
const dow = toDayOfWeekUTC(date);
|
|
208
|
-
return pattern.dayOfWeek.includes(dow);
|
|
310
|
+
return pattern.dayOfWeek.includes(day.dayOfWeek);
|
|
209
311
|
}
|
|
210
312
|
patternDuration(patternId) {
|
|
211
313
|
const pattern = this.#shiftPatternMap.get(patternId);
|
|
@@ -216,11 +318,11 @@ export class ModelBuilder {
|
|
|
216
318
|
return end - start;
|
|
217
319
|
}
|
|
218
320
|
startMinutes(pattern, day) {
|
|
219
|
-
const base = this.#dayOffset(day);
|
|
321
|
+
const base = this.#dayOffset(day.iso);
|
|
220
322
|
return base + timeOfDayToMinutes(pattern.startTime);
|
|
221
323
|
}
|
|
222
324
|
endMinutes(pattern, day) {
|
|
223
|
-
const base = this.#dayOffset(day);
|
|
325
|
+
const base = this.#dayOffset(day.iso);
|
|
224
326
|
const startOffset = timeOfDayToMinutes(pattern.startTime);
|
|
225
327
|
const endOffset = normalizeEndMinutes(startOffset, timeOfDayToMinutes(pattern.endTime));
|
|
226
328
|
return base + endOffset;
|
|
@@ -229,12 +331,33 @@ export class ModelBuilder {
|
|
|
229
331
|
if (this.#builtCompilation) {
|
|
230
332
|
return this.#builtCompilation;
|
|
231
333
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
// Build exclusion lookup from reporter (populated by rules during compile)
|
|
334
|
+
this.#runRulePrechecks();
|
|
335
|
+
this.#applyRuleArtifacts();
|
|
336
|
+
// Build exclusion lookup from rule artifacts for coverage feasibility analysis.
|
|
237
337
|
const mandatoryExclusions = buildExclusionLookup(this.reporter.getExclusions());
|
|
338
|
+
// Link concrete role choices to the aggregate assignment used by intervals and
|
|
339
|
+
// shift-level rules. This is the CP-SAT channeling pattern: exactly one role
|
|
340
|
+
// literal is true iff the aggregate assignment is true. Role-less members on
|
|
341
|
+
// unrestricted shifts deliberately skip this link; skill-only coverage uses
|
|
342
|
+
// the aggregate variable directly because there is no role decision to report.
|
|
343
|
+
for (const emp of this.members) {
|
|
344
|
+
for (const pattern of this.shiftPatterns) {
|
|
345
|
+
if (!this.canAssign(emp, pattern))
|
|
346
|
+
continue;
|
|
347
|
+
for (const day of this.days) {
|
|
348
|
+
if (!this.patternAvailableOnDay(pattern, day))
|
|
349
|
+
continue;
|
|
350
|
+
const assignmentVar = this.assignment(emp.id, pattern.id, day);
|
|
351
|
+
const roleTerms = this.#assignableRoleIds(emp, pattern).map((roleId) => ({
|
|
352
|
+
var: this.#roleAssignment(emp.id, pattern.id, roleId, day),
|
|
353
|
+
coeff: 1,
|
|
354
|
+
}));
|
|
355
|
+
if (roleTerms.length === 0)
|
|
356
|
+
continue;
|
|
357
|
+
this.addLinear([...roleTerms, { var: assignmentVar, coeff: -1 }], "==", 0);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
238
361
|
// 1. Assignment implies shift is active
|
|
239
362
|
for (const emp of this.members) {
|
|
240
363
|
for (const pattern of this.shiftPatterns) {
|
|
@@ -261,7 +384,7 @@ export class ModelBuilder {
|
|
|
261
384
|
const start = this.startMinutes(pattern, day);
|
|
262
385
|
const end = this.endMinutes(pattern, day);
|
|
263
386
|
const size = end - start;
|
|
264
|
-
const intervalName = `interval:${emp.id}:${pattern.id}:${day}`;
|
|
387
|
+
const intervalName = `interval:${emp.id}:${pattern.id}:${day.iso}`;
|
|
265
388
|
this.intervalVar(intervalName, start, end, size, presenceVar);
|
|
266
389
|
empIntervals.push(intervalName);
|
|
267
390
|
}
|
|
@@ -304,6 +427,9 @@ export class ModelBuilder {
|
|
|
304
427
|
patternsByBucketStart.set(bucketStart, patterns);
|
|
305
428
|
}
|
|
306
429
|
for (const cov of this.coverage) {
|
|
430
|
+
const covDay = this.#dayByIso.get(cov.day);
|
|
431
|
+
if (!covDay)
|
|
432
|
+
continue;
|
|
307
433
|
const covStart = timeOfDayToMinutes(cov.startTime);
|
|
308
434
|
const covEnd = normalizeEndMinutes(covStart, timeOfDayToMinutes(cov.endTime));
|
|
309
435
|
const covKey = cov.roleIds?.join(",") ?? cov.skillIds?.join(",") ?? "unknown";
|
|
@@ -345,7 +471,7 @@ export class ModelBuilder {
|
|
|
345
471
|
const lookupBucketStart = bucketStart % MINUTES_PER_DAY;
|
|
346
472
|
// Get patterns that overlap this time bucket, then filter by day availability
|
|
347
473
|
const allPatterns = patternsByBucketStart.get(lookupBucketStart) ?? [];
|
|
348
|
-
const patterns = allPatterns.filter((p) => this.patternAvailableOnDay(p,
|
|
474
|
+
const patterns = allPatterns.filter((p) => this.patternAvailableOnDay(p, covDay));
|
|
349
475
|
if (patterns.length === 0) {
|
|
350
476
|
recordBucketIssue(bucketIssues, {
|
|
351
477
|
key: "no_patterns",
|
|
@@ -368,6 +494,8 @@ export class ModelBuilder {
|
|
|
368
494
|
for (const pattern of patterns) {
|
|
369
495
|
if (!this.canAssign(emp, pattern))
|
|
370
496
|
continue;
|
|
497
|
+
if (!this.#canCoverCoverage(emp, pattern, cov))
|
|
498
|
+
continue;
|
|
371
499
|
assignableMembers.add(emp.id);
|
|
372
500
|
break;
|
|
373
501
|
}
|
|
@@ -425,7 +553,9 @@ export class ModelBuilder {
|
|
|
425
553
|
for (const emp of eligibleMembers) {
|
|
426
554
|
if (!this.canAssign(emp, pattern))
|
|
427
555
|
continue;
|
|
428
|
-
|
|
556
|
+
for (const roleAssignment of this.#coverageRoleAssignmentVars(emp, pattern, cov, covDay)) {
|
|
557
|
+
coveringVarsSet.add(roleAssignment);
|
|
558
|
+
}
|
|
429
559
|
}
|
|
430
560
|
}
|
|
431
561
|
const coveringVars = [...coveringVarsSet];
|
|
@@ -435,28 +565,31 @@ export class ModelBuilder {
|
|
|
435
565
|
const terms = coveringVars.map((v) => ({ var: v, coeff: 1 }));
|
|
436
566
|
const constraintId = `coverage:${covKey}:${cov.day}:${bucketStart}`;
|
|
437
567
|
if (cov.priority === "MANDATORY") {
|
|
438
|
-
this.addLinear(terms, ">=", cov.targetCount);
|
|
568
|
+
this.addLinear(terms, ">=", cov.targetCount, constraintId);
|
|
439
569
|
}
|
|
440
|
-
else {
|
|
570
|
+
else if (this.#isOptimizing()) {
|
|
441
571
|
this.addSoftLinear(terms, ">=", cov.targetCount, priorityToPenalty(cov.priority), constraintId);
|
|
442
572
|
}
|
|
443
|
-
// Track
|
|
444
|
-
this
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
573
|
+
// Track coverage constraints that can produce post-solve feedback.
|
|
574
|
+
if (cov.priority === "MANDATORY" || this.#isOptimizing()) {
|
|
575
|
+
this.reporter.trackConstraint({
|
|
576
|
+
id: constraintId,
|
|
577
|
+
type: "coverage",
|
|
578
|
+
source: cov.priority === "MANDATORY" ? "hard" : "soft",
|
|
579
|
+
description: `${cov.targetCount}x ${covKey} on ${cov.day} at ${formatMinutes(bucketStart)}`,
|
|
580
|
+
targetValue: cov.targetCount,
|
|
581
|
+
comparator: ">=",
|
|
582
|
+
day: cov.day,
|
|
583
|
+
timeSlot: formatMinutes(bucketStart),
|
|
584
|
+
roleIds: cov.roleIds,
|
|
585
|
+
skillIds: cov.skillIds,
|
|
586
|
+
context: {
|
|
587
|
+
days: [cov.day],
|
|
588
|
+
memberIds: eligibleMembers.map((e) => e.id),
|
|
589
|
+
},
|
|
590
|
+
group: cov.group,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
460
593
|
}
|
|
461
594
|
for (const issue of bucketIssues.values()) {
|
|
462
595
|
const ranges = bucketStartsToRanges(issue.bucketStarts, bucket, covEnd).map((range) => `${formatMinutes(range.start)}-${formatMinutes(range.end)}`);
|
|
@@ -474,10 +607,22 @@ export class ModelBuilder {
|
|
|
474
607
|
group: cov.group,
|
|
475
608
|
});
|
|
476
609
|
}
|
|
477
|
-
// Note: soft coverage
|
|
610
|
+
// Note: optimized solves report soft coverage post-solve via tracked constraints.
|
|
478
611
|
}
|
|
479
612
|
}
|
|
480
|
-
|
|
613
|
+
if (this.#isOptimizing()) {
|
|
614
|
+
this.#addDefaultObjective();
|
|
615
|
+
}
|
|
616
|
+
this.#builtRequest = this.#buildRequest();
|
|
617
|
+
this.#builtCompilation = {
|
|
618
|
+
request: this.#builtRequest,
|
|
619
|
+
validation: this.reporter.getValidation(),
|
|
620
|
+
canSolve: !this.reporter.hasErrors(),
|
|
621
|
+
};
|
|
622
|
+
return this.#builtCompilation;
|
|
623
|
+
}
|
|
624
|
+
#addDefaultObjective() {
|
|
625
|
+
// Default objective: shift minimization with optional fair distribution
|
|
481
626
|
//
|
|
482
627
|
// The objective has three components (see OBJECTIVE_WEIGHTS in utils.ts):
|
|
483
628
|
// a) Minimize number of active shift patterns (SHIFT_ACTIVE=1000)
|
|
@@ -538,13 +683,6 @@ export class ModelBuilder {
|
|
|
538
683
|
}
|
|
539
684
|
}
|
|
540
685
|
}
|
|
541
|
-
this.#builtRequest = this.#buildRequest();
|
|
542
|
-
this.#builtCompilation = {
|
|
543
|
-
request: this.#builtRequest,
|
|
544
|
-
validation: this.reporter.getValidation(),
|
|
545
|
-
canSolve: !this.reporter.hasErrors(),
|
|
546
|
-
};
|
|
547
|
-
return this.#builtCompilation;
|
|
548
686
|
}
|
|
549
687
|
/**
|
|
550
688
|
* Run post-solve validation on all rules.
|
|
@@ -555,12 +693,160 @@ export class ModelBuilder {
|
|
|
555
693
|
members: this.members,
|
|
556
694
|
days: this.days,
|
|
557
695
|
shiftPatterns: this.shiftPatterns,
|
|
696
|
+
weekStartsOn: this.weekStartsOn,
|
|
697
|
+
};
|
|
698
|
+
for (const validator of this.#postSolveValidators) {
|
|
699
|
+
validator.run(assignments, this.reporter, context);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
calculateCost(assignments) {
|
|
703
|
+
const context = {
|
|
704
|
+
members: this.members,
|
|
705
|
+
days: this.days,
|
|
706
|
+
shiftPatterns: this.shiftPatterns,
|
|
707
|
+
weekStartsOn: this.weekStartsOn,
|
|
708
|
+
};
|
|
709
|
+
const entries = this.#costArtifacts.flatMap((artifact) => artifact.calculateCost ? [...artifact.calculateCost(assignments, context).entries] : []);
|
|
710
|
+
return { entries };
|
|
711
|
+
}
|
|
712
|
+
#runRulePrechecks() {
|
|
713
|
+
const context = {
|
|
714
|
+
members: this.members,
|
|
715
|
+
days: this.days,
|
|
716
|
+
shiftPatterns: this.shiftPatterns,
|
|
717
|
+
weekStartsOn: this.weekStartsOn,
|
|
558
718
|
};
|
|
559
719
|
for (const rule of this.rules) {
|
|
560
|
-
rule.
|
|
720
|
+
const hasHardConstraint = rule.artifacts.some((artifact) => artifact.kind === "hard-constraint");
|
|
721
|
+
const shouldRunPrechecks = this.#isOptimizing() || hasHardConstraint;
|
|
722
|
+
if (!shouldRunPrechecks)
|
|
723
|
+
continue;
|
|
724
|
+
for (const artifact of rule.artifacts) {
|
|
725
|
+
if (artifact.kind === "pre-solve-feedback") {
|
|
726
|
+
artifact.run(context, this.reporter);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
#applyRuleArtifacts() {
|
|
732
|
+
this.#postSolveValidators = [];
|
|
733
|
+
this.#costArtifacts = [];
|
|
734
|
+
for (const { rule, artifact } of this.#compiledArtifacts) {
|
|
735
|
+
switch (artifact.kind) {
|
|
736
|
+
case "variable": {
|
|
737
|
+
if (artifact.variable.type === "bool") {
|
|
738
|
+
this.boolVar(artifact.variable.name);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
this.intVar(artifact.variable.name, artifact.variable.min, artifact.variable.max);
|
|
742
|
+
}
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
case "hard-constraint": {
|
|
746
|
+
const constraintId = artifact.validation.strategy === "report" ? artifact.validation.id : undefined;
|
|
747
|
+
this.addLinear([...artifact.terms], artifact.comparator, artifact.targetValue, constraintId);
|
|
748
|
+
this.#reportHardConstraint(rule, artifact);
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
case "soft-constraint": {
|
|
752
|
+
if (!this.#isOptimizing()) {
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
const solverConstraintId = artifact.validation.strategy === "report" ? artifact.constraintId : undefined;
|
|
756
|
+
this.addSoftLinear([...artifact.terms], artifact.comparator, artifact.targetValue, artifact.penalty, solverConstraintId, artifact.stage);
|
|
757
|
+
this.#reportSoftConstraint(rule, artifact);
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
case "objective": {
|
|
761
|
+
if (!this.#isOptimizing()) {
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
this.#markObjectiveStageReference(artifact.stage);
|
|
765
|
+
for (const term of artifact.terms) {
|
|
766
|
+
this.#addPenalty(term.var, term.coeff, artifact.stage);
|
|
767
|
+
}
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
case "coverage-exclusion": {
|
|
771
|
+
this.reporter.excludeFromCoverage({
|
|
772
|
+
memberId: artifact.memberId,
|
|
773
|
+
day: artifact.day,
|
|
774
|
+
startTime: artifact.startTime,
|
|
775
|
+
endTime: artifact.endTime,
|
|
776
|
+
});
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
case "pre-solve-feedback": {
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
case "post-solve-feedback": {
|
|
783
|
+
if (!this.#isOptimizing()) {
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
this.#postSolveValidators.push(artifact);
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
case "cost": {
|
|
790
|
+
if (!this.#isOptimizing()) {
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
artifact.compileObjective?.(this);
|
|
794
|
+
this.#costArtifacts.push(artifact);
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
561
798
|
}
|
|
562
799
|
}
|
|
800
|
+
#reportHardConstraint(rule, artifact) {
|
|
801
|
+
if (artifact.validation.strategy !== "report") {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
this.reporter.trackConstraint({
|
|
805
|
+
id: artifact.validation.id,
|
|
806
|
+
type: "rule",
|
|
807
|
+
source: "hard",
|
|
808
|
+
rule,
|
|
809
|
+
description: artifact.description,
|
|
810
|
+
targetValue: artifact.targetValue,
|
|
811
|
+
comparator: artifact.comparator,
|
|
812
|
+
context: artifact.context,
|
|
813
|
+
group: artifact.group,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
#reportSoftConstraint(rule, artifact) {
|
|
817
|
+
if (artifact.validation.strategy !== "report") {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
this.reporter.trackConstraint({
|
|
821
|
+
id: artifact.constraintId,
|
|
822
|
+
type: "rule",
|
|
823
|
+
source: "soft",
|
|
824
|
+
rule,
|
|
825
|
+
description: artifact.description,
|
|
826
|
+
targetValue: artifact.targetValue,
|
|
827
|
+
comparator: artifact.comparator,
|
|
828
|
+
context: artifact.context,
|
|
829
|
+
group: artifact.group,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
563
832
|
#buildRequest() {
|
|
833
|
+
if (!this.#isOptimizing()) {
|
|
834
|
+
return {
|
|
835
|
+
variables: Array.from(this.#variables.values()),
|
|
836
|
+
constraints: this.#constraints,
|
|
837
|
+
mode: "satisfy",
|
|
838
|
+
options: this.#satisfyOptions(),
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
const objectiveStageOrder = this.#resolveObjectiveStageOrder();
|
|
842
|
+
if (objectiveStageOrder) {
|
|
843
|
+
return {
|
|
844
|
+
variables: Array.from(this.#variables.values()),
|
|
845
|
+
constraints: this.#constraintsForStagedRequest(),
|
|
846
|
+
objectiveStages: this.#buildObjectiveStages(objectiveStageOrder),
|
|
847
|
+
options: this.options,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
564
850
|
return {
|
|
565
851
|
variables: Array.from(this.#variables.values()),
|
|
566
852
|
constraints: this.#constraints,
|
|
@@ -568,6 +854,102 @@ export class ModelBuilder {
|
|
|
568
854
|
options: this.options,
|
|
569
855
|
};
|
|
570
856
|
}
|
|
857
|
+
#markObjectiveStageReference(stage) {
|
|
858
|
+
if (stage === undefined) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
this.#referencedObjectiveStages.add(stage);
|
|
862
|
+
}
|
|
863
|
+
#isOptimizing() {
|
|
864
|
+
return this.strategy.type === "optimize";
|
|
865
|
+
}
|
|
866
|
+
#satisfyOptions() {
|
|
867
|
+
return {
|
|
868
|
+
...this.options,
|
|
869
|
+
solutionLimit: this.options?.solutionLimit ?? 1,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
#resolveObjectiveStageOrder() {
|
|
873
|
+
const referencedStages = [...this.#referencedObjectiveStages];
|
|
874
|
+
if (referencedStages.length === 0) {
|
|
875
|
+
return undefined;
|
|
876
|
+
}
|
|
877
|
+
this.#validateReferencedObjectiveStages(referencedStages);
|
|
878
|
+
if (this.options?.solutionLimit === 1) {
|
|
879
|
+
throw new Error("ModelBuilder objectiveStageOrder cannot be used with solverOptions.solutionLimit=1.");
|
|
880
|
+
}
|
|
881
|
+
const stageOrder = this.#objectiveStageOrder;
|
|
882
|
+
if (stageOrder === undefined) {
|
|
883
|
+
if (referencedStages.length === 1) {
|
|
884
|
+
return [referencedStages[0]];
|
|
885
|
+
}
|
|
886
|
+
throw new Error(`Multiple objective stages were referenced (${referencedStages.join(", ")}); provide ModelBuilder objectiveStageOrder explicitly.`);
|
|
887
|
+
}
|
|
888
|
+
if (referencedStages.includes(TARGET_PEAK_CONCURRENT_ASSIGNMENTS_OBJECTIVE_STAGE_ID)) {
|
|
889
|
+
throw new Error("targetPeakConcurrentAssignments cannot be combined with ModelBuilder objectiveStageOrder until the multi-stage objective API is public.");
|
|
890
|
+
}
|
|
891
|
+
if (stageOrder.length === 0) {
|
|
892
|
+
throw new Error("ModelBuilder objectiveStageOrder cannot be empty when provided.");
|
|
893
|
+
}
|
|
894
|
+
const declaredStages = new Set();
|
|
895
|
+
for (const stage of stageOrder) {
|
|
896
|
+
if (stage.trim() === "") {
|
|
897
|
+
throw new Error("ModelBuilder objectiveStageOrder cannot contain an empty stage id.");
|
|
898
|
+
}
|
|
899
|
+
if (stage === UNSTAGED_OBJECTIVE_STAGE_ID) {
|
|
900
|
+
throw new Error(`ModelBuilder objectiveStageOrder cannot contain reserved stage id "${UNSTAGED_OBJECTIVE_STAGE_ID}".`);
|
|
901
|
+
}
|
|
902
|
+
if (declaredStages.has(stage)) {
|
|
903
|
+
throw new Error(`Duplicate objective stage id "${stage}" in objectiveStageOrder.`);
|
|
904
|
+
}
|
|
905
|
+
declaredStages.add(stage);
|
|
906
|
+
}
|
|
907
|
+
const allowedStages = new Set(declaredStages);
|
|
908
|
+
allowedStages.add(UNSTAGED_OBJECTIVE_STAGE_ID);
|
|
909
|
+
for (const stage of referencedStages) {
|
|
910
|
+
if (stage === UNSTAGED_OBJECTIVE_STAGE_ID) {
|
|
911
|
+
throw new Error(`Rule artifacts cannot use reserved objective stage id "${UNSTAGED_OBJECTIVE_STAGE_ID}".`);
|
|
912
|
+
}
|
|
913
|
+
if (!allowedStages.has(stage)) {
|
|
914
|
+
throw new Error(`Objective stage "${stage}" is not declared in ModelBuilder objectiveStageOrder.`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return stageOrder;
|
|
918
|
+
}
|
|
919
|
+
#validateReferencedObjectiveStages(referencedStages) {
|
|
920
|
+
for (const stage of referencedStages) {
|
|
921
|
+
if (stage.trim() === "") {
|
|
922
|
+
throw new Error("Rule artifacts cannot use an empty objective stage id.");
|
|
923
|
+
}
|
|
924
|
+
if (stage === UNSTAGED_OBJECTIVE_STAGE_ID) {
|
|
925
|
+
throw new Error(`Rule artifacts cannot use reserved objective stage id "${UNSTAGED_OBJECTIVE_STAGE_ID}".`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
#constraintsForStagedRequest() {
|
|
930
|
+
return this.#constraints.map((constraint) => {
|
|
931
|
+
if (constraint.type !== "soft_linear" || constraint.stage !== undefined) {
|
|
932
|
+
return constraint;
|
|
933
|
+
}
|
|
934
|
+
return { ...constraint, stage: UNSTAGED_OBJECTIVE_STAGE_ID };
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
#buildObjectiveStages(objectiveStageOrder) {
|
|
938
|
+
const stages = objectiveStageOrder.map((id) => ({
|
|
939
|
+
id,
|
|
940
|
+
sense: "minimize",
|
|
941
|
+
terms: this.#objectiveStageTerms.get(id) ?? [],
|
|
942
|
+
}));
|
|
943
|
+
const hasTailSoftConstraint = this.#constraints.some((constraint) => constraint.type === "soft_linear" && constraint.stage === undefined);
|
|
944
|
+
if (this.#objective.length > 0 || hasTailSoftConstraint) {
|
|
945
|
+
stages.push({
|
|
946
|
+
id: UNSTAGED_OBJECTIVE_STAGE_ID,
|
|
947
|
+
sense: "minimize",
|
|
948
|
+
terms: this.#objective,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
return stages;
|
|
952
|
+
}
|
|
571
953
|
#dayOffset(day) {
|
|
572
954
|
const idx = this.#dayIndex.get(day);
|
|
573
955
|
if (idx === undefined) {
|
|
@@ -576,6 +958,19 @@ export class ModelBuilder {
|
|
|
576
958
|
return idx * MINUTES_PER_DAY;
|
|
577
959
|
}
|
|
578
960
|
}
|
|
961
|
+
function resolveModelSolveStrategy(strategy) {
|
|
962
|
+
if (strategy === undefined) {
|
|
963
|
+
return { type: "optimize" };
|
|
964
|
+
}
|
|
965
|
+
if (typeof strategy !== "object" || strategy === null || !("type" in strategy)) {
|
|
966
|
+
throw new Error("ModelBuilder strategy must be an object with a type field.");
|
|
967
|
+
}
|
|
968
|
+
const strategyType = strategy.type;
|
|
969
|
+
if (strategyType === "optimize" || strategyType === "feasibility-only") {
|
|
970
|
+
return { type: strategyType };
|
|
971
|
+
}
|
|
972
|
+
throw new Error(`Unknown ModelBuilder strategy "${String(strategyType)}".`);
|
|
973
|
+
}
|
|
579
974
|
function recordBucketIssue(bucketIssues, issue, bucketStart) {
|
|
580
975
|
const existing = bucketIssues.get(issue.key);
|
|
581
976
|
if (existing) {
|