dabke 0.82.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 +44 -0
- package/README.md +22 -24
- 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 -21
- package/dist/cpsat/rules/index.d.ts.map +1 -1
- package/dist/cpsat/rules/index.js +30 -21
- 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 +30 -0
- package/dist/cpsat/rules/max-days-week.d.ts.map +1 -0
- package/dist/cpsat/rules/max-days-week.js +83 -0
- package/dist/cpsat/rules/max-days-week.js.map +1 -0
- 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 +21 -0
- package/dist/cpsat/rules/min-days-week.d.ts.map +1 -0
- package/dist/cpsat/rules/min-days-week.js +95 -0
- package/dist/cpsat/rules/min-days-week.js.map +1 -0
- 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 +12 -0
- package/dist/cpsat/rules/must-assign.d.ts.map +1 -0
- package/dist/cpsat/rules/must-assign.js +54 -0
- package/dist/cpsat/rules/must-assign.js.map +1 -0
- 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 -27
- package/dist/cpsat/rules/registry.d.ts.map +1 -1
- package/dist/cpsat/rules/registry.js +98 -31
- 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 +93 -17
- package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
- package/dist/cpsat/rules/scope.types.d.ts +25 -12
- 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 +15 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/schedule/cost.d.ts +192 -0
- package/dist/schedule/cost.d.ts.map +1 -0
- package/dist/schedule/cost.js +151 -0
- package/dist/schedule/cost.js.map +1 -0
- package/dist/schedule/coverage.d.ts +114 -0
- package/dist/schedule/coverage.d.ts.map +1 -0
- package/dist/schedule/coverage.js +56 -0
- package/dist/schedule/coverage.js.map +1 -0
- package/dist/schedule/definition.d.ts +346 -0
- package/dist/schedule/definition.d.ts.map +1 -0
- package/dist/schedule/definition.js +668 -0
- package/dist/schedule/definition.js.map +1 -0
- package/dist/schedule/index.d.ts +67 -0
- package/dist/schedule/index.d.ts.map +1 -0
- package/dist/schedule/index.js +69 -0
- package/dist/schedule/index.js.map +1 -0
- package/dist/schedule/rules.d.ts +678 -0
- package/dist/schedule/rules.d.ts.map +1 -0
- package/dist/schedule/rules.js +554 -0
- package/dist/schedule/rules.js.map +1 -0
- package/dist/schedule/shift-patterns.d.ts +17 -0
- package/dist/schedule/shift-patterns.d.ts.map +1 -0
- package/dist/schedule/shift-patterns.js +24 -0
- package/dist/schedule/shift-patterns.js.map +1 -0
- package/dist/schedule/time-periods.d.ts +79 -0
- package/dist/schedule/time-periods.d.ts.map +1 -0
- package/dist/schedule/time-periods.js +101 -0
- package/dist/schedule/time-periods.js.map +1 -0
- 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 -14
- 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 -21
- 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 +117 -0
- 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 +122 -0
- 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 +71 -0
- 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 -56
- package/src/cpsat/rules/resolver.ts +58 -30
- package/src/cpsat/rules/role-preference.ts +92 -0
- package/src/cpsat/rules/rules.types.ts +92 -17
- package/src/cpsat/rules/scope.types.ts +32 -27
- 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 +72 -15
- package/src/schedule/cost.ts +221 -0
- package/src/schedule/coverage.ts +164 -0
- package/src/schedule/definition.ts +1447 -0
- package/src/schedule/index.ts +132 -0
- package/src/schedule/rules.ts +1028 -0
- package/src/schedule/shift-patterns.ts +29 -0
- package/src/schedule/time-periods.ts +120 -0
- package/src/types.ts +114 -2
- package/dist/llms.d.ts +0 -2
- package/dist/llms.d.ts.map +0 -1
- package/dist/llms.js +0 -3
- package/dist/llms.js.map +0 -1
- package/dist/schedule.d.ts +0 -917
- package/dist/schedule.d.ts.map +0 -1
- package/dist/schedule.js +0 -1323
- package/dist/schedule.js.map +0 -1
- package/llms.txt +0 -758
- package/src/llms.ts +0 -3
- package/src/schedule.ts +0 -1960
package/src/cpsat/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TimeOfDay, DayOfWeek } from "../types.js";
|
|
1
|
+
import type { DateString, TimeOfDay, DayOfWeek } from "../types.js";
|
|
2
2
|
import type { SolverRequest, SolverTerm } from "../client.types.js";
|
|
3
3
|
import type { ValidationGroup } from "./validation.types.js";
|
|
4
4
|
|
|
@@ -15,11 +15,12 @@ export interface HourlyPay {
|
|
|
15
15
|
/**
|
|
16
16
|
* Annual salary with contracted weekly hours.
|
|
17
17
|
*
|
|
18
|
+
* @remarks
|
|
18
19
|
* The solver treats salaried members as having a fixed weekly cost
|
|
19
20
|
* (`annual / 52`) that is incurred once they work any shift in a week.
|
|
20
21
|
* Additional shifts within the same week have zero marginal cost.
|
|
21
22
|
*
|
|
22
|
-
*
|
|
23
|
+
* Overtime multiplier rules apply only to hourly members.
|
|
23
24
|
* Overtime surcharge rules apply to all members regardless of pay type.
|
|
24
25
|
*
|
|
25
26
|
* @category Supporting Types
|
|
@@ -34,12 +35,37 @@ export interface SalariedPay {
|
|
|
34
35
|
/**
|
|
35
36
|
* How strictly the solver enforces a rule.
|
|
36
37
|
*
|
|
38
|
+
* @remarks
|
|
39
|
+
* Priority is part of the schedule design, not a cosmetic option. Use it to
|
|
40
|
+
* preserve the distinction between hard requirements and softer preferences
|
|
41
|
+
* from the requirements. In practice, words like "must", "at least", "needs", and
|
|
42
|
+
* "cannot" usually map to `"MANDATORY"`, while words like "ideally",
|
|
43
|
+
* "prefer", "mainly", and "where possible" usually map to a soft priority.
|
|
44
|
+
* The same coverage shape or rule scope can be modeled with different
|
|
45
|
+
* priorities when the business language is subtler than a pure hard limit.
|
|
46
|
+
*
|
|
37
47
|
* - `"LOW"`, `"MEDIUM"`, `"HIGH"`: soft constraints with increasing penalty for violations
|
|
38
48
|
* - `"MANDATORY"`: hard constraint; the solver will not produce a solution that violates it
|
|
39
49
|
*
|
|
40
50
|
* @category Supporting Types
|
|
41
51
|
*/
|
|
42
|
-
export
|
|
52
|
+
export const PRIORITY_VALUES = ["LOW", "MEDIUM", "HIGH", "MANDATORY"] as const;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The soft-only priority values.
|
|
56
|
+
*
|
|
57
|
+
* @category Supporting Types
|
|
58
|
+
*/
|
|
59
|
+
export const SOFT_PRIORITY_VALUES = ["LOW", "MEDIUM", "HIGH"] as const;
|
|
60
|
+
|
|
61
|
+
export type Priority = (typeof PRIORITY_VALUES)[number];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Soft-only priority values.
|
|
65
|
+
*
|
|
66
|
+
* @category Supporting Types
|
|
67
|
+
*/
|
|
68
|
+
export type SoftPriority = (typeof SOFT_PRIORITY_VALUES)[number];
|
|
43
69
|
|
|
44
70
|
/**
|
|
45
71
|
* A team member available for scheduling.
|
|
@@ -131,7 +157,7 @@ export interface ShiftPattern {
|
|
|
131
157
|
}
|
|
132
158
|
|
|
133
159
|
export interface TimeInterval {
|
|
134
|
-
day:
|
|
160
|
+
day: DateString;
|
|
135
161
|
startTime: TimeOfDay;
|
|
136
162
|
endTime: TimeOfDay;
|
|
137
163
|
}
|
|
@@ -215,12 +241,35 @@ interface SkillBasedCoverageRequirement extends CoverageRequirementBase {
|
|
|
215
241
|
*/
|
|
216
242
|
export type CoverageRequirement = RoleBasedCoverageRequirement | SkillBasedCoverageRequirement;
|
|
217
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Compile-time solve profile used by model builders.
|
|
246
|
+
*
|
|
247
|
+
* @category Model Builder
|
|
248
|
+
*/
|
|
249
|
+
export type ModelSolveStrategy =
|
|
250
|
+
| {
|
|
251
|
+
/** Build the normal optimized solver request. */
|
|
252
|
+
readonly type: "optimize";
|
|
253
|
+
}
|
|
254
|
+
| {
|
|
255
|
+
/** Build a hard-feasibility request that ignores objectives and soft constraints. */
|
|
256
|
+
readonly type: "feasibility-only";
|
|
257
|
+
};
|
|
258
|
+
|
|
218
259
|
/** Optional settings for the model builder. */
|
|
219
260
|
export interface ModelBuilderOptions {
|
|
220
261
|
/** Which day starts the week for weekly rules. Defaults to "monday". */
|
|
221
262
|
weekStartsOn?: DayOfWeek;
|
|
222
263
|
/** Solver-level options (time limit, solution limit). */
|
|
223
264
|
solverOptions?: SolverRequest["options"];
|
|
265
|
+
/** Compile-time solve profile. Defaults to optimized solving. */
|
|
266
|
+
strategy?: ModelSolveStrategy;
|
|
267
|
+
/**
|
|
268
|
+
* Internal objective stage order for staged solver requests.
|
|
269
|
+
*
|
|
270
|
+
* @internal
|
|
271
|
+
*/
|
|
272
|
+
objectiveStageOrder?: readonly string[];
|
|
224
273
|
/**
|
|
225
274
|
* Bucket size used when translating coverage requirements into time-indexed constraints.
|
|
226
275
|
* Smaller buckets are more accurate but increase the number of constraints.
|
package/src/cpsat/utils.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
import type { Priority } from "./types.js";
|
|
1
|
+
import { DAY_OF_WEEK_INDEX, type DayOfWeek, type SchedulingDay, type TimeOfDay } from "../types.js";
|
|
2
|
+
import type { Priority, SoftPriority } from "./types.js";
|
|
4
3
|
|
|
5
4
|
export const MINUTES_PER_DAY = 24 * 60;
|
|
6
5
|
|
|
@@ -43,12 +42,17 @@ export const OBJECTIVE_WEIGHTS = {
|
|
|
43
42
|
} as const;
|
|
44
43
|
|
|
45
44
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* Maps {@link Priority} levels to penalty weights for preference rules.
|
|
46
|
+
*
|
|
47
|
+
* Used by `assignment-priority`, `location-preference`, and `role-preference`
|
|
48
|
+
* to scale the strength of their soft constraints.
|
|
48
49
|
*/
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
export const PREFERENCE_WEIGHTS: Record<Priority, number> = {
|
|
51
|
+
LOW: OBJECTIVE_WEIGHTS.FAIRNESS,
|
|
52
|
+
MEDIUM: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE,
|
|
53
|
+
HIGH: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE * 2.5,
|
|
54
|
+
MANDATORY: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE * 5,
|
|
55
|
+
};
|
|
52
56
|
|
|
53
57
|
export function timeOfDayToMinutes(time: TimeOfDay): number {
|
|
54
58
|
return time.hours * 60 + (time.minutes ?? 0);
|
|
@@ -70,6 +74,16 @@ export function priorityToPenalty(priority: Priority): number {
|
|
|
70
74
|
return PRIORITY_PENALTIES[priority];
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
const TARGET_PEAK_PRIORITY_PENALTIES = {
|
|
78
|
+
LOW: OBJECTIVE_WEIGHTS.COST,
|
|
79
|
+
MEDIUM: OBJECTIVE_WEIGHTS.SHIFT_ACTIVE,
|
|
80
|
+
HIGH: OBJECTIVE_WEIGHTS.SHIFT_ACTIVE * 2.5,
|
|
81
|
+
} as const satisfies Record<SoftPriority, number>;
|
|
82
|
+
|
|
83
|
+
export function targetPeakPriorityToPenalty(priority: SoftPriority): number {
|
|
84
|
+
return TARGET_PEAK_PRIORITY_PENALTIES[priority];
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
/**
|
|
74
88
|
* Computes the total duration of the union of time ranges.
|
|
75
89
|
*
|
|
@@ -80,8 +94,10 @@ export function priorityToPenalty(priority: Priority): number {
|
|
|
80
94
|
export function unionMinutes(ranges: ReadonlyArray<{ start: number; end: number }>): number {
|
|
81
95
|
if (ranges.length === 0) return 0;
|
|
82
96
|
const sorted = ranges.toSorted((a, b) => a.start - b.start);
|
|
97
|
+
const first = sorted[0];
|
|
98
|
+
if (!first) return 0;
|
|
83
99
|
let total = 0;
|
|
84
|
-
let currentEnd =
|
|
100
|
+
let currentEnd = first.start;
|
|
85
101
|
for (const r of sorted) {
|
|
86
102
|
const effectiveStart = Math.max(r.start, currentEnd);
|
|
87
103
|
if (effectiveStart < r.end) {
|
|
@@ -92,28 +108,84 @@ export function unionMinutes(ranges: ReadonlyArray<{ start: number; end: number
|
|
|
92
108
|
return total;
|
|
93
109
|
}
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Splits scheduling days into non-overlapping chunks of N calendar months.
|
|
113
|
+
*
|
|
114
|
+
* Days are grouped by their {@link SchedulingDay.yearMonth}, then consecutive
|
|
115
|
+
* months are merged into chunks of the requested size.
|
|
116
|
+
*/
|
|
117
|
+
export function splitIntoMonths(days: SchedulingDay[], monthsPerChunk: number): SchedulingDay[][] {
|
|
118
|
+
if (days.length === 0) return [];
|
|
119
|
+
|
|
120
|
+
const monthGroups = new Map<string, SchedulingDay[]>();
|
|
121
|
+
for (const day of days) {
|
|
122
|
+
let group = monthGroups.get(day.yearMonth);
|
|
123
|
+
if (!group) {
|
|
124
|
+
group = [];
|
|
125
|
+
monthGroups.set(day.yearMonth, group);
|
|
126
|
+
}
|
|
127
|
+
group.push(day);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const sortedKeys = [...monthGroups.keys()].toSorted();
|
|
131
|
+
const chunks: SchedulingDay[][] = [];
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < sortedKeys.length; i += monthsPerChunk) {
|
|
134
|
+
const chunkKeys = sortedKeys.slice(i, i + monthsPerChunk);
|
|
135
|
+
const chunk = chunkKeys.flatMap((key) => monthGroups.get(key)!);
|
|
136
|
+
if (chunk.length > 0) {
|
|
137
|
+
chunks.push(chunk);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return chunks;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Groups days into chunks of N weeks, aligned to weekStartsOn.
|
|
146
|
+
*
|
|
147
|
+
* Uses {@link splitIntoWeeks} internally, then merges consecutive weeks
|
|
148
|
+
* into larger chunks.
|
|
149
|
+
*/
|
|
150
|
+
export function groupWeekChunks(
|
|
151
|
+
days: SchedulingDay[],
|
|
152
|
+
weeksPerChunk: number,
|
|
153
|
+
weekStartsOn: DayOfWeek,
|
|
154
|
+
): SchedulingDay[][] {
|
|
155
|
+
const weeks = splitIntoWeeks(days, weekStartsOn);
|
|
156
|
+
const chunks: SchedulingDay[][] = [];
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < weeks.length; i += weeksPerChunk) {
|
|
159
|
+
const chunk = weeks.slice(i, i + weeksPerChunk).flat();
|
|
160
|
+
if (chunk.length > 0) {
|
|
161
|
+
chunks.push(chunk);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return chunks;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function splitIntoWeeks(days: SchedulingDay[], weekStartsOn: DayOfWeek): SchedulingDay[][] {
|
|
96
169
|
if (days.length === 0) return [];
|
|
97
170
|
|
|
98
|
-
const weekStartIndex =
|
|
99
|
-
const result:
|
|
100
|
-
let currentWeek:
|
|
101
|
-
let
|
|
171
|
+
const weekStartIndex = DAY_OF_WEEK_INDEX[weekStartsOn];
|
|
172
|
+
const result: SchedulingDay[][] = [];
|
|
173
|
+
let currentWeek: SchedulingDay[] = [];
|
|
174
|
+
let currentWeekStartEpoch: number | null = null;
|
|
102
175
|
|
|
103
176
|
for (const day of days) {
|
|
104
|
-
const
|
|
105
|
-
const isWeekStartDay = date.getUTCDay() === weekStartIndex;
|
|
177
|
+
const isWeekStartDay = DAY_OF_WEEK_INDEX[day.dayOfWeek] === weekStartIndex;
|
|
106
178
|
const isNewWeek =
|
|
107
|
-
isWeekStartDay &&
|
|
179
|
+
isWeekStartDay && currentWeekStartEpoch !== null && day.epochDay !== currentWeekStartEpoch;
|
|
108
180
|
|
|
109
181
|
if (isNewWeek) {
|
|
110
182
|
result.push(currentWeek);
|
|
111
183
|
currentWeek = [];
|
|
112
|
-
|
|
184
|
+
currentWeekStartEpoch = null;
|
|
113
185
|
}
|
|
114
186
|
|
|
115
|
-
if (
|
|
116
|
-
|
|
187
|
+
if (currentWeekStartEpoch === null) {
|
|
188
|
+
currentWeekStartEpoch = day.epochDay;
|
|
117
189
|
}
|
|
118
190
|
currentWeek.push(day);
|
|
119
191
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SolverResponse } from "../client.types.js";
|
|
2
|
+
import type { DateString } from "../types.js";
|
|
2
3
|
import {
|
|
3
4
|
type ScheduleError,
|
|
4
5
|
type ScheduleViolation,
|
|
@@ -24,7 +25,7 @@ export interface ValidationReporter {
|
|
|
24
25
|
reportRuleError(error: Omit<RuleError, "type" | "id">): void;
|
|
25
26
|
reportSolverError(message: string): void;
|
|
26
27
|
|
|
27
|
-
// Violations (
|
|
28
|
+
// Violations (non-fatal feedback)
|
|
28
29
|
reportCoverageViolation(violation: Omit<CoverageViolation, "type" | "id">): void;
|
|
29
30
|
reportRuleViolation(violation: Omit<RuleViolation, "type" | "id">): void;
|
|
30
31
|
|
|
@@ -41,9 +42,17 @@ export interface ValidationReporter {
|
|
|
41
42
|
getExclusions(): CoverageExclusion[];
|
|
42
43
|
|
|
43
44
|
// Post-solve analysis
|
|
44
|
-
analyzeSolution(response: SolverResponse): void;
|
|
45
|
+
analyzeSolution(response: SolverResponse, options?: AnalyzeSolutionOptions): void;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
export interface AnalyzeSolutionOptions {
|
|
49
|
+
/** Whether solver-reported soft constraint diagnostics should update validation. */
|
|
50
|
+
analyzeSoftConstraints?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const MISSING_SOFT_CONSTRAINT_VIOLATIONS_ERROR =
|
|
54
|
+
"Solver response missing softConstraintViolations for tracked soft constraints.";
|
|
55
|
+
|
|
47
56
|
/**
|
|
48
57
|
* Generates a deterministic ID for a coverage-based validation item.
|
|
49
58
|
* Format: {category}:coverage:{day}:{timeSlots}:{roles}:{skills}
|
|
@@ -91,19 +100,28 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
91
100
|
#passed: SchedulePassed[] = [];
|
|
92
101
|
#trackedConstraints = new Map<string, TrackedConstraint>();
|
|
93
102
|
#exclusions: CoverageExclusion[] = [];
|
|
103
|
+
#validationIdCounts = new Map<string, number>();
|
|
94
104
|
#solverErrorCount = 0;
|
|
95
105
|
|
|
106
|
+
#nextValidationId(baseId: string): string {
|
|
107
|
+
const count = (this.#validationIdCounts.get(baseId) ?? 0) + 1;
|
|
108
|
+
this.#validationIdCounts.set(baseId, count);
|
|
109
|
+
return count === 1 ? baseId : `${baseId}:${count}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
96
112
|
excludeFromCoverage(exclusion: CoverageExclusion): void {
|
|
97
113
|
this.#exclusions.push(exclusion);
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
reportCoverageError(error: Omit<CoverageError, "type" | "id">): void {
|
|
101
|
-
const id =
|
|
117
|
+
const id = this.#nextValidationId(
|
|
118
|
+
coverageId("error", error.day, error.timeSlots, error.roleIds, error.skillIds),
|
|
119
|
+
);
|
|
102
120
|
this.#errors.push({ id, type: "coverage", ...error });
|
|
103
121
|
}
|
|
104
122
|
|
|
105
123
|
reportRuleError(error: Omit<RuleError, "type" | "id">): void {
|
|
106
|
-
const id = ruleId("error", error.rule, error.context);
|
|
124
|
+
const id = this.#nextValidationId(ruleId("error", error.rule, error.context));
|
|
107
125
|
this.#errors.push({ id, type: "rule", ...error });
|
|
108
126
|
}
|
|
109
127
|
|
|
@@ -114,28 +132,32 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
reportCoverageViolation(violation: Omit<CoverageViolation, "type" | "id">): void {
|
|
117
|
-
const id =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
const id = this.#nextValidationId(
|
|
136
|
+
coverageId(
|
|
137
|
+
"violation",
|
|
138
|
+
violation.day,
|
|
139
|
+
violation.timeSlots,
|
|
140
|
+
violation.roleIds,
|
|
141
|
+
violation.skillIds,
|
|
142
|
+
),
|
|
123
143
|
);
|
|
124
144
|
this.#violations.push({ id, type: "coverage", ...violation });
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
reportRuleViolation(violation: Omit<RuleViolation, "type" | "id">): void {
|
|
128
|
-
const id = ruleId("violation", violation.rule, violation.context);
|
|
148
|
+
const id = this.#nextValidationId(ruleId("violation", violation.rule, violation.context));
|
|
129
149
|
this.#violations.push({ id, type: "rule", ...violation });
|
|
130
150
|
}
|
|
131
151
|
|
|
132
152
|
reportCoveragePassed(passed: Omit<CoveragePassed, "type" | "id">): void {
|
|
133
|
-
const id =
|
|
153
|
+
const id = this.#nextValidationId(
|
|
154
|
+
coverageId("passed", passed.day, passed.timeSlots, passed.roleIds, passed.skillIds),
|
|
155
|
+
);
|
|
134
156
|
this.#passed.push({ id, type: "coverage", ...passed });
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
reportRulePassed(passed: Omit<RulePassed, "type" | "id">): void {
|
|
138
|
-
const id = ruleId("passed", passed.rule, passed.context);
|
|
160
|
+
const id = this.#nextValidationId(ruleId("passed", passed.rule, passed.context));
|
|
139
161
|
this.#passed.push({ id, type: "rule", ...passed });
|
|
140
162
|
}
|
|
141
163
|
|
|
@@ -163,10 +185,16 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
163
185
|
return [...this.#trackedConstraints.values()];
|
|
164
186
|
}
|
|
165
187
|
|
|
166
|
-
analyzeSolution(response: SolverResponse): void {
|
|
188
|
+
analyzeSolution(response: SolverResponse, options?: AnalyzeSolutionOptions): void {
|
|
167
189
|
if (response.status !== "OPTIMAL" && response.status !== "FEASIBLE") {
|
|
168
190
|
if (response.status === "INFEASIBLE") {
|
|
191
|
+
this.#reportHardConstraintConflicts(
|
|
192
|
+
(response.hardConstraintConflicts ?? []).map((conflict) => conflict.constraintId),
|
|
193
|
+
);
|
|
169
194
|
this.reportSolverError(response.solutionInfo ?? "Schedule is infeasible");
|
|
195
|
+
if (response.error) {
|
|
196
|
+
this.reportSolverError(response.error);
|
|
197
|
+
}
|
|
170
198
|
} else if (response.status === "TIMEOUT") {
|
|
171
199
|
this.reportSolverError("Solver timed out");
|
|
172
200
|
} else if (response.error) {
|
|
@@ -175,16 +203,33 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
175
203
|
return;
|
|
176
204
|
}
|
|
177
205
|
|
|
178
|
-
const
|
|
206
|
+
const analyzeSoftConstraints = options?.analyzeSoftConstraints ?? true;
|
|
207
|
+
|
|
208
|
+
const hasTrackedSoftConstraints = [...this.#trackedConstraints.values()].some(
|
|
209
|
+
(constraint) => constraint.source === "soft",
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (
|
|
213
|
+
analyzeSoftConstraints &&
|
|
214
|
+
hasTrackedSoftConstraints &&
|
|
215
|
+
response.softConstraintViolations === undefined
|
|
216
|
+
) {
|
|
217
|
+
throw new Error(MISSING_SOFT_CONSTRAINT_VIOLATIONS_ERROR);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const solverViolations = analyzeSoftConstraints
|
|
221
|
+
? (response.softConstraintViolations ?? [])
|
|
222
|
+
: [];
|
|
179
223
|
|
|
180
224
|
for (const violation of solverViolations) {
|
|
181
225
|
const tracked = this.#trackedConstraints.get(violation.constraintId);
|
|
182
226
|
|
|
183
227
|
if (tracked?.type === "coverage") {
|
|
228
|
+
if (!tracked.day) continue;
|
|
184
229
|
const roles = tracked.roleIds?.join(", ") ?? "staff";
|
|
185
230
|
const slot = tracked.timeSlot ?? "all day";
|
|
186
231
|
this.reportCoverageViolation({
|
|
187
|
-
day: tracked.day
|
|
232
|
+
day: tracked.day,
|
|
188
233
|
timeSlots: tracked.timeSlot ? [tracked.timeSlot] : [],
|
|
189
234
|
roleIds: tracked.roleIds,
|
|
190
235
|
skillIds: tracked.skillIds,
|
|
@@ -216,11 +261,13 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
216
261
|
// Mark tracked coverage constraints as passed if not violated
|
|
217
262
|
const violatedIds = new Set(solverViolations.map((v) => v.constraintId));
|
|
218
263
|
for (const tracked of this.#trackedConstraints.values()) {
|
|
264
|
+
if (!analyzeSoftConstraints && tracked.source === "soft") continue;
|
|
219
265
|
if (violatedIds.has(tracked.id)) continue;
|
|
220
266
|
|
|
221
267
|
if (tracked.type === "coverage") {
|
|
268
|
+
if (!tracked.day) continue;
|
|
222
269
|
this.reportCoveragePassed({
|
|
223
|
-
day: tracked.day
|
|
270
|
+
day: tracked.day,
|
|
224
271
|
timeSlots: tracked.timeSlot ? [tracked.timeSlot] : [],
|
|
225
272
|
roleIds: tracked.roleIds,
|
|
226
273
|
skillIds: tracked.skillIds,
|
|
@@ -230,6 +277,110 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
230
277
|
}
|
|
231
278
|
}
|
|
232
279
|
}
|
|
280
|
+
|
|
281
|
+
#reportHardConstraintConflicts(constraintIds: readonly string[]): void {
|
|
282
|
+
if (constraintIds.length === 0) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
type CoverageGroup = {
|
|
287
|
+
day: DateString;
|
|
288
|
+
timeSlots: Set<string>;
|
|
289
|
+
roleIds?: string[];
|
|
290
|
+
skillIds?: readonly string[];
|
|
291
|
+
group?: TrackedConstraint["group"];
|
|
292
|
+
descriptions: string[];
|
|
293
|
+
};
|
|
294
|
+
type RuleGroup = {
|
|
295
|
+
rule: string;
|
|
296
|
+
days: Set<DateString>;
|
|
297
|
+
memberIds: Set<string>;
|
|
298
|
+
group?: TrackedConstraint["group"];
|
|
299
|
+
descriptions: string[];
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const coverageGroups = new Map<string, CoverageGroup>();
|
|
303
|
+
const ruleGroups = new Map<string, RuleGroup>();
|
|
304
|
+
|
|
305
|
+
for (const constraintId of constraintIds) {
|
|
306
|
+
const tracked = this.#trackedConstraints.get(constraintId);
|
|
307
|
+
if (!tracked) continue;
|
|
308
|
+
|
|
309
|
+
if (tracked.type === "coverage") {
|
|
310
|
+
if (!tracked.day) continue;
|
|
311
|
+
const key = [
|
|
312
|
+
tracked.group?.key ?? tracked.id,
|
|
313
|
+
tracked.day,
|
|
314
|
+
tracked.roleIds?.join(",") ?? "_",
|
|
315
|
+
tracked.skillIds?.join(",") ?? "_",
|
|
316
|
+
].join(":");
|
|
317
|
+
const existing = coverageGroups.get(key);
|
|
318
|
+
if (existing) {
|
|
319
|
+
if (tracked.timeSlot) existing.timeSlots.add(tracked.timeSlot);
|
|
320
|
+
existing.descriptions.push(tracked.description);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
coverageGroups.set(key, {
|
|
324
|
+
day: tracked.day,
|
|
325
|
+
timeSlots: new Set(tracked.timeSlot ? [tracked.timeSlot] : []),
|
|
326
|
+
roleIds: tracked.roleIds,
|
|
327
|
+
skillIds: tracked.skillIds,
|
|
328
|
+
group: tracked.group,
|
|
329
|
+
descriptions: [tracked.description],
|
|
330
|
+
});
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const key = tracked.group?.key ?? tracked.id;
|
|
335
|
+
const existing = ruleGroups.get(key);
|
|
336
|
+
if (existing) {
|
|
337
|
+
for (const day of tracked.context.days ?? []) existing.days.add(day);
|
|
338
|
+
for (const memberId of tracked.context.memberIds ?? []) existing.memberIds.add(memberId);
|
|
339
|
+
existing.descriptions.push(tracked.description);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
ruleGroups.set(key, {
|
|
343
|
+
rule: tracked.rule ?? "unknown",
|
|
344
|
+
days: new Set(tracked.context.days ?? []),
|
|
345
|
+
memberIds: new Set(tracked.context.memberIds ?? []),
|
|
346
|
+
group: tracked.group,
|
|
347
|
+
descriptions: [tracked.description],
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const coverage of coverageGroups.values()) {
|
|
352
|
+
const label = coverage.group?.title ?? coverage.descriptions[0] ?? "Coverage requirement";
|
|
353
|
+
this.reportCoverageError({
|
|
354
|
+
day: coverage.day,
|
|
355
|
+
timeSlots: [...coverage.timeSlots].toSorted(),
|
|
356
|
+
roleIds: coverage.roleIds,
|
|
357
|
+
skillIds: coverage.skillIds,
|
|
358
|
+
message: `${label} is part of a sufficient infeasible constraint set.`,
|
|
359
|
+
suggestions: [
|
|
360
|
+
"Relax this mandatory coverage requirement",
|
|
361
|
+
"Relax conflicting mandatory rules or add eligible team members",
|
|
362
|
+
],
|
|
363
|
+
group: coverage.group,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const rule of ruleGroups.values()) {
|
|
368
|
+
const label = rule.group?.title ?? rule.descriptions[0] ?? rule.rule;
|
|
369
|
+
this.reportRuleError({
|
|
370
|
+
rule: rule.rule,
|
|
371
|
+
message: `${label} is part of a sufficient infeasible constraint set.`,
|
|
372
|
+
context: {
|
|
373
|
+
days: [...rule.days].toSorted(),
|
|
374
|
+
memberIds: [...rule.memberIds].toSorted(),
|
|
375
|
+
},
|
|
376
|
+
suggestions: [
|
|
377
|
+
"Relax this mandatory rule",
|
|
378
|
+
"Relax conflicting mandatory coverage requirements or add eligible team members",
|
|
379
|
+
],
|
|
380
|
+
group: rule.group,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
233
384
|
}
|
|
234
385
|
|
|
235
386
|
// =============================================================================
|
|
@@ -265,7 +416,7 @@ export function summarizeValidation(validation: ScheduleValidation): readonly Va
|
|
|
265
416
|
{
|
|
266
417
|
type: "coverage" | "rule";
|
|
267
418
|
title: string;
|
|
268
|
-
days: Set<
|
|
419
|
+
days: Set<DateString>;
|
|
269
420
|
passedCount: number;
|
|
270
421
|
violatedCount: number;
|
|
271
422
|
errorCount: number;
|
|
@@ -280,7 +431,7 @@ export function summarizeValidation(validation: ScheduleValidation): readonly Va
|
|
|
280
431
|
groups.set(key, {
|
|
281
432
|
type: item.type,
|
|
282
433
|
title,
|
|
283
|
-
days: new Set(),
|
|
434
|
+
days: new Set<DateString>(),
|
|
284
435
|
passedCount: 0,
|
|
285
436
|
violatedCount: 0,
|
|
286
437
|
errorCount: 0,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { TimeOfDay } from "../types.js";
|
|
1
|
+
import type { DateString, TimeOfDay } from "../types.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Context shared across validation results for grouping/display.
|
|
5
5
|
*/
|
|
6
6
|
export interface ValidationContext {
|
|
7
|
-
days?:
|
|
7
|
+
days?: DateString[];
|
|
8
8
|
timeSlots?: string[];
|
|
9
9
|
memberIds?: string[];
|
|
10
10
|
}
|
|
@@ -60,7 +60,7 @@ export function assertSafeKeySegments(values: readonly string[], label: string):
|
|
|
60
60
|
export interface CoverageError {
|
|
61
61
|
readonly id: string;
|
|
62
62
|
readonly type: "coverage";
|
|
63
|
-
readonly day:
|
|
63
|
+
readonly day: DateString;
|
|
64
64
|
readonly timeSlots: readonly string[];
|
|
65
65
|
readonly roleIds?: string[];
|
|
66
66
|
readonly skillIds?: readonly string[];
|
|
@@ -88,13 +88,13 @@ export interface SolverError {
|
|
|
88
88
|
export type ScheduleError = CoverageError | RuleError | SolverError;
|
|
89
89
|
|
|
90
90
|
// =============================================================================
|
|
91
|
-
// Violations -
|
|
91
|
+
// Violations - non-fatal unmet requirements or preferences
|
|
92
92
|
// =============================================================================
|
|
93
93
|
|
|
94
94
|
export interface CoverageViolation {
|
|
95
95
|
readonly id: string;
|
|
96
96
|
readonly type: "coverage";
|
|
97
|
-
readonly day:
|
|
97
|
+
readonly day: DateString;
|
|
98
98
|
readonly timeSlots: readonly string[];
|
|
99
99
|
readonly roleIds?: string[];
|
|
100
100
|
readonly skillIds?: readonly string[];
|
|
@@ -125,7 +125,7 @@ export type ScheduleViolation = CoverageViolation | RuleViolation;
|
|
|
125
125
|
export interface CoveragePassed {
|
|
126
126
|
readonly id: string;
|
|
127
127
|
readonly type: "coverage";
|
|
128
|
-
readonly day:
|
|
128
|
+
readonly day: DateString;
|
|
129
129
|
readonly timeSlots: readonly string[];
|
|
130
130
|
readonly roleIds?: string[];
|
|
131
131
|
readonly skillIds?: readonly string[];
|
|
@@ -148,7 +148,17 @@ export type SchedulePassed = CoveragePassed | RulePassed;
|
|
|
148
148
|
// Complete validation result
|
|
149
149
|
// =============================================================================
|
|
150
150
|
|
|
151
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Public validation feedback collected across pre-solve, solver-stage, and
|
|
153
|
+
* post-solve analysis.
|
|
154
|
+
*
|
|
155
|
+
* @remarks
|
|
156
|
+
* This is not a direct mirror of the raw solver response. Solver transport
|
|
157
|
+
* fields such as `softConstraintViolations` are translated into these
|
|
158
|
+
* phase-agnostic `errors`, `violations`, and `passed` items.
|
|
159
|
+
*
|
|
160
|
+
* @category Validation
|
|
161
|
+
*/
|
|
152
162
|
export interface ScheduleValidation {
|
|
153
163
|
readonly errors: readonly ScheduleError[];
|
|
154
164
|
readonly violations: readonly ScheduleViolation[];
|
|
@@ -171,7 +181,7 @@ export interface ValidationSummary {
|
|
|
171
181
|
readonly type: "coverage" | "rule";
|
|
172
182
|
/** Human-readable title for this group (e.g., "3x nurse during day_ward"). */
|
|
173
183
|
readonly title: string;
|
|
174
|
-
readonly days: readonly
|
|
184
|
+
readonly days: readonly DateString[];
|
|
175
185
|
readonly status: "passed" | "partial" | "failed";
|
|
176
186
|
readonly passedCount: number;
|
|
177
187
|
readonly violatedCount: number;
|
|
@@ -185,12 +195,13 @@ export interface ValidationSummary {
|
|
|
185
195
|
export interface TrackedConstraint {
|
|
186
196
|
readonly id: string;
|
|
187
197
|
readonly type: "coverage" | "rule";
|
|
198
|
+
readonly source?: "hard" | "soft";
|
|
188
199
|
readonly rule?: string;
|
|
189
200
|
/** Per-constraint description used to generate violation messages. */
|
|
190
201
|
readonly description: string;
|
|
191
202
|
readonly targetValue: number;
|
|
192
|
-
readonly comparator: "<=" | ">=";
|
|
193
|
-
readonly day?:
|
|
203
|
+
readonly comparator: "<=" | ">=" | "==";
|
|
204
|
+
readonly day?: DateString;
|
|
194
205
|
readonly timeSlot?: string;
|
|
195
206
|
readonly roleIds?: string[];
|
|
196
207
|
readonly skillIds?: readonly string[];
|
|
@@ -204,7 +215,7 @@ export interface TrackedConstraint {
|
|
|
204
215
|
*/
|
|
205
216
|
export interface CoverageExclusion {
|
|
206
217
|
memberId: string;
|
|
207
|
-
day:
|
|
218
|
+
day: DateString;
|
|
208
219
|
startTime?: TimeOfDay;
|
|
209
220
|
endTime?: TimeOfDay;
|
|
210
221
|
}
|