dabke 0.80.0 → 0.81.1

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 (205) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +108 -299
  3. package/dist/cpsat/cost.d.ts +65 -0
  4. package/dist/cpsat/cost.d.ts.map +1 -0
  5. package/dist/cpsat/cost.js +77 -0
  6. package/dist/cpsat/cost.js.map +1 -0
  7. package/dist/cpsat/model-builder.d.ts +51 -17
  8. package/dist/cpsat/model-builder.d.ts.map +1 -1
  9. package/dist/cpsat/model-builder.js +76 -76
  10. package/dist/cpsat/model-builder.js.map +1 -1
  11. package/dist/cpsat/response.d.ts +11 -11
  12. package/dist/cpsat/response.d.ts.map +1 -1
  13. package/dist/cpsat/response.js +10 -10
  14. package/dist/cpsat/response.js.map +1 -1
  15. package/dist/cpsat/rules/assign-together.d.ts +4 -4
  16. package/dist/cpsat/rules/assign-together.d.ts.map +1 -1
  17. package/dist/cpsat/rules/assign-together.js +17 -21
  18. package/dist/cpsat/rules/assign-together.js.map +1 -1
  19. package/dist/cpsat/rules/assignment-priority.d.ts +38 -0
  20. package/dist/cpsat/rules/assignment-priority.d.ts.map +1 -0
  21. package/dist/cpsat/rules/{employee-assignment-priority.js → assignment-priority.js} +15 -15
  22. package/dist/cpsat/rules/assignment-priority.js.map +1 -0
  23. package/dist/cpsat/rules/day-cost-multiplier.d.ts +19 -0
  24. package/dist/cpsat/rules/day-cost-multiplier.d.ts.map +1 -0
  25. package/dist/cpsat/rules/day-cost-multiplier.js +104 -0
  26. package/dist/cpsat/rules/day-cost-multiplier.js.map +1 -0
  27. package/dist/cpsat/rules/day-cost-surcharge.d.ts +19 -0
  28. package/dist/cpsat/rules/day-cost-surcharge.d.ts.map +1 -0
  29. package/dist/cpsat/rules/day-cost-surcharge.js +84 -0
  30. package/dist/cpsat/rules/day-cost-surcharge.js.map +1 -0
  31. package/dist/cpsat/rules/index.d.ts +10 -1
  32. package/dist/cpsat/rules/index.d.ts.map +1 -1
  33. package/dist/cpsat/rules/index.js +10 -1
  34. package/dist/cpsat/rules/index.js.map +1 -1
  35. package/dist/cpsat/rules/location-preference.d.ts +4 -4
  36. package/dist/cpsat/rules/location-preference.d.ts.map +1 -1
  37. package/dist/cpsat/rules/location-preference.js +6 -11
  38. package/dist/cpsat/rules/location-preference.js.map +1 -1
  39. package/dist/cpsat/rules/max-consecutive-days.d.ts +3 -3
  40. package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
  41. package/dist/cpsat/rules/max-consecutive-days.js +5 -10
  42. package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
  43. package/dist/cpsat/rules/max-hours-day.d.ts +3 -3
  44. package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
  45. package/dist/cpsat/rules/max-hours-day.js +6 -11
  46. package/dist/cpsat/rules/max-hours-day.js.map +1 -1
  47. package/dist/cpsat/rules/max-hours-week.d.ts +3 -3
  48. package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
  49. package/dist/cpsat/rules/max-hours-week.js +6 -11
  50. package/dist/cpsat/rules/max-hours-week.js.map +1 -1
  51. package/dist/cpsat/rules/max-shifts-day.d.ts +3 -3
  52. package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
  53. package/dist/cpsat/rules/max-shifts-day.js +6 -11
  54. package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
  55. package/dist/cpsat/rules/min-consecutive-days.d.ts +3 -3
  56. package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
  57. package/dist/cpsat/rules/min-consecutive-days.js +5 -10
  58. package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
  59. package/dist/cpsat/rules/min-hours-day.d.ts +3 -3
  60. package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
  61. package/dist/cpsat/rules/min-hours-day.js +5 -10
  62. package/dist/cpsat/rules/min-hours-day.js.map +1 -1
  63. package/dist/cpsat/rules/min-hours-week.d.ts +3 -3
  64. package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
  65. package/dist/cpsat/rules/min-hours-week.js +5 -10
  66. package/dist/cpsat/rules/min-hours-week.js.map +1 -1
  67. package/dist/cpsat/rules/min-rest-between-shifts.d.ts +3 -3
  68. package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
  69. package/dist/cpsat/rules/min-rest-between-shifts.js +5 -10
  70. package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
  71. package/dist/cpsat/rules/minimize-cost.d.ts +23 -0
  72. package/dist/cpsat/rules/minimize-cost.d.ts.map +1 -0
  73. package/dist/cpsat/rules/minimize-cost.js +206 -0
  74. package/dist/cpsat/rules/minimize-cost.js.map +1 -0
  75. package/dist/cpsat/rules/overtime-daily-multiplier.d.ts +16 -0
  76. package/dist/cpsat/rules/overtime-daily-multiplier.d.ts.map +1 -0
  77. package/dist/cpsat/rules/overtime-daily-multiplier.js +142 -0
  78. package/dist/cpsat/rules/overtime-daily-multiplier.js.map +1 -0
  79. package/dist/cpsat/rules/overtime-daily-surcharge.d.ts +17 -0
  80. package/dist/cpsat/rules/overtime-daily-surcharge.d.ts.map +1 -0
  81. package/dist/cpsat/rules/overtime-daily-surcharge.js +120 -0
  82. package/dist/cpsat/rules/overtime-daily-surcharge.js.map +1 -0
  83. package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts +37 -0
  84. package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts.map +1 -0
  85. package/dist/cpsat/rules/overtime-tiered-multiplier.js +243 -0
  86. package/dist/cpsat/rules/overtime-tiered-multiplier.js.map +1 -0
  87. package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts +23 -0
  88. package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts.map +1 -0
  89. package/dist/cpsat/rules/overtime-weekly-multiplier.js +178 -0
  90. package/dist/cpsat/rules/overtime-weekly-multiplier.js.map +1 -0
  91. package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts +18 -0
  92. package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts.map +1 -0
  93. package/dist/cpsat/rules/overtime-weekly-surcharge.js +140 -0
  94. package/dist/cpsat/rules/overtime-weekly-surcharge.js.map +1 -0
  95. package/dist/cpsat/rules/registry.d.ts.map +1 -1
  96. package/dist/cpsat/rules/registry.js +11 -2
  97. package/dist/cpsat/rules/registry.js.map +1 -1
  98. package/dist/cpsat/rules/resolver.d.ts +5 -10
  99. package/dist/cpsat/rules/resolver.d.ts.map +1 -1
  100. package/dist/cpsat/rules/resolver.js +33 -27
  101. package/dist/cpsat/rules/resolver.js.map +1 -1
  102. package/dist/cpsat/rules/rules.types.d.ts +31 -5
  103. package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
  104. package/dist/cpsat/rules/scope.types.d.ts +30 -23
  105. package/dist/cpsat/rules/scope.types.d.ts.map +1 -1
  106. package/dist/cpsat/rules/scope.types.js +33 -21
  107. package/dist/cpsat/rules/scope.types.js.map +1 -1
  108. package/dist/cpsat/rules/time-cost-surcharge.d.ts +29 -0
  109. package/dist/cpsat/rules/time-cost-surcharge.d.ts.map +1 -0
  110. package/dist/cpsat/rules/time-cost-surcharge.js +124 -0
  111. package/dist/cpsat/rules/time-cost-surcharge.js.map +1 -0
  112. package/dist/cpsat/rules/time-off.d.ts +7 -7
  113. package/dist/cpsat/rules/time-off.d.ts.map +1 -1
  114. package/dist/cpsat/rules/time-off.js +14 -20
  115. package/dist/cpsat/rules/time-off.js.map +1 -1
  116. package/dist/cpsat/rules.d.ts +2 -2
  117. package/dist/cpsat/rules.d.ts.map +1 -1
  118. package/dist/cpsat/rules.js +2 -2
  119. package/dist/cpsat/rules.js.map +1 -1
  120. package/dist/cpsat/semantic-time.d.ts +139 -30
  121. package/dist/cpsat/semantic-time.d.ts.map +1 -1
  122. package/dist/cpsat/semantic-time.js +91 -33
  123. package/dist/cpsat/semantic-time.js.map +1 -1
  124. package/dist/cpsat/types.d.ts +55 -34
  125. package/dist/cpsat/types.d.ts.map +1 -1
  126. package/dist/cpsat/utils.d.ts +16 -2
  127. package/dist/cpsat/utils.d.ts.map +1 -1
  128. package/dist/cpsat/utils.js +27 -2
  129. package/dist/cpsat/utils.js.map +1 -1
  130. package/dist/cpsat/validation-reporter.js +13 -13
  131. package/dist/cpsat/validation-reporter.js.map +1 -1
  132. package/dist/cpsat/validation.types.d.ts +10 -10
  133. package/dist/cpsat/validation.types.d.ts.map +1 -1
  134. package/dist/datetime.utils.d.ts +4 -4
  135. package/dist/datetime.utils.js +6 -6
  136. package/dist/datetime.utils.js.map +1 -1
  137. package/dist/index.d.ts +80 -69
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +74 -56
  140. package/dist/index.js.map +1 -1
  141. package/dist/llms.d.ts +1 -4
  142. package/dist/llms.d.ts.map +1 -1
  143. package/dist/llms.js +2 -7
  144. package/dist/llms.js.map +1 -1
  145. package/dist/schedule.d.ts +724 -0
  146. package/dist/schedule.d.ts.map +1 -0
  147. package/dist/schedule.js +899 -0
  148. package/dist/schedule.js.map +1 -0
  149. package/dist/testing/solver-container.d.ts.map +1 -1
  150. package/dist/testing/solver-container.js +5 -0
  151. package/dist/testing/solver-container.js.map +1 -1
  152. package/dist/types.d.ts +11 -21
  153. package/dist/types.d.ts.map +1 -1
  154. package/dist/types.js.map +1 -1
  155. package/dist/validation.d.ts +21 -21
  156. package/dist/validation.d.ts.map +1 -1
  157. package/dist/validation.js +30 -30
  158. package/dist/validation.js.map +1 -1
  159. package/llms.txt +629 -2145
  160. package/package.json +1 -1
  161. package/src/cpsat/cost.ts +128 -0
  162. package/src/cpsat/model-builder.ts +124 -84
  163. package/src/cpsat/response.ts +16 -16
  164. package/src/cpsat/rules/assign-together.ts +18 -22
  165. package/src/cpsat/rules/{employee-assignment-priority.ts → assignment-priority.ts} +17 -19
  166. package/src/cpsat/rules/day-cost-multiplier.ts +124 -0
  167. package/src/cpsat/rules/day-cost-surcharge.ts +106 -0
  168. package/src/cpsat/rules/index.ts +10 -1
  169. package/src/cpsat/rules/location-preference.ts +12 -12
  170. package/src/cpsat/rules/max-consecutive-days.ts +11 -11
  171. package/src/cpsat/rules/max-hours-day.ts +8 -12
  172. package/src/cpsat/rules/max-hours-week.ts +8 -12
  173. package/src/cpsat/rules/max-shifts-day.ts +8 -12
  174. package/src/cpsat/rules/min-consecutive-days.ts +11 -11
  175. package/src/cpsat/rules/min-hours-day.ts +11 -11
  176. package/src/cpsat/rules/min-hours-week.ts +11 -11
  177. package/src/cpsat/rules/min-rest-between-shifts.ts +11 -11
  178. package/src/cpsat/rules/minimize-cost.ts +250 -0
  179. package/src/cpsat/rules/overtime-daily-multiplier.ts +181 -0
  180. package/src/cpsat/rules/overtime-daily-surcharge.ts +161 -0
  181. package/src/cpsat/rules/overtime-tiered-multiplier.ts +323 -0
  182. package/src/cpsat/rules/overtime-weekly-multiplier.ts +226 -0
  183. package/src/cpsat/rules/overtime-weekly-surcharge.ts +190 -0
  184. package/src/cpsat/rules/registry.ts +20 -2
  185. package/src/cpsat/rules/resolver.ts +39 -36
  186. package/src/cpsat/rules/rules.types.ts +29 -5
  187. package/src/cpsat/rules/scope.types.ts +46 -32
  188. package/src/cpsat/rules/time-cost-surcharge.ts +169 -0
  189. package/src/cpsat/rules/time-off.ts +18 -24
  190. package/src/cpsat/rules.ts +2 -2
  191. package/src/cpsat/semantic-time.ts +259 -53
  192. package/src/cpsat/types.ts +57 -35
  193. package/src/cpsat/utils.ts +27 -2
  194. package/src/cpsat/validation-reporter.ts +16 -16
  195. package/src/cpsat/validation.types.ts +10 -10
  196. package/src/datetime.utils.ts +6 -6
  197. package/src/index.ts +127 -145
  198. package/src/llms.ts +2 -8
  199. package/src/schedule.ts +1419 -0
  200. package/src/testing/solver-container.ts +7 -0
  201. package/src/types.ts +11 -21
  202. package/src/validation.ts +31 -31
  203. package/dist/cpsat/rules/employee-assignment-priority.d.ts +0 -38
  204. package/dist/cpsat/rules/employee-assignment-priority.d.ts.map +0 -1
  205. package/dist/cpsat/rules/employee-assignment-priority.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,59 @@ All notable changes to this project will be documented in this file.
