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.
Files changed (231) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +45 -27
  3. package/dist/client.d.ts +20 -2
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +4 -1
  6. package/dist/client.js.map +1 -1
  7. package/dist/client.types.d.ts +9 -0
  8. package/dist/client.types.d.ts.map +1 -1
  9. package/dist/client.types.js +1 -0
  10. package/dist/client.types.js.map +1 -1
  11. package/dist/cpsat/model-builder.d.ts +9 -0
  12. package/dist/cpsat/model-builder.d.ts.map +1 -1
  13. package/dist/cpsat/model-builder.js +36 -34
  14. package/dist/cpsat/model-builder.js.map +1 -1
  15. package/dist/cpsat/response.d.ts +13 -1
  16. package/dist/cpsat/response.d.ts.map +1 -1
  17. package/dist/cpsat/response.js +4 -0
  18. package/dist/cpsat/response.js.map +1 -1
  19. package/dist/cpsat/rules/cost-utils.d.ts +11 -0
  20. package/dist/cpsat/rules/cost-utils.d.ts.map +1 -0
  21. package/dist/cpsat/rules/cost-utils.js +24 -0
  22. package/dist/cpsat/rules/cost-utils.js.map +1 -0
  23. package/dist/cpsat/rules/day-cost-multiplier.d.ts.map +1 -1
  24. package/dist/cpsat/rules/day-cost-multiplier.js +3 -14
  25. package/dist/cpsat/rules/day-cost-multiplier.js.map +1 -1
  26. package/dist/cpsat/rules/day-cost-surcharge.d.ts.map +1 -1
  27. package/dist/cpsat/rules/day-cost-surcharge.js +3 -7
  28. package/dist/cpsat/rules/day-cost-surcharge.js.map +1 -1
  29. package/dist/cpsat/rules/index.d.ts +3 -0
  30. package/dist/cpsat/rules/index.d.ts.map +1 -1
  31. package/dist/cpsat/rules/index.js +3 -0
  32. package/dist/cpsat/rules/index.js.map +1 -1
  33. package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
  34. package/dist/cpsat/rules/max-consecutive-days.js +16 -2
  35. package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
  36. package/dist/cpsat/rules/max-days-week.d.ts +44 -0
  37. package/dist/cpsat/rules/max-days-week.d.ts.map +1 -0
  38. package/dist/cpsat/rules/max-days-week.js +95 -0
  39. package/dist/cpsat/rules/max-days-week.js.map +1 -0
  40. package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
  41. package/dist/cpsat/rules/max-hours-day.js +15 -2
  42. package/dist/cpsat/rules/max-hours-day.js.map +1 -1
  43. package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
  44. package/dist/cpsat/rules/max-hours-week.js +16 -2
  45. package/dist/cpsat/rules/max-hours-week.js.map +1 -1
  46. package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
  47. package/dist/cpsat/rules/max-shifts-day.js +15 -2
  48. package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
  49. package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
  50. package/dist/cpsat/rules/min-consecutive-days.js +15 -2
  51. package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
  52. package/dist/cpsat/rules/min-days-week.d.ts +34 -0
  53. package/dist/cpsat/rules/min-days-week.d.ts.map +1 -0
  54. package/dist/cpsat/rules/min-days-week.js +84 -0
  55. package/dist/cpsat/rules/min-days-week.js.map +1 -0
  56. package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
  57. package/dist/cpsat/rules/min-hours-day.js +15 -2
  58. package/dist/cpsat/rules/min-hours-day.js.map +1 -1
  59. package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
  60. package/dist/cpsat/rules/min-hours-week.js +16 -2
  61. package/dist/cpsat/rules/min-hours-week.js.map +1 -1
  62. package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
  63. package/dist/cpsat/rules/min-rest-between-shifts.js +72 -2
  64. package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
  65. package/dist/cpsat/rules/minimize-cost.d.ts.map +1 -1
  66. package/dist/cpsat/rules/minimize-cost.js +2 -23
  67. package/dist/cpsat/rules/minimize-cost.js.map +1 -1
  68. package/dist/cpsat/rules/must-assign.d.ts +49 -0
  69. package/dist/cpsat/rules/must-assign.d.ts.map +1 -0
  70. package/dist/cpsat/rules/must-assign.js +86 -0
  71. package/dist/cpsat/rules/must-assign.js.map +1 -0
  72. package/dist/cpsat/rules/overtime-daily-multiplier.d.ts.map +1 -1
  73. package/dist/cpsat/rules/overtime-daily-multiplier.js +1 -12
  74. package/dist/cpsat/rules/overtime-daily-multiplier.js.map +1 -1
  75. package/dist/cpsat/rules/overtime-daily-surcharge.d.ts.map +1 -1
  76. package/dist/cpsat/rules/overtime-daily-surcharge.js +1 -5
  77. package/dist/cpsat/rules/overtime-daily-surcharge.js.map +1 -1
  78. package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts +5 -1
  79. package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts.map +1 -1
  80. package/dist/cpsat/rules/overtime-tiered-multiplier.js +1 -12
  81. package/dist/cpsat/rules/overtime-tiered-multiplier.js.map +1 -1
  82. package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts.map +1 -1
  83. package/dist/cpsat/rules/overtime-weekly-multiplier.js +1 -12
  84. package/dist/cpsat/rules/overtime-weekly-multiplier.js.map +1 -1
  85. package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts.map +1 -1
  86. package/dist/cpsat/rules/overtime-weekly-surcharge.js +1 -5
  87. package/dist/cpsat/rules/overtime-weekly-surcharge.js.map +1 -1
  88. package/dist/cpsat/rules/registry.d.ts +28 -2
  89. package/dist/cpsat/rules/registry.d.ts.map +1 -1
  90. package/dist/cpsat/rules/registry.js +4 -1
  91. package/dist/cpsat/rules/registry.js.map +1 -1
  92. package/dist/cpsat/rules/resolver.js +2 -2
  93. package/dist/cpsat/rules/resolver.js.map +1 -1
  94. package/dist/cpsat/rules/rules.types.d.ts +3 -0
  95. package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
  96. package/dist/cpsat/rules/scope.types.d.ts +18 -1
  97. package/dist/cpsat/rules/scope.types.d.ts.map +1 -1
  98. package/dist/cpsat/rules/scope.types.js +59 -16
  99. package/dist/cpsat/rules/scope.types.js.map +1 -1
  100. package/dist/cpsat/rules/time-cost-surcharge.d.ts.map +1 -1
  101. package/dist/cpsat/rules/time-cost-surcharge.js +2 -1
  102. package/dist/cpsat/rules/time-cost-surcharge.js.map +1 -1
  103. package/dist/cpsat/rules/time-off.d.ts.map +1 -1
  104. package/dist/cpsat/rules/time-off.js +6 -3
  105. package/dist/cpsat/rules/time-off.js.map +1 -1
  106. package/dist/cpsat/semantic-time.d.ts +44 -42
  107. package/dist/cpsat/semantic-time.d.ts.map +1 -1
  108. package/dist/cpsat/semantic-time.js +64 -46
  109. package/dist/cpsat/semantic-time.js.map +1 -1
  110. package/dist/cpsat/types.d.ts +37 -27
  111. package/dist/cpsat/types.d.ts.map +1 -1
  112. package/dist/cpsat/utils.d.ts.map +1 -1
  113. package/dist/cpsat/utils.js +7 -12
  114. package/dist/cpsat/utils.js.map +1 -1
  115. package/dist/cpsat/validation-reporter.d.ts +10 -7
  116. package/dist/cpsat/validation-reporter.d.ts.map +1 -1
  117. package/dist/cpsat/validation-reporter.js +44 -72
  118. package/dist/cpsat/validation-reporter.js.map +1 -1
  119. package/dist/cpsat/validation.types.d.ts +54 -44
  120. package/dist/cpsat/validation.types.d.ts.map +1 -1
  121. package/dist/cpsat/validation.types.js +15 -10
  122. package/dist/cpsat/validation.types.js.map +1 -1
  123. package/dist/datetime.utils.d.ts +3 -203
  124. package/dist/datetime.utils.d.ts.map +1 -1
  125. package/dist/datetime.utils.js +1 -288
  126. package/dist/datetime.utils.js.map +1 -1
  127. package/dist/index.d.ts +14 -83
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +11 -83
  130. package/dist/index.js.map +1 -1
  131. package/dist/schedule/cost.d.ts +204 -0
  132. package/dist/schedule/cost.d.ts.map +1 -0
  133. package/dist/schedule/cost.js +187 -0
  134. package/dist/schedule/cost.js.map +1 -0
  135. package/dist/schedule/coverage.d.ts +85 -0
  136. package/dist/schedule/coverage.d.ts.map +1 -0
  137. package/dist/schedule/coverage.js +33 -0
  138. package/dist/schedule/coverage.js.map +1 -0
  139. package/dist/schedule/definition.d.ts +227 -0
  140. package/dist/schedule/definition.d.ts.map +1 -0
  141. package/dist/schedule/definition.js +659 -0
  142. package/dist/schedule/definition.js.map +1 -0
  143. package/dist/schedule/index.d.ts +67 -0
  144. package/dist/schedule/index.d.ts.map +1 -0
  145. package/dist/schedule/index.js +69 -0
  146. package/dist/schedule/index.js.map +1 -0
  147. package/dist/schedule/rules.d.ts +353 -0
  148. package/dist/schedule/rules.d.ts.map +1 -0
  149. package/dist/schedule/rules.js +352 -0
  150. package/dist/schedule/rules.js.map +1 -0
  151. package/dist/schedule/shift-patterns.d.ts +34 -0
  152. package/dist/schedule/shift-patterns.d.ts.map +1 -0
  153. package/dist/schedule/shift-patterns.js +41 -0
  154. package/dist/schedule/shift-patterns.js.map +1 -0
  155. package/dist/schedule/time-periods.d.ts +69 -0
  156. package/dist/schedule/time-periods.d.ts.map +1 -0
  157. package/dist/schedule/time-periods.js +91 -0
  158. package/dist/schedule/time-periods.js.map +1 -0
  159. package/dist/types.d.ts +14 -78
  160. package/dist/types.d.ts.map +1 -1
  161. package/dist/types.js.map +1 -1
  162. package/package.json +4 -9
  163. package/solver/src/solver/app.py +1 -1
  164. package/solver/src/solver/solver.py +7 -4
  165. package/src/client.ts +6 -8
  166. package/src/client.types.ts +9 -0
  167. package/src/cpsat/model-builder.ts +44 -35
  168. package/src/cpsat/response.ts +13 -1
  169. package/src/cpsat/rules/cost-utils.ts +25 -0
  170. package/src/cpsat/rules/day-cost-multiplier.ts +3 -14
  171. package/src/cpsat/rules/day-cost-surcharge.ts +3 -8
  172. package/src/cpsat/rules/index.ts +3 -0
  173. package/src/cpsat/rules/max-consecutive-days.ts +17 -0
  174. package/src/cpsat/rules/max-days-week.ts +143 -0
  175. package/src/cpsat/rules/max-hours-day.ts +21 -1
  176. package/src/cpsat/rules/max-hours-week.ts +22 -1
  177. package/src/cpsat/rules/max-shifts-day.ts +21 -1
  178. package/src/cpsat/rules/min-consecutive-days.ts +16 -1
  179. package/src/cpsat/rules/min-days-week.ts +120 -0
  180. package/src/cpsat/rules/min-hours-day.ts +16 -1
  181. package/src/cpsat/rules/min-hours-week.ts +17 -1
  182. package/src/cpsat/rules/min-rest-between-shifts.ts +92 -2
  183. package/src/cpsat/rules/minimize-cost.ts +2 -29
  184. package/src/cpsat/rules/must-assign.ts +108 -0
  185. package/src/cpsat/rules/overtime-daily-multiplier.ts +1 -12
  186. package/src/cpsat/rules/overtime-daily-surcharge.ts +1 -6
  187. package/src/cpsat/rules/overtime-tiered-multiplier.ts +6 -13
  188. package/src/cpsat/rules/overtime-weekly-multiplier.ts +1 -12
  189. package/src/cpsat/rules/overtime-weekly-surcharge.ts +1 -6
  190. package/src/cpsat/rules/registry.ts +8 -2
  191. package/src/cpsat/rules/resolver.ts +2 -2
  192. package/src/cpsat/rules/rules.types.ts +3 -0
  193. package/src/cpsat/rules/scope.types.ts +73 -20
  194. package/src/cpsat/rules/time-cost-surcharge.ts +2 -1
  195. package/src/cpsat/rules/time-off.ts +6 -2
  196. package/src/cpsat/semantic-time.ts +115 -91
  197. package/src/cpsat/types.ts +37 -27
  198. package/src/cpsat/utils.ts +8 -12
  199. package/src/cpsat/validation-reporter.ts +51 -82
  200. package/src/cpsat/validation.types.ts +72 -47
  201. package/src/datetime.utils.ts +3 -334
  202. package/src/index.ts +35 -107
  203. package/src/schedule/cost.ts +242 -0
  204. package/src/schedule/coverage.ts +135 -0
  205. package/src/schedule/definition.ts +958 -0
  206. package/src/schedule/index.ts +112 -0
  207. package/src/schedule/rules.ts +529 -0
  208. package/src/schedule/shift-patterns.ts +46 -0
  209. package/src/schedule/time-periods.ts +110 -0
  210. package/src/types.ts +14 -88
  211. package/dist/errors.d.ts +0 -12
  212. package/dist/errors.d.ts.map +0 -1
  213. package/dist/errors.js +0 -17
  214. package/dist/errors.js.map +0 -1
  215. package/dist/llms.d.ts +0 -2
  216. package/dist/llms.d.ts.map +0 -1
  217. package/dist/llms.js +0 -3
  218. package/dist/llms.js.map +0 -1
  219. package/dist/schedule.d.ts +0 -724
  220. package/dist/schedule.d.ts.map +0 -1
  221. package/dist/schedule.js +0 -899
  222. package/dist/schedule.js.map +0 -1
  223. package/dist/validation.d.ts +0 -105
  224. package/dist/validation.d.ts.map +0 -1
  225. package/dist/validation.js +0 -130
  226. package/dist/validation.js.map +0 -1
  227. package/llms.txt +0 -925
  228. package/src/errors.ts +0 -17
  229. package/src/llms.ts +0 -3
  230. package/src/schedule.ts +0 -1419
  231. package/src/validation.ts +0 -188
