dabke 0.81.1 → 0.83.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 +58 -0
- package/README.md +45 -27
- package/dist/client.d.ts +20 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -1
- package/dist/client.js.map +1 -1
- package/dist/client.types.d.ts +9 -0
- package/dist/client.types.d.ts.map +1 -1
- package/dist/client.types.js +1 -0
- package/dist/client.types.js.map +1 -1
- package/dist/cpsat/model-builder.d.ts +9 -0
- package/dist/cpsat/model-builder.d.ts.map +1 -1
- package/dist/cpsat/model-builder.js +36 -34
- package/dist/cpsat/model-builder.js.map +1 -1
- package/dist/cpsat/response.d.ts +13 -1
- package/dist/cpsat/response.d.ts.map +1 -1
- package/dist/cpsat/response.js +4 -0
- package/dist/cpsat/response.js.map +1 -1
- package/dist/cpsat/rules/cost-utils.d.ts +11 -0
- package/dist/cpsat/rules/cost-utils.d.ts.map +1 -0
- package/dist/cpsat/rules/cost-utils.js +24 -0
- package/dist/cpsat/rules/cost-utils.js.map +1 -0
- package/dist/cpsat/rules/day-cost-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/day-cost-multiplier.js +3 -14
- package/dist/cpsat/rules/day-cost-multiplier.js.map +1 -1
- package/dist/cpsat/rules/day-cost-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/day-cost-surcharge.js +3 -7
- package/dist/cpsat/rules/day-cost-surcharge.js.map +1 -1
- package/dist/cpsat/rules/index.d.ts +3 -0
- package/dist/cpsat/rules/index.d.ts.map +1 -1
- package/dist/cpsat/rules/index.js +3 -0
- package/dist/cpsat/rules/index.js.map +1 -1
- package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/max-consecutive-days.js +16 -2
- package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/max-days-week.d.ts +44 -0
- package/dist/cpsat/rules/max-days-week.d.ts.map +1 -0
- package/dist/cpsat/rules/max-days-week.js +95 -0
- package/dist/cpsat/rules/max-days-week.js.map +1 -0
- package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-day.js +15 -2
- package/dist/cpsat/rules/max-hours-day.js.map +1 -1
- package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-week.js +16 -2
- package/dist/cpsat/rules/max-hours-week.js.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.js +15 -2
- package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.js +15 -2
- package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/min-days-week.d.ts +34 -0
- package/dist/cpsat/rules/min-days-week.d.ts.map +1 -0
- package/dist/cpsat/rules/min-days-week.js +84 -0
- package/dist/cpsat/rules/min-days-week.js.map +1 -0
- package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-day.js +15 -2
- package/dist/cpsat/rules/min-hours-day.js.map +1 -1
- package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-week.js +16 -2
- package/dist/cpsat/rules/min-hours-week.js.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.js +72 -2
- package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
- package/dist/cpsat/rules/minimize-cost.d.ts.map +1 -1
- package/dist/cpsat/rules/minimize-cost.js +2 -23
- package/dist/cpsat/rules/minimize-cost.js.map +1 -1
- package/dist/cpsat/rules/must-assign.d.ts +49 -0
- package/dist/cpsat/rules/must-assign.d.ts.map +1 -0
- package/dist/cpsat/rules/must-assign.js +86 -0
- package/dist/cpsat/rules/must-assign.js.map +1 -0
- package/dist/cpsat/rules/overtime-daily-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-daily-multiplier.js +1 -12
- package/dist/cpsat/rules/overtime-daily-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-daily-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-daily-surcharge.js +1 -5
- package/dist/cpsat/rules/overtime-daily-surcharge.js.map +1 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts +5 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.js +1 -12
- package/dist/cpsat/rules/overtime-tiered-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-multiplier.js +1 -12
- package/dist/cpsat/rules/overtime-weekly-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-surcharge.js +1 -5
- package/dist/cpsat/rules/overtime-weekly-surcharge.js.map +1 -1
- package/dist/cpsat/rules/registry.d.ts +28 -2
- package/dist/cpsat/rules/registry.d.ts.map +1 -1
- package/dist/cpsat/rules/registry.js +4 -1
- package/dist/cpsat/rules/registry.js.map +1 -1
- package/dist/cpsat/rules/resolver.js +2 -2
- package/dist/cpsat/rules/resolver.js.map +1 -1
- package/dist/cpsat/rules/rules.types.d.ts +3 -0
- package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
- package/dist/cpsat/rules/scope.types.d.ts +18 -1
- package/dist/cpsat/rules/scope.types.d.ts.map +1 -1
- package/dist/cpsat/rules/scope.types.js +59 -16
- package/dist/cpsat/rules/scope.types.js.map +1 -1
- package/dist/cpsat/rules/time-cost-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/time-cost-surcharge.js +2 -1
- package/dist/cpsat/rules/time-cost-surcharge.js.map +1 -1
- package/dist/cpsat/rules/time-off.d.ts.map +1 -1
- package/dist/cpsat/rules/time-off.js +6 -3
- package/dist/cpsat/rules/time-off.js.map +1 -1
- package/dist/cpsat/semantic-time.d.ts +44 -42
- package/dist/cpsat/semantic-time.d.ts.map +1 -1
- package/dist/cpsat/semantic-time.js +64 -46
- package/dist/cpsat/semantic-time.js.map +1 -1
- package/dist/cpsat/types.d.ts +37 -27
- package/dist/cpsat/types.d.ts.map +1 -1
- package/dist/cpsat/utils.d.ts.map +1 -1
- package/dist/cpsat/utils.js +7 -12
- package/dist/cpsat/utils.js.map +1 -1
- package/dist/cpsat/validation-reporter.d.ts +10 -7
- package/dist/cpsat/validation-reporter.d.ts.map +1 -1
- package/dist/cpsat/validation-reporter.js +44 -72
- package/dist/cpsat/validation-reporter.js.map +1 -1
- package/dist/cpsat/validation.types.d.ts +54 -44
- package/dist/cpsat/validation.types.d.ts.map +1 -1
- package/dist/cpsat/validation.types.js +15 -10
- package/dist/cpsat/validation.types.js.map +1 -1
- package/dist/datetime.utils.d.ts +3 -203
- package/dist/datetime.utils.d.ts.map +1 -1
- package/dist/datetime.utils.js +1 -288
- package/dist/datetime.utils.js.map +1 -1
- package/dist/index.d.ts +14 -83
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -83
- package/dist/index.js.map +1 -1
- package/dist/schedule/cost.d.ts +204 -0
- package/dist/schedule/cost.d.ts.map +1 -0
- package/dist/schedule/cost.js +187 -0
- package/dist/schedule/cost.js.map +1 -0
- package/dist/schedule/coverage.d.ts +85 -0
- package/dist/schedule/coverage.d.ts.map +1 -0
- package/dist/schedule/coverage.js +33 -0
- package/dist/schedule/coverage.js.map +1 -0
- package/dist/schedule/definition.d.ts +227 -0
- package/dist/schedule/definition.d.ts.map +1 -0
- package/dist/schedule/definition.js +659 -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 +353 -0
- package/dist/schedule/rules.d.ts.map +1 -0
- package/dist/schedule/rules.js +352 -0
- package/dist/schedule/rules.js.map +1 -0
- package/dist/schedule/shift-patterns.d.ts +34 -0
- package/dist/schedule/shift-patterns.d.ts.map +1 -0
- package/dist/schedule/shift-patterns.js +41 -0
- package/dist/schedule/shift-patterns.js.map +1 -0
- package/dist/schedule/time-periods.d.ts +69 -0
- package/dist/schedule/time-periods.d.ts.map +1 -0
- package/dist/schedule/time-periods.js +91 -0
- package/dist/schedule/time-periods.js.map +1 -0
- package/dist/types.d.ts +14 -78
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -9
- package/solver/src/solver/app.py +1 -1
- package/solver/src/solver/solver.py +7 -4
- package/src/client.ts +6 -8
- package/src/client.types.ts +9 -0
- package/src/cpsat/model-builder.ts +44 -35
- package/src/cpsat/response.ts +13 -1
- package/src/cpsat/rules/cost-utils.ts +25 -0
- package/src/cpsat/rules/day-cost-multiplier.ts +3 -14
- package/src/cpsat/rules/day-cost-surcharge.ts +3 -8
- package/src/cpsat/rules/index.ts +3 -0
- package/src/cpsat/rules/max-consecutive-days.ts +17 -0
- package/src/cpsat/rules/max-days-week.ts +143 -0
- package/src/cpsat/rules/max-hours-day.ts +21 -1
- package/src/cpsat/rules/max-hours-week.ts +22 -1
- package/src/cpsat/rules/max-shifts-day.ts +21 -1
- package/src/cpsat/rules/min-consecutive-days.ts +16 -1
- package/src/cpsat/rules/min-days-week.ts +120 -0
- package/src/cpsat/rules/min-hours-day.ts +16 -1
- package/src/cpsat/rules/min-hours-week.ts +17 -1
- package/src/cpsat/rules/min-rest-between-shifts.ts +92 -2
- package/src/cpsat/rules/minimize-cost.ts +2 -29
- package/src/cpsat/rules/must-assign.ts +108 -0
- package/src/cpsat/rules/overtime-daily-multiplier.ts +1 -12
- package/src/cpsat/rules/overtime-daily-surcharge.ts +1 -6
- package/src/cpsat/rules/overtime-tiered-multiplier.ts +6 -13
- package/src/cpsat/rules/overtime-weekly-multiplier.ts +1 -12
- package/src/cpsat/rules/overtime-weekly-surcharge.ts +1 -6
- package/src/cpsat/rules/registry.ts +8 -2
- package/src/cpsat/rules/resolver.ts +2 -2
- package/src/cpsat/rules/rules.types.ts +3 -0
- package/src/cpsat/rules/scope.types.ts +73 -20
- package/src/cpsat/rules/time-cost-surcharge.ts +2 -1
- package/src/cpsat/rules/time-off.ts +6 -2
- package/src/cpsat/semantic-time.ts +115 -91
- package/src/cpsat/types.ts +37 -27
- package/src/cpsat/utils.ts +8 -12
- package/src/cpsat/validation-reporter.ts +51 -82
- package/src/cpsat/validation.types.ts +72 -47
- package/src/datetime.utils.ts +3 -334
- package/src/index.ts +35 -107
- package/src/schedule/cost.ts +242 -0
- package/src/schedule/coverage.ts +135 -0
- package/src/schedule/definition.ts +958 -0
- package/src/schedule/index.ts +112 -0
- package/src/schedule/rules.ts +529 -0
- package/src/schedule/shift-patterns.ts +46 -0
- package/src/schedule/time-periods.ts +110 -0
- package/src/types.ts +14 -88
- package/dist/errors.d.ts +0 -12
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -17
- package/dist/errors.js.map +0 -1
- 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 -724
- package/dist/schedule.d.ts.map +0 -1
- package/dist/schedule.js +0 -899
- package/dist/schedule.js.map +0 -1
- package/dist/validation.d.ts +0 -105
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -130
- package/dist/validation.js.map +0 -1
- package/llms.txt +0 -925
- package/src/errors.ts +0 -17
- package/src/llms.ts +0 -3
- package/src/schedule.ts +0 -1419
- package/src/validation.ts +0 -188
package/src/cpsat/utils.ts
CHANGED
|
@@ -59,19 +59,15 @@ export function normalizeEndMinutes(startMinutes: number, endMinutes: number): n
|
|
|
59
59
|
return endMinutes < startMinutes ? endMinutes + MINUTES_PER_DAY : endMinutes;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
const PRIORITY_PENALTIES = {
|
|
63
|
+
LOW: 1,
|
|
64
|
+
MEDIUM: 10,
|
|
65
|
+
HIGH: 25,
|
|
66
|
+
MANDATORY: 0,
|
|
67
|
+
} as const satisfies Record<Priority, number>;
|
|
68
|
+
|
|
62
69
|
export function priorityToPenalty(priority: Priority): number {
|
|
63
|
-
|
|
64
|
-
case "HIGH":
|
|
65
|
-
return 25;
|
|
66
|
-
case "MEDIUM":
|
|
67
|
-
return 10;
|
|
68
|
-
case "LOW":
|
|
69
|
-
return 1;
|
|
70
|
-
case "MANDATORY":
|
|
71
|
-
return 0;
|
|
72
|
-
default:
|
|
73
|
-
return 0;
|
|
74
|
-
}
|
|
70
|
+
return PRIORITY_PENALTIES[priority];
|
|
75
71
|
}
|
|
76
72
|
|
|
77
73
|
/**
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { SolverResponse } from "../client.types.js";
|
|
2
2
|
import {
|
|
3
|
-
groupKey,
|
|
4
3
|
type ScheduleError,
|
|
5
4
|
type ScheduleViolation,
|
|
6
5
|
type SchedulePassed,
|
|
@@ -14,7 +13,6 @@ import {
|
|
|
14
13
|
type RulePassed,
|
|
15
14
|
type CoverageExclusion,
|
|
16
15
|
type ValidationSummary,
|
|
17
|
-
type GroupKey,
|
|
18
16
|
} from "./validation.types.js";
|
|
19
17
|
|
|
20
18
|
export interface ValidationReporter {
|
|
@@ -24,7 +22,7 @@ export interface ValidationReporter {
|
|
|
24
22
|
// Errors (block generation)
|
|
25
23
|
reportCoverageError(error: Omit<CoverageError, "type" | "id">): void;
|
|
26
24
|
reportRuleError(error: Omit<RuleError, "type" | "id">): void;
|
|
27
|
-
reportSolverError(
|
|
25
|
+
reportSolverError(message: string): void;
|
|
28
26
|
|
|
29
27
|
// Violations (soft constraint issues)
|
|
30
28
|
reportCoverageViolation(violation: Omit<CoverageViolation, "type" | "id">): void;
|
|
@@ -54,16 +52,16 @@ function coverageId(
|
|
|
54
52
|
category: "error" | "violation" | "passed",
|
|
55
53
|
day: string,
|
|
56
54
|
timeSlots: readonly string[],
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
roleIds?: readonly string[],
|
|
56
|
+
skillIds?: readonly string[],
|
|
59
57
|
): string {
|
|
60
58
|
const parts = [
|
|
61
59
|
category,
|
|
62
60
|
"coverage",
|
|
63
61
|
day,
|
|
64
62
|
[...timeSlots].toSorted().join(",") || "_",
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
roleIds && roleIds.length > 0 ? [...roleIds].toSorted().join(",") : "_",
|
|
64
|
+
skillIds ? [...skillIds].toSorted().join(",") : "_",
|
|
67
65
|
];
|
|
68
66
|
return parts.join(":");
|
|
69
67
|
}
|
|
@@ -100,7 +98,7 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
reportCoverageError(error: Omit<CoverageError, "type" | "id">): void {
|
|
103
|
-
const id = coverageId("error", error.day, error.timeSlots, error.
|
|
101
|
+
const id = coverageId("error", error.day, error.timeSlots, error.roleIds, error.skillIds);
|
|
104
102
|
this.#errors.push({ id, type: "coverage", ...error });
|
|
105
103
|
}
|
|
106
104
|
|
|
@@ -109,10 +107,10 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
109
107
|
this.#errors.push({ id, type: "rule", ...error });
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
reportSolverError(
|
|
110
|
+
reportSolverError(message: string): void {
|
|
113
111
|
this.#solverErrorCount++;
|
|
114
112
|
const id = `error:solver:${this.#solverErrorCount}`;
|
|
115
|
-
this.#errors.push({ id, type: "solver",
|
|
113
|
+
this.#errors.push({ id, type: "solver", message });
|
|
116
114
|
}
|
|
117
115
|
|
|
118
116
|
reportCoverageViolation(violation: Omit<CoverageViolation, "type" | "id">): void {
|
|
@@ -120,8 +118,8 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
120
118
|
"violation",
|
|
121
119
|
violation.day,
|
|
122
120
|
violation.timeSlots,
|
|
123
|
-
violation.
|
|
124
|
-
violation.
|
|
121
|
+
violation.roleIds,
|
|
122
|
+
violation.skillIds,
|
|
125
123
|
);
|
|
126
124
|
this.#violations.push({ id, type: "coverage", ...violation });
|
|
127
125
|
}
|
|
@@ -132,7 +130,7 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
132
130
|
}
|
|
133
131
|
|
|
134
132
|
reportCoveragePassed(passed: Omit<CoveragePassed, "type" | "id">): void {
|
|
135
|
-
const id = coverageId("passed", passed.day, passed.timeSlots, passed.
|
|
133
|
+
const id = coverageId("passed", passed.day, passed.timeSlots, passed.roleIds, passed.skillIds);
|
|
136
134
|
this.#passed.push({ id, type: "coverage", ...passed });
|
|
137
135
|
}
|
|
138
136
|
|
|
@@ -183,31 +181,33 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
183
181
|
const tracked = this.#trackedConstraints.get(violation.constraintId);
|
|
184
182
|
|
|
185
183
|
if (tracked?.type === "coverage") {
|
|
184
|
+
const roles = tracked.roleIds?.join(", ") ?? "staff";
|
|
185
|
+
const slot = tracked.timeSlot ?? "all day";
|
|
186
186
|
this.reportCoverageViolation({
|
|
187
187
|
day: tracked.day ?? "",
|
|
188
188
|
timeSlots: tracked.timeSlot ? [tracked.timeSlot] : [],
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
roleIds: tracked.roleIds,
|
|
190
|
+
skillIds: tracked.skillIds,
|
|
191
191
|
targetCount: violation.targetValue,
|
|
192
192
|
actualCount: violation.actualValue,
|
|
193
193
|
shortfall: violation.violationAmount,
|
|
194
|
-
|
|
194
|
+
message: `${roles} on ${tracked.day} (${slot}): ${violation.actualValue} assigned, need ${violation.targetValue}`,
|
|
195
|
+
group: tracked.group,
|
|
195
196
|
});
|
|
196
197
|
} else if (tracked?.type === "rule") {
|
|
197
198
|
const isShortfall = tracked.comparator === ">=";
|
|
198
199
|
this.reportRuleViolation({
|
|
199
200
|
rule: tracked.rule ?? "unknown",
|
|
200
|
-
|
|
201
|
+
message: `${tracked.description}: needed ${violation.targetValue}, got ${violation.actualValue}`,
|
|
201
202
|
context: tracked.context,
|
|
202
203
|
shortfall: isShortfall ? violation.violationAmount : undefined,
|
|
203
204
|
overflow: !isShortfall ? violation.violationAmount : undefined,
|
|
204
|
-
|
|
205
|
+
group: tracked.group,
|
|
205
206
|
});
|
|
206
207
|
} else {
|
|
207
|
-
// Unknown constraint - create generic rule violation
|
|
208
208
|
this.reportRuleViolation({
|
|
209
209
|
rule: "unknown",
|
|
210
|
-
|
|
210
|
+
message: `Constraint ${violation.constraintId} violated by ${violation.violationAmount}`,
|
|
211
211
|
context: {},
|
|
212
212
|
});
|
|
213
213
|
}
|
|
@@ -222,10 +222,10 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
222
222
|
this.reportCoveragePassed({
|
|
223
223
|
day: tracked.day ?? "",
|
|
224
224
|
timeSlots: tracked.timeSlot ? [tracked.timeSlot] : [],
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
roleIds: tracked.roleIds,
|
|
226
|
+
skillIds: tracked.skillIds,
|
|
227
|
+
message: tracked.description,
|
|
228
|
+
group: tracked.group,
|
|
229
229
|
});
|
|
230
230
|
}
|
|
231
231
|
}
|
|
@@ -233,23 +233,26 @@ export class ValidationReporterImpl implements ValidationReporter {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
// =============================================================================
|
|
236
|
-
// Validation Summary
|
|
236
|
+
// Validation Summary
|
|
237
237
|
// =============================================================================
|
|
238
238
|
|
|
239
239
|
type ValidationItem = ScheduleError | ScheduleViolation | SchedulePassed;
|
|
240
240
|
|
|
241
241
|
/**
|
|
242
|
-
* Aggregates validation items by their
|
|
243
|
-
* This is a pure function that doesn't modify the input.
|
|
242
|
+
* Aggregates validation items by their group into summaries.
|
|
244
243
|
*
|
|
245
|
-
* Items
|
|
244
|
+
* Items sharing the same `group.key` are merged into a single summary.
|
|
245
|
+
* The title comes from the first item's `group.title`; for ungrouped items
|
|
246
|
+
* the item's `message` is used instead.
|
|
247
|
+
*
|
|
248
|
+
* @category Validation
|
|
246
249
|
*
|
|
247
250
|
* @example
|
|
248
251
|
* ```typescript
|
|
249
|
-
* const validation = reporter.getValidation();
|
|
250
252
|
* const summaries = summarizeValidation(validation);
|
|
251
253
|
* // summaries[0] = {
|
|
252
|
-
* // groupKey: "
|
|
254
|
+
* // groupKey: "coverage:day_ward:nurse:3:dow:monday,tuesday,...",
|
|
255
|
+
* // title: "3x nurse during day_ward (weekdays)",
|
|
253
256
|
* // status: "passed",
|
|
254
257
|
* // passedCount: 180,
|
|
255
258
|
* // days: ["2026-02-02", "2026-02-03", ...]
|
|
@@ -258,10 +261,10 @@ type ValidationItem = ScheduleError | ScheduleViolation | SchedulePassed;
|
|
|
258
261
|
*/
|
|
259
262
|
export function summarizeValidation(validation: ScheduleValidation): readonly ValidationSummary[] {
|
|
260
263
|
const groups = new Map<
|
|
261
|
-
|
|
264
|
+
string,
|
|
262
265
|
{
|
|
263
266
|
type: "coverage" | "rule";
|
|
264
|
-
|
|
267
|
+
title: string;
|
|
265
268
|
days: Set<string>;
|
|
266
269
|
passedCount: number;
|
|
267
270
|
violatedCount: number;
|
|
@@ -269,11 +272,14 @@ export function summarizeValidation(validation: ScheduleValidation): readonly Va
|
|
|
269
272
|
}
|
|
270
273
|
>();
|
|
271
274
|
|
|
272
|
-
const
|
|
275
|
+
const getOrCreateStats = (item: Exclude<ValidationItem, { type: "solver" }>) => {
|
|
276
|
+
const key = ("group" in item && item.group?.key) || `ungrouped:${item.id}`;
|
|
277
|
+
const title = ("group" in item && item.group?.title) || item.message;
|
|
278
|
+
|
|
273
279
|
if (!groups.has(key)) {
|
|
274
280
|
groups.set(key, {
|
|
275
|
-
type,
|
|
276
|
-
|
|
281
|
+
type: item.type,
|
|
282
|
+
title,
|
|
277
283
|
days: new Set(),
|
|
278
284
|
passedCount: 0,
|
|
279
285
|
violatedCount: 0,
|
|
@@ -283,41 +289,31 @@ export function summarizeValidation(validation: ScheduleValidation): readonly Va
|
|
|
283
289
|
return groups.get(key)!;
|
|
284
290
|
};
|
|
285
291
|
|
|
286
|
-
// Group passed items
|
|
287
292
|
for (const item of validation.passed) {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
group.items.push(item);
|
|
291
|
-
group.passedCount++;
|
|
293
|
+
const stats = getOrCreateStats(item);
|
|
294
|
+
stats.passedCount++;
|
|
292
295
|
if (item.type === "coverage" && item.day) {
|
|
293
|
-
|
|
296
|
+
stats.days.add(item.day);
|
|
294
297
|
}
|
|
295
298
|
}
|
|
296
299
|
|
|
297
|
-
// Group violations
|
|
298
300
|
for (const item of validation.violations) {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
group.items.push(item);
|
|
302
|
-
group.violatedCount++;
|
|
301
|
+
const stats = getOrCreateStats(item);
|
|
302
|
+
stats.violatedCount++;
|
|
303
303
|
if (item.type === "coverage" && item.day) {
|
|
304
|
-
|
|
304
|
+
stats.days.add(item.day);
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
// Group errors (except solver errors which don't have groupKey)
|
|
309
308
|
for (const item of validation.errors) {
|
|
310
309
|
if (item.type === "solver") continue;
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
group.items.push(item);
|
|
314
|
-
group.errorCount++;
|
|
310
|
+
const stats = getOrCreateStats(item);
|
|
311
|
+
stats.errorCount++;
|
|
315
312
|
if (item.type === "coverage" && item.day) {
|
|
316
|
-
|
|
313
|
+
stats.days.add(item.day);
|
|
317
314
|
}
|
|
318
315
|
}
|
|
319
316
|
|
|
320
|
-
// Build summaries
|
|
321
317
|
const summaries: ValidationSummary[] = [];
|
|
322
318
|
for (const [key, group] of groups) {
|
|
323
319
|
const status: ValidationSummary["status"] =
|
|
@@ -326,7 +322,7 @@ export function summarizeValidation(validation: ScheduleValidation): readonly Va
|
|
|
326
322
|
summaries.push({
|
|
327
323
|
groupKey: key,
|
|
328
324
|
type: group.type,
|
|
329
|
-
|
|
325
|
+
title: group.title,
|
|
330
326
|
days: [...group.days].toSorted(),
|
|
331
327
|
status,
|
|
332
328
|
passedCount: group.passedCount,
|
|
@@ -337,30 +333,3 @@ export function summarizeValidation(validation: ScheduleValidation): readonly Va
|
|
|
337
333
|
|
|
338
334
|
return summaries;
|
|
339
335
|
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Infers a human-readable description from the groupKey or items.
|
|
343
|
-
*/
|
|
344
|
-
function inferDescription(key: GroupKey, items: ValidationItem[]): string {
|
|
345
|
-
// If the key looks like a human-readable description, use it
|
|
346
|
-
if (!key.startsWith("ungrouped:")) {
|
|
347
|
-
return key;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Try to infer from the first item with a description
|
|
351
|
-
for (const item of items) {
|
|
352
|
-
if ("description" in item && item.description) {
|
|
353
|
-
// Extract the meaningful part (e.g., "2x waiter" from "2x waiter on 2026-02-03 at 09:00")
|
|
354
|
-
const match = item.description.match(/^(\d+x \w+)/);
|
|
355
|
-
if (match?.[1]) {
|
|
356
|
-
return match[1];
|
|
357
|
-
}
|
|
358
|
-
return item.description;
|
|
359
|
-
}
|
|
360
|
-
if ("reason" in item && item.reason) {
|
|
361
|
-
return item.reason;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return key;
|
|
366
|
-
}
|
|
@@ -1,38 +1,56 @@
|
|
|
1
1
|
import type { TimeOfDay } from "../types.js";
|
|
2
2
|
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// Group Key - for aggregating related validation items
|
|
5
|
-
// =============================================================================
|
|
6
|
-
|
|
7
|
-
declare const GroupKeyBrand: unique symbol;
|
|
8
|
-
|
|
9
3
|
/**
|
|
10
|
-
*
|
|
11
|
-
* Groups related validation items that originated from the same instruction.
|
|
4
|
+
* Context shared across validation results for grouping/display.
|
|
12
5
|
*/
|
|
13
|
-
export
|
|
6
|
+
export interface ValidationContext {
|
|
7
|
+
days?: string[];
|
|
8
|
+
timeSlots?: string[];
|
|
9
|
+
memberIds?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Validation Group
|
|
14
|
+
// =============================================================================
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* Groups related validation items under a deterministic, structural key.
|
|
18
|
+
*
|
|
19
|
+
* The `key` is derived from the rule/coverage structure (e.g.,
|
|
20
|
+
* `"rule:max-hours-week:40:roles:nurse"`), ensuring that identical
|
|
21
|
+
* configurations always produce the same key. The `title` is the
|
|
22
|
+
* human-readable label shown in summaries.
|
|
18
23
|
*
|
|
19
|
-
* @
|
|
20
|
-
*
|
|
21
|
-
* const key = groupKey("2x waiter during lunch");
|
|
22
|
-
* coverage.groupKey = key;
|
|
23
|
-
* ```
|
|
24
|
+
* Rule authors create groups via {@link ruleGroup} in `scope.types.ts`;
|
|
25
|
+
* coverage groups are produced by the semantic-time resolver.
|
|
24
26
|
*/
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
+
export interface ValidationGroup {
|
|
28
|
+
/** Deterministic key derived from rule/coverage structure. */
|
|
29
|
+
readonly key: string;
|
|
30
|
+
/** Human-readable label for summaries (e.g., "Max 40h per week"). */
|
|
31
|
+
readonly title: string;
|
|
27
32
|
}
|
|
28
33
|
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Key safety helpers
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
const KEY_DELIMITERS = /[,:/+]/;
|
|
39
|
+
|
|
29
40
|
/**
|
|
30
|
-
*
|
|
41
|
+
* Throws if a value contains characters used as key delimiters.
|
|
42
|
+
* Call before interpolating user-provided IDs into group keys.
|
|
31
43
|
*/
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
export function assertSafeKeySegment(value: string, label: string): void {
|
|
45
|
+
if (KEY_DELIMITERS.test(value)) {
|
|
46
|
+
throw new Error(`${label} "${value}" contains reserved delimiter characters (: , / +)`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function assertSafeKeySegments(values: readonly string[], label: string): void {
|
|
51
|
+
for (const v of values) {
|
|
52
|
+
assertSafeKeySegment(v, label);
|
|
53
|
+
}
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
// =============================================================================
|
|
@@ -44,27 +62,27 @@ export interface CoverageError {
|
|
|
44
62
|
readonly type: "coverage";
|
|
45
63
|
readonly day: string;
|
|
46
64
|
readonly timeSlots: readonly string[];
|
|
47
|
-
readonly
|
|
48
|
-
readonly
|
|
49
|
-
readonly
|
|
65
|
+
readonly roleIds?: string[];
|
|
66
|
+
readonly skillIds?: readonly string[];
|
|
67
|
+
readonly message: string;
|
|
50
68
|
readonly suggestions?: readonly string[];
|
|
51
|
-
readonly
|
|
69
|
+
readonly group?: ValidationGroup;
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
export interface RuleError {
|
|
55
73
|
readonly id: string;
|
|
56
74
|
readonly type: "rule";
|
|
57
75
|
readonly rule: string;
|
|
58
|
-
readonly
|
|
76
|
+
readonly message: string;
|
|
59
77
|
readonly context: ValidationContext;
|
|
60
78
|
readonly suggestions?: readonly string[];
|
|
61
|
-
readonly
|
|
79
|
+
readonly group?: ValidationGroup;
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
export interface SolverError {
|
|
65
83
|
readonly id: string;
|
|
66
84
|
readonly type: "solver";
|
|
67
|
-
readonly
|
|
85
|
+
readonly message: string;
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
export type ScheduleError = CoverageError | RuleError | SolverError;
|
|
@@ -78,23 +96,24 @@ export interface CoverageViolation {
|
|
|
78
96
|
readonly type: "coverage";
|
|
79
97
|
readonly day: string;
|
|
80
98
|
readonly timeSlots: readonly string[];
|
|
81
|
-
readonly
|
|
82
|
-
readonly
|
|
99
|
+
readonly roleIds?: string[];
|
|
100
|
+
readonly skillIds?: readonly string[];
|
|
83
101
|
readonly targetCount: number;
|
|
84
102
|
readonly actualCount: number;
|
|
85
103
|
readonly shortfall: number;
|
|
86
|
-
readonly
|
|
104
|
+
readonly message: string;
|
|
105
|
+
readonly group?: ValidationGroup;
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
export interface RuleViolation {
|
|
90
109
|
readonly id: string;
|
|
91
110
|
readonly type: "rule";
|
|
92
111
|
readonly rule: string;
|
|
93
|
-
readonly
|
|
112
|
+
readonly message: string;
|
|
94
113
|
readonly context: ValidationContext;
|
|
95
114
|
readonly shortfall?: number;
|
|
96
115
|
readonly overflow?: number;
|
|
97
|
-
readonly
|
|
116
|
+
readonly group?: ValidationGroup;
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
export type ScheduleViolation = CoverageViolation | RuleViolation;
|
|
@@ -108,19 +127,19 @@ export interface CoveragePassed {
|
|
|
108
127
|
readonly type: "coverage";
|
|
109
128
|
readonly day: string;
|
|
110
129
|
readonly timeSlots: readonly string[];
|
|
111
|
-
readonly
|
|
112
|
-
readonly
|
|
113
|
-
readonly
|
|
114
|
-
readonly
|
|
130
|
+
readonly roleIds?: string[];
|
|
131
|
+
readonly skillIds?: readonly string[];
|
|
132
|
+
readonly message: string;
|
|
133
|
+
readonly group?: ValidationGroup;
|
|
115
134
|
}
|
|
116
135
|
|
|
117
136
|
export interface RulePassed {
|
|
118
137
|
readonly id: string;
|
|
119
138
|
readonly type: "rule";
|
|
120
139
|
readonly rule: string;
|
|
121
|
-
readonly
|
|
140
|
+
readonly message: string;
|
|
122
141
|
readonly context: ValidationContext;
|
|
123
|
-
readonly
|
|
142
|
+
readonly group?: ValidationGroup;
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
export type SchedulePassed = CoveragePassed | RulePassed;
|
|
@@ -129,6 +148,7 @@ export type SchedulePassed = CoveragePassed | RulePassed;
|
|
|
129
148
|
// Complete validation result
|
|
130
149
|
// =============================================================================
|
|
131
150
|
|
|
151
|
+
/** @category Validation */
|
|
132
152
|
export interface ScheduleValidation {
|
|
133
153
|
readonly errors: readonly ScheduleError[];
|
|
134
154
|
readonly violations: readonly ScheduleViolation[];
|
|
@@ -141,12 +161,16 @@ export interface ScheduleValidation {
|
|
|
141
161
|
|
|
142
162
|
/**
|
|
143
163
|
* Summary of validation items grouped by their source instruction.
|
|
144
|
-
* Use `summarizeValidation()` to create these from a ScheduleValidation
|
|
164
|
+
* Use `summarizeValidation()` to create these from a `ScheduleValidation`.
|
|
165
|
+
*
|
|
166
|
+
* @category Validation
|
|
145
167
|
*/
|
|
146
168
|
export interface ValidationSummary {
|
|
147
|
-
|
|
169
|
+
/** Deterministic group key derived from rule/coverage structure. */
|
|
170
|
+
readonly groupKey: string;
|
|
148
171
|
readonly type: "coverage" | "rule";
|
|
149
|
-
|
|
172
|
+
/** Human-readable title for this group (e.g., "3x nurse during day_ward"). */
|
|
173
|
+
readonly title: string;
|
|
150
174
|
readonly days: readonly string[];
|
|
151
175
|
readonly status: "passed" | "partial" | "failed";
|
|
152
176
|
readonly passedCount: number;
|
|
@@ -162,15 +186,16 @@ export interface TrackedConstraint {
|
|
|
162
186
|
readonly id: string;
|
|
163
187
|
readonly type: "coverage" | "rule";
|
|
164
188
|
readonly rule?: string;
|
|
189
|
+
/** Per-constraint description used to generate violation messages. */
|
|
165
190
|
readonly description: string;
|
|
166
191
|
readonly targetValue: number;
|
|
167
192
|
readonly comparator: "<=" | ">=";
|
|
168
193
|
readonly day?: string;
|
|
169
194
|
readonly timeSlot?: string;
|
|
170
|
-
readonly
|
|
171
|
-
readonly
|
|
195
|
+
readonly roleIds?: string[];
|
|
196
|
+
readonly skillIds?: readonly string[];
|
|
172
197
|
readonly context: ValidationContext;
|
|
173
|
-
readonly
|
|
198
|
+
readonly group?: ValidationGroup;
|
|
174
199
|
}
|
|
175
200
|
|
|
176
201
|
/**
|