5
5
  This changelog was generated from the git history of the project when it was
6
6
  named `scheduling-core`, prior to the rename to `dabke` in v0.78.0.
7
7
 
8
+ ## 0.81.1 (2026-02-20)
9
+
10
+ ### Documentation
11
+
12
+ - Rewrite README for the v2 `defineSchedule` API with a verified cafe scheduling example.
13
+
14
+ ## 0.81.0 (2026-02-19)
15
+
16
+ ### Breaking Changes
17
+
18
+ - **Rename employee to member**: `SchedulingEmployee` is now `SchedulingMember`,
19
+ `employeeIds` scope fields are now `memberIds`, and `Employee` type alias is removed.
20
+ The `employee-assignment-priority` rule is renamed to `assignment-priority`.
21
+ - **Rename roleIds/skillIds to roles/skills**: on `SchedulingMember`, `ShiftPattern`,
22
+ and all coverage requirement types. Scope fields on rules remain `roleIds`/`skillIds`.
23
+ - **Merge ShiftPatternDef into ShiftPattern**: the separate `ShiftPatternDef` type is
24
+ removed. `ShiftPattern` now directly includes `startTime`, `endTime`, and optional
25
+ `roles`/`skills` fields.
26
+ - **Flatten CpsatRuleConfigEntry**: rule config entries change from
27
+ `{ name, config: { ... } }` to `{ name, ...config }`. Config fields sit at the same
28
+ level as `name`. The type is now a distributive union preventing mismatched name/config
29
+ pairings at compile time.
30
+ - **Rename daysOfWeek to dayOfWeek**: on `SchedulingPeriod`, `SemanticTimeVariant`,
31
+ and coverage requirement types.
32
+ - **Remove v1-only exports**: `Employee`, `ShiftPatternDef`, `defineSemanticTimes`,
33
+ and related types removed from the public API. Use `defineSchedule` instead.
34
+
35
+ ### Features
36
+
37
+ - **defineSchedule API**: new primary API with composable factory functions (`time`,
38
+ `cover`, `shift`, rule helpers like `maxHoursPerWeek`, `timeOff`, etc.) for building
39
+ a complete scheduling configuration declaratively.
40
+ - **Cost optimization**: base pay tracking (`HourlyPay`, `SalariedPay` on members),
41
+ cost minimization rule (`minimize-cost`), day/time cost surcharges and multipliers,
42
+ and post-solve cost calculation.
43
+ - **Overtime rules**: daily and weekly overtime multipliers and surcharges,
44
+ tiered overtime multiplier for graduated rates.
45
+ - **Variant coverage**: day-specific count overrides on coverage requirements
46
+ via `countByDay` on the `cover()` helper.
47
+
48
+ ### Fixes
49
+
50
+ - Clean up stale solver containers on startup in the test harness.
51
+ - Strip all entity-scope fields consistently during scope resolution.
52
+
53
+ ### Improvements
54
+
55
+ - Simplify rule translation layer; rule-specific knowledge (schemas, defaults,
56
+ validation, cost) is co-located in each rule file.
57
+ - Rewrite llms.txt generator as whitelist-based, aligned with the v2 API.
58
+ - Enrich TSDoc throughout: schedule.ts, semantic-time.ts, coverage types,
59
+ and package-level documentation updated for the defineSchedule API.
60
+
8
61
  ## 0.80.0 (2026-02-14)