@@ -12,6 +12,7 @@ import {
12
12
  parseTimeScope,
13
13
  resolveMembersFromScope,
14
14
  resolveActiveDaysFromScope,
15
+ ruleGroup,
15
16
  } from "./scope.types.js";
16
17
 
17
18
  const timeOfDaySchema = z.object({
@@ -108,6 +109,7 @@ export function createTimeOffRule(config: TimeOffConfig): CompilationRule {
108
109
 
109
110
  const entityScopeValue = parseEntityScope(parsed);
110
111
  const timeScopeValue = parseTimeScope(parsed);
112
+ const group = ruleGroup("time-off", "Time off", entityScopeValue, timeScopeValue);
111
113
 
112
114
  return {
113
115
  compile(builder) {
@@ -182,20 +184,22 @@ export function createTimeOffRule(config: TimeOffConfig): CompilationRule {
182
184
  if (violated) {
183
185
  reporter.reportRuleViolation({
184
186
  rule: "time-off",
185
- reason: `Time-off request for ${emp.id} on ${day} could not be honored`,
187
+ message: `Time-off request for ${emp.id} on ${day} could not be honored`,
186
188
  context: {
187
189
  memberIds: [emp.id],
188
190
  days: [day],
189
191
  },
192
+ group,
190
193
  });
191
194
  } else {
192
195
  reporter.reportRulePassed({
193
196
  rule: "time-off",
194
- description: `Time-off honored for ${emp.id} on ${day}`,
197
+ message: `Time-off honored for ${emp.id} on ${day}`,
195
198
  context: {
196
199
  memberIds: [emp.id],
197
200
  days: [day],
198
201
  },
202
+ group,
199
203
  });
200
204
  }
201
205
  }
@@ -2,7 +2,20 @@ import type { DayOfWeek, TimeOfDay } from "../types.js";
2
2
  import { toDayOfWeekUTC } from "../datetime.utils.js";
3
3
  import { parseDayString } from "./utils.js";
4
4
  import type { CoverageRequirement, Priority } from "./types.js";
5
- import { groupKey, type GroupKey } from "./validation.types.js";
5
+ import { assertSafeKeySegment, type ValidationGroup } from "./validation.types.js";
6
+
7
+ /** Validates and joins role/skill IDs into a key-safe segment. */
8
+ function safeRoleOrSkills(roleIds?: readonly string[], skillIds?: readonly string[]): string {
9
+ if (roleIds && roleIds.length > 0) {
10
+ for (const r of roleIds) assertSafeKeySegment(r, "role ID");
11
+ return roleIds.join("/");
12
+ }
13
+ if (skillIds && skillIds.length > 0) {
14
+ for (const s of skillIds) assertSafeKeySegment(s, "skill ID");
15
+ return skillIds.join("+");
16
+ }
17
+ throw new Error("At least one role or skill is required");
18
+ }
6
19
 
7
20
  /**
8
21
  * Base definition for a semantic time period.
@@ -25,7 +38,7 @@ export interface SemanticTimeDef {
25
38
  */
26
39
  export interface SemanticTimeVariant extends SemanticTimeDef {
27
40
  /** Restrict this entry to specific days of the week. */
28
- dayOfWeek?: readonly DayOfWeek[];
41
+ dayOfWeek?: readonly [DayOfWeek, ...DayOfWeek[]];
29
42
  /** Restrict this entry to specific dates (YYYY-MM-DD). */
30
43
  dates?: string[];
31
44
  }
@@ -44,15 +57,15 @@ interface SemanticCoverageRequirementBase<S extends string> {
44
57
  targetCount: number;
45
58
  priority?: Priority;
46
59
  /** Scope this requirement to specific days of the week */
47
- dayOfWeek?: DayOfWeek[];
60
+ dayOfWeek?: [DayOfWeek, ...DayOfWeek[]];
48
61
  /** Scope this requirement to specific dates (YYYY-MM-DD) */
49
62
  dates?: string[];
50
63
  /**
51
- * Override the auto-generated group key for validation reporting.
52
- * If not provided, a key is auto-generated from the semantic time name,
64
+ * Override the auto-generated group for validation reporting.
65
+ * If not provided, a group is auto-generated from the semantic time name,
53
66
  * role/skills, and target count.
54
67
  */
55
- groupKey?: GroupKey;
68
+ group?: ValidationGroup;
56
69
  }
57
70
 
58
71
  /**
@@ -62,14 +75,14 @@ interface RoleBasedSemanticCoverageRequirement<
62
75
  S extends string,
63
76
  > extends SemanticCoverageRequirementBase<S> {
64
77
  /**
65
- * Roles that satisfy this coverage (OR logic).
78
+ * Role IDs that satisfy this coverage (OR logic).
66
79
  * Must have at least one role.
67
80
  */
68
- roles: [string, ...string[]];
81
+ roleIds: [string, ...string[]];
69
82
  /**
70
- * Additional skill filter (AND logic with roles).
83
+ * Additional skill ID filter (AND logic with roles).
71
84
  */
72
- skills?: [string, ...string[]];
85
+ skillIds?: [string, ...string[]];
73
86
  }
74
87
 
75
88
  /**
@@ -78,12 +91,12 @@ interface RoleBasedSemanticCoverageRequirement<
78
91
  interface SkillBasedSemanticCoverageRequirement<
79
92
  S extends string,
80
93
  > extends SemanticCoverageRequirementBase<S> {
81
- roles?: never;
94
+ roleIds?: never;
82
95
  /**
83
- * Skills required (ALL required, AND logic).
96
+ * Skill IDs required (ALL required, AND logic).
84
97
  * Must have at least one skill.
85
98
  */
86
- skills: [string, ...string[]];
99
+ skillIds: [string, ...string[]];
87
100
  }
88
101
 
89
102
  /**
@@ -91,14 +104,14 @@ interface SkillBasedSemanticCoverageRequirement<
91
104
  * Type-safe: S is constrained to known semantic time names.
92
105
  *
93
106
  * This is a discriminated union enforcing at compile time that at least
94
- * one of `roles` or `skills` must be provided.
107
+ * one of `roleIds` or `skillIds` must be provided.
95
108
  *
96
109
  * @remarks
97
110
  * **Fields:**
98
111
  * - `semanticTime` (required) — name of a defined semantic time
99
112
  * - `targetCount` (required) — how many people are needed
100
- * - `roles` — roles that satisfy this (OR logic); at least one of `roles`/`skills` required
101
- * - `skills` — skills required (AND logic); at least one of `roles`/`skills` required
113
+ * - `roleIds` — role IDs that satisfy this (OR logic); at least one of `roleIds`/`skillIds` required
114
+ * - `skillIds` — skill IDs required (AND logic); at least one of `roleIds`/`skillIds` required
102
115
  * - `dayOfWeek` — scope to specific days of the week (e.g. `["monday", "tuesday"]`)
103
116
  * - `dates` — scope to specific dates (`"YYYY-MM-DD"` strings)
104
117
  * - `priority` — `"MANDATORY"` | `"HIGH"` | `"MEDIUM"` | `"LOW"`
@@ -109,15 +122,15 @@ interface SkillBasedSemanticCoverageRequirement<
109
122
  *
110
123
  * @example Weekday vs weekend (mutually exclusive dayOfWeek)
111
124
  * ```typescript
112
- * { semanticTime: "day_shift", roles: ["nurse"], targetCount: 3,
125
+ * { semanticTime: "day_shift", roleIds: ["nurse"], targetCount: 3,
113
126
  * dayOfWeek: ["monday", "tuesday", "wednesday", "thursday", "friday"] },
114
- * { semanticTime: "day_shift", roles: ["nurse"], targetCount: 2,
127
+ * { semanticTime: "day_shift", roleIds: ["nurse"], targetCount: 2,
115
128
  * dayOfWeek: ["saturday", "sunday"] },
116
129
  * ```
117
130
  *
118
131
  * @example Skill-based coverage (any role with the skill)
119
132
  * ```typescript
120
- * { semanticTime: "night_shift", skills: ["charge_nurse"], targetCount: 1 },
133
+ * { semanticTime: "night_shift", skillIds: ["charge_nurse"], targetCount: 1 },
121
134
  * ```
122
135
  */
123
136
  export type SemanticCoverageRequirement<S extends string> =
@@ -134,11 +147,11 @@ interface ConcreteCoverageRequirementBase {
134
147
  targetCount: number;
135
148
  priority?: Priority;
136
149
  /**
137
- * Override the auto-generated group key for validation reporting.
138
- * If not provided, a key is auto-generated from the day, time range,
150
+ * Override the auto-generated group for validation reporting.
151
+ * If not provided, a group is auto-generated from the day, time range,
139
152
  * role/skills, and target count.
140
153
  */
141
- groupKey?: GroupKey;
154
+ group?: ValidationGroup;
142
155
  }
143
156
 
144
157
  /**
@@ -146,26 +159,26 @@ interface ConcreteCoverageRequirementBase {
146
159
  */
147
160
  interface RoleBasedConcreteCoverageRequirement extends ConcreteCoverageRequirementBase {
148
161
  /**
149
- * Roles that satisfy this coverage (OR logic).
162
+ * Role IDs that satisfy this coverage (OR logic).
150
163
  * Must have at least one role.
151
164
  */
152
- roles: [string, ...string[]];
165
+ roleIds: [string, ...string[]];
153
166
  /**
154
- * Additional skill filter (AND logic with roles).
167
+ * Additional skill ID filter (AND logic with roles).
155
168
  */
156
- skills?: [string, ...string[]];
169
+ skillIds?: [string, ...string[]];
157
170
  }
158
171
 
159
172
  /**
160
173
  * Concrete coverage requiring specific skills only (any role).
161
174
  */
162
175
  interface SkillBasedConcreteCoverageRequirement extends ConcreteCoverageRequirementBase {
163
- roles?: never;
176
+ roleIds?: never;
164
177
  /**
165
- * Skills required (ALL required, AND logic).
178
+ * Skill IDs required (ALL required, AND logic).
166
179
  * Must have at least one skill.
167
180
  */
168
- skills: [string, ...string[]];
181
+ skillIds: [string, ...string[]];
169
182
  }
170
183
 
171
184
  /**
@@ -173,7 +186,7 @@ interface SkillBasedConcreteCoverageRequirement extends ConcreteCoverageRequirem
173
186
  * Used for one-off requirements that don't fit a semantic time.
174
187
  *
175
188
  * This is a discriminated union enforcing at compile time that at least
176
- * one of `roles` or `skills` must be provided.
189
+ * one of `roleIds` or `skillIds` must be provided.
177
190
  */
178
191
  export type ConcreteCoverageRequirement =
179
192
  | RoleBasedConcreteCoverageRequirement
@@ -199,12 +212,14 @@ export type ConcreteCoverageRequirement =
199
212
  * { count: 2, dates: ["2025-12-24"] },
200
213
  * )
201
214
  * ```
215
+ *
216
+ * @category Coverage
202
217
  */
203
218
  export interface CoverageVariant {
204
219
  /** Number of people needed. */
205
220
  count: number;
206
221
  /** Restrict this variant to specific days of the week. */
207
- dayOfWeek?: readonly DayOfWeek[];
222
+ dayOfWeek?: readonly [DayOfWeek, ...DayOfWeek[]];
208
223
  /** Restrict this variant to specific dates (YYYY-MM-DD). */
209
224
  dates?: string[];
210
225
  /** Defaults to `"MANDATORY"`. */
@@ -218,7 +233,7 @@ interface VariantCoverageRequirementBase<S extends string> {
218
233
  semanticTime: S;
219
234
  /** At least one variant is required. At most one may be unscoped (the default). */
220
235
  variants: [CoverageVariant, ...CoverageVariant[]];
221
- groupKey?: GroupKey;
236
+ group?: ValidationGroup;
222
237
  }
223
238
 
224
239
  /**
@@ -227,8 +242,8 @@ interface VariantCoverageRequirementBase<S extends string> {
227
242
  interface RoleBasedVariantCoverageRequirement<
228
243
  S extends string,
229
244
  > extends VariantCoverageRequirementBase<S> {
230
- roles: [string, ...string[]];
231
- skills?: [string, ...string[]];
245
+ roleIds: [string, ...string[]];
246
+ skillIds?: [string, ...string[]];
232
247
  }
233
248
 
234
249
  /**
@@ -237,8 +252,8 @@ interface RoleBasedVariantCoverageRequirement<
237
252
  interface SkillBasedVariantCoverageRequirement<
238
253
  S extends string,
239
254
  > extends VariantCoverageRequirementBase<S> {
240
- roles?: never;
241
- skills: [string, ...string[]];
255
+ roleIds?: never;
256
+ skillIds: [string, ...string[]];
242
257
  }
243
258
 
244
259
  /**
@@ -254,7 +269,7 @@ interface SkillBasedVariantCoverageRequirement<
254
269
  * @example Decrease from default on a specific date
255
270
  * ```typescript
256
271
  * {
257
- * semanticTime: "peak_hours", roles: ["agent"],
272
+ * semanticTime: "peak_hours", roleIds: ["agent"],
258
273
  * variants: [
259
274
  * { count: 4 }, // default
260
275
  * { count: 2, dates: ["2025-12-24"] }, // Christmas Eve
@@ -341,10 +356,10 @@ export interface SemanticTimeContext<S extends string> {
341
356
  * });
342
357
  *
343
358
  * const coverage = times.coverage([
344
- * { semanticTime: "day_shift", roles: ["nurse"], targetCount: 3 },
345
- * { semanticTime: "morning_round", skills: ["charge_nurse"], targetCount: 1, priority: "MANDATORY" },
359
+ * { semanticTime: "day_shift", roleIds: ["nurse"], targetCount: 3 },
360
+ * { semanticTime: "morning_round", skillIds: ["charge_nurse"], targetCount: 1, priority: "MANDATORY" },
346
361
  * // Type error: "evening" is not a defined semantic time
347
- * // { semanticTime: "evening", roles: ["nurse"], targetCount: 2 },
362
+ * // { semanticTime: "evening", roleIds: ["nurse"], targetCount: 2 },
348
363
  * ]);
349
364
  * ```
350
365
  *
@@ -361,9 +376,9 @@ export interface SemanticTimeContext<S extends string> {
361
376
  * @example Mixed semantic and concrete coverage
362
377
  * ```typescript
363
378
  * const coverage = times.coverage([
364
- * { semanticTime: "day_shift", roles: ["nurse"], targetCount: 3 },
379
+ * { semanticTime: "day_shift", roleIds: ["nurse"], targetCount: 3 },
365
380
  * // One-off event requiring extra staff
366
- * { day: "2026-01-14", startTime: { hours: 8 }, endTime: { hours: 12 }, roles: ["nurse"], targetCount: 5 },
381
+ * { day: "2026-01-14", startTime: { hours: 8 }, endTime: { hours: 12 }, roleIds: ["nurse"], targetCount: 5 },
367
382
  * ]);
368
383
  * ```
369
384
  */
@@ -393,25 +408,25 @@ function buildCoverageRequirement(
393
408
  day: string,
394
409
  startTime: TimeOfDay,
395
410
  endTime: TimeOfDay,
396
- roles: [string, ...string[]] | undefined,
397
- skills: [string, ...string[]] | undefined,
411
+ roleIds: [string, ...string[]] | undefined,
412
+ skillIds: [string, ...string[]] | undefined,
398
413
  targetCount: number,
399
414
  priority: Priority,
400
- gKey: GroupKey,
415
+ group: ValidationGroup,
401
416
  ): CoverageRequirement {
402
- const base = { day, startTime, endTime, targetCount, priority, groupKey: gKey };
417
+ const base = { day, startTime, endTime, targetCount, priority, group };
403
418
 
404
- if (roles && roles.length > 0) {
419
+ if (roleIds && roleIds.length > 0) {
405
420
  // Role-based (with optional skills)
406
- return skills && skills.length > 0 ? { ...base, roles, skills } : { ...base, roles };
407
- } else if (skills && skills.length > 0) {
421
+ return skillIds && skillIds.length > 0 ? { ...base, roleIds, skillIds } : { ...base, roleIds };
422
+ } else if (skillIds && skillIds.length > 0) {
408
423
  // Skill-only
409
- return { ...base, skills };
424
+ return { ...base, skillIds };
410
425
  }
411
426
 
412
427
  // This shouldn't happen if input types are correct, but handle gracefully
413
428
  throw new Error(
414
- `Coverage requirement for day "${day}" must have at least one of roles or skills`,
429
+ `Coverage requirement for day "${day}" must have at least one of roleIds or skillIds`,
415
430
  );
416
431
  }
417
432
 
@@ -431,17 +446,17 @@ function resolveSemanticCoverage<S extends string>(
431
446
  if (isConcreteCoverage(req)) {
432
447
  // Concrete requirement - pass through if day is in horizon
433
448
  if (daySet.has(req.day)) {
434
- const autoGroupKey = generateConcreteGroupKey(req);
449
+ const defaultGroup = concreteCoverageGroup(req);
435
450
  result.push(
436
451
  buildCoverageRequirement(
437
452
  req.day,
438
453
  req.startTime,
439
454
  req.endTime,
440
- req.roles,
441
- req.skills,
455
+ req.roleIds,
456
+ req.skillIds,
442
457
  req.targetCount,
443
458
  req.priority ?? "MANDATORY",
444
- req.groupKey ?? autoGroupKey,
459
+ req.group ?? defaultGroup,
445
460
  ),
446
461
  );
447
462
  }
@@ -452,7 +467,7 @@ function resolveSemanticCoverage<S extends string>(
452
467
  throw new Error(`Unknown semantic time: ${req.semanticTime}`);
453
468
  }
454
469
 
455
- const autoGroupKey = generateVariantGroupKey(req);
470
+ const defaultGroup = variantCoverageGroup(req);
456
471
 
457
472
  for (const day of days) {
458
473
  const resolved = resolveTimeForDay(entry, day);
@@ -466,11 +481,11 @@ function resolveSemanticCoverage<S extends string>(
466
481
  day,
467
482
  resolved.startTime,
468
483
  resolved.endTime,
469
- req.roles,
470
- req.skills,
484
+ req.roleIds,
485
+ req.skillIds,
471
486
  variant.count,
472
487
  variant.priority ?? "MANDATORY",
473
- req.groupKey ?? autoGroupKey,
488
+ req.group ?? defaultGroup,
474
489
  ),
475
490
  );
476
491
  }
@@ -481,7 +496,7 @@ function resolveSemanticCoverage<S extends string>(
481
496
  throw new Error(`Unknown semantic time: ${req.semanticTime}`);
482
497
  }
483
498
 
484
- const autoGroupKey = generateSemanticGroupKey(req);
499
+ const defaultGroup = semanticCoverageGroup(req);
485
500
  const applicableDays = filterDays(days, req.dayOfWeek, req.dates);
486
501
 
487
502
  for (const day of applicableDays) {
@@ -492,11 +507,11 @@ function resolveSemanticCoverage<S extends string>(
492
507
  day,
493
508
  resolved.startTime,
494
509
  resolved.endTime,
495
- req.roles,
496
- req.skills,
510
+ req.roleIds,
511
+ req.skillIds,
497
512
  req.targetCount,
498
513
  req.priority ?? "MANDATORY",
499
- req.groupKey ?? autoGroupKey,
514
+ req.group ?? defaultGroup,
500
515
  ),
501
516
  );
502
517
  }
@@ -507,33 +522,42 @@ function resolveSemanticCoverage<S extends string>(
507
522
  return result;
508
523
  }
509
524
 
510
- /**
511
- * Generates a human-readable group key for a semantic coverage requirement.
512
- * Format: "{count}x {role/skills} during {semanticTime}" with optional scope
513
- */
514
- function generateSemanticGroupKey<S extends string>(req: SemanticCoverageRequirement<S>): GroupKey {
515
- const roleOrSkills = req.roles?.join("/") ?? req.skills?.join("+") ?? "staff";
516
- const base = `${req.targetCount}x ${roleOrSkills} during ${req.semanticTime}`;
525
+ /** Generates deterministic group for a semantic coverage requirement. */
526
+ function semanticCoverageGroup<S extends string>(
527
+ req: SemanticCoverageRequirement<S>,
528
+ ): ValidationGroup {
529
+ assertSafeKeySegment(req.semanticTime, "semantic time name");
530
+ const roleOrSkills = safeRoleOrSkills(req.roleIds, req.skillIds);
531
+ const title = `${req.targetCount}x ${roleOrSkills} during ${req.semanticTime}`;
517
532
 
518
- // Add scope qualifier if scoped to specific days
533
+ let scopeSuffix = "";
519
534
  if (req.dayOfWeek && req.dayOfWeek.length > 0 && req.dayOfWeek.length < 7) {
520
- return groupKey(`${base} (${formatDaysScope(req.dayOfWeek)})`);
521
- }
522
- if (req.dates && req.dates.length > 0) {
523
- return groupKey(`${base} (specific dates)`);
535
+ scopeSuffix = `:dow:${req.dayOfWeek.toSorted().join(",")}`;
536
+ } else if (req.dates && req.dates.length > 0) {
537
+ scopeSuffix = `:dates:${req.dates.toSorted().join(",")}`;
524
538
  }
525
539
 
526
- return groupKey(base);
540
+ const key = `coverage:${req.semanticTime}:${roleOrSkills}:${req.targetCount}${scopeSuffix}`;
541
+ const titleSuffix =
542
+ scopeSuffix && req.dayOfWeek
543
+ ? ` (${formatDaysScope(req.dayOfWeek)})`
544
+ : scopeSuffix && req.dates
545
+ ? " (specific dates)"
546
+ : "";
547
+
548
+ return { key, title: `${title}${titleSuffix}` };
527
549
  }
528
550
 
529
- /**
530
- * Generates a human-readable group key for a variant coverage requirement.
531
- * Format: "{role/skills} during {semanticTime}" — shared across all days since
532
- * the count varies per variant.
533
- */
534
- function generateVariantGroupKey<S extends string>(req: VariantCoverageRequirement<S>): GroupKey {
535
- const roleOrSkills = req.roles?.join("/") ?? req.skills?.join("+") ?? "staff";
536
- return groupKey(`${roleOrSkills} during ${req.semanticTime}`);
551
+ /** Generates deterministic group for a variant coverage requirement. */
552
+ function variantCoverageGroup<S extends string>(
553
+ req: VariantCoverageRequirement<S>,
554
+ ): ValidationGroup {
555
+ assertSafeKeySegment(req.semanticTime, "semantic time name");
556
+ const roleOrSkills = safeRoleOrSkills(req.roleIds, req.skillIds);
557
+ return {
558
+ key: `coverage-variant:${req.semanticTime}:${roleOrSkills}`,
559
+ title: `${roleOrSkills} during ${req.semanticTime}`,
560
+ };
537
561
  }
538
562
 
539
563
  /**
@@ -569,14 +593,14 @@ function resolveVariantForDay(
569
593
  return dateMatch ?? dowMatch ?? defaultMatch;
570
594
  }
571
595
 
572
- /**
573
- * Generates a human-readable group key for a concrete coverage requirement.
574
- * Format: "{count}x {role/skills} on {day} {time}"
575
- */
576
- function generateConcreteGroupKey(req: ConcreteCoverageRequirement): GroupKey {
577
- const roleOrSkills = req.roles?.join("/") ?? req.skills?.join("+") ?? "staff";
596
+ /** Generates deterministic group for a concrete coverage requirement. */
597
+ function concreteCoverageGroup(req: ConcreteCoverageRequirement): ValidationGroup {
598
+ const roleOrSkills = safeRoleOrSkills(req.roleIds, req.skillIds);
578
599
  const timeRange = `${formatTime(req.startTime)}-${formatTime(req.endTime)}`;
579
- return groupKey(`${req.targetCount}x ${roleOrSkills} on ${req.day} ${timeRange}`);
600
+ return {
601
+ key: `coverage-concrete:${req.day}:${roleOrSkills}:${req.targetCount}:${timeRange}`,
602
+ title: `${req.targetCount}x ${roleOrSkills} on ${req.day} ${timeRange}`,
603
+ };
580
604
  }
581
605
 
582
606
  /**
@@ -1,9 +1,11 @@
1
1
  import type { TimeOfDay, DayOfWeek } from "../types.js";
2
2
  import type { SolverRequest, SolverTerm } from "../client.types.js";
3
- import type { GroupKey } from "./validation.types.js";
3
+ import type { ValidationGroup } from "./validation.types.js";
4
4
 
5
5
  /**
6
6
  * Pay per hour in the caller's smallest currency unit (e.g., pence, cents).
7
+ *
8
+ * @category Supporting Types
7
9
  */
8
10
  export interface HourlyPay {
9
11
  /** Pay per hour in smallest currency unit. */
@@ -19,6 +21,8 @@ export interface HourlyPay {
19
21
  *
20
22
  * Note: overtime multiplier rules apply only to hourly members.
21
23
  * Overtime surcharge rules apply to all members regardless of pay type.
24
+ *
25
+ * @category Supporting Types
22
26
  */
23
27
  export interface SalariedPay {
24
28
  /** Annual salary in smallest currency unit. */
@@ -32,6 +36,8 @@ export interface SalariedPay {
32
36
  *
33
37
  * - `"LOW"`, `"MEDIUM"`, `"HIGH"`: soft constraints with increasing penalty for violations
34
38
  * - `"MANDATORY"`: hard constraint; the solver will not produce a solution that violates it
39
+ *
40
+ * @category Supporting Types
35
41
  */
36
42
  export type Priority = "LOW" | "MEDIUM" | "HIGH" | "MANDATORY";
37
43
 
@@ -40,14 +46,16 @@ export type Priority = "LOW" | "MEDIUM" | "HIGH" | "MANDATORY";
40
46
  *
41
47
  * Members are assigned to shift patterns by the solver based on
42
48
  * coverage requirements, rules, and constraints.
49
+ *
50
+ * @category Supporting Types
43
51
  */
44
52
  export interface SchedulingMember {
45
53
  /** Unique identifier for this member. Must not contain colons. */
46
54
  id: string;
47
- /** Roles this member can fill (e.g. "nurse", "doctor"). */
48
- roles: string[];
49
- /** Skills this member has (e.g. "charge_nurse", "forklift"). */
50
- skills?: string[];
55
+ /** Role IDs this member can fill (e.g. "nurse", "doctor"). */
56
+ roleIds: string[];
57
+ /** Skill IDs this member has (e.g. "charge_nurse", "forklift"). */
58
+ skillIds?: string[];
51
59
  /** Base pay. Required when cost rules are used. */
52
60
  pay?: HourlyPay | SalariedPay;
53
61
  }
@@ -67,9 +75,11 @@ export interface SchedulingMember {
67
75
  * @example
68
76
  * // Role-restricted shifts
69
77
  * const patterns: ShiftPattern[] = [
70
- * { id: "ward_day", startTime: { hours: 7 }, endTime: { hours: 15 }, roles: ["nurse", "doctor"] },
71
- * { id: "reception", startTime: { hours: 8 }, endTime: { hours: 16 }, roles: ["admin"] },
78
+ * { id: "ward_day", startTime: { hours: 7 }, endTime: { hours: 15 }, roleIds: ["nurse", "doctor"] },
79
+ * { id: "reception", startTime: { hours: 8 }, endTime: { hours: 16 }, roleIds: ["admin"] },
72
80
  * ];
81
+ *
82
+ * @category Supporting Types
73
83
  */
74
84
  export interface ShiftPattern {
75
85
  /**
@@ -79,7 +89,7 @@ export interface ShiftPattern {
79
89
  id: string;
80
90
 
81
91
  /**
82
- * Restricts who can be assigned to this shift based on their roles.
92
+ * Restricts who can be assigned to this shift based on their role IDs.
83
93
  *
84
94
  * - If omitted: anyone can work this shift
85
95
  * - If provided: only team members whose roles overlap with this list can be assigned
@@ -88,7 +98,7 @@ export interface ShiftPattern {
88
98
  * Use it when different roles have different schedules (e.g., kitchen staff starts
89
99
  * earlier than floor staff).
90
100
  */
91
- roles?: [string, ...string[]];
101
+ roleIds?: [string, ...string[]];
92
102
 
93
103
  /**
94
104
  * Restricts which days of the week this shift pattern can be used.
@@ -105,7 +115,7 @@ export interface ShiftPattern {
105
115
  * { id: "full_shift", startTime: t(9), endTime: t(18), dayOfWeek: ["monday", "tuesday", "wednesday", "thursday", "friday"] }
106
116
  * ```
107
117
  */
108
- dayOfWeek?: DayOfWeek[];
118
+ dayOfWeek?: readonly [DayOfWeek, ...DayOfWeek[]];
109
119
 
110
120
  /**
111
121
  * Physical location where this shift takes place.
@@ -133,13 +143,13 @@ interface CoverageRequirementBase extends TimeInterval {
133
143
  targetCount: number;
134
144
  priority: Priority;
135
145
  /**
136
- * Groups this requirement with others sharing the same key for validation reporting.
137
- * When provided, all coverage constraints generated from this requirement will
138
- * share the same groupKey, enabling meaningful aggregation in reports.
146
+ * Groups this requirement with others for validation reporting.
147
+ * All coverage constraints generated from this requirement will share the
148
+ * same group, enabling meaningful aggregation in reports.
139
149
  *
140
- * If not provided, an auto-generated key will be used based on the coverage parameters.
150
+ * If not provided, an auto-generated group will be used based on the coverage parameters.
141
151
  */
142
- groupKey?: GroupKey;
152
+ group?: ValidationGroup;
143
153
  }
144
154
 
145
155
  /**
@@ -149,16 +159,16 @@ interface CoverageRequirementBase extends TimeInterval {
149
159
  */
150
160
  interface RoleBasedCoverageRequirement extends CoverageRequirementBase {
151
161
  /**
152
- * Roles that satisfy this coverage (OR logic).
162
+ * Role IDs that satisfy this coverage (OR logic).
153
163
  * A person matches if they have ANY of these roles.
154
164
  * Must have at least one role.
155
165
  */
156
- roles: [string, ...string[]];
166
+ roleIds: [string, ...string[]];
157
167
  /**
158
- * Additional skill filter (AND logic with roles).
168
+ * Additional skill ID filter (AND logic with roles).
159
169
  * If provided, team members must have ALL specified skills in addition to matching a role.
160
170
  */
161
- skills?: [string, ...string[]];
171
+ skillIds?: [string, ...string[]];
162
172
  }
163
173
 
164
174
  /**
@@ -169,19 +179,19 @@ interface SkillBasedCoverageRequirement extends CoverageRequirementBase {
169
179
  /**
170
180
  * Must not be present for skill-based coverage.
171
181
  */
172
- roles?: never;
182
+ roleIds?: never;
173
183
  /**
174
- * Skills required to satisfy this coverage (ALL required, AND logic).
184
+ * Skill IDs required to satisfy this coverage (ALL required, AND logic).
175
185
  * Must have at least one skill.
176
186
  */
177
- skills: [string, ...string[]];
187
+ skillIds: [string, ...string[]];
178
188
  }
179
189
 
180
190
  /**
181
191
  * Defines staffing needs for a specific time period.
182
192
  *
183
193
  * This is a discriminated union that enforces at compile time that at least
184
- * one of `roles` or `skills` must be provided:
194
+ * one of `roleIds` or `skillIds` must be provided:
185
195
  *
186
196
  * - Role-based: `{ roles: ["waiter"], ... }` - anyone with ANY of these roles (OR logic)
187
197
  * - Role + skill: `{ roles: ["waiter"], skills: ["senior"], ... }` - role AND skills
@@ -189,19 +199,19 @@ interface SkillBasedCoverageRequirement extends CoverageRequirementBase {
189
199
  *
190
200
  * @example
191
201
  * // Need 2 waiters during lunch (role-based)
192
- * { day: "2024-01-01", startTime: { hours: 11 }, endTime: { hours: 14 }, roles: ["waiter"], targetCount: 2, priority: "MANDATORY" }
202
+ * { day: "2024-01-01", startTime: { hours: 11 }, endTime: { hours: 14 }, roleIds: ["waiter"], targetCount: 2, priority: "MANDATORY" }
193
203
  *
194
204
  * @example
195
205
  * // Need 1 manager OR supervisor during service (OR logic on roles)
196
- * { day: "2024-01-01", startTime: { hours: 11 }, endTime: { hours: 22 }, roles: ["manager", "supervisor"], targetCount: 1, priority: "MANDATORY" }
206
+ * { day: "2024-01-01", startTime: { hours: 11 }, endTime: { hours: 22 }, roleIds: ["manager", "supervisor"], targetCount: 1, priority: "MANDATORY" }
197
207
  *
198
208
  * @example
199
209
  * // Need 1 keyholder for opening (skill-only, any role)
200
- * { day: "2024-01-01", startTime: { hours: 6 }, endTime: { hours: 8 }, skills: ["keyholder"], targetCount: 1, priority: "MANDATORY" }
210
+ * { day: "2024-01-01", startTime: { hours: 6 }, endTime: { hours: 8 }, skillIds: ["keyholder"], targetCount: 1, priority: "MANDATORY" }
201
211
  *
202
212
  * @example
203
213
  * // Need 1 senior waiter for training shift (role + skill filter)
204
- * { day: "2024-01-01", startTime: { hours: 9 }, endTime: { hours: 17 }, roles: ["waiter"], skills: ["senior"], targetCount: 1, priority: "HIGH" }
214
+ * { day: "2024-01-01", startTime: { hours: 9 }, endTime: { hours: 17 }, roleIds: ["waiter"], skillIds: ["senior"], targetCount: 1, priority: "HIGH" }
205
215
  */
206
216
  export type CoverageRequirement = RoleBasedCoverageRequirement | SkillBasedCoverageRequirement;
207
217