dabke 0.78.0 → 0.78.2
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 +12 -0
- package/README.md +176 -18
- package/package.json +1 -1
- package/solver/README.md +36 -11
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ 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.78.2 (2026-02-08)
|
|
9
|
+
|
|
10
|
+
- Fix solver Docker image to support both amd64 and arm64 (Apple Silicon)
|
|
11
|
+
- Fix npm publish by upgrading npm before publish in CI
|
|
12
|
+
|
|
13
|
+
## 0.78.1 (2026-02-08)
|
|
14
|
+
|
|
15
|
+
- Add AGENTS.md and CONTRIBUTING.md
|
|
16
|
+
- Improve README with hero example, "Why dabke?" section, solver quickstart, scoping and custom rules docs
|
|
17
|
+
- Improve solver README with Docker Hub image reference and API docs
|
|
18
|
+
- Add GitHub Actions workflow for publishing solver Docker image to Docker Hub
|
|
19
|
+
|
|
8
20
|
## 0.78.0 (2026-02-07)
|
|
9
21
|
|
|
10
22
|
- Rename package from `scheduling-core` to `dabke`
|
package/README.md
CHANGED
|
@@ -1,18 +1,56 @@
|
|
|
1
1
|
# dabke
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/dabke)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://github.com/christianklotz/dabke/actions/workflows/ci.yml)
|
|
6
|
+
|
|
3
7
|
Scheduling library powered by constraint programming (CP-SAT).
|
|
4
8
|
|
|
5
9
|
Define your team, shifts, coverage, and rules — dabke turns them into an optimized schedule.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
```typescript
|
|
12
|
+
import { ModelBuilder, HttpSolverClient } from "dabke";
|
|
13
|
+
|
|
14
|
+
const builder = new ModelBuilder({
|
|
15
|
+
employees: [
|
|
16
|
+
{ id: "alice", roleIds: ["server"] },
|
|
17
|
+
{ id: "bob", roleIds: ["server"] },
|
|
18
|
+
],
|
|
19
|
+
shiftPatterns: [
|
|
20
|
+
{ id: "morning", startTime: { hours: 8 }, endTime: { hours: 16 } },
|
|
21
|
+
{ id: "evening", startTime: { hours: 16 }, endTime: { hours: 23 } },
|
|
22
|
+
],
|
|
23
|
+
coverage: [
|
|
24
|
+
{ day: "2026-02-09", startTime: { hours: 8 }, endTime: { hours: 16 },
|
|
25
|
+
roleIds: ["server"], targetCount: 1, priority: "MANDATORY" },
|
|
26
|
+
{ day: "2026-02-09", startTime: { hours: 16 }, endTime: { hours: 23 },
|
|
27
|
+
roleIds: ["server"], targetCount: 1, priority: "MANDATORY" },
|
|
28
|
+
],
|
|
29
|
+
schedulingPeriod: { specificDates: ["2026-02-09"] },
|
|
30
|
+
ruleConfigs: [
|
|
31
|
+
{ name: "max-hours-day", config: { hours: 8, priority: "MANDATORY" } },
|
|
32
|
+
{ name: "min-rest-between-shifts", config: { hours: 10, priority: "MANDATORY" } },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const { request } = builder.compile();
|
|
37
|
+
const solver = new HttpSolverClient(fetch, "http://localhost:8080");
|
|
38
|
+
const response = await solver.solve(request);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Why dabke?
|
|
8
42
|
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- **
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
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.
|
|
46
|
+
|
|
47
|
+
**Key differences from rolling your own:**
|
|
48
|
+
|
|
49
|
+
- **Declarative rules** — express constraints like "max 8 hours/day" or "11 hours rest between shifts" as config, not code
|
|
50
|
+
- **Semantic time** — define named periods ("lunch_rush", "closing") that vary by day of week
|
|
51
|
+
- **Soft and hard constraints** — some rules are mandatory, others are preferences the solver optimizes for
|
|
52
|
+
- **Scoped rules** — apply constraints globally, per person, per role, per skill, or during specific time periods
|
|
53
|
+
- **Validation** — detailed reporting on coverage gaps and rule violations before and after solving
|
|
16
54
|
|
|
17
55
|
## Install
|
|
18
56
|
|
|
@@ -22,6 +60,28 @@ npm install dabke
|
|
|
22
60
|
|
|
23
61
|
## Quick Start
|
|
24
62
|
|
|
63
|
+
### 1. Start the solver
|
|
64
|
+
|
|
65
|
+
dabke compiles scheduling problems into a constraint model. You need a CP-SAT solver to solve it. A ready-to-use solver is included:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Pull the solver image
|
|
69
|
+
docker pull christianklotz/dabke-solver
|
|
70
|
+
|
|
71
|
+
# Start it
|
|
72
|
+
docker run -p 8080:8080 christianklotz/dabke-solver
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or build from source:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cd node_modules/dabke/solver
|
|
79
|
+
docker build -t dabke-solver .
|
|
80
|
+
docker run -p 8080:8080 dabke-solver
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2. Build and solve a schedule
|
|
84
|
+
|
|
25
85
|
```typescript
|
|
26
86
|
import {
|
|
27
87
|
ModelBuilder,
|
|
@@ -34,7 +94,6 @@ import {
|
|
|
34
94
|
const employees = [
|
|
35
95
|
{ id: "alice", roleIds: ["server"] },
|
|
36
96
|
{ id: "bob", roleIds: ["server"] },
|
|
37
|
-
{ id: "charlie", roleIds: ["chef"] },
|
|
38
97
|
];
|
|
39
98
|
|
|
40
99
|
// Define shift patterns
|
|
@@ -49,7 +108,7 @@ const coverage = [
|
|
|
49
108
|
day: "2026-02-09",
|
|
50
109
|
startTime: { hours: 8 },
|
|
51
110
|
endTime: { hours: 16 },
|
|
52
|
-
roleIds: ["server"],
|
|
111
|
+
roleIds: ["server"] as [string],
|
|
53
112
|
targetCount: 1,
|
|
54
113
|
priority: "MANDATORY" as const,
|
|
55
114
|
},
|
|
@@ -57,7 +116,7 @@ const coverage = [
|
|
|
57
116
|
day: "2026-02-09",
|
|
58
117
|
startTime: { hours: 16 },
|
|
59
118
|
endTime: { hours: 23 },
|
|
60
|
-
roleIds: ["server"],
|
|
119
|
+
roleIds: ["server"] as [string],
|
|
61
120
|
targetCount: 1,
|
|
62
121
|
priority: "MANDATORY" as const,
|
|
63
122
|
},
|
|
@@ -77,10 +136,15 @@ const builder = new ModelBuilder({
|
|
|
77
136
|
],
|
|
78
137
|
});
|
|
79
138
|
|
|
80
|
-
const { request } = builder.compile();
|
|
139
|
+
const { request, canSolve, validation } = builder.compile();
|
|
81
140
|
|
|
82
|
-
|
|
83
|
-
|
|
141
|
+
if (!canSolve) {
|
|
142
|
+
console.error("Cannot solve:", validation.errors);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Solve
|
|
147
|
+
const solver = new HttpSolverClient(fetch, "http://localhost:8080");
|
|
84
148
|
const response = await solver.solve(request);
|
|
85
149
|
const result = parseSolverResponse(response);
|
|
86
150
|
|
|
@@ -103,8 +167,16 @@ import { defineSemanticTimes } from "dabke";
|
|
|
103
167
|
|
|
104
168
|
const times = defineSemanticTimes({
|
|
105
169
|
lunch: [
|
|
106
|
-
{
|
|
107
|
-
|
|
170
|
+
{
|
|
171
|
+
startTime: { hours: 11, minutes: 30 },
|
|
172
|
+
endTime: { hours: 14 },
|
|
173
|
+
days: ["monday", "tuesday", "wednesday", "thursday", "friday"],
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
startTime: { hours: 12 },
|
|
177
|
+
endTime: { hours: 15 },
|
|
178
|
+
days: ["saturday", "sunday"],
|
|
179
|
+
},
|
|
108
180
|
],
|
|
109
181
|
closing: { startTime: { hours: 21 }, endTime: { hours: 23 } },
|
|
110
182
|
});
|
|
@@ -113,6 +185,10 @@ const coverage = times.coverage([
|
|
|
113
185
|
{ semanticTime: "lunch", roleIds: ["server"], targetCount: 3, priority: "MANDATORY" },
|
|
114
186
|
{ semanticTime: "closing", roleIds: ["server"], targetCount: 1, priority: "HIGH" },
|
|
115
187
|
]);
|
|
188
|
+
|
|
189
|
+
// Resolve against actual scheduling days
|
|
190
|
+
const days = ["2026-02-09", "2026-02-10", "2026-02-11"];
|
|
191
|
+
const resolved = times.resolve(coverage, days);
|
|
116
192
|
```
|
|
117
193
|
|
|
118
194
|
## Built-in Rules
|
|
@@ -134,6 +210,43 @@ const coverage = times.coverage([
|
|
|
134
210
|
|
|
135
211
|
All rules support scoping by person, role, skill, and time period (date ranges, days of week, recurring periods).
|
|
136
212
|
|
|
213
|
+
### Scoping Example
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// Students can work max 4 hours on weekdays
|
|
217
|
+
{
|
|
218
|
+
name: "max-hours-day",
|
|
219
|
+
config: {
|
|
220
|
+
roleIds: ["student"],
|
|
221
|
+
hours: 4,
|
|
222
|
+
dayOfWeek: ["monday", "tuesday", "wednesday", "thursday", "friday"],
|
|
223
|
+
priority: "MANDATORY",
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Alice has time off next week
|
|
228
|
+
{
|
|
229
|
+
name: "time-off",
|
|
230
|
+
config: {
|
|
231
|
+
employeeIds: ["alice"],
|
|
232
|
+
dateRange: { start: "2026-02-09", end: "2026-02-13" },
|
|
233
|
+
priority: "MANDATORY",
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Soft vs. Hard Constraints
|
|
239
|
+
|
|
240
|
+
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.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// Hard: legally required rest period
|
|
244
|
+
{ name: "min-rest-between-shifts", config: { hours: 11, priority: "MANDATORY" } }
|
|
245
|
+
|
|
246
|
+
// Soft: prefer to keep Bob under 30 hours/week
|
|
247
|
+
{ name: "max-hours-week", config: { employeeIds: ["bob"], hours: 30, weekStartsOn: "monday", priority: "MEDIUM" } }
|
|
248
|
+
```
|
|
249
|
+
|
|
137
250
|
## Validation
|
|
138
251
|
|
|
139
252
|
dabke validates schedules and reports coverage gaps and rule violations:
|
|
@@ -162,6 +275,37 @@ for (const s of summaries) {
|
|
|
162
275
|
}
|
|
163
276
|
```
|
|
164
277
|
|
|
278
|
+
## Custom Rules
|
|
279
|
+
|
|
280
|
+
dabke's rule system is extensible. You can create custom rules that integrate with the model builder:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { createCpsatRuleFactory, type CompilationRule } from "dabke";
|
|
284
|
+
|
|
285
|
+
function createNoSundayWorkRule(config: { priority: "MANDATORY" | "HIGH" }): CompilationRule {
|
|
286
|
+
return {
|
|
287
|
+
compile(b) {
|
|
288
|
+
for (const emp of b.employees) {
|
|
289
|
+
for (const day of b.days) {
|
|
290
|
+
const date = new Date(`${day}T00:00:00Z`);
|
|
291
|
+
if (date.getUTCDay() !== 0) continue; // Sunday = 0
|
|
292
|
+
for (const pattern of b.shiftPatterns) {
|
|
293
|
+
if (!b.canAssign(emp, pattern)) continue;
|
|
294
|
+
b.addLinear([{ var: b.assignment(emp.id, pattern.id, day), coeff: 1 }], "<=", 0);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const customRules = createCpsatRuleFactory({
|
|
303
|
+
"no-sunday-work": createNoSundayWorkRule,
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide on writing custom rules.
|
|
308
|
+
|
|
165
309
|
## LLM Integration
|
|
166
310
|
|
|
167
311
|
dabke ships an `llms.txt` file with complete API documentation, designed for AI code generation:
|
|
@@ -178,9 +322,23 @@ Or read the file directly:
|
|
|
178
322
|
cat node_modules/dabke/llms.txt
|
|
179
323
|
```
|
|
180
324
|
|
|
181
|
-
##
|
|
325
|
+
## Testing
|
|
326
|
+
|
|
327
|
+
dabke provides test utilities for running integration tests against the solver:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { startSolverContainer } from "dabke/testing";
|
|
331
|
+
|
|
332
|
+
const solver = await startSolverContainer();
|
|
333
|
+
const response = await solver.client.solve(request);
|
|
334
|
+
solver.stop();
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
This starts a Docker container with the solver, waits for it to be healthy, and provides a pre-configured `HttpSolverClient`.
|
|
338
|
+
|
|
339
|
+
## Contributing
|
|
182
340
|
|
|
183
|
-
|
|
341
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, how to add rules, and contribution guidelines.
|
|
184
342
|
|
|
185
343
|
## License
|
|
186
344
|
|
package/package.json
CHANGED
package/solver/README.md
CHANGED
|
@@ -1,23 +1,48 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dabke solver
|
|
2
2
|
|
|
3
|
-
Minimal FastAPI service that
|
|
3
|
+
Minimal FastAPI service that solves scheduling models with [OR-Tools CP-SAT](https://developers.google.com/optimization/cp/cp_solver).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Receives a JSON constraint model from dabke's `ModelBuilder`, returns optimized assignments.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
## Quick start (Docker)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Use the published image
|
|
11
|
+
docker pull christianklotz/dabke-solver
|
|
12
|
+
docker run -p 8080:8080 christianklotz/dabke-solver
|
|
13
|
+
|
|
14
|
+
# Or build from source
|
|
15
|
+
docker build -t dabke-solver .
|
|
16
|
+
docker run -p 8080:8080 dabke-solver
|
|
10
17
|
```
|
|
11
18
|
|
|
12
|
-
|
|
19
|
+
Then point `HttpSolverClient` at it:
|
|
13
20
|
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
```typescript
|
|
22
|
+
import { HttpSolverClient } from "dabke";
|
|
23
|
+
|
|
24
|
+
const solver = new HttpSolverClient(fetch, "http://localhost:8080");
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Local development
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv sync
|
|
31
|
+
uvicorn solver.app:app --reload --port 8080
|
|
17
32
|
```
|
|
18
33
|
|
|
19
34
|
## Tests
|
|
20
35
|
|
|
21
|
-
```
|
|
36
|
+
```bash
|
|
22
37
|
uv run pytest
|
|
23
38
|
```
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
### `GET /health`
|
|
43
|
+
|
|
44
|
+
Returns `{ "status": "ok" }`.
|
|
45
|
+
|
|
46
|
+
### `POST /solve`
|
|
47
|
+
|
|
48
|
+
Accepts a solver request JSON body, returns assignments. See dabke's `SolverRequest` type for the schema.
|