9
62
 
10
63
  ### Breaking Changes
package/README.md CHANGED
@@ -4,378 +4,187 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/christianklotz/dabke/actions/workflows/ci.yml/badge.svg)](https://github.com/christianklotz/dabke/actions/workflows/ci.yml)
6
6
 
7
- Scheduling library powered by constraint programming (CP-SAT).
7
+ TypeScript scheduling library powered by constraint programming.
8
8
 
9
- Define your team, shifts, coverage, and rules dabke turns them into an optimized schedule.
9
+ Define teams, shifts, coverage, and rules declaratively. dabke compiles them into a CP-SAT model and solves for an optimized schedule.
10
10
 
11
11
  ```typescript
12
- import { ModelBuilder, HttpSolverClient } from "dabke";
12
+ import {
13
+ defineSchedule,
14
+ t,
15
+ time,
16
+ cover,
17
+ shift,
18
+ maxHoursPerWeek,
19
+ minRestBetweenShifts,
20
+ timeOff,
21
+ minimizeCost,
22
+ weekdays,
23
+ weekend,
24
+ } from "dabke";
25
+
26
+ const schedule = defineSchedule({
27
+ roles: ["barista", "server"],
28
+
29
+ // Times: WHEN you need people (named periods for coverage + rules)
30
+ times: {
31
+ breakfast: time({ startTime: t(7), endTime: t(10) }),
32
+ lunch_rush: time(
33
+ { startTime: t(11, 30), endTime: t(14) },
34
+ { startTime: t(11), endTime: t(15), dayOfWeek: weekend },
35
+ ),
36
+ closing: time({ startTime: t(20), endTime: t(22) }),
37
+ },
13
38
 
14
- const builder = new ModelBuilder({
15
- employees: [
16
- { id: "alice", roleIds: ["server"] },
17
- { id: "bob", roleIds: ["server"] },
39
+ // Coverage: HOW MANY people you need during each time
40
+ coverage: [
41
+ cover("breakfast", "barista", 1),
42
+ cover("lunch_rush", "barista", 2, { dayOfWeek: weekdays }),
43
+ cover("lunch_rush", "barista", 3, { dayOfWeek: weekend }),
44
+ cover("lunch_rush", "server", 1),
45
+ cover("closing", "server", 1),
18
46
  ],
47
+
48
+ // Shifts: WHEN people CAN work (the actual time slots)
49
+ // An opener covers breakfast + lunch; a closer covers lunch + closing
19
50
  shiftPatterns: [
20
- { id: "morning", startTime: { hours: 8 }, endTime: { hours: 16 } },
21
- { id: "evening", startTime: { hours: 16 }, endTime: { hours: 23 } },
22
- ],
23
- coverage: [
24
- {
25
- day: "2026-02-09",
26
- startTime: { hours: 8 },
27
- endTime: { hours: 16 },
28
- roleIds: ["server"],
29
- targetCount: 1,
30
- priority: "MANDATORY",
31
- },
32
- {
33
- day: "2026-02-09",
34
- startTime: { hours: 16 },
35
- endTime: { hours: 23 },
36
- roleIds: ["server"],
37
- targetCount: 1,
38
- priority: "MANDATORY",
39
- },
51
+ shift("opener", t(6), t(14)),
52
+ shift("mid", t(10), t(18)),
53
+ shift("closer", t(14), t(22)),
40
54
  ],
41
- schedulingPeriod: {
42
- dateRange: { start: "2026-02-09", end: "2026-02-09" },
43
- },
44
- ruleConfigs: [
45
- { name: "max-hours-day", config: { hours: 8, priority: "MANDATORY" } },
46
- { name: "min-rest-between-shifts", config: { hours: 10, priority: "MANDATORY" } },
55
+
56
+ rules: [
57
+ maxHoursPerWeek(40),
58
+ minRestBetweenShifts(11),
59
+ timeOff({ appliesTo: "alice", dayOfWeek: weekend }),
60
+ minimizeCost(),
47
61
  ],
48
62
  });
49
-
50
- const { request } = builder.compile();
51
- const solver = new HttpSolverClient(fetch, "http://localhost:8080");
52
- const response = await solver.solve(request);
53
63
  ```
54
64
 
55
- ## Why dabke?
56
-
57
- Staff scheduling with constraint programming is dominated by Python ([OR-Tools](https://developers.google.com/optimization)) and Java ([Timefold](https://timefold.ai/)). If you're building in TypeScript, your options have been: call a Python service and figure out the model yourself, or write scheduling heuristics by hand.
58
-
59
- dabke gives you a **TypeScript-native API** for expressing scheduling problems declaratively — employees, shifts, coverage requirements, and rules — and compiles them into a CP-SAT model solved by OR-Tools. You describe _what_ you need, not _how_ to solve it.
60
-
61
- **Key differences from rolling your own:**
62
-
63
- - **Declarative rules** — express constraints like "max 8 hours/day" or "11 hours rest between shifts" as config, not code
64
- - **Semantic time** — define named periods ("lunch_rush", "closing") that vary by day of week
65
- - **Soft and hard constraints** — some rules are mandatory, others are preferences the solver optimizes for
66
- - **Scoped rules** — apply constraints globally, per person, per role, per skill, or during specific time periods
67
- - **Validation** — detailed reporting on coverage gaps and rule violations before and after solving
68
-
69
- ## Install
65
+ ## Quick Start
70
66
 
71
67
  ```bash
72
68
  npm install dabke
73
69
  ```
74
70
 
75
- ## Quick Start
76
-
77
- ### 1. Start the solver
78
-
79
- dabke compiles scheduling problems into a constraint model. You need a CP-SAT solver to solve it. A ready-to-use solver is included:
71
+ dabke compiles scheduling problems into a constraint model. You need a CP-SAT solver to find the solution:
80
72
 
81
73
  ```bash
82
- # Pull the solver image
83
74
  docker pull christianklotz/dabke-solver
84
-
85
- # Start it
86
75
  docker run -p 8080:8080 christianklotz/dabke-solver
87
76
  ```
88
77
 
89
- Or build from source:
90
-
91
- ```bash
92
- cd node_modules/dabke/solver
93
- docker build -t dabke-solver .
94
- docker run -p 8080:8080 dabke-solver
95
- ```
96
-
97
- ### 2. Build and solve a schedule
78
+ Once you have a schedule definition, create a config with runtime data and solve:
98
79
 
99
80
  ```typescript
100
81
  import { ModelBuilder, HttpSolverClient, parseSolverResponse, resolveAssignments } from "dabke";
101
82
 
102
- // Define team
103
- const employees = [
104
- { id: "alice", roleIds: ["server"] },
105
- { id: "bob", roleIds: ["server"] },
106
- ];
107
-
108
- // Define shift patterns
109
- const shiftPatterns = [
110
- { id: "morning", startTime: { hours: 8 }, endTime: { hours: 16 } },
111
- { id: "evening", startTime: { hours: 16 }, endTime: { hours: 23 } },
112
- ];
113
-
114
- // Define coverage requirements
115
- const coverage = [
116
- {
117
- day: "2026-02-09",
118
- startTime: { hours: 8 },
119
- endTime: { hours: 16 },
120
- roleIds: ["server"] as [string],
121
- targetCount: 1,
122
- priority: "MANDATORY" as const,
123
- },
124
- {
125
- day: "2026-02-09",
126
- startTime: { hours: 16 },
127
- endTime: { hours: 23 },
128
- roleIds: ["server"] as [string],
129
- targetCount: 1,
130
- priority: "MANDATORY" as const,
131
- },
132
- ];
133
-
134
- // Build the model
135
- const builder = new ModelBuilder({
136
- employees,
137
- shiftPatterns,
138
- coverage,
83
+ const config = schedule.createSchedulerConfig({
139
84
  schedulingPeriod: {
140
- dateRange: { start: "2026-02-09", end: "2026-02-09" },
85
+ dateRange: { start: "2026-02-09", end: "2026-02-15" },
141
86
  },
142
- ruleConfigs: [
143
- { name: "max-hours-day", config: { hours: 8, priority: "MANDATORY" } },
144
- { name: "min-rest-between-shifts", config: { hours: 10, priority: "MANDATORY" } },
87
+ members: [
88
+ { id: "alice", roles: ["barista", "server"], pay: { hourlyRate: 1500 } },
89
+ { id: "bob", roles: ["barista"], pay: { hourlyRate: 1200 } },
90
+ { id: "carol", roles: ["barista", "server"], pay: { hourlyRate: 1400 } },
91
+ { id: "dave", roles: ["barista", "server"], pay: { hourlyRate: 1200 } },
92
+ { id: "eve", roles: ["barista", "server"], pay: { hourlyRate: 1300 } },
145
93
  ],
146
94
  });
147
95
 
148
- const { request, canSolve, validation } = builder.compile();
96
+ const builder = new ModelBuilder(config);
97
+ const { request, canSolve } = builder.compile();
149
98
 
150
- if (!canSolve) {
151
- console.error("Cannot solve:", validation.errors);
152
- process.exit(1);
153
- }
154
-
155
- // Solve
156
99
  const solver = new HttpSolverClient(fetch, "http://localhost:8080");
157
100
  const response = await solver.solve(request);
158
101
  const result = parseSolverResponse(response);
159
-
160
- if (result.status === "OPTIMAL" || result.status === "FEASIBLE") {
161
- const shifts = resolveAssignments(result.assignments, shiftPatterns);
162
- for (const shift of shifts) {
163
- console.log(
164
- `${shift.employeeId}: ${shift.day} ${shift.startTime.hours}:00–${shift.endTime.hours}:00`,
165
- );
166
- }
167
- }
102
+ const shifts = resolveAssignments(result.assignments, config.shiftPatterns);
168
103
  ```
169
104
 
170
- ## Semantic Time
171
-
172
- Define named time periods that can vary by day, then write coverage requirements in business terms:
173
-
174
- ```typescript
175
- import { defineSemanticTimes } from "dabke";
176
-
177
- const times = defineSemanticTimes({
178
- lunch: [
179
- {
180
- startTime: { hours: 11, minutes: 30 },
181
- endTime: { hours: 14 },
182
- days: ["monday", "tuesday", "wednesday", "thursday", "friday"],
183
- },
184
- {
185
- startTime: { hours: 12 },
186
- endTime: { hours: 15 },
187
- days: ["saturday", "sunday"],
188
- },
189
- ],
190
- closing: { startTime: { hours: 21 }, endTime: { hours: 23 } },
191
- });
192
-
193
- const coverage = times.coverage([
194
- { semanticTime: "lunch", roleIds: ["server"], targetCount: 3, priority: "MANDATORY" },
195
- { semanticTime: "closing", roleIds: ["server"], targetCount: 1, priority: "HIGH" },
196
- ]);
105
+ ---
197
106
 
198
- // Resolve against actual scheduling days
199
- const days = ["2026-02-09", "2026-02-10", "2026-02-11"];
200
- const resolved = times.resolve(coverage, days);
201
- ```
107
+ ## Key Concepts
202
108
 
203
- ## Built-in Rules
109
+ **Times vs shift patterns.** These are two distinct things. Times are named periods you need coverage for ("breakfast", "lunch_rush"). Shift patterns are the time slots people actually work ("opener 6-14", "closer 14-22"). They don't need to match. The solver assigns members to shift patterns whose hours overlap with times to satisfy coverage.
204
110
 
205
- | Rule | Description |
206
- | ------------------------------ | ----------------------------------------- |
207
- | `max-hours-day` | Max hours per person per day |
208
- | `max-hours-week` | Max hours per person per week |
209
- | `min-hours-day` | Min hours per person per day |
210
- | `min-hours-week` | Min hours per person per week |
211
- | `max-shifts-day` | Max shift assignments per day |
212
- | `max-consecutive-days` | Max consecutive working days |
213
- | `min-consecutive-days` | Min consecutive working days |
214
- | `min-rest-between-shifts` | Min rest hours between shifts |
215
- | `time-off` | Block assignments during periods |
216
- | `employee-assignment-priority` | Prefer or avoid assigning team members |
217
- | `assign-together` | Keep team members on the same shifts |
218
- | `location-preference` | Prefer team members at specific locations |
111
+ **Coverage.** How many people with which roles you need during each time. Coverage can vary by day of week or specific dates using scoping options or the variant form of `cover()`.
219
112
 
220
- All rules support scoping by person, role, skill, and time period (date ranges, days of week, recurring periods).
113
+ **Rules.** Business constraints expressed as function calls. Rules with `priority: "MANDATORY"` (the default) are hard constraints the solver will never violate. Rules with `LOW`, `MEDIUM`, or `HIGH` priority are soft preferences the solver optimizes across.
221
114
 
222
- ### Scoping Example
115
+ **Scoping.** Every rule accepts `appliesTo` (a role, skill, or member ID), plus time scoping (`dayOfWeek`, `dateRange`, `dates`, `recurringPeriods`) to target when and who:
223
116
 
224
117
  ```typescript
225
- // Students can work max 4 hours on weekdays
226
- {
227
- name: "max-hours-day",
228
- config: {
229
- roleIds: ["student"],
230
- hours: 4,
231
- dayOfWeek: ["monday", "tuesday", "wednesday", "thursday", "friday"],
232
- priority: "MANDATORY",
233
- },
234
- }
235
-
236
- // Alice has time off next week
237
- {
238
- name: "time-off",
239
- config: {
240
- employeeIds: ["alice"],
241
- dateRange: { start: "2026-02-09", end: "2026-02-13" },
242
- priority: "MANDATORY",
243
- },
244
- }
118
+ maxHoursPerDay(8, { appliesTo: "barista", dayOfWeek: weekdays }),
119
+ timeOff({ appliesTo: "alice", dateRange: { start: "2026-02-09", end: "2026-02-13" } }),
120
+ maxHoursPerWeek(30, { appliesTo: "bob", priority: "MEDIUM" }),
245
121
  ```
246
122
 
247
- ### Soft vs. Hard Constraints
123
+ **Cost optimization.** Members declare `pay` as hourly or salaried. Cost rules (`minimizeCost`, `dayMultiplier`, `overtimeMultiplier`, `tieredOvertimeMultiplier`, etc.) tell the solver to minimize total labor cost, factoring in overtime, day premiums, and time-based surcharges.
248
124
 
249
- Rules with `priority: "MANDATORY"` are hard constraints the solver will not violate them. Rules with `LOW`, `MEDIUM`, or `HIGH` priority are soft constraints — the solver tries to satisfy them but will trade them off against each other to find the best overall schedule.
125
+ **Validation.** `ModelBuilder` reports coverage gaps and rule violations both pre-solve (via `compile()`) and post-solve (via `builder.reporter.analyzeSolution()`). Use `summarizeValidation()` for grouped reporting.
250
126
 
251
- ```typescript
252
- // Hard: legally required rest period
253
- { name: "min-rest-between-shifts", config: { hours: 11, priority: "MANDATORY" } }
127
+ ---
254
128
 
255
- // Soft: prefer to keep Bob under 30 hours/week
256
- { name: "max-hours-week", config: { employeeIds: ["bob"], hours: 30, weekStartsOn: "monday", priority: "MEDIUM" } }
257
- ```
129
+ ## Rules
258
130
 
259
- ## Validation
131
+ ### Scheduling
260
132
 
261
- dabke validates schedules and reports coverage gaps and rule violations:
133
+ | Function | Description |
134
+ | -------------------------- | ------------------------------------ |
135
+ | `maxHoursPerDay(hours)` | Max hours per person per day |
136
+ | `maxHoursPerWeek(hours)` | Max hours per person per week |
137
+ | `minHoursPerDay(hours)` | Min hours per person per day |
138
+ | `minHoursPerWeek(hours)` | Min hours per person per week |
139
+ | `maxShiftsPerDay(n)` | Max shift assignments per day |
140
+ | `maxConsecutiveDays(n)` | Max consecutive working days |
141
+ | `minConsecutiveDays(n)` | Min consecutive working days |
142
+ | `minRestBetweenShifts(h)` | Min rest hours between shifts |
143
+ | `timeOff(opts)` | Block assignments during periods |
144
+ | `preference(level, opts)` | Prefer or avoid assigning members |
145
+ | `preferLocation(id, opts)` | Prefer members at specific locations |
146
+ | `assignTogether(opts)` | Keep members on the same shifts |
262
147
 
263
- ```typescript
264
- import { ValidationReporterImpl, summarizeValidation } from "dabke";
265
-
266
- const reporter = new ValidationReporterImpl();
267
- const builder = new ModelBuilder({ ...config, reporter });
268
- const { request, validation, canSolve } = builder.compile();
269
-
270
- // Pre-solve validation
271
- if (!canSolve) {
272
- for (const error of validation.errors) {
273
- console.error(error.reason);
274
- }
275
- }
276
-
277
- // Post-solve validation (after solving)
278
- reporter.analyzeSolution(response);
279
- const results = reporter.getValidation();
280
- const summaries = summarizeValidation(results);
281
-
282
- for (const s of summaries) {
283
- console.log(
284
- `${s.description}: ${s.status} (${s.passedCount}/${s.passedCount + s.violatedCount})`,
285
- );
286
- }
287
- ```
148
+ ### Cost
288
149
 
289
- ## Custom Rules
150
+ | Function | Description |
151
+ | ---------------------------------------- | ---------------------------------------- |
152
+ | `minimizeCost()` | Minimize total labor cost |
153
+ | `dayMultiplier(factor, opts?)` | Pay multiplier for specific days |
154
+ | `daySurcharge(amount, opts?)` | Flat surcharge per hour on specific days |
155
+ | `timeSurcharge(amount, window, opts?)` | Surcharge during a time-of-day window |
156
+ | `overtimeMultiplier(opts)` | Weekly overtime pay multiplier |
157
+ | `overtimeSurcharge(opts)` | Weekly overtime flat surcharge |
158
+ | `dailyOvertimeMultiplier(opts)` | Daily overtime pay multiplier |
159
+ | `dailyOvertimeSurcharge(opts)` | Daily overtime flat surcharge |
160
+ | `tieredOvertimeMultiplier(tiers, opts?)` | Graduated overtime rates |
290
161
 
291
- dabke's rule system is extensible. You can create custom rules that integrate with the model builder:
292
-
293
- ```typescript
294
- import { createCpsatRuleFactory, type CompilationRule } from "dabke";
295
-
296
- function createNoSundayWorkRule(config: { priority: "MANDATORY" | "HIGH" }): CompilationRule {
297
- return {
298
- compile(b) {
299
- for (const emp of b.employees) {
300
- for (const day of b.days) {
301
- const date = new Date(`${day}T00:00:00Z`);
302
- if (date.getUTCDay() !== 0) continue; // Sunday = 0
303
- for (const pattern of b.shiftPatterns) {
304
- if (!b.canAssign(emp, pattern)) continue;
305
- b.addLinear([{ var: b.assignment(emp.id, pattern.id, day), coeff: 1 }], "<=", 0);
306
- }
307
- }
308
- }
309
- },
310
- };
311
- }
312
-
313
- const customRules = createCpsatRuleFactory({
314
- "no-sunday-work": createNoSundayWorkRule,
315
- });
316
- ```
317
-
318
- See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide on writing custom rules.
162
+ ---
319
163
 
320
164
  ## LLM Integration
321
165
 
322
- dabke ships an `llms.txt` file with complete API documentation, designed for AI code generation:
166
+ dabke ships an `llms.txt` with complete API documentation for AI code generation:
323
167
 
324
168
  ```typescript
325
169
  import { apiDocs } from "dabke/llms";
326
-
327
- // Pass to your LLM as context for generating scheduling code
328
170
  ```
329
171
 
330
- Or read the file directly:
331
-
332
- ```bash
333
- cat node_modules/dabke/llms.txt
334
- ```
172
+ ---
335
173
 
336
174
  ## Development
337
175
 
338
- ### Running tests
339
-
340
- Unit tests have no external dependencies:
341
-
342
- ```bash
343
- npm run test:unit
344
- ```
345
-
346
- Integration tests require Docker to run the solver container:
347
-
348
- ```bash
349
- # Ensure Docker is running, then:
350
- npm run test:integration
351
- ```
176
+ Unit tests (no dependencies): `npm run test:unit`
352
177
 
353
- The integration test harness (`startSolverContainer`) builds and starts a Docker container from `solver/Dockerfile` automatically. It binds to port 18080 by default — make sure no other container is using that port.
178
+ Integration tests (requires Docker): `npm run test:integration`
354
179
 
355
- To run both unit and integration tests:
356
-
357
- ```bash
358
- npm test # runs typecheck + unit tests only
359
- npm run test:integration # solver integration tests (requires Docker)
360
- ```
361
-
362
- ### Test utilities
363
-
364
- dabke exports test helpers for writing your own integration tests:
180
+ The test harness builds and starts a solver container from `solver/Dockerfile` automatically.
365
181
 
366
182
  ```typescript
367
183
  import { startSolverContainer } from "dabke/testing";
368
-
369
184
  const solver = await startSolverContainer();
370
- const response = await solver.client.solve(request);
371
- solver.stop();
372
185
  ```
373
186
 
374
- This builds the solver Docker image, starts a container, waits for the health check, and returns a pre-configured `HttpSolverClient`.
375
-
376
- ## Contributing
377
-
378
- See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, how to add rules, and contribution guidelines.
187
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for how to add rules and contribute.
379
188
 
380
189
  ## License
381
190
 
@@ -0,0 +1,65 @@
1
+ import type { ShiftAssignment } from "./response.js";
2
+ import type { SchedulingMember, ShiftPattern } from "./types.js";
3
+ import type { CompilationRule } from "./model-builder.js";
4
+ /**
5
+ * Well-known cost categories used by built-in rules.
6
+ *
7
+ * Custom rules can use any string as a category. These constants
8
+ * are provided for convenience and consistency.
9
+ */
10
+ export declare const COST_CATEGORY: {
11
+ /** Base pay cost (from {@link minimizeCost}). */
12
+ readonly BASE: "base";
13
+ /** Overtime cost (from overtime rules). */
14
+ readonly OVERTIME: "overtime";
15
+ /** Premium cost (from day/time multipliers and surcharges). */
16
+ readonly PREMIUM: "premium";
17
+ };
18
+ /**
19
+ * Per-member cost breakdown.
20
+ *
21
+ * Categories are open-ended strings. Built-in rules use categories
22
+ * from {@link COST_CATEGORY}. Custom rules can introduce their own.
23
+ */
24
+ export interface MemberCostDetail {
25
+ /** Sum of all category costs. */
26
+ totalCost: number;
27
+ /** Total hours worked (computed from assignments, not from rules). */
28
+ totalHours: number;
29
+ /** Cost per category. Only categories with nonzero cost appear. */
30
+ categories: ReadonlyMap<string, number>;
31
+ }
32
+ /**
33
+ * Full cost breakdown for a solved schedule.
34
+ */
35
+ export interface CostBreakdown {
36
+ /** Total cost in the caller's currency unit. */
37
+ total: number;
38
+ /** Cost per member. */
39
+ byMember: ReadonlyMap<string, MemberCostDetail>;
40
+ /** Cost per day. */
41
+ byDay: ReadonlyMap<string, number>;
42
+ }
43
+ /**
44
+ * Configuration accepted by {@link calculateScheduleCost}.
45
+ *
46
+ * In practice, callers pass the `ModelBuilderConfig` (which satisfies this)
47
+ * or the relevant subset.
48
+ */
49
+ export interface CostCalculationConfig {
50
+ members: ReadonlyArray<SchedulingMember>;
51
+ shiftPatterns: ReadonlyArray<ShiftPattern>;
52
+ rules: ReadonlyArray<CompilationRule>;
53
+ }
54
+ /**
55
+ * Computes the actual cost of a solved schedule using the same cost model
56
+ * as the solver rules.
57
+ *
58
+ * Collects `CostEntry` values from all rules' `cost()` methods and
59
+ * aggregates them into a {@link CostBreakdown}.
60
+ *
61
+ * @param assignments - Shift assignments from {@link parseSolverResponse}
62
+ * @param config - Members, shift patterns, and compiled rules
63
+ */
64
+ export declare function calculateScheduleCost(assignments: ShiftAssignment[], config: CostCalculationConfig): CostBreakdown;
65
+ //# sourceMappingURL=cost.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cost.d.ts","sourceRoot":"","sources":["../../src/cpsat/cost.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAa,MAAM,oBAAoB,CAAC;AAGrE;;;;;GAKG;AACH,eAAO,MAAM,aAAa;IACxB,iDAAiD;;IAEjD,2CAA2C;;IAE3C,+DAA+D;;CAEvD,CAAC;AAEX;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAChD,oBAAoB;IACpB,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACzC,aAAa,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC3C,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;CACvC;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,eAAe,EAAE,EAC9B,MAAM,EAAE,qBAAqB,GAC5B,aAAa,CAuDf"}