blackopsrepl commited on
Commit
195a426
·
verified ·
1 Parent(s): fb7bd10

Upload 30 files

Browse files
Cargo.lock ADDED
@@ -0,0 +1,1476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "aho-corasick"
7
+ version = "1.1.4"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
10
+ dependencies = [
11
+ "memchr",
12
+ ]
13
+
14
+ [[package]]
15
+ name = "android_system_properties"
16
+ version = "0.1.5"
17
+ source = "registry+https://github.com/rust-lang/crates.io-index"
18
+ checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
19
+ dependencies = [
20
+ "libc",
21
+ ]
22
+
23
+ [[package]]
24
+ name = "arrayvec"
25
+ version = "0.7.6"
26
+ source = "registry+https://github.com/rust-lang/crates.io-index"
27
+ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
28
+
29
+ [[package]]
30
+ name = "atomic-waker"
31
+ version = "1.1.2"
32
+ source = "registry+https://github.com/rust-lang/crates.io-index"
33
+ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
34
+
35
+ [[package]]
36
+ name = "autocfg"
37
+ version = "1.5.0"
38
+ source = "registry+https://github.com/rust-lang/crates.io-index"
39
+ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
40
+
41
+ [[package]]
42
+ name = "axum"
43
+ version = "0.8.8"
44
+ source = "registry+https://github.com/rust-lang/crates.io-index"
45
+ checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
46
+ dependencies = [
47
+ "axum-core",
48
+ "bytes",
49
+ "form_urlencoded",
50
+ "futures-util",
51
+ "http",
52
+ "http-body",
53
+ "http-body-util",
54
+ "hyper",
55
+ "hyper-util",
56
+ "itoa",
57
+ "matchit",
58
+ "memchr",
59
+ "mime",
60
+ "percent-encoding",
61
+ "pin-project-lite",
62
+ "serde_core",
63
+ "serde_json",
64
+ "serde_path_to_error",
65
+ "serde_urlencoded",
66
+ "sync_wrapper",
67
+ "tokio",
68
+ "tower",
69
+ "tower-layer",
70
+ "tower-service",
71
+ "tracing",
72
+ ]
73
+
74
+ [[package]]
75
+ name = "axum-core"
76
+ version = "0.5.6"
77
+ source = "registry+https://github.com/rust-lang/crates.io-index"
78
+ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
79
+ dependencies = [
80
+ "bytes",
81
+ "futures-core",
82
+ "http",
83
+ "http-body",
84
+ "http-body-util",
85
+ "mime",
86
+ "pin-project-lite",
87
+ "sync_wrapper",
88
+ "tower-layer",
89
+ "tower-service",
90
+ "tracing",
91
+ ]
92
+
93
+ [[package]]
94
+ name = "bitflags"
95
+ version = "2.10.0"
96
+ source = "registry+https://github.com/rust-lang/crates.io-index"
97
+ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
98
+
99
+ [[package]]
100
+ name = "bumpalo"
101
+ version = "3.19.1"
102
+ source = "registry+https://github.com/rust-lang/crates.io-index"
103
+ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
104
+
105
+ [[package]]
106
+ name = "bytes"
107
+ version = "1.11.0"
108
+ source = "registry+https://github.com/rust-lang/crates.io-index"
109
+ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
110
+
111
+ [[package]]
112
+ name = "cc"
113
+ version = "1.2.51"
114
+ source = "registry+https://github.com/rust-lang/crates.io-index"
115
+ checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
116
+ dependencies = [
117
+ "find-msvc-tools",
118
+ "shlex",
119
+ ]
120
+
121
+ [[package]]
122
+ name = "cfg-if"
123
+ version = "1.0.4"
124
+ source = "registry+https://github.com/rust-lang/crates.io-index"
125
+ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
126
+
127
+ [[package]]
128
+ name = "chrono"
129
+ version = "0.4.42"
130
+ source = "registry+https://github.com/rust-lang/crates.io-index"
131
+ checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
132
+ dependencies = [
133
+ "iana-time-zone",
134
+ "js-sys",
135
+ "num-traits",
136
+ "serde",
137
+ "wasm-bindgen",
138
+ "windows-link",
139
+ ]
140
+
141
+ [[package]]
142
+ name = "core-foundation-sys"
143
+ version = "0.8.7"
144
+ source = "registry+https://github.com/rust-lang/crates.io-index"
145
+ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
146
+
147
+ [[package]]
148
+ name = "crossbeam-deque"
149
+ version = "0.8.6"
150
+ source = "registry+https://github.com/rust-lang/crates.io-index"
151
+ checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
152
+ dependencies = [
153
+ "crossbeam-epoch",
154
+ "crossbeam-utils",
155
+ ]
156
+
157
+ [[package]]
158
+ name = "crossbeam-epoch"
159
+ version = "0.9.18"
160
+ source = "registry+https://github.com/rust-lang/crates.io-index"
161
+ checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
162
+ dependencies = [
163
+ "crossbeam-utils",
164
+ ]
165
+
166
+ [[package]]
167
+ name = "crossbeam-utils"
168
+ version = "0.8.21"
169
+ source = "registry+https://github.com/rust-lang/crates.io-index"
170
+ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
171
+
172
+ [[package]]
173
+ name = "either"
174
+ version = "1.15.0"
175
+ source = "registry+https://github.com/rust-lang/crates.io-index"
176
+ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
177
+
178
+ [[package]]
179
+ name = "employee-scheduling"
180
+ version = "0.5.0"
181
+ dependencies = [
182
+ "axum",
183
+ "chrono",
184
+ "parking_lot",
185
+ "rand 0.8.5",
186
+ "rayon",
187
+ "serde",
188
+ "serde_json",
189
+ "solverforge",
190
+ "tokio",
191
+ "tower",
192
+ "tower-http",
193
+ "uuid",
194
+ ]
195
+
196
+ [[package]]
197
+ name = "equivalent"
198
+ version = "1.0.2"
199
+ source = "registry+https://github.com/rust-lang/crates.io-index"
200
+ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
201
+
202
+ [[package]]
203
+ name = "errno"
204
+ version = "0.3.14"
205
+ source = "registry+https://github.com/rust-lang/crates.io-index"
206
+ checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
207
+ dependencies = [
208
+ "libc",
209
+ "windows-sys 0.61.2",
210
+ ]
211
+
212
+ [[package]]
213
+ name = "find-msvc-tools"
214
+ version = "0.1.6"
215
+ source = "registry+https://github.com/rust-lang/crates.io-index"
216
+ checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
217
+
218
+ [[package]]
219
+ name = "form_urlencoded"
220
+ version = "1.2.2"
221
+ source = "registry+https://github.com/rust-lang/crates.io-index"
222
+ checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
223
+ dependencies = [
224
+ "percent-encoding",
225
+ ]
226
+
227
+ [[package]]
228
+ name = "futures-channel"
229
+ version = "0.3.31"
230
+ source = "registry+https://github.com/rust-lang/crates.io-index"
231
+ checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
232
+ dependencies = [
233
+ "futures-core",
234
+ ]
235
+
236
+ [[package]]
237
+ name = "futures-core"
238
+ version = "0.3.31"
239
+ source = "registry+https://github.com/rust-lang/crates.io-index"
240
+ checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
241
+
242
+ [[package]]
243
+ name = "futures-sink"
244
+ version = "0.3.31"
245
+ source = "registry+https://github.com/rust-lang/crates.io-index"
246
+ checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
247
+
248
+ [[package]]
249
+ name = "futures-task"
250
+ version = "0.3.31"
251
+ source = "registry+https://github.com/rust-lang/crates.io-index"
252
+ checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
253
+
254
+ [[package]]
255
+ name = "futures-util"
256
+ version = "0.3.31"
257
+ source = "registry+https://github.com/rust-lang/crates.io-index"
258
+ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
259
+ dependencies = [
260
+ "futures-core",
261
+ "futures-task",
262
+ "pin-project-lite",
263
+ "pin-utils",
264
+ ]
265
+
266
+ [[package]]
267
+ name = "getrandom"
268
+ version = "0.2.16"
269
+ source = "registry+https://github.com/rust-lang/crates.io-index"
270
+ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
271
+ dependencies = [
272
+ "cfg-if",
273
+ "libc",
274
+ "wasi",
275
+ ]
276
+
277
+ [[package]]
278
+ name = "getrandom"
279
+ version = "0.3.4"
280
+ source = "registry+https://github.com/rust-lang/crates.io-index"
281
+ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
282
+ dependencies = [
283
+ "cfg-if",
284
+ "libc",
285
+ "r-efi",
286
+ "wasip2",
287
+ ]
288
+
289
+ [[package]]
290
+ name = "hashbrown"
291
+ version = "0.16.1"
292
+ source = "registry+https://github.com/rust-lang/crates.io-index"
293
+ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
294
+
295
+ [[package]]
296
+ name = "http"
297
+ version = "1.4.0"
298
+ source = "registry+https://github.com/rust-lang/crates.io-index"
299
+ checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
300
+ dependencies = [
301
+ "bytes",
302
+ "itoa",
303
+ ]
304
+
305
+ [[package]]
306
+ name = "http-body"
307
+ version = "1.0.1"
308
+ source = "registry+https://github.com/rust-lang/crates.io-index"
309
+ checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
310
+ dependencies = [
311
+ "bytes",
312
+ "http",
313
+ ]
314
+
315
+ [[package]]
316
+ name = "http-body-util"
317
+ version = "0.1.3"
318
+ source = "registry+https://github.com/rust-lang/crates.io-index"
319
+ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
320
+ dependencies = [
321
+ "bytes",
322
+ "futures-core",
323
+ "http",
324
+ "http-body",
325
+ "pin-project-lite",
326
+ ]
327
+
328
+ [[package]]
329
+ name = "http-range-header"
330
+ version = "0.4.2"
331
+ source = "registry+https://github.com/rust-lang/crates.io-index"
332
+ checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
333
+
334
+ [[package]]
335
+ name = "httparse"
336
+ version = "1.10.1"
337
+ source = "registry+https://github.com/rust-lang/crates.io-index"
338
+ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
339
+
340
+ [[package]]
341
+ name = "httpdate"
342
+ version = "1.0.3"
343
+ source = "registry+https://github.com/rust-lang/crates.io-index"
344
+ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
345
+
346
+ [[package]]
347
+ name = "hyper"
348
+ version = "1.8.1"
349
+ source = "registry+https://github.com/rust-lang/crates.io-index"
350
+ checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
351
+ dependencies = [
352
+ "atomic-waker",
353
+ "bytes",
354
+ "futures-channel",
355
+ "futures-core",
356
+ "http",
357
+ "http-body",
358
+ "httparse",
359
+ "httpdate",
360
+ "itoa",
361
+ "pin-project-lite",
362
+ "pin-utils",
363
+ "smallvec",
364
+ "tokio",
365
+ ]
366
+
367
+ [[package]]
368
+ name = "hyper-util"
369
+ version = "0.1.19"
370
+ source = "registry+https://github.com/rust-lang/crates.io-index"
371
+ checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
372
+ dependencies = [
373
+ "bytes",
374
+ "futures-core",
375
+ "http",
376
+ "http-body",
377
+ "hyper",
378
+ "pin-project-lite",
379
+ "tokio",
380
+ "tower-service",
381
+ ]
382
+
383
+ [[package]]
384
+ name = "iana-time-zone"
385
+ version = "0.1.64"
386
+ source = "registry+https://github.com/rust-lang/crates.io-index"
387
+ checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
388
+ dependencies = [
389
+ "android_system_properties",
390
+ "core-foundation-sys",
391
+ "iana-time-zone-haiku",
392
+ "js-sys",
393
+ "log",
394
+ "wasm-bindgen",
395
+ "windows-core",
396
+ ]
397
+
398
+ [[package]]
399
+ name = "iana-time-zone-haiku"
400
+ version = "0.1.2"
401
+ source = "registry+https://github.com/rust-lang/crates.io-index"
402
+ checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
403
+ dependencies = [
404
+ "cc",
405
+ ]
406
+
407
+ [[package]]
408
+ name = "indexmap"
409
+ version = "2.12.1"
410
+ source = "registry+https://github.com/rust-lang/crates.io-index"
411
+ checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
412
+ dependencies = [
413
+ "equivalent",
414
+ "hashbrown",
415
+ ]
416
+
417
+ [[package]]
418
+ name = "itoa"
419
+ version = "1.0.17"
420
+ source = "registry+https://github.com/rust-lang/crates.io-index"
421
+ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
422
+
423
+ [[package]]
424
+ name = "js-sys"
425
+ version = "0.3.83"
426
+ source = "registry+https://github.com/rust-lang/crates.io-index"
427
+ checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
428
+ dependencies = [
429
+ "once_cell",
430
+ "wasm-bindgen",
431
+ ]
432
+
433
+ [[package]]
434
+ name = "lazy_static"
435
+ version = "1.5.0"
436
+ source = "registry+https://github.com/rust-lang/crates.io-index"
437
+ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
438
+
439
+ [[package]]
440
+ name = "libc"
441
+ version = "0.2.179"
442
+ source = "registry+https://github.com/rust-lang/crates.io-index"
443
+ checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
444
+
445
+ [[package]]
446
+ name = "lock_api"
447
+ version = "0.4.14"
448
+ source = "registry+https://github.com/rust-lang/crates.io-index"
449
+ checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
450
+ dependencies = [
451
+ "scopeguard",
452
+ ]
453
+
454
+ [[package]]
455
+ name = "log"
456
+ version = "0.4.29"
457
+ source = "registry+https://github.com/rust-lang/crates.io-index"
458
+ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
459
+
460
+ [[package]]
461
+ name = "matchers"
462
+ version = "0.2.0"
463
+ source = "registry+https://github.com/rust-lang/crates.io-index"
464
+ checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
465
+ dependencies = [
466
+ "regex-automata",
467
+ ]
468
+
469
+ [[package]]
470
+ name = "matchit"
471
+ version = "0.8.4"
472
+ source = "registry+https://github.com/rust-lang/crates.io-index"
473
+ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
474
+
475
+ [[package]]
476
+ name = "memchr"
477
+ version = "2.7.6"
478
+ source = "registry+https://github.com/rust-lang/crates.io-index"
479
+ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
480
+
481
+ [[package]]
482
+ name = "mime"
483
+ version = "0.3.17"
484
+ source = "registry+https://github.com/rust-lang/crates.io-index"
485
+ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
486
+
487
+ [[package]]
488
+ name = "mime_guess"
489
+ version = "2.0.5"
490
+ source = "registry+https://github.com/rust-lang/crates.io-index"
491
+ checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
492
+ dependencies = [
493
+ "mime",
494
+ "unicase",
495
+ ]
496
+
497
+ [[package]]
498
+ name = "mio"
499
+ version = "1.1.1"
500
+ source = "registry+https://github.com/rust-lang/crates.io-index"
501
+ checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
502
+ dependencies = [
503
+ "libc",
504
+ "wasi",
505
+ "windows-sys 0.61.2",
506
+ ]
507
+
508
+ [[package]]
509
+ name = "nu-ansi-term"
510
+ version = "0.50.3"
511
+ source = "registry+https://github.com/rust-lang/crates.io-index"
512
+ checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
513
+ dependencies = [
514
+ "windows-sys 0.61.2",
515
+ ]
516
+
517
+ [[package]]
518
+ name = "num-format"
519
+ version = "0.4.4"
520
+ source = "registry+https://github.com/rust-lang/crates.io-index"
521
+ checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
522
+ dependencies = [
523
+ "arrayvec",
524
+ "itoa",
525
+ ]
526
+
527
+ [[package]]
528
+ name = "num-traits"
529
+ version = "0.2.19"
530
+ source = "registry+https://github.com/rust-lang/crates.io-index"
531
+ checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
532
+ dependencies = [
533
+ "autocfg",
534
+ ]
535
+
536
+ [[package]]
537
+ name = "once_cell"
538
+ version = "1.21.3"
539
+ source = "registry+https://github.com/rust-lang/crates.io-index"
540
+ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
541
+
542
+ [[package]]
543
+ name = "owo-colors"
544
+ version = "4.2.3"
545
+ source = "registry+https://github.com/rust-lang/crates.io-index"
546
+ checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
547
+
548
+ [[package]]
549
+ name = "parking_lot"
550
+ version = "0.12.5"
551
+ source = "registry+https://github.com/rust-lang/crates.io-index"
552
+ checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
553
+ dependencies = [
554
+ "lock_api",
555
+ "parking_lot_core",
556
+ ]
557
+
558
+ [[package]]
559
+ name = "parking_lot_core"
560
+ version = "0.9.12"
561
+ source = "registry+https://github.com/rust-lang/crates.io-index"
562
+ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
563
+ dependencies = [
564
+ "cfg-if",
565
+ "libc",
566
+ "redox_syscall",
567
+ "smallvec",
568
+ "windows-link",
569
+ ]
570
+
571
+ [[package]]
572
+ name = "percent-encoding"
573
+ version = "2.3.2"
574
+ source = "registry+https://github.com/rust-lang/crates.io-index"
575
+ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
576
+
577
+ [[package]]
578
+ name = "pin-project-lite"
579
+ version = "0.2.16"
580
+ source = "registry+https://github.com/rust-lang/crates.io-index"
581
+ checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
582
+
583
+ [[package]]
584
+ name = "pin-utils"
585
+ version = "0.1.0"
586
+ source = "registry+https://github.com/rust-lang/crates.io-index"
587
+ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
588
+
589
+ [[package]]
590
+ name = "ppv-lite86"
591
+ version = "0.2.21"
592
+ source = "registry+https://github.com/rust-lang/crates.io-index"
593
+ checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
594
+ dependencies = [
595
+ "zerocopy",
596
+ ]
597
+
598
+ [[package]]
599
+ name = "proc-macro2"
600
+ version = "1.0.104"
601
+ source = "registry+https://github.com/rust-lang/crates.io-index"
602
+ checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
603
+ dependencies = [
604
+ "unicode-ident",
605
+ ]
606
+
607
+ [[package]]
608
+ name = "quote"
609
+ version = "1.0.42"
610
+ source = "registry+https://github.com/rust-lang/crates.io-index"
611
+ checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
612
+ dependencies = [
613
+ "proc-macro2",
614
+ ]
615
+
616
+ [[package]]
617
+ name = "r-efi"
618
+ version = "5.3.0"
619
+ source = "registry+https://github.com/rust-lang/crates.io-index"
620
+ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
621
+
622
+ [[package]]
623
+ name = "rand"
624
+ version = "0.8.5"
625
+ source = "registry+https://github.com/rust-lang/crates.io-index"
626
+ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
627
+ dependencies = [
628
+ "libc",
629
+ "rand_chacha 0.3.1",
630
+ "rand_core 0.6.4",
631
+ ]
632
+
633
+ [[package]]
634
+ name = "rand"
635
+ version = "0.9.2"
636
+ source = "registry+https://github.com/rust-lang/crates.io-index"
637
+ checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
638
+ dependencies = [
639
+ "rand_chacha 0.9.0",
640
+ "rand_core 0.9.3",
641
+ ]
642
+
643
+ [[package]]
644
+ name = "rand_chacha"
645
+ version = "0.3.1"
646
+ source = "registry+https://github.com/rust-lang/crates.io-index"
647
+ checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
648
+ dependencies = [
649
+ "ppv-lite86",
650
+ "rand_core 0.6.4",
651
+ ]
652
+
653
+ [[package]]
654
+ name = "rand_chacha"
655
+ version = "0.9.0"
656
+ source = "registry+https://github.com/rust-lang/crates.io-index"
657
+ checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
658
+ dependencies = [
659
+ "ppv-lite86",
660
+ "rand_core 0.9.3",
661
+ ]
662
+
663
+ [[package]]
664
+ name = "rand_core"
665
+ version = "0.6.4"
666
+ source = "registry+https://github.com/rust-lang/crates.io-index"
667
+ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
668
+ dependencies = [
669
+ "getrandom 0.2.16",
670
+ ]
671
+
672
+ [[package]]
673
+ name = "rand_core"
674
+ version = "0.9.3"
675
+ source = "registry+https://github.com/rust-lang/crates.io-index"
676
+ checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
677
+ dependencies = [
678
+ "getrandom 0.3.4",
679
+ ]
680
+
681
+ [[package]]
682
+ name = "rayon"
683
+ version = "1.11.0"
684
+ source = "registry+https://github.com/rust-lang/crates.io-index"
685
+ checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
686
+ dependencies = [
687
+ "either",
688
+ "rayon-core",
689
+ ]
690
+
691
+ [[package]]
692
+ name = "rayon-core"
693
+ version = "1.13.0"
694
+ source = "registry+https://github.com/rust-lang/crates.io-index"
695
+ checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
696
+ dependencies = [
697
+ "crossbeam-deque",
698
+ "crossbeam-utils",
699
+ ]
700
+
701
+ [[package]]
702
+ name = "redox_syscall"
703
+ version = "0.5.18"
704
+ source = "registry+https://github.com/rust-lang/crates.io-index"
705
+ checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
706
+ dependencies = [
707
+ "bitflags",
708
+ ]
709
+
710
+ [[package]]
711
+ name = "regex-automata"
712
+ version = "0.4.13"
713
+ source = "registry+https://github.com/rust-lang/crates.io-index"
714
+ checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
715
+ dependencies = [
716
+ "aho-corasick",
717
+ "memchr",
718
+ "regex-syntax",
719
+ ]
720
+
721
+ [[package]]
722
+ name = "regex-syntax"
723
+ version = "0.8.8"
724
+ source = "registry+https://github.com/rust-lang/crates.io-index"
725
+ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
726
+
727
+ [[package]]
728
+ name = "rustversion"
729
+ version = "1.0.22"
730
+ source = "registry+https://github.com/rust-lang/crates.io-index"
731
+ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
732
+
733
+ [[package]]
734
+ name = "ryu"
735
+ version = "1.0.22"
736
+ source = "registry+https://github.com/rust-lang/crates.io-index"
737
+ checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
738
+
739
+ [[package]]
740
+ name = "scopeguard"
741
+ version = "1.2.0"
742
+ source = "registry+https://github.com/rust-lang/crates.io-index"
743
+ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
744
+
745
+ [[package]]
746
+ name = "serde"
747
+ version = "1.0.228"
748
+ source = "registry+https://github.com/rust-lang/crates.io-index"
749
+ checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
750
+ dependencies = [
751
+ "serde_core",
752
+ "serde_derive",
753
+ ]
754
+
755
+ [[package]]
756
+ name = "serde_core"
757
+ version = "1.0.228"
758
+ source = "registry+https://github.com/rust-lang/crates.io-index"
759
+ checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
760
+ dependencies = [
761
+ "serde_derive",
762
+ ]
763
+
764
+ [[package]]
765
+ name = "serde_derive"
766
+ version = "1.0.228"
767
+ source = "registry+https://github.com/rust-lang/crates.io-index"
768
+ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
769
+ dependencies = [
770
+ "proc-macro2",
771
+ "quote",
772
+ "syn",
773
+ ]
774
+
775
+ [[package]]
776
+ name = "serde_json"
777
+ version = "1.0.148"
778
+ source = "registry+https://github.com/rust-lang/crates.io-index"
779
+ checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
780
+ dependencies = [
781
+ "itoa",
782
+ "memchr",
783
+ "serde",
784
+ "serde_core",
785
+ "zmij",
786
+ ]
787
+
788
+ [[package]]
789
+ name = "serde_path_to_error"
790
+ version = "0.1.20"
791
+ source = "registry+https://github.com/rust-lang/crates.io-index"
792
+ checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
793
+ dependencies = [
794
+ "itoa",
795
+ "serde",
796
+ "serde_core",
797
+ ]
798
+
799
+ [[package]]
800
+ name = "serde_spanned"
801
+ version = "0.6.9"
802
+ source = "registry+https://github.com/rust-lang/crates.io-index"
803
+ checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
804
+ dependencies = [
805
+ "serde",
806
+ ]
807
+
808
+ [[package]]
809
+ name = "serde_urlencoded"
810
+ version = "0.7.1"
811
+ source = "registry+https://github.com/rust-lang/crates.io-index"
812
+ checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
813
+ dependencies = [
814
+ "form_urlencoded",
815
+ "itoa",
816
+ "ryu",
817
+ "serde",
818
+ ]
819
+
820
+ [[package]]
821
+ name = "serde_yaml"
822
+ version = "0.9.34+deprecated"
823
+ source = "registry+https://github.com/rust-lang/crates.io-index"
824
+ checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
825
+ dependencies = [
826
+ "indexmap",
827
+ "itoa",
828
+ "ryu",
829
+ "serde",
830
+ "unsafe-libyaml",
831
+ ]
832
+
833
+ [[package]]
834
+ name = "sharded-slab"
835
+ version = "0.1.7"
836
+ source = "registry+https://github.com/rust-lang/crates.io-index"
837
+ checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
838
+ dependencies = [
839
+ "lazy_static",
840
+ ]
841
+
842
+ [[package]]
843
+ name = "shlex"
844
+ version = "1.3.0"
845
+ source = "registry+https://github.com/rust-lang/crates.io-index"
846
+ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
847
+
848
+ [[package]]
849
+ name = "signal-hook-registry"
850
+ version = "1.4.8"
851
+ source = "registry+https://github.com/rust-lang/crates.io-index"
852
+ checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
853
+ dependencies = [
854
+ "errno",
855
+ "libc",
856
+ ]
857
+
858
+ [[package]]
859
+ name = "smallvec"
860
+ version = "1.15.1"
861
+ source = "registry+https://github.com/rust-lang/crates.io-index"
862
+ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
863
+
864
+ [[package]]
865
+ name = "socket2"
866
+ version = "0.6.1"
867
+ source = "registry+https://github.com/rust-lang/crates.io-index"
868
+ checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
869
+ dependencies = [
870
+ "libc",
871
+ "windows-sys 0.60.2",
872
+ ]
873
+
874
+ [[package]]
875
+ name = "solverforge"
876
+ version = "0.5.0"
877
+ source = "registry+https://github.com/rust-lang/crates.io-index"
878
+ checksum = "bb513309e056520689d2e85b5b0cd41bfe683dbf8cb2e9551e8251a38fa7dbdd"
879
+ dependencies = [
880
+ "num-format",
881
+ "owo-colors",
882
+ "solverforge-config",
883
+ "solverforge-core",
884
+ "solverforge-macros",
885
+ "solverforge-scoring",
886
+ "solverforge-solver",
887
+ "tracing",
888
+ "tracing-subscriber",
889
+ ]
890
+
891
+ [[package]]
892
+ name = "solverforge-config"
893
+ version = "0.5.0"
894
+ source = "registry+https://github.com/rust-lang/crates.io-index"
895
+ checksum = "dd73530a3b29a90f34778f7326d812605bf2deb4c253027fffef39a587bf1880"
896
+ dependencies = [
897
+ "serde",
898
+ "serde_yaml",
899
+ "solverforge-core",
900
+ "thiserror",
901
+ "toml",
902
+ ]
903
+
904
+ [[package]]
905
+ name = "solverforge-core"
906
+ version = "0.5.0"
907
+ source = "registry+https://github.com/rust-lang/crates.io-index"
908
+ checksum = "f9340b4e2e13d3cf0f2525a6b4e7fbb674819ccacb6cd6ea8777abe8baa463f3"
909
+ dependencies = [
910
+ "num-traits",
911
+ "serde",
912
+ "thiserror",
913
+ ]
914
+
915
+ [[package]]
916
+ name = "solverforge-macros"
917
+ version = "0.5.0"
918
+ source = "registry+https://github.com/rust-lang/crates.io-index"
919
+ checksum = "27b9c7720596cef52b919937b08eb65ec3e341010c249e7f5270e4ab4b879956"
920
+ dependencies = [
921
+ "proc-macro2",
922
+ "quote",
923
+ "syn",
924
+ ]
925
+
926
+ [[package]]
927
+ name = "solverforge-scoring"
928
+ version = "0.5.0"
929
+ source = "registry+https://github.com/rust-lang/crates.io-index"
930
+ checksum = "924e94cad37c0942ffa549cc92c5eb65f2d7567e31c57c32751a5222d23b2523"
931
+ dependencies = [
932
+ "solverforge-core",
933
+ "thiserror",
934
+ ]
935
+
936
+ [[package]]
937
+ name = "solverforge-solver"
938
+ version = "0.5.0"
939
+ source = "registry+https://github.com/rust-lang/crates.io-index"
940
+ checksum = "07696c23f16d5b9f5f0eb5afbacdd2f44daf1fd1d123ba31bbdbf9e5bb8f4b63"
941
+ dependencies = [
942
+ "rand 0.9.2",
943
+ "rand_chacha 0.9.0",
944
+ "rayon",
945
+ "serde",
946
+ "smallvec",
947
+ "solverforge-config",
948
+ "solverforge-core",
949
+ "solverforge-scoring",
950
+ "thiserror",
951
+ "tokio",
952
+ "tracing",
953
+ ]
954
+
955
+ [[package]]
956
+ name = "syn"
957
+ version = "2.0.113"
958
+ source = "registry+https://github.com/rust-lang/crates.io-index"
959
+ checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4"
960
+ dependencies = [
961
+ "proc-macro2",
962
+ "quote",
963
+ "unicode-ident",
964
+ ]
965
+
966
+ [[package]]
967
+ name = "sync_wrapper"
968
+ version = "1.0.2"
969
+ source = "registry+https://github.com/rust-lang/crates.io-index"
970
+ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
971
+
972
+ [[package]]
973
+ name = "thiserror"
974
+ version = "2.0.17"
975
+ source = "registry+https://github.com/rust-lang/crates.io-index"
976
+ checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
977
+ dependencies = [
978
+ "thiserror-impl",
979
+ ]
980
+
981
+ [[package]]
982
+ name = "thiserror-impl"
983
+ version = "2.0.17"
984
+ source = "registry+https://github.com/rust-lang/crates.io-index"
985
+ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
986
+ dependencies = [
987
+ "proc-macro2",
988
+ "quote",
989
+ "syn",
990
+ ]
991
+
992
+ [[package]]
993
+ name = "thread_local"
994
+ version = "1.1.9"
995
+ source = "registry+https://github.com/rust-lang/crates.io-index"
996
+ checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
997
+ dependencies = [
998
+ "cfg-if",
999
+ ]
1000
+
1001
+ [[package]]
1002
+ name = "tokio"
1003
+ version = "1.49.0"
1004
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1005
+ checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
1006
+ dependencies = [
1007
+ "bytes",
1008
+ "libc",
1009
+ "mio",
1010
+ "parking_lot",
1011
+ "pin-project-lite",
1012
+ "signal-hook-registry",
1013
+ "socket2",
1014
+ "tokio-macros",
1015
+ "windows-sys 0.61.2",
1016
+ ]
1017
+
1018
+ [[package]]
1019
+ name = "tokio-macros"
1020
+ version = "2.6.0"
1021
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1022
+ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
1023
+ dependencies = [
1024
+ "proc-macro2",
1025
+ "quote",
1026
+ "syn",
1027
+ ]
1028
+
1029
+ [[package]]
1030
+ name = "tokio-util"
1031
+ version = "0.7.18"
1032
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1033
+ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
1034
+ dependencies = [
1035
+ "bytes",
1036
+ "futures-core",
1037
+ "futures-sink",
1038
+ "pin-project-lite",
1039
+ "tokio",
1040
+ ]
1041
+
1042
+ [[package]]
1043
+ name = "toml"
1044
+ version = "0.8.23"
1045
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1046
+ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
1047
+ dependencies = [
1048
+ "serde",
1049
+ "serde_spanned",
1050
+ "toml_datetime",
1051
+ "toml_edit",
1052
+ ]
1053
+
1054
+ [[package]]
1055
+ name = "toml_datetime"
1056
+ version = "0.6.11"
1057
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1058
+ checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
1059
+ dependencies = [
1060
+ "serde",
1061
+ ]
1062
+
1063
+ [[package]]
1064
+ name = "toml_edit"
1065
+ version = "0.22.27"
1066
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1067
+ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
1068
+ dependencies = [
1069
+ "indexmap",
1070
+ "serde",
1071
+ "serde_spanned",
1072
+ "toml_datetime",
1073
+ "toml_write",
1074
+ "winnow",
1075
+ ]
1076
+
1077
+ [[package]]
1078
+ name = "toml_write"
1079
+ version = "0.1.2"
1080
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1081
+ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
1082
+
1083
+ [[package]]
1084
+ name = "tower"
1085
+ version = "0.5.2"
1086
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1087
+ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
1088
+ dependencies = [
1089
+ "futures-core",
1090
+ "futures-util",
1091
+ "pin-project-lite",
1092
+ "sync_wrapper",
1093
+ "tokio",
1094
+ "tower-layer",
1095
+ "tower-service",
1096
+ "tracing",
1097
+ ]
1098
+
1099
+ [[package]]
1100
+ name = "tower-http"
1101
+ version = "0.6.8"
1102
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1103
+ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
1104
+ dependencies = [
1105
+ "bitflags",
1106
+ "bytes",
1107
+ "futures-core",
1108
+ "futures-util",
1109
+ "http",
1110
+ "http-body",
1111
+ "http-body-util",
1112
+ "http-range-header",
1113
+ "httpdate",
1114
+ "mime",
1115
+ "mime_guess",
1116
+ "percent-encoding",
1117
+ "pin-project-lite",
1118
+ "tokio",
1119
+ "tokio-util",
1120
+ "tower-layer",
1121
+ "tower-service",
1122
+ "tracing",
1123
+ ]
1124
+
1125
+ [[package]]
1126
+ name = "tower-layer"
1127
+ version = "0.3.3"
1128
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1129
+ checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
1130
+
1131
+ [[package]]
1132
+ name = "tower-service"
1133
+ version = "0.3.3"
1134
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1135
+ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
1136
+
1137
+ [[package]]
1138
+ name = "tracing"
1139
+ version = "0.1.44"
1140
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1141
+ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
1142
+ dependencies = [
1143
+ "log",
1144
+ "pin-project-lite",
1145
+ "tracing-attributes",
1146
+ "tracing-core",
1147
+ ]
1148
+
1149
+ [[package]]
1150
+ name = "tracing-attributes"
1151
+ version = "0.1.31"
1152
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1153
+ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
1154
+ dependencies = [
1155
+ "proc-macro2",
1156
+ "quote",
1157
+ "syn",
1158
+ ]
1159
+
1160
+ [[package]]
1161
+ name = "tracing-core"
1162
+ version = "0.1.36"
1163
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1164
+ checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
1165
+ dependencies = [
1166
+ "once_cell",
1167
+ "valuable",
1168
+ ]
1169
+
1170
+ [[package]]
1171
+ name = "tracing-log"
1172
+ version = "0.2.0"
1173
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1174
+ checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
1175
+ dependencies = [
1176
+ "log",
1177
+ "once_cell",
1178
+ "tracing-core",
1179
+ ]
1180
+
1181
+ [[package]]
1182
+ name = "tracing-subscriber"
1183
+ version = "0.3.22"
1184
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1185
+ checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
1186
+ dependencies = [
1187
+ "matchers",
1188
+ "nu-ansi-term",
1189
+ "once_cell",
1190
+ "regex-automata",
1191
+ "sharded-slab",
1192
+ "smallvec",
1193
+ "thread_local",
1194
+ "tracing",
1195
+ "tracing-core",
1196
+ "tracing-log",
1197
+ ]
1198
+
1199
+ [[package]]
1200
+ name = "unicase"
1201
+ version = "2.8.1"
1202
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1203
+ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
1204
+
1205
+ [[package]]
1206
+ name = "unicode-ident"
1207
+ version = "1.0.22"
1208
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1209
+ checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
1210
+
1211
+ [[package]]
1212
+ name = "unsafe-libyaml"
1213
+ version = "0.2.11"
1214
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1215
+ checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
1216
+
1217
+ [[package]]
1218
+ name = "uuid"
1219
+ version = "1.19.0"
1220
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1221
+ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
1222
+ dependencies = [
1223
+ "getrandom 0.3.4",
1224
+ "js-sys",
1225
+ "serde_core",
1226
+ "wasm-bindgen",
1227
+ ]
1228
+
1229
+ [[package]]
1230
+ name = "valuable"
1231
+ version = "0.1.1"
1232
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1233
+ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
1234
+
1235
+ [[package]]
1236
+ name = "wasi"
1237
+ version = "0.11.1+wasi-snapshot-preview1"
1238
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1239
+ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
1240
+
1241
+ [[package]]
1242
+ name = "wasip2"
1243
+ version = "1.0.1+wasi-0.2.4"
1244
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1245
+ checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
1246
+ dependencies = [
1247
+ "wit-bindgen",
1248
+ ]
1249
+
1250
+ [[package]]
1251
+ name = "wasm-bindgen"
1252
+ version = "0.2.106"
1253
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1254
+ checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
1255
+ dependencies = [
1256
+ "cfg-if",
1257
+ "once_cell",
1258
+ "rustversion",
1259
+ "wasm-bindgen-macro",
1260
+ "wasm-bindgen-shared",
1261
+ ]
1262
+
1263
+ [[package]]
1264
+ name = "wasm-bindgen-macro"
1265
+ version = "0.2.106"
1266
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1267
+ checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
1268
+ dependencies = [
1269
+ "quote",
1270
+ "wasm-bindgen-macro-support",
1271
+ ]
1272
+
1273
+ [[package]]
1274
+ name = "wasm-bindgen-macro-support"
1275
+ version = "0.2.106"
1276
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1277
+ checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
1278
+ dependencies = [
1279
+ "bumpalo",
1280
+ "proc-macro2",
1281
+ "quote",
1282
+ "syn",
1283
+ "wasm-bindgen-shared",
1284
+ ]
1285
+
1286
+ [[package]]
1287
+ name = "wasm-bindgen-shared"
1288
+ version = "0.2.106"
1289
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1290
+ checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
1291
+ dependencies = [
1292
+ "unicode-ident",
1293
+ ]
1294
+
1295
+ [[package]]
1296
+ name = "windows-core"
1297
+ version = "0.62.2"
1298
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1299
+ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
1300
+ dependencies = [
1301
+ "windows-implement",
1302
+ "windows-interface",
1303
+ "windows-link",
1304
+ "windows-result",
1305
+ "windows-strings",
1306
+ ]
1307
+
1308
+ [[package]]
1309
+ name = "windows-implement"
1310
+ version = "0.60.2"
1311
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1312
+ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
1313
+ dependencies = [
1314
+ "proc-macro2",
1315
+ "quote",
1316
+ "syn",
1317
+ ]
1318
+
1319
+ [[package]]
1320
+ name = "windows-interface"
1321
+ version = "0.59.3"
1322
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1323
+ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
1324
+ dependencies = [
1325
+ "proc-macro2",
1326
+ "quote",
1327
+ "syn",
1328
+ ]
1329
+
1330
+ [[package]]
1331
+ name = "windows-link"
1332
+ version = "0.2.1"
1333
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1334
+ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
1335
+
1336
+ [[package]]
1337
+ name = "windows-result"
1338
+ version = "0.4.1"
1339
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1340
+ checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
1341
+ dependencies = [
1342
+ "windows-link",
1343
+ ]
1344
+
1345
+ [[package]]
1346
+ name = "windows-strings"
1347
+ version = "0.5.1"
1348
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1349
+ checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
1350
+ dependencies = [
1351
+ "windows-link",
1352
+ ]
1353
+
1354
+ [[package]]
1355
+ name = "windows-sys"
1356
+ version = "0.60.2"
1357
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1358
+ checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
1359
+ dependencies = [
1360
+ "windows-targets",
1361
+ ]
1362
+
1363
+ [[package]]
1364
+ name = "windows-sys"
1365
+ version = "0.61.2"
1366
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1367
+ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
1368
+ dependencies = [
1369
+ "windows-link",
1370
+ ]
1371
+
1372
+ [[package]]
1373
+ name = "windows-targets"
1374
+ version = "0.53.5"
1375
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1376
+ checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
1377
+ dependencies = [
1378
+ "windows-link",
1379
+ "windows_aarch64_gnullvm",
1380
+ "windows_aarch64_msvc",
1381
+ "windows_i686_gnu",
1382
+ "windows_i686_gnullvm",
1383
+ "windows_i686_msvc",
1384
+ "windows_x86_64_gnu",
1385
+ "windows_x86_64_gnullvm",
1386
+ "windows_x86_64_msvc",
1387
+ ]
1388
+
1389
+ [[package]]
1390
+ name = "windows_aarch64_gnullvm"
1391
+ version = "0.53.1"
1392
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1393
+ checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
1394
+
1395
+ [[package]]
1396
+ name = "windows_aarch64_msvc"
1397
+ version = "0.53.1"
1398
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1399
+ checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
1400
+
1401
+ [[package]]
1402
+ name = "windows_i686_gnu"
1403
+ version = "0.53.1"
1404
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1405
+ checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
1406
+
1407
+ [[package]]
1408
+ name = "windows_i686_gnullvm"
1409
+ version = "0.53.1"
1410
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1411
+ checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
1412
+
1413
+ [[package]]
1414
+ name = "windows_i686_msvc"
1415
+ version = "0.53.1"
1416
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1417
+ checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
1418
+
1419
+ [[package]]
1420
+ name = "windows_x86_64_gnu"
1421
+ version = "0.53.1"
1422
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1423
+ checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
1424
+
1425
+ [[package]]
1426
+ name = "windows_x86_64_gnullvm"
1427
+ version = "0.53.1"
1428
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1429
+ checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
1430
+
1431
+ [[package]]
1432
+ name = "windows_x86_64_msvc"
1433
+ version = "0.53.1"
1434
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1435
+ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
1436
+
1437
+ [[package]]
1438
+ name = "winnow"
1439
+ version = "0.7.14"
1440
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1441
+ checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
1442
+ dependencies = [
1443
+ "memchr",
1444
+ ]
1445
+
1446
+ [[package]]
1447
+ name = "wit-bindgen"
1448
+ version = "0.46.0"
1449
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1450
+ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
1451
+
1452
+ [[package]]
1453
+ name = "zerocopy"
1454
+ version = "0.8.31"
1455
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1456
+ checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
1457
+ dependencies = [
1458
+ "zerocopy-derive",
1459
+ ]
1460
+
1461
+ [[package]]
1462
+ name = "zerocopy-derive"
1463
+ version = "0.8.31"
1464
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1465
+ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
1466
+ dependencies = [
1467
+ "proc-macro2",
1468
+ "quote",
1469
+ "syn",
1470
+ ]
1471
+
1472
+ [[package]]
1473
+ name = "zmij"
1474
+ version = "1.0.10"
1475
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1476
+ checksum = "30e0d8dffbae3d840f64bda38e28391faef673a7b5a6017840f2a106c8145868"
Cargo.toml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "employee-scheduling"
3
+ version = "0.5.0"
4
+ edition = "2021"
5
+ description = "Employee scheduling quickstart for SolverForge"
6
+ publish = false
7
+
8
+ [dependencies]
9
+ solverforge = { version = "0.5.0", features = ["serde", "console", "verbose-logging"] }
10
+ rayon = "1"
11
+ rand = "0.8"
12
+
13
+ axum = "0.8"
14
+ tokio = { version = "1", features = ["full"] }
15
+ tower-http = { version = "0.6", features = ["fs", "cors"] }
16
+ tower = "0.5"
17
+ serde = { version = "1", features = ["derive"] }
18
+ serde_json = "1"
19
+ chrono = { version = "0.4", features = ["serde"] }
20
+ uuid = { version = "1", features = ["v4", "serde"] }
21
+ parking_lot = "0.12"
Dockerfile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build for SolverForge Employee Scheduling (Rust)
2
+ #
3
+ # Build context: rust/employee-scheduling/
4
+ # Uses published solverforge crate from crates.io
5
+
6
+ FROM rust:1.83-alpine AS builder
7
+
8
+ # Install build dependencies
9
+ RUN apk add --no-cache musl-dev
10
+
11
+ WORKDIR /build
12
+
13
+ # Copy workspace files
14
+ COPY Cargo.toml Cargo.lock ./
15
+ COPY src/ ./src/
16
+ COPY static/ ./static/
17
+ COPY solver.toml ./
18
+
19
+ # Build release binary with musl target for static linking
20
+ RUN cargo build --release --target x86_64-unknown-linux-musl
21
+
22
+ # Runtime stage - minimal Alpine image
23
+ FROM alpine:latest
24
+
25
+ RUN apk add --no-cache ca-certificates
26
+
27
+ WORKDIR /app
28
+
29
+ # Copy binary from builder (musl static binary)
30
+ COPY --from=builder /build/target/x86_64-unknown-linux-musl/release/employee-scheduling ./employee-scheduling
31
+
32
+ # Copy static files
33
+ COPY --from=builder /build/static/ ./static/
34
+
35
+ # Copy solver config
36
+ COPY --from=builder /build/solver.toml ./solver.toml
37
+
38
+ # Expose port 7860 (HF Spaces default)
39
+ EXPOSE 7860
40
+
41
+ # Run the application
42
+ CMD ["./employee-scheduling"]
README.md CHANGED
@@ -1,12 +1,97 @@
1
  ---
2
- title: Employee Scheduling Rust
3
- emoji: 🐠
4
- colorFrom: blue
5
- colorTo: gray
6
  sdk: docker
 
7
  pinned: false
8
  license: apache-2.0
9
- short_description: SolverForge Quickstart for the Employee Scheduling Problem
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Employee Scheduling (Rust)
3
+ emoji: 📅
4
+ colorFrom: yellow
5
+ colorTo: red
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  license: apache-2.0
10
+ short_description: SolverForge Quickstart for Employee Scheduling in Rust
11
  ---
12
 
13
+ # Employee Scheduling (Rust)
14
+
15
+ Schedule shifts to employees, accounting for employee availability and shift skill requirements.
16
+
17
+ - [Prerequisites](#prerequisites)
18
+ - [Run the application](#run-the-application)
19
+ - [Test the application](#test-the-application)
20
+ - [REST API](#rest-api)
21
+ - [Constraints](#constraints)
22
+ - [More information](#more-information)
23
+
24
+ ## Prerequisites
25
+
26
+ 1. Install [Rust](https://www.rust-lang.org/tools/install) (1.70 or later):
27
+
28
+ ```sh
29
+ $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
30
+ ```
31
+
32
+ ## Run the application
33
+
34
+ 1. Git clone the solverforge-quickstarts repo and navigate to this directory:
35
+
36
+ ```sh
37
+ $ git clone https://github.com/SolverForge/solverforge-quickstarts.git
38
+ ...
39
+ $ cd solverforge-quickstarts/rust/employee-scheduling
40
+ ```
41
+
42
+ 2. Build and run the application:
43
+
44
+ ```sh
45
+ $ cargo run --release
46
+ ```
47
+
48
+ 3. Visit [http://localhost:7860](http://localhost:7860) in your browser.
49
+
50
+ 4. Click on the **Solve** button.
51
+
52
+ ## Test the application
53
+
54
+ 1. Run tests:
55
+
56
+ ```sh
57
+ $ cargo test
58
+ ```
59
+
60
+ ## Docker
61
+
62
+ You can also run the application using Docker:
63
+
64
+ ```bash
65
+ # From repository root
66
+ $ docker build -f rust/employee-scheduling/Dockerfile -t employee-scheduling-rust .
67
+ $ docker run -p 7860:7860 employee-scheduling-rust
68
+ ```
69
+
70
+ Then visit [http://localhost:7860](http://localhost:7860) in your browser.
71
+
72
+ ## REST API
73
+
74
+ - `GET /demo-data` - List available demo datasets
75
+ - `GET /demo-data/{id}` - Get specific demo data
76
+ - `POST /schedules` - Start solving (returns job ID)
77
+ - `GET /schedules/{id}` - Get current solution
78
+ - `DELETE /schedules/{id}` - Stop solving
79
+ - `PUT /schedules/analyze` - Analyze constraint violations
80
+
81
+ ## Constraints
82
+
83
+ **Hard Constraints** (must be satisfied):
84
+ - Required skill match
85
+ - No overlapping shifts
86
+ - Minimum 10 hours between shifts
87
+ - One shift per day per employee
88
+ - Respect unavailable dates
89
+
90
+ **Soft Constraints** (optimized):
91
+ - Avoid undesired dates
92
+ - Prefer desired dates
93
+ - Balance shift assignments across employees
94
+
95
+ ## More information
96
+
97
+ Visit [solverforge.org](https://www.solverforge.org).
helm/employee-scheduling/.helmignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ .git/
3
+ .gitignore
4
+ .bzr/
5
+ .bzrignore
6
+ .hg/
7
+ .hgignore
8
+ .svn/
9
+ *.swp
10
+ *.bak
11
+ *.tmp
12
+ *.orig
13
+ *~
14
+ .project
15
+ .idea/
16
+ *.tmproj
17
+ .vscode/
helm/employee-scheduling/Chart.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v2
2
+ name: employee-scheduling
3
+ description: Employee Scheduling optimization using SolverForge (Rust/Axum)
4
+ type: application
5
+ version: 0.5.0
6
+ appVersion: "0.5.0"
7
+ keywords:
8
+ - solverforge
9
+ - optimization
10
+ - scheduling
11
+ - rust
12
+ - axum
helm/employee-scheduling/templates/NOTES.txt ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Thank you for installing {{ .Chart.Name }}.
2
+
3
+ Your release is named {{ .Release.Name }}.
4
+
5
+ To learn more about the release, try:
6
+
7
+ $ helm status {{ .Release.Name }}
8
+ $ helm get all {{ .Release.Name }}
9
+
10
+ {{- if .Values.ingress.enabled }}
11
+
12
+ The application is accessible via:
13
+ {{- range $host := .Values.ingress.hosts }}
14
+ {{- range .paths }}
15
+ http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
16
+ {{- end }}
17
+ {{- end }}
18
+
19
+ {{- else }}
20
+
21
+ To access the application, run:
22
+
23
+ export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "app.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
24
+ export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
25
+ kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
26
+
27
+ Then open http://localhost:8080 in your browser.
28
+
29
+ {{- end }}
30
+
31
+ Demo Data: http://localhost:8080/demo-data
helm/employee-scheduling/templates/_helpers.tpl ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{/*
2
+ Expand the name of the chart.
3
+ */}}
4
+ {{- define "app.name" -}}
5
+ {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
6
+ {{- end }}
7
+
8
+ {{/*
9
+ Create a default fully qualified app name.
10
+ */}}
11
+ {{- define "app.fullname" -}}
12
+ {{- if .Values.fullnameOverride }}
13
+ {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
14
+ {{- else }}
15
+ {{- $name := default .Chart.Name .Values.nameOverride }}
16
+ {{- if contains $name .Release.Name }}
17
+ {{- .Release.Name | trunc 63 | trimSuffix "-" }}
18
+ {{- else }}
19
+ {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
20
+ {{- end }}
21
+ {{- end }}
22
+ {{- end }}
23
+
24
+ {{/*
25
+ Create chart name and version as used by the chart label.
26
+ */}}
27
+ {{- define "app.chart" -}}
28
+ {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
29
+ {{- end }}
30
+
31
+ {{/*
32
+ Common labels
33
+ */}}
34
+ {{- define "app.labels" -}}
35
+ helm.sh/chart: {{ include "app.chart" . }}
36
+ {{ include "app.selectorLabels" . }}
37
+ {{- if .Chart.AppVersion }}
38
+ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
39
+ {{- end }}
40
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
41
+ {{- end }}
42
+
43
+ {{/*
44
+ Selector labels
45
+ */}}
46
+ {{- define "app.selectorLabels" -}}
47
+ app.kubernetes.io/name: {{ include "app.name" . }}
48
+ app.kubernetes.io/instance: {{ .Release.Name }}
49
+ {{- end }}
helm/employee-scheduling/templates/deployment.yaml ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: {{ include "app.fullname" . }}
5
+ labels:
6
+ {{- include "app.labels" . | nindent 4 }}
7
+ spec:
8
+ {{- if not .Values.autoscaling.enabled }}
9
+ replicas: {{ .Values.replicaCount }}
10
+ {{- end }}
11
+ selector:
12
+ matchLabels:
13
+ {{- include "app.selectorLabels" . | nindent 6 }}
14
+ template:
15
+ metadata:
16
+ {{- with .Values.podAnnotations }}
17
+ annotations:
18
+ {{- toYaml . | nindent 8 }}
19
+ {{- end }}
20
+ labels:
21
+ {{- include "app.labels" . | nindent 8 }}
22
+ {{- with .Values.podLabels }}
23
+ {{- toYaml . | nindent 8 }}
24
+ {{- end }}
25
+ spec:
26
+ {{- with .Values.imagePullSecrets }}
27
+ imagePullSecrets:
28
+ {{- toYaml . | nindent 8 }}
29
+ {{- end }}
30
+ securityContext:
31
+ {{- toYaml .Values.podSecurityContext | nindent 8 }}
32
+ containers:
33
+ - name: {{ .Chart.Name }}
34
+ securityContext:
35
+ {{- toYaml .Values.securityContext | nindent 12 }}
36
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
37
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
38
+ ports:
39
+ - name: http
40
+ containerPort: 8080
41
+ protocol: TCP
42
+ {{- with .Values.livenessProbe }}
43
+ livenessProbe:
44
+ {{- toYaml . | nindent 12 }}
45
+ {{- end }}
46
+ {{- with .Values.readinessProbe }}
47
+ readinessProbe:
48
+ {{- toYaml . | nindent 12 }}
49
+ {{- end }}
50
+ resources:
51
+ {{- toYaml .Values.resources | nindent 12 }}
52
+ {{- with .Values.env }}
53
+ env:
54
+ {{- toYaml . | nindent 12 }}
55
+ {{- end }}
56
+ {{- with .Values.nodeSelector }}
57
+ nodeSelector:
58
+ {{- toYaml . | nindent 8 }}
59
+ {{- end }}
60
+ {{- with .Values.affinity }}
61
+ affinity:
62
+ {{- toYaml . | nindent 8 }}
63
+ {{- end }}
64
+ {{- with .Values.tolerations }}
65
+ tolerations:
66
+ {{- toYaml . | nindent 8 }}
67
+ {{- end }}
helm/employee-scheduling/templates/ingress.yaml ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{- if .Values.ingress.enabled -}}
2
+ apiVersion: networking.k8s.io/v1
3
+ kind: Ingress
4
+ metadata:
5
+ name: {{ include "app.fullname" . }}
6
+ labels:
7
+ {{- include "app.labels" . | nindent 4 }}
8
+ {{- with .Values.ingress.annotations }}
9
+ annotations:
10
+ {{- toYaml . | nindent 4 }}
11
+ {{- end }}
12
+ spec:
13
+ {{- if .Values.ingress.className }}
14
+ ingressClassName: {{ .Values.ingress.className }}
15
+ {{- end }}
16
+ {{- if .Values.ingress.tls }}
17
+ tls:
18
+ {{- range .Values.ingress.tls }}
19
+ - hosts:
20
+ {{- range .hosts }}
21
+ - {{ . | quote }}
22
+ {{- end }}
23
+ secretName: {{ .secretName }}
24
+ {{- end }}
25
+ {{- end }}
26
+ rules:
27
+ {{- range .Values.ingress.hosts }}
28
+ - host: {{ .host | quote }}
29
+ http:
30
+ paths:
31
+ {{- range .paths }}
32
+ - path: {{ .path }}
33
+ pathType: {{ .pathType }}
34
+ backend:
35
+ service:
36
+ name: {{ include "app.fullname" $ }}
37
+ port:
38
+ number: {{ $.Values.service.port }}
39
+ {{- end }}
40
+ {{- end }}
41
+ {{- end }}
helm/employee-scheduling/templates/service.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v1
2
+ kind: Service
3
+ metadata:
4
+ name: {{ include "app.fullname" . }}
5
+ labels:
6
+ {{- include "app.labels" . | nindent 4 }}
7
+ spec:
8
+ type: {{ .Values.service.type }}
9
+ ports:
10
+ - port: {{ .Values.service.port }}
11
+ targetPort: http
12
+ protocol: TCP
13
+ name: http
14
+ selector:
15
+ {{- include "app.selectorLabels" . | nindent 4 }}
helm/employee-scheduling/values.yaml ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ replicaCount: 1
2
+
3
+ image:
4
+ repository: ghcr.io/solverforge/employee-scheduling
5
+ pullPolicy: IfNotPresent
6
+ tag: "latest"
7
+
8
+ imagePullSecrets: []
9
+ nameOverride: ""
10
+ fullnameOverride: ""
11
+
12
+ podAnnotations: {}
13
+ podLabels: {}
14
+
15
+ podSecurityContext: {}
16
+
17
+ securityContext: {}
18
+
19
+ service:
20
+ type: ClusterIP
21
+ port: 8080
22
+
23
+ ingress:
24
+ enabled: false
25
+ className: ""
26
+ annotations: {}
27
+ hosts:
28
+ - host: employee-scheduling.local
29
+ paths:
30
+ - path: /
31
+ pathType: ImplementationSpecific
32
+ tls: []
33
+
34
+ resources:
35
+ limits:
36
+ cpu: 2000m
37
+ memory: 2Gi
38
+ requests:
39
+ cpu: 250m
40
+ memory: 256Mi
41
+
42
+ livenessProbe:
43
+ httpGet:
44
+ path: /demo-data
45
+ port: http
46
+ initialDelaySeconds: 10
47
+ periodSeconds: 10
48
+ timeoutSeconds: 5
49
+ failureThreshold: 3
50
+
51
+ readinessProbe:
52
+ httpGet:
53
+ path: /demo-data
54
+ port: http
55
+ initialDelaySeconds: 5
56
+ periodSeconds: 5
57
+ timeoutSeconds: 3
58
+ failureThreshold: 3
59
+
60
+ autoscaling:
61
+ enabled: false
62
+ minReplicas: 1
63
+ maxReplicas: 3
64
+ targetCPUUtilizationPercentage: 80
65
+
66
+ nodeSelector: {}
67
+
68
+ tolerations: []
69
+
70
+ affinity: {}
71
+
72
+ env: []
solver.toml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # SolverForge Configuration for Employee Scheduling
2
+
3
+ [termination]
4
+ seconds_spent_limit = 30
5
+ unimproved_seconds_spent_limit = 5
src/api.rs ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! REST API handlers for Employee Scheduling.
2
+
3
+ use axum::{
4
+ extract::{Path, State},
5
+ http::StatusCode,
6
+ routing::{delete, get, post, put},
7
+ Json, Router,
8
+ };
9
+ use chrono::{NaiveDate, NaiveDateTime};
10
+ use parking_lot::RwLock;
11
+ use serde::{Deserialize, Serialize};
12
+ use std::collections::{HashMap, HashSet};
13
+ use std::sync::Arc;
14
+ use uuid::Uuid;
15
+
16
+ use crate::demo_data::{self, DemoData};
17
+ use crate::domain::{Employee, EmployeeSchedule, Shift};
18
+
19
+ /// Job tracking for active solves.
20
+ struct SolveJob {
21
+ solution: EmployeeSchedule,
22
+ solver_status: String,
23
+ }
24
+
25
+ /// Application state shared across handlers.
26
+ pub struct AppState {
27
+ jobs: RwLock<HashMap<String, SolveJob>>,
28
+ }
29
+
30
+ impl AppState {
31
+ pub fn new() -> Self {
32
+ Self {
33
+ jobs: RwLock::new(HashMap::new()),
34
+ }
35
+ }
36
+ }
37
+
38
+ impl Default for AppState {
39
+ fn default() -> Self {
40
+ Self::new()
41
+ }
42
+ }
43
+
44
+ // ============================================================================
45
+ // DTOs
46
+ // ============================================================================
47
+
48
+ /// Employee DTO for API requests/responses.
49
+ #[derive(Debug, Clone, Serialize, Deserialize)]
50
+ #[serde(rename_all = "camelCase")]
51
+ pub struct EmployeeDto {
52
+ pub name: String,
53
+ pub skills: Vec<String>,
54
+ #[serde(default)]
55
+ pub unavailable_dates: Vec<NaiveDate>,
56
+ #[serde(default)]
57
+ pub undesired_dates: Vec<NaiveDate>,
58
+ #[serde(default)]
59
+ pub desired_dates: Vec<NaiveDate>,
60
+ }
61
+
62
+ impl From<&Employee> for EmployeeDto {
63
+ fn from(e: &Employee) -> Self {
64
+ Self {
65
+ name: e.name.clone(),
66
+ skills: e.skills.iter().cloned().collect(),
67
+ unavailable_dates: e.unavailable_dates.iter().cloned().collect(),
68
+ undesired_dates: e.undesired_dates.iter().cloned().collect(),
69
+ desired_dates: e.desired_dates.iter().cloned().collect(),
70
+ }
71
+ }
72
+ }
73
+
74
+ impl EmployeeDto {
75
+ fn to_employee(&self, index: usize) -> Employee {
76
+ let unavailable_dates: HashSet<NaiveDate> =
77
+ self.unavailable_dates.iter().cloned().collect();
78
+ let undesired_dates: HashSet<NaiveDate> =
79
+ self.undesired_dates.iter().cloned().collect();
80
+ let desired_dates: HashSet<NaiveDate> =
81
+ self.desired_dates.iter().cloned().collect();
82
+
83
+ let mut unavailable_days: Vec<NaiveDate> = unavailable_dates.iter().copied().collect();
84
+ unavailable_days.sort();
85
+ let mut undesired_days: Vec<NaiveDate> = undesired_dates.iter().copied().collect();
86
+ undesired_days.sort();
87
+ let mut desired_days: Vec<NaiveDate> = desired_dates.iter().copied().collect();
88
+ desired_days.sort();
89
+
90
+ Employee {
91
+ index,
92
+ name: self.name.clone(),
93
+ skills: self.skills.iter().cloned().collect(),
94
+ unavailable_dates,
95
+ undesired_dates,
96
+ desired_dates,
97
+ unavailable_days,
98
+ undesired_days,
99
+ desired_days,
100
+ }
101
+ }
102
+ }
103
+
104
+ /// Shift DTO with embedded Employee object.
105
+ #[derive(Debug, Clone, Serialize, Deserialize)]
106
+ #[serde(rename_all = "camelCase")]
107
+ pub struct ShiftDto {
108
+ pub id: String,
109
+ pub start: NaiveDateTime,
110
+ pub end: NaiveDateTime,
111
+ pub location: String,
112
+ pub required_skill: String,
113
+ pub employee: Option<EmployeeDto>,
114
+ }
115
+
116
+ /// Full schedule DTO for request/response.
117
+ #[derive(Debug, Serialize, Deserialize)]
118
+ #[serde(rename_all = "camelCase")]
119
+ pub struct ScheduleDto {
120
+ pub employees: Vec<EmployeeDto>,
121
+ pub shifts: Vec<ShiftDto>,
122
+ #[serde(default)]
123
+ pub score: Option<String>,
124
+ #[serde(default, skip_deserializing)]
125
+ pub solver_status: Option<String>,
126
+ }
127
+
128
+ impl ScheduleDto {
129
+ pub fn from_schedule(schedule: &EmployeeSchedule, solver_status: Option<String>) -> Self {
130
+ let employees: Vec<EmployeeDto> = schedule.employees.iter().map(EmployeeDto::from).collect();
131
+
132
+ let shifts: Vec<ShiftDto> = schedule
133
+ .shifts
134
+ .iter()
135
+ .map(|s| ShiftDto {
136
+ id: s.id.clone(),
137
+ start: s.start,
138
+ end: s.end,
139
+ location: s.location.clone(),
140
+ required_skill: s.required_skill.clone(),
141
+ employee: s.employee_idx
142
+ .and_then(|idx| schedule.employees.get(idx))
143
+ .map(EmployeeDto::from),
144
+ })
145
+ .collect();
146
+
147
+ Self {
148
+ employees,
149
+ shifts,
150
+ score: schedule.score.map(|s| format!("{}", s)),
151
+ solver_status,
152
+ }
153
+ }
154
+
155
+ pub fn to_domain(&self) -> EmployeeSchedule {
156
+ // Build employees with their indices set correctly
157
+ let employees: Vec<Employee> = self
158
+ .employees
159
+ .iter()
160
+ .enumerate()
161
+ .map(|(i, dto)| dto.to_employee(i))
162
+ .collect();
163
+ let name_to_idx: std::collections::HashMap<&str, usize> = employees
164
+ .iter()
165
+ .map(|e| (e.name.as_str(), e.index))
166
+ .collect();
167
+
168
+ let shifts: Vec<Shift> = self
169
+ .shifts
170
+ .iter()
171
+ .map(|s| Shift {
172
+ id: s.id.clone(),
173
+ start: s.start,
174
+ end: s.end,
175
+ location: s.location.clone(),
176
+ required_skill: s.required_skill.clone(),
177
+ employee_idx: s.employee.as_ref().and_then(|e| name_to_idx.get(e.name.as_str()).copied()),
178
+ })
179
+ .collect();
180
+
181
+ EmployeeSchedule::new(employees, shifts)
182
+ }
183
+ }
184
+
185
+ // ============================================================================
186
+ // Router and Handlers
187
+ // ============================================================================
188
+
189
+ /// Creates the API router.
190
+ pub fn router(state: Arc<AppState>) -> Router {
191
+ Router::new()
192
+ // Health & Info
193
+ .route("/health", get(health))
194
+ .route("/info", get(info))
195
+ // Demo data
196
+ .route("/demo-data", get(list_demo_data))
197
+ .route("/demo-data/{id}", get(get_demo_data))
198
+ // Schedules
199
+ .route("/schedules", post(create_schedule))
200
+ .route("/schedules", get(list_schedules))
201
+ .route("/schedules/analyze", put(analyze_schedule))
202
+ .route("/schedules/{id}", get(get_schedule))
203
+ .route("/schedules/{id}/status", get(get_schedule_status))
204
+ .route("/schedules/{id}", delete(stop_solving))
205
+ .with_state(state)
206
+ }
207
+
208
+ // ============================================================================
209
+ // Health & Info
210
+ // ============================================================================
211
+
212
+ #[derive(Debug, Serialize)]
213
+ pub struct HealthResponse {
214
+ pub status: &'static str,
215
+ }
216
+
217
+ /// GET /health - Health check endpoint.
218
+ async fn health() -> Json<HealthResponse> {
219
+ Json(HealthResponse { status: "UP" })
220
+ }
221
+
222
+ #[derive(Debug, Serialize)]
223
+ #[serde(rename_all = "camelCase")]
224
+ pub struct InfoResponse {
225
+ pub name: &'static str,
226
+ pub version: &'static str,
227
+ pub solver_engine: &'static str,
228
+ }
229
+
230
+ /// GET /info - Application info endpoint.
231
+ async fn info() -> Json<InfoResponse> {
232
+ Json(InfoResponse {
233
+ name: "Employee Scheduling",
234
+ version: env!("CARGO_PKG_VERSION"),
235
+ solver_engine: "SolverForge-RS",
236
+ })
237
+ }
238
+
239
+ /// GET /demo-data - List available demo data sets.
240
+ async fn list_demo_data() -> Json<Vec<&'static str>> {
241
+ Json(demo_data::list_demo_data())
242
+ }
243
+
244
+ /// GET /demo-data/{id} - Get a specific demo data set.
245
+ async fn get_demo_data(Path(id): Path<String>) -> Result<Json<ScheduleDto>, StatusCode> {
246
+ match id.parse::<DemoData>() {
247
+ Ok(demo) => {
248
+ let schedule = demo_data::generate(demo);
249
+ Ok(Json(ScheduleDto::from_schedule(&schedule, None)))
250
+ }
251
+ Err(_) => Err(StatusCode::NOT_FOUND),
252
+ }
253
+ }
254
+
255
+ /// POST /schedules - Create and start solving a schedule.
256
+ /// Returns the job ID as plain text.
257
+ async fn create_schedule(
258
+ State(state): State<Arc<AppState>>,
259
+ Json(dto): Json<ScheduleDto>,
260
+ ) -> String {
261
+ let id = Uuid::new_v4().to_string();
262
+ let schedule = dto.to_domain();
263
+
264
+ // Store initial state
265
+ {
266
+ let mut jobs = state.jobs.write();
267
+ jobs.insert(id.clone(), SolveJob {
268
+ solution: schedule.clone(),
269
+ solver_status: "SOLVING".to_string(),
270
+ });
271
+ }
272
+
273
+ // Start solving in background via library API
274
+ let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
275
+ let job_id = id.clone();
276
+ let state_clone = state.clone();
277
+
278
+ tokio::spawn(async move {
279
+ while let Some((solution, _score)) = rx.recv().await {
280
+ let mut jobs = state_clone.jobs.write();
281
+ if let Some(job) = jobs.get_mut(&job_id) {
282
+ job.solution = solution;
283
+ }
284
+ }
285
+ // Channel closed - solver finished
286
+ let mut jobs = state_clone.jobs.write();
287
+ if let Some(job) = jobs.get_mut(&job_id) {
288
+ job.solver_status = "NOT_SOLVING".to_string();
289
+ }
290
+ });
291
+
292
+ // Solvable trait auto-implemented by #[planning_solution] macro
293
+ use solverforge::Solvable;
294
+ rayon::spawn(move || {
295
+ schedule.solve(None, tx);
296
+ });
297
+
298
+ id
299
+ }
300
+
301
+ /// GET /schedules - List all schedule IDs.
302
+ async fn list_schedules(State(state): State<Arc<AppState>>) -> Json<Vec<String>> {
303
+ Json(state.jobs.read().keys().cloned().collect())
304
+ }
305
+
306
+ /// GET /schedules/{id} - Get a schedule's current state.
307
+ async fn get_schedule(
308
+ State(state): State<Arc<AppState>>,
309
+ Path(id): Path<String>,
310
+ ) -> Result<Json<ScheduleDto>, StatusCode> {
311
+ match state.jobs.read().get(&id) {
312
+ Some(job) => {
313
+ Ok(Json(ScheduleDto::from_schedule(&job.solution, Some(job.solver_status.clone()))))
314
+ }
315
+ None => Err(StatusCode::NOT_FOUND),
316
+ }
317
+ }
318
+
319
+ /// Response for schedule status only.
320
+ #[derive(Debug, Serialize)]
321
+ #[serde(rename_all = "camelCase")]
322
+ pub struct StatusResponse {
323
+ pub score: Option<String>,
324
+ }
325
+
326
+ /// GET /schedules/{id}/status - Get a schedule's status.
327
+ async fn get_schedule_status(
328
+ State(state): State<Arc<AppState>>,
329
+ Path(id): Path<String>,
330
+ ) -> Result<Json<StatusResponse>, StatusCode> {
331
+ match state.jobs.read().get(&id) {
332
+ Some(job) => {
333
+ Ok(Json(StatusResponse {
334
+ score: job.solution.score.map(|s| format!("{}", s)),
335
+ }))
336
+ }
337
+ None => Err(StatusCode::NOT_FOUND),
338
+ }
339
+ }
340
+
341
+ /// DELETE /schedules/{id} - Stop solving and remove a schedule.
342
+ async fn stop_solving(
343
+ State(state): State<Arc<AppState>>,
344
+ Path(id): Path<String>,
345
+ ) -> StatusCode {
346
+ if state.jobs.write().remove(&id).is_some() {
347
+ StatusCode::NO_CONTENT
348
+ } else {
349
+ StatusCode::NOT_FOUND
350
+ }
351
+ }
352
+
353
+ /// Constraint analysis result.
354
+ #[derive(Debug, Serialize)]
355
+ #[serde(rename_all = "camelCase")]
356
+ pub struct ConstraintAnalysisDto {
357
+ pub name: String,
358
+ #[serde(rename = "type")]
359
+ pub constraint_type: String,
360
+ pub weight: String,
361
+ pub score: String,
362
+ pub matches: Vec<ConstraintMatchDto>,
363
+ }
364
+
365
+ /// A single constraint match.
366
+ #[derive(Debug, Serialize)]
367
+ #[serde(rename_all = "camelCase")]
368
+ pub struct ConstraintMatchDto {
369
+ pub score: String,
370
+ pub justification: String,
371
+ }
372
+
373
+ /// Response for constraint analysis.
374
+ #[derive(Debug, Serialize)]
375
+ #[serde(rename_all = "camelCase")]
376
+ pub struct AnalyzeResponse {
377
+ pub score: String,
378
+ pub constraints: Vec<ConstraintAnalysisDto>,
379
+ }
380
+
381
+ /// PUT /schedules/analyze - Analyze constraints for a schedule.
382
+ ///
383
+ /// Uses TypedScoreDirector for incremental scoring.
384
+ async fn analyze_schedule(
385
+ Json(dto): Json<ScheduleDto>,
386
+ ) -> Json<AnalyzeResponse> {
387
+ use crate::constraints::create_fluent_constraints;
388
+ use solverforge::{ConstraintSet, TypedScoreDirector};
389
+
390
+ let schedule = dto.to_domain();
391
+
392
+ // Use fluent API constraints for zero-erasure scoring
393
+ let constraints = create_fluent_constraints();
394
+ let mut director = TypedScoreDirector::new(schedule, constraints);
395
+
396
+ let score = director.calculate_score();
397
+
398
+ // Get per-constraint breakdown with detailed matches
399
+ let analyses = director.constraints().evaluate_detailed(director.working_solution());
400
+
401
+ let constraints_dto: Vec<ConstraintAnalysisDto> = analyses
402
+ .into_iter()
403
+ .map(|analysis| {
404
+ ConstraintAnalysisDto {
405
+ name: analysis.constraint_ref.name.clone(),
406
+ constraint_type: if analysis.is_hard { "hard" } else { "soft" }.to_string(),
407
+ weight: format!("{}", analysis.weight),
408
+ score: format!("{}", analysis.score),
409
+ matches: analysis
410
+ .matches
411
+ .iter()
412
+ .map(|m| ConstraintMatchDto {
413
+ score: format!("{}", m.score),
414
+ justification: m.justification.description.clone(),
415
+ })
416
+ .collect(),
417
+ }
418
+ })
419
+ .collect();
420
+
421
+ Json(AnalyzeResponse {
422
+ score: format!("{}", score),
423
+ constraints: constraints_dto,
424
+ })
425
+ }
src/bin/bench.rs ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Benchmark for incremental scoring performance.
2
+ //!
3
+ //! Run with: cargo run --release -p employee-scheduling --bin bench
4
+
5
+ use employee_scheduling::{constraints, demo_data};
6
+ use solverforge::TypedScoreDirector;
7
+ use std::time::Instant;
8
+
9
+ fn main() {
10
+ let schedule = demo_data::generate(demo_data::DemoData::Large);
11
+ let n_shifts = schedule.shifts.len();
12
+ let n_employees = schedule.employees.len();
13
+
14
+ println!("Benchmark: Incremental Scoring (Fluent API)");
15
+ println!(" Shifts: {}", n_shifts);
16
+ println!(" Employees: {}", n_employees);
17
+ println!();
18
+
19
+ let constraint_set = constraints::create_fluent_constraints();
20
+ let mut director = TypedScoreDirector::new(schedule, constraint_set);
21
+
22
+ // Initialize
23
+ let init_start = Instant::now();
24
+ let initial_score = director.calculate_score();
25
+ println!("Initial score: {} ({:?})", initial_score, init_start.elapsed());
26
+ println!();
27
+
28
+ // Benchmark: deterministic do/undo cycle for each shift×employee combination
29
+ // This measures pure incremental scoring throughput
30
+ let bench_start = Instant::now();
31
+ let mut moves: u64 = 0;
32
+
33
+ for shift_idx in 0..n_shifts {
34
+ let old_idx = director.working_solution().shifts[shift_idx].employee_idx;
35
+
36
+ for emp_idx in 0..n_employees {
37
+ // Do move
38
+ director.before_variable_changed(shift_idx);
39
+ director.working_solution_mut().shifts[shift_idx].employee_idx = Some(emp_idx);
40
+ director.after_variable_changed(shift_idx);
41
+ let _ = director.get_score();
42
+ moves += 1;
43
+
44
+ // Undo move
45
+ director.before_variable_changed(shift_idx);
46
+ director.working_solution_mut().shifts[shift_idx].employee_idx = old_idx;
47
+ director.after_variable_changed(shift_idx);
48
+ let _ = director.get_score();
49
+ moves += 1;
50
+ }
51
+ }
52
+
53
+ let elapsed = bench_start.elapsed();
54
+ let moves_per_sec = moves as f64 / elapsed.as_secs_f64();
55
+
56
+ println!("Results:");
57
+ println!(" Moves: {}", moves);
58
+ println!(" Time: {:.2?}", elapsed);
59
+ println!(" Moves/sec: {:.0}", moves_per_sec);
60
+
61
+ // Verify score unchanged after all do/undo cycles
62
+ let final_score = director.get_score();
63
+ assert_eq!(initial_score, final_score, "Score corrupted!");
64
+ println!(" Final score: {} (verified)", final_score);
65
+ }
src/console.rs ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Colorful console output for solver metrics.
2
+
3
+ use num_format::{Locale, ToFormattedString};
4
+ use owo_colors::OwoColorize;
5
+ use std::time::{Duration, Instant};
6
+
7
+ /// ASCII art banner for solver startup.
8
+ pub fn print_banner() {
9
+ let banner = r#"
10
+ ____ _ _____
11
+ / ___| ___ | |_ _____ _ __| ___|__ _ __ __ _ ___
12
+ \___ \ / _ \| \ \ / / _ \ '__| |_ / _ \| '__/ _` |/ _ \
13
+ ___) | (_) | |\ V / __/ | | _| (_) | | | (_| | __/
14
+ |____/ \___/|_| \_/ \___|_| |_| \___/|_| \__, |\___|
15
+ |___/
16
+ "#;
17
+ println!("{}", banner.cyan().bold());
18
+ println!(
19
+ " {} {}\n",
20
+ format!("v{}", env!("CARGO_PKG_VERSION")).bright_black(),
21
+ "Employee Scheduling".bright_cyan()
22
+ );
23
+ }
24
+
25
+ /// Prints "Solving started" message.
26
+ pub fn print_solving_started(
27
+ time_spent_ms: u64,
28
+ best_score: &str,
29
+ entity_count: usize,
30
+ variable_count: usize,
31
+ value_count: usize,
32
+ ) {
33
+ println!(
34
+ "{} {} {} time spent ({}), best score ({}), random ({})",
35
+ timestamp().bright_black(),
36
+ "INFO".bright_green(),
37
+ "[Solver]".bright_cyan(),
38
+ format!("{}ms", time_spent_ms).yellow(),
39
+ format_score(best_score),
40
+ "StdRng".white()
41
+ );
42
+
43
+ // Problem scale
44
+ let scale = calculate_problem_scale(entity_count, value_count);
45
+ println!(
46
+ "{} {} {} entity count ({}), variable count ({}), value count ({}), problem scale ({})",
47
+ timestamp().bright_black(),
48
+ "INFO".bright_green(),
49
+ "[Solver]".bright_cyan(),
50
+ entity_count.to_formatted_string(&Locale::en).bright_yellow(),
51
+ variable_count.to_formatted_string(&Locale::en).bright_yellow(),
52
+ value_count.to_formatted_string(&Locale::en).bright_yellow(),
53
+ scale.bright_magenta()
54
+ );
55
+ }
56
+
57
+ /// Prints a phase start message.
58
+ pub fn print_phase_start(phase_name: &str, phase_index: usize) {
59
+ println!(
60
+ "{} {} {} {} phase ({}) started",
61
+ timestamp().bright_black(),
62
+ "INFO".bright_green(),
63
+ format!("[{}]", phase_name).bright_cyan(),
64
+ phase_name.white().bold(),
65
+ phase_index.to_string().yellow()
66
+ );
67
+ }
68
+
69
+ /// Prints a phase end message with metrics.
70
+ pub fn print_phase_end(
71
+ phase_name: &str,
72
+ phase_index: usize,
73
+ duration: Duration,
74
+ steps_accepted: u64,
75
+ moves_evaluated: u64,
76
+ best_score: &str,
77
+ ) {
78
+ let moves_per_sec = if duration.as_secs_f64() > 0.0 {
79
+ (moves_evaluated as f64 / duration.as_secs_f64()) as u64
80
+ } else {
81
+ 0
82
+ };
83
+ let acceptance_rate = if moves_evaluated > 0 {
84
+ (steps_accepted as f64 / moves_evaluated as f64) * 100.0
85
+ } else {
86
+ 0.0
87
+ };
88
+
89
+ println!(
90
+ "{} {} {} {} phase ({}) ended: time spent ({}), best score ({}), move evaluation speed ({}/sec), step total ({}, {:.1}% accepted)",
91
+ timestamp().bright_black(),
92
+ "INFO".bright_green(),
93
+ format!("[{}]", phase_name).bright_cyan(),
94
+ phase_name.white().bold(),
95
+ phase_index.to_string().yellow(),
96
+ format_duration(duration).yellow(),
97
+ format_score(best_score),
98
+ moves_per_sec.to_formatted_string(&Locale::en).bright_magenta().bold(),
99
+ steps_accepted.to_formatted_string(&Locale::en).white(),
100
+ acceptance_rate
101
+ );
102
+ }
103
+
104
+ /// Prints a step progress update with moves/sec prominently displayed.
105
+ pub fn print_step_progress(
106
+ step: u64,
107
+ elapsed: Duration,
108
+ moves_evaluated: u64,
109
+ score: &str,
110
+ ) {
111
+ let moves_per_sec = if elapsed.as_secs_f64() > 0.0 {
112
+ (moves_evaluated as f64 / elapsed.as_secs_f64()) as u64
113
+ } else {
114
+ 0
115
+ };
116
+
117
+ println!(
118
+ " {} Step {:>7} │ {} │ {}/sec │ {}",
119
+ "→".bright_blue(),
120
+ step.to_formatted_string(&Locale::en).white(),
121
+ format!("{:>6}", format_duration(elapsed)).bright_black(),
122
+ format!("{:>8}", moves_per_sec.to_formatted_string(&Locale::en)).bright_magenta().bold(),
123
+ format_score(score)
124
+ );
125
+ }
126
+
127
+ /// Prints solver completion summary.
128
+ pub fn print_solving_ended(
129
+ total_duration: Duration,
130
+ total_moves: u64,
131
+ phase_count: usize,
132
+ final_score: &str,
133
+ is_feasible: bool,
134
+ ) {
135
+ let moves_per_sec = if total_duration.as_secs_f64() > 0.0 {
136
+ (total_moves as f64 / total_duration.as_secs_f64()) as u64
137
+ } else {
138
+ 0
139
+ };
140
+
141
+ println!(
142
+ "{} {} {} Solving ended: time spent ({}), best score ({}), move evaluation speed ({}/sec), phase total ({})",
143
+ timestamp().bright_black(),
144
+ "INFO".bright_green(),
145
+ "[Solver]".bright_cyan(),
146
+ format_duration(total_duration).yellow(),
147
+ format_score(final_score),
148
+ moves_per_sec.to_formatted_string(&Locale::en).bright_magenta().bold(),
149
+ phase_count.to_string().white()
150
+ );
151
+
152
+ // Pretty summary box (60 chars wide, 56 char content area)
153
+ println!();
154
+ println!("{}", "╔════════════════════���═════════════════════════════════════╗".bright_cyan());
155
+
156
+ let status_text = if is_feasible {
157
+ "✓ FEASIBLE SOLUTION FOUND"
158
+ } else {
159
+ "✗ INFEASIBLE (hard constraints violated)"
160
+ };
161
+ let status_colored = if is_feasible {
162
+ status_text.bright_green().bold().to_string()
163
+ } else {
164
+ status_text.bright_red().bold().to_string()
165
+ };
166
+ let status_padding = 56 - status_text.chars().count();
167
+ let left_pad = status_padding / 2;
168
+ let right_pad = status_padding - left_pad;
169
+ println!(
170
+ "{}{}{}{}{}",
171
+ "║".bright_cyan(),
172
+ " ".repeat(left_pad),
173
+ status_colored,
174
+ " ".repeat(right_pad),
175
+ "║".bright_cyan()
176
+ );
177
+
178
+ println!("{}", "╠══════════════════════════════════════════════════════════╣".bright_cyan());
179
+
180
+ let score_str = final_score;
181
+ println!(
182
+ "{} {:<18}{:>36} {}",
183
+ "║".bright_cyan(),
184
+ "Final Score:",
185
+ score_str,
186
+ "║".bright_cyan()
187
+ );
188
+
189
+ let time_str = format!("{:.2}s", total_duration.as_secs_f64());
190
+ println!(
191
+ "{} {:<18}{:>36} {}",
192
+ "║".bright_cyan(),
193
+ "Solving Time:",
194
+ time_str,
195
+ "║".bright_cyan()
196
+ );
197
+
198
+ let speed_str = format!("{}/sec", moves_per_sec.to_formatted_string(&Locale::en));
199
+ println!(
200
+ "{} {:<18}{:>36} {}",
201
+ "║".bright_cyan(),
202
+ "Move Speed:",
203
+ speed_str,
204
+ "║".bright_cyan()
205
+ );
206
+
207
+ println!("{}", "╚══════════════════════════════════════════════════════════╝".bright_cyan());
208
+ println!();
209
+ }
210
+
211
+ /// Prints constraint analysis results.
212
+ pub fn print_constraint_analysis(constraints: &[(String, String, usize)]) {
213
+ println!(
214
+ "{} {} {} Constraint Analysis:",
215
+ timestamp().bright_black(),
216
+ "INFO".bright_green(),
217
+ "[Solver]".bright_cyan()
218
+ );
219
+
220
+ for (name, score, match_count) in constraints {
221
+ let status = if score.starts_with("0") || score == "0hard/0soft" {
222
+ "✓".bright_green().to_string()
223
+ } else if score.contains("hard") && !score.starts_with("0hard") {
224
+ "✗".bright_red().to_string()
225
+ } else {
226
+ "○".yellow().to_string()
227
+ };
228
+
229
+ println!(
230
+ " {} {:<40} {:>15} ({} matches)",
231
+ status,
232
+ name.white(),
233
+ format_score(score),
234
+ match_count.to_formatted_string(&Locale::en).bright_black()
235
+ );
236
+ }
237
+ println!();
238
+ }
239
+
240
+ /// Prints the solver configuration.
241
+ pub fn print_config(shifts: usize, employees: usize) {
242
+ println!(
243
+ "{} {} {} Solver configuration: shifts ({}), employees ({})",
244
+ timestamp().bright_black(),
245
+ "INFO".bright_green(),
246
+ "[Solver]".bright_cyan(),
247
+ shifts.to_formatted_string(&Locale::en).bright_yellow(),
248
+ employees.to_formatted_string(&Locale::en).bright_yellow()
249
+ );
250
+ }
251
+
252
+ /// Formats a duration nicely.
253
+ fn format_duration(d: Duration) -> String {
254
+ let total_ms = d.as_millis();
255
+ if total_ms < 1000 {
256
+ format!("{}ms", total_ms)
257
+ } else if total_ms < 60_000 {
258
+ format!("{:.2}s", d.as_secs_f64())
259
+ } else {
260
+ let mins = total_ms / 60_000;
261
+ let secs = (total_ms % 60_000) / 1000;
262
+ format!("{}m {}s", mins, secs)
263
+ }
264
+ }
265
+
266
+ /// Formats a score with colors based on feasibility.
267
+ fn format_score(score: &str) -> String {
268
+ // Parse HardSoftScore format like "-2hard/5soft" or "0hard/10soft"
269
+ if score.contains("hard") {
270
+ let parts: Vec<&str> = score.split('/').collect();
271
+ if parts.len() == 2 {
272
+ let hard = parts[0].trim_end_matches("hard");
273
+ let soft = parts[1].trim_end_matches("soft");
274
+
275
+ let hard_num: f64 = hard.parse().unwrap_or(0.0);
276
+ let soft_num: f64 = soft.parse().unwrap_or(0.0);
277
+
278
+ let hard_str = if hard_num < 0.0 {
279
+ format!("{}hard", hard).bright_red().to_string()
280
+ } else {
281
+ format!("{}hard", hard).bright_green().to_string()
282
+ };
283
+
284
+ let soft_str = if soft_num < 0.0 {
285
+ format!("{}soft", soft).yellow().to_string()
286
+ } else if soft_num > 0.0 {
287
+ format!("{}soft", soft).bright_green().to_string()
288
+ } else {
289
+ format!("{}soft", soft).white().to_string()
290
+ };
291
+
292
+ return format!("{}/{}", hard_str, soft_str);
293
+ }
294
+ }
295
+
296
+ // Simple score
297
+ if let Ok(n) = score.parse::<i32>() {
298
+ if n < 0 {
299
+ return score.bright_red().to_string();
300
+ } else if n > 0 {
301
+ return score.bright_green().to_string();
302
+ }
303
+ }
304
+
305
+ score.white().to_string()
306
+ }
307
+
308
+ /// Returns a timestamp string.
309
+ fn timestamp() -> String {
310
+ std::time::SystemTime::now()
311
+ .duration_since(std::time::UNIX_EPOCH)
312
+ .map(|d| {
313
+ let secs = d.as_secs();
314
+ let millis = d.subsec_millis();
315
+ format!("{}.{:03}", secs, millis)
316
+ })
317
+ .unwrap_or_else(|_| "0.000".to_string())
318
+ }
319
+
320
+ /// Calculates an approximate problem scale.
321
+ fn calculate_problem_scale(entity_count: usize, value_count: usize) -> String {
322
+ if entity_count == 0 || value_count == 0 {
323
+ return "0".to_string();
324
+ }
325
+
326
+ // value_count ^ entity_count
327
+ let log_scale = (entity_count as f64) * (value_count as f64).log10();
328
+ let exponent = log_scale.floor() as i32;
329
+ let mantissa = 10f64.powf(log_scale - exponent as f64);
330
+
331
+ format!("{:.3} × 10^{}", mantissa, exponent)
332
+ }
333
+
334
+ /// A timer for tracking phase/step durations.
335
+ pub struct PhaseTimer {
336
+ start: Instant,
337
+ phase_name: String,
338
+ phase_index: usize,
339
+ steps_accepted: u64,
340
+ moves_evaluated: u64,
341
+ last_score: String,
342
+ }
343
+
344
+ impl PhaseTimer {
345
+ pub fn start(phase_name: impl Into<String>, phase_index: usize) -> Self {
346
+ let name = phase_name.into();
347
+ print_phase_start(&name, phase_index);
348
+ Self {
349
+ start: Instant::now(),
350
+ phase_name: name,
351
+ phase_index,
352
+ steps_accepted: 0,
353
+ moves_evaluated: 0,
354
+ last_score: String::new(),
355
+ }
356
+ }
357
+
358
+ pub fn record_accepted(&mut self, score: &str) {
359
+ self.steps_accepted += 1;
360
+ self.last_score = score.to_string();
361
+ }
362
+
363
+ pub fn record_move(&mut self) {
364
+ self.moves_evaluated += 1;
365
+ }
366
+
367
+ pub fn elapsed(&self) -> Duration {
368
+ self.start.elapsed()
369
+ }
370
+
371
+ pub fn moves_evaluated(&self) -> u64 {
372
+ self.moves_evaluated
373
+ }
374
+
375
+ pub fn finish(self) {
376
+ print_phase_end(
377
+ &self.phase_name,
378
+ self.phase_index,
379
+ self.start.elapsed(),
380
+ self.steps_accepted,
381
+ self.moves_evaluated,
382
+ &self.last_score,
383
+ );
384
+ }
385
+
386
+ pub fn steps_accepted(&self) -> u64 {
387
+ self.steps_accepted
388
+ }
389
+ }
src/constraints.rs ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Zero-erasure constraints for Employee Scheduling using fluent API.
2
+ //!
3
+ //! All constraints use the fluent constraint stream API with concrete generic
4
+ //! types - no Arc, no dyn, fully monomorphized.
5
+
6
+ use chrono::NaiveDate;
7
+ use solverforge::prelude::*;
8
+ use solverforge::stream::joiner::equal_bi;
9
+
10
+ use crate::domain::{Employee, EmployeeSchedule, Shift};
11
+
12
+ /// Creates all constraints using the fluent API (fully monomorphized).
13
+ pub fn create_fluent_constraints() -> impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore> {
14
+ let factory = ConstraintFactory::<EmployeeSchedule, HardSoftDecimalScore>::new();
15
+
16
+ // =========================================================================
17
+ // HARD: Required Skill
18
+ // =========================================================================
19
+ let required_skill = factory
20
+ .clone()
21
+ .for_each(|s: &EmployeeSchedule| s.shifts.as_slice())
22
+ .join(
23
+ |s: &EmployeeSchedule| s.employees.as_slice(),
24
+ equal_bi(
25
+ |shift: &Shift| shift.employee_idx,
26
+ |emp: &Employee| Some(emp.index),
27
+ ),
28
+ )
29
+ .filter(|shift: &Shift, emp: &Employee| {
30
+ shift.employee_idx.is_some() && !emp.skills.contains(&shift.required_skill)
31
+ })
32
+ .penalize(HardSoftDecimalScore::ONE_HARD)
33
+ .as_constraint("Required skill");
34
+
35
+ // =========================================================================
36
+ // HARD: No Overlapping Shifts
37
+ // =========================================================================
38
+ // Note: overlapping joiner can't be composed with equality joiner for self-joins
39
+ // because for_each_unique_pair requires EqualJoiner for hash indexing.
40
+ // The filter approach is correct for self-join overlap detection.
41
+ let no_overlap = factory
42
+ .clone()
43
+ .for_each_unique_pair(
44
+ |s: &EmployeeSchedule| s.shifts.as_slice(),
45
+ joiner::equal(|shift: &Shift| shift.employee_idx),
46
+ )
47
+ .filter(|a: &Shift, b: &Shift| {
48
+ a.employee_idx.is_some() && a.start < b.end && b.start < a.end
49
+ })
50
+ .penalize_hard_with(|a: &Shift, b: &Shift| {
51
+ HardSoftDecimalScore::of_hard_scaled(overlap_minutes(a, b) * 100000)
52
+ })
53
+ .as_constraint("Overlapping shift");
54
+
55
+ // =========================================================================
56
+ // HARD: At Least 10 Hours Between Shifts
57
+ // =========================================================================
58
+ let at_least_10_hours = factory
59
+ .clone()
60
+ .for_each_unique_pair(
61
+ |s: &EmployeeSchedule| s.shifts.as_slice(),
62
+ joiner::equal(|shift: &Shift| shift.employee_idx),
63
+ )
64
+ .filter(|a: &Shift, b: &Shift| a.employee_idx.is_some() && gap_penalty_minutes(a, b) > 0)
65
+ .penalize_hard_with(|a: &Shift, b: &Shift| {
66
+ HardSoftDecimalScore::of_hard_scaled(gap_penalty_minutes(a, b) * 100000)
67
+ })
68
+ .as_constraint("At least 10 hours between 2 shifts");
69
+
70
+ // =========================================================================
71
+ // HARD: One Shift Per Day
72
+ // =========================================================================
73
+ let one_per_day = factory
74
+ .clone()
75
+ .for_each_unique_pair(
76
+ |s: &EmployeeSchedule| s.shifts.as_slice(),
77
+ joiner::equal(|shift: &Shift| (shift.employee_idx, shift.date())),
78
+ )
79
+ .filter(|a: &Shift, b: &Shift| a.employee_idx.is_some() && b.employee_idx.is_some())
80
+ .penalize(HardSoftDecimalScore::ONE_HARD)
81
+ .as_constraint("One shift per day");
82
+
83
+ // =========================================================================
84
+ // HARD: Unavailable Employee
85
+ // =========================================================================
86
+ // Uses flatten_last for O(1) lookup by date.
87
+ // Pre-indexes unavailable dates, looks up by shift.date() in O(1).
88
+ let unavailable = factory
89
+ .clone()
90
+ .for_each(|s: &EmployeeSchedule| s.shifts.as_slice())
91
+ .join(
92
+ |s: &EmployeeSchedule| s.employees.as_slice(),
93
+ equal_bi(
94
+ |shift: &Shift| shift.employee_idx,
95
+ |emp: &Employee| Some(emp.index),
96
+ ),
97
+ )
98
+ .flatten_last(
99
+ |emp: &Employee| emp.unavailable_days.as_slice(),
100
+ |date: &NaiveDate| *date, // C → index key
101
+ |shift: &Shift| shift.date(), // A → lookup key
102
+ )
103
+ .filter(|shift: &Shift, date: &NaiveDate| {
104
+ shift.employee_idx.is_some() && shift_date_overlap_minutes(shift, *date) > 0
105
+ })
106
+ .penalize_hard_with(|shift: &Shift, date: &NaiveDate| {
107
+ HardSoftDecimalScore::of_hard_scaled(shift_date_overlap_minutes(shift, *date) * 100000)
108
+ })
109
+ .as_constraint("Unavailable employee");
110
+
111
+ // =========================================================================
112
+ // SOFT: Undesired Day
113
+ // =========================================================================
114
+ // Uses flatten_last for O(1) lookup. Penalizes 1 per match (Timefold pattern).
115
+ let undesired = factory
116
+ .clone()
117
+ .for_each(|s: &EmployeeSchedule| s.shifts.as_slice())
118
+ .join(
119
+ |s: &EmployeeSchedule| s.employees.as_slice(),
120
+ equal_bi(
121
+ |shift: &Shift| shift.employee_idx,
122
+ |emp: &Employee| Some(emp.index),
123
+ ),
124
+ )
125
+ .flatten_last(
126
+ |emp: &Employee| emp.undesired_days.as_slice(),
127
+ |date: &NaiveDate| *date,
128
+ |shift: &Shift| shift.date(),
129
+ )
130
+ .filter(|shift: &Shift, _date: &NaiveDate| shift.employee_idx.is_some())
131
+ .penalize(HardSoftDecimalScore::ONE_SOFT)
132
+ .as_constraint("Undesired day for employee");
133
+
134
+ // =========================================================================
135
+ // SOFT: Desired Day
136
+ // =========================================================================
137
+ // Uses flatten_last for O(1) lookup. Rewards 1 per match (Timefold pattern).
138
+ let desired = factory
139
+ .clone()
140
+ .for_each(|s: &EmployeeSchedule| s.shifts.as_slice())
141
+ .join(
142
+ |s: &EmployeeSchedule| s.employees.as_slice(),
143
+ equal_bi(
144
+ |shift: &Shift| shift.employee_idx,
145
+ |emp: &Employee| Some(emp.index),
146
+ ),
147
+ )
148
+ .flatten_last(
149
+ |emp: &Employee| emp.desired_days.as_slice(),
150
+ |date: &NaiveDate| *date,
151
+ |shift: &Shift| shift.date(),
152
+ )
153
+ .filter(|shift: &Shift, _date: &NaiveDate| shift.employee_idx.is_some())
154
+ .reward(HardSoftDecimalScore::ONE_SOFT)
155
+ .as_constraint("Desired day for employee");
156
+
157
+ // =========================================================================
158
+ // SOFT: Balance Assignments
159
+ // =========================================================================
160
+ // Uses simple balance() for O(1) incremental std-dev calculation.
161
+ let balanced = factory
162
+ .for_each(|s: &EmployeeSchedule| s.shifts.as_slice())
163
+ .balance(|shift: &Shift| shift.employee_idx)
164
+ .penalize(HardSoftDecimalScore::of_soft(1))
165
+ .as_constraint("Balance employee assignments");
166
+
167
+ (
168
+ required_skill,
169
+ no_overlap,
170
+ at_least_10_hours,
171
+ one_per_day,
172
+ unavailable,
173
+ undesired,
174
+ desired,
175
+ balanced,
176
+ )
177
+ }
178
+
179
+ // ============================================================================
180
+ // Helper functions
181
+ // ============================================================================
182
+
183
+ #[inline]
184
+ fn overlap_minutes(a: &Shift, b: &Shift) -> i64 {
185
+ let start = a.start.max(b.start);
186
+ let end = a.end.min(b.end);
187
+ if start < end {
188
+ (end - start).num_minutes()
189
+ } else {
190
+ 0
191
+ }
192
+ }
193
+
194
+ #[inline]
195
+ fn gap_penalty_minutes(a: &Shift, b: &Shift) -> i64 {
196
+ const MIN_GAP_MINUTES: i64 = 600;
197
+
198
+ let (earlier, later) = if a.end <= b.start {
199
+ (a, b)
200
+ } else if b.end <= a.start {
201
+ (b, a)
202
+ } else {
203
+ return 0;
204
+ };
205
+
206
+ let gap = (later.start - earlier.end).num_minutes();
207
+ if (0..MIN_GAP_MINUTES).contains(&gap) {
208
+ MIN_GAP_MINUTES - gap
209
+ } else {
210
+ 0
211
+ }
212
+ }
213
+
214
+ #[inline]
215
+ fn shift_date_overlap_minutes(shift: &Shift, date: NaiveDate) -> i64 {
216
+ let day_start = date.and_hms_opt(0, 0, 0).unwrap();
217
+ let day_end = date.succ_opt().unwrap_or(date).and_hms_opt(0, 0, 0).unwrap();
218
+
219
+ let start = shift.start.max(day_start);
220
+ let end = shift.end.min(day_end);
221
+
222
+ if start < end {
223
+ (end - start).num_minutes()
224
+ } else {
225
+ 0
226
+ }
227
+ }
228
+
src/demo_data.rs ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Demo data generators for Employee Scheduling.
2
+
3
+ use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Weekday};
4
+ use rand::prelude::*;
5
+ use rand::rngs::StdRng;
6
+ use rand::SeedableRng;
7
+
8
+ use crate::domain::{Employee, EmployeeSchedule, Shift};
9
+
10
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
11
+ pub enum DemoData {
12
+ Small,
13
+ Large,
14
+ }
15
+
16
+ impl std::str::FromStr for DemoData {
17
+ type Err = ();
18
+
19
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
20
+ match s.to_uppercase().as_str() {
21
+ "SMALL" => Ok(DemoData::Small),
22
+ "LARGE" => Ok(DemoData::Large),
23
+ _ => Err(()),
24
+ }
25
+ }
26
+ }
27
+
28
+ impl DemoData {
29
+ pub fn as_str(&self) -> &'static str {
30
+ match self {
31
+ DemoData::Small => "SMALL",
32
+ DemoData::Large => "LARGE",
33
+ }
34
+ }
35
+
36
+ fn parameters(&self) -> DemoDataParameters {
37
+ match self {
38
+ DemoData::Small => DemoDataParameters {
39
+ locations: vec![
40
+ "Ambulatory care".to_string(),
41
+ "Critical care".to_string(),
42
+ "Pediatric care".to_string(),
43
+ ],
44
+ required_skills: vec!["Doctor".to_string(), "Nurse".to_string()],
45
+ optional_skills: vec!["Anaesthetics".to_string(), "Cardiology".to_string()],
46
+ days_in_schedule: 14,
47
+ employee_count: 15,
48
+ optional_skill_distribution: vec![(1, 3.0), (2, 1.0)],
49
+ shift_count_distribution: vec![(1, 0.9), (2, 0.1)],
50
+ availability_count_distribution: vec![(1, 4.0), (2, 3.0), (3, 2.0), (4, 1.0)],
51
+ },
52
+ DemoData::Large => DemoDataParameters {
53
+ locations: vec![
54
+ "Ambulatory care".to_string(),
55
+ "Neurology".to_string(),
56
+ "Critical care".to_string(),
57
+ "Pediatric care".to_string(),
58
+ "Surgery".to_string(),
59
+ "Radiology".to_string(),
60
+ "Outpatient".to_string(),
61
+ ],
62
+ required_skills: vec!["Doctor".to_string(), "Nurse".to_string()],
63
+ optional_skills: vec![
64
+ "Anaesthetics".to_string(),
65
+ "Cardiology".to_string(),
66
+ "Radiology".to_string(),
67
+ ],
68
+ days_in_schedule: 28,
69
+ employee_count: 50,
70
+ optional_skill_distribution: vec![(1, 3.0), (2, 1.0)],
71
+ shift_count_distribution: vec![(1, 0.5), (2, 0.3), (3, 0.2)],
72
+ availability_count_distribution: vec![(5, 4.0), (10, 3.0), (15, 2.0), (20, 1.0)],
73
+ },
74
+ }
75
+ }
76
+ }
77
+
78
+ struct DemoDataParameters {
79
+ locations: Vec<String>,
80
+ required_skills: Vec<String>,
81
+ optional_skills: Vec<String>,
82
+ days_in_schedule: i64,
83
+ employee_count: usize,
84
+ optional_skill_distribution: Vec<(usize, f64)>,
85
+ shift_count_distribution: Vec<(usize, f64)>,
86
+ availability_count_distribution: Vec<(usize, f64)>,
87
+ }
88
+
89
+ /// List of available demo data sets.
90
+ pub fn list_demo_data() -> Vec<&'static str> {
91
+ vec!["SMALL", "LARGE"]
92
+ }
93
+
94
+ /// Generates a demo schedule for the given size.
95
+ pub fn generate(demo: DemoData) -> EmployeeSchedule {
96
+ let params = demo.parameters();
97
+ let mut rng = StdRng::seed_from_u64(0);
98
+
99
+ // First Monday from a reference date
100
+ let start_date = find_next_monday(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
101
+
102
+ // Build location -> shift start times map (cycling through templates)
103
+ let shift_start_times_combos: Vec<Vec<NaiveTime>> = vec![
104
+ vec![time(6, 0), time(14, 0)],
105
+ vec![time(6, 0), time(14, 0), time(22, 0)],
106
+ vec![time(6, 0), time(9, 0), time(14, 0), time(22, 0)],
107
+ ];
108
+
109
+ let location_to_shift_times: Vec<(&String, &Vec<NaiveTime>)> = params
110
+ .locations
111
+ .iter()
112
+ .enumerate()
113
+ .map(|(i, loc)| {
114
+ (
115
+ loc,
116
+ &shift_start_times_combos[i % shift_start_times_combos.len()],
117
+ )
118
+ })
119
+ .collect();
120
+
121
+ // Generate employee names (FIRST × LAST)
122
+ let name_permutations = generate_name_permutations(&mut rng);
123
+
124
+ // Generate employees
125
+ let mut employees = Vec::new();
126
+ for i in 0..params.employee_count {
127
+ let name = name_permutations[i % name_permutations.len()].clone();
128
+
129
+ // Pick optional skills based on distribution
130
+ let optional_count = pick_count(&mut rng, &params.optional_skill_distribution);
131
+ let mut skills: Vec<String> = params
132
+ .optional_skills
133
+ .choose_multiple(&mut rng, optional_count.min(params.optional_skills.len()))
134
+ .cloned()
135
+ .collect();
136
+
137
+ // Add one required skill
138
+ if let Some(required) = params.required_skills.choose(&mut rng) {
139
+ skills.push(required.clone());
140
+ }
141
+
142
+ employees.push(Employee::new(i, &name).with_skills(skills));
143
+ }
144
+
145
+ // Generate shifts and assign availabilities
146
+ let mut shifts = Vec::new();
147
+ let mut shift_id = 0usize;
148
+
149
+ for day in 0..params.days_in_schedule {
150
+ let date = start_date + Duration::days(day);
151
+
152
+ // Pick employees to have availability entries on this day
153
+ let availability_count = pick_count(&mut rng, &params.availability_count_distribution);
154
+ let employees_with_availability: Vec<usize> = (0..params.employee_count)
155
+ .collect::<Vec<_>>()
156
+ .choose_multiple(&mut rng, availability_count.min(params.employee_count))
157
+ .copied()
158
+ .collect();
159
+
160
+ for emp_idx in employees_with_availability {
161
+ match rng.gen_range(0..3) {
162
+ 0 => {
163
+ employees[emp_idx].unavailable_dates.insert(date);
164
+ }
165
+ 1 => {
166
+ employees[emp_idx].undesired_dates.insert(date);
167
+ }
168
+ 2 => {
169
+ employees[emp_idx].desired_dates.insert(date);
170
+ }
171
+ _ => {}
172
+ }
173
+ }
174
+
175
+ // Generate shifts for each location/timeslot
176
+ for (location, shift_times) in &location_to_shift_times {
177
+ for &shift_start in *shift_times {
178
+ let start = NaiveDateTime::new(date, shift_start);
179
+ let end = start + Duration::hours(8);
180
+
181
+ // How many shifts at this timeslot?
182
+ let shift_count = pick_count(&mut rng, &params.shift_count_distribution);
183
+
184
+ for _ in 0..shift_count {
185
+ // Pick required skill (50% required, 50% optional)
186
+ let required_skill = if rng.gen_bool(0.5) {
187
+ params.required_skills.choose(&mut rng)
188
+ } else {
189
+ params.optional_skills.choose(&mut rng)
190
+ }
191
+ .cloned()
192
+ .unwrap_or_else(|| "Doctor".to_string());
193
+
194
+ shifts.push(Shift::new(
195
+ shift_id.to_string(),
196
+ start,
197
+ end,
198
+ (*location).clone(),
199
+ required_skill,
200
+ ));
201
+ shift_id += 1;
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ // Finalize employees to populate derived Vec fields
208
+ for emp in &mut employees {
209
+ emp.finalize();
210
+ }
211
+
212
+ EmployeeSchedule::new(employees, shifts)
213
+ }
214
+
215
+ fn time(hour: u32, minute: u32) -> NaiveTime {
216
+ NaiveTime::from_hms_opt(hour, minute, 0).unwrap()
217
+ }
218
+
219
+ fn find_next_monday(date: NaiveDate) -> NaiveDate {
220
+ let days_until_monday = match date.weekday() {
221
+ Weekday::Mon => 0,
222
+ Weekday::Tue => 6,
223
+ Weekday::Wed => 5,
224
+ Weekday::Thu => 4,
225
+ Weekday::Fri => 3,
226
+ Weekday::Sat => 2,
227
+ Weekday::Sun => 1,
228
+ };
229
+ date + Duration::days(days_until_monday)
230
+ }
231
+
232
+ /// Pick a count based on weighted distribution.
233
+ fn pick_count(rng: &mut StdRng, distribution: &[(usize, f64)]) -> usize {
234
+ let total_weight: f64 = distribution.iter().map(|(_, w)| w).sum();
235
+ let mut choice = rng.gen::<f64>() * total_weight;
236
+
237
+ for (count, weight) in distribution {
238
+ if choice < *weight {
239
+ return *count;
240
+ }
241
+ choice -= weight;
242
+ }
243
+ distribution.last().map(|(c, _)| *c).unwrap_or(1)
244
+ }
245
+
246
+ const FIRST_NAMES: &[&str] = &[
247
+ "Amy", "Beth", "Carl", "Dan", "Elsa", "Flo", "Gus", "Hugo", "Ivy", "Jay",
248
+ ];
249
+ const LAST_NAMES: &[&str] = &[
250
+ "Cole", "Fox", "Green", "Jones", "King", "Li", "Poe", "Rye", "Smith", "Watt",
251
+ ];
252
+
253
+ fn generate_name_permutations(rng: &mut StdRng) -> Vec<String> {
254
+ let mut names = Vec::with_capacity(FIRST_NAMES.len() * LAST_NAMES.len());
255
+ for first in FIRST_NAMES {
256
+ for last in LAST_NAMES {
257
+ names.push(format!("{} {}", first, last));
258
+ }
259
+ }
260
+ names.shuffle(rng);
261
+ names
262
+ }
263
+
264
+ #[cfg(test)]
265
+ mod tests {
266
+ use super::*;
267
+
268
+ #[test]
269
+ fn test_generate_small() {
270
+ let schedule = generate(DemoData::Small);
271
+
272
+ assert_eq!(schedule.employees.len(), 15);
273
+ // 14 days × 3 locations × varying timeslots × varying shifts per timeslot
274
+ // Should be roughly 14 * 3 * avg(2,3,4) * avg(1,2) ≈ 14 * 3 * 3 * 1.1 ≈ 139
275
+ assert!(
276
+ schedule.shifts.len() >= 100,
277
+ "Expected >= 100 shifts, got {}",
278
+ schedule.shifts.len()
279
+ );
280
+
281
+ // All shifts should be unassigned initially
282
+ assert!(schedule.shifts.iter().all(|s| s.employee_idx.is_none()));
283
+ }
284
+
285
+ #[test]
286
+ fn test_generate_large() {
287
+ let schedule = generate(DemoData::Large);
288
+
289
+ assert_eq!(schedule.employees.len(), 50);
290
+ // 28 days × 7 locations × varying timeslots × varying shifts per timeslot
291
+ assert!(
292
+ schedule.shifts.len() >= 500,
293
+ "Expected >= 500 shifts, got {}",
294
+ schedule.shifts.len()
295
+ );
296
+ }
297
+
298
+ #[test]
299
+ fn test_employees_have_skills() {
300
+ let schedule = generate(DemoData::Small);
301
+
302
+ for employee in &schedule.employees {
303
+ assert!(
304
+ !employee.skills.is_empty(),
305
+ "Employee {} has no skills",
306
+ employee.name
307
+ );
308
+ }
309
+ }
310
+
311
+ #[test]
312
+ fn test_demo_data_from_str() {
313
+ assert_eq!("SMALL".parse::<DemoData>(), Ok(DemoData::Small));
314
+ assert_eq!("small".parse::<DemoData>(), Ok(DemoData::Small));
315
+ assert_eq!("LARGE".parse::<DemoData>(), Ok(DemoData::Large));
316
+ assert!("invalid".parse::<DemoData>().is_err());
317
+ }
318
+
319
+ #[test]
320
+ fn test_medical_domain() {
321
+ let schedule = generate(DemoData::Small);
322
+
323
+ // Check for medical skills
324
+ let all_skills: std::collections::HashSet<_> = schedule
325
+ .employees
326
+ .iter()
327
+ .flat_map(|e| e.skills.iter())
328
+ .collect();
329
+
330
+ assert!(
331
+ all_skills.iter().any(|s| *s == "Doctor" || *s == "Nurse"),
332
+ "Should have Doctor or Nurse skills"
333
+ );
334
+
335
+ // Check for medical locations
336
+ let locations: std::collections::HashSet<_> = schedule
337
+ .shifts
338
+ .iter()
339
+ .map(|s| s.location.as_str())
340
+ .collect();
341
+
342
+ assert!(
343
+ locations.contains("Ambulatory care") || locations.contains("Critical care"),
344
+ "Should have medical locations"
345
+ );
346
+ }
347
+
348
+ #[test]
349
+ fn test_empty_schedule_has_score() {
350
+ use crate::domain::EmployeeSchedule;
351
+ use solverforge::Solvable;
352
+ use tokio::sync::mpsc::unbounded_channel;
353
+
354
+ // Empty schedule with no shifts and no employees
355
+ let schedule = EmployeeSchedule::new(vec![], vec![]);
356
+ let (sender, mut receiver) = unbounded_channel();
357
+ schedule.solve(None, sender);
358
+
359
+ // Try to receive solution - with 0 entities, solver may close channel without sending
360
+ if let Some((result, _score)) = receiver.blocking_recv() {
361
+ assert!(
362
+ result.score.is_some(),
363
+ "Empty schedule should have a score after solving, got None"
364
+ );
365
+ } else {
366
+ // If no solution was sent (channel closed), that's acceptable for 0 entities
367
+ // The solver may optimize this case by not running at all
368
+ }
369
+ }
370
+ }
src/domain.rs ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Domain model for Employee Scheduling Problem.
2
+
3
+ use chrono::{NaiveDate, NaiveDateTime};
4
+ use serde::{Deserialize, Serialize};
5
+ use solverforge::prelude::*;
6
+ use std::collections::HashSet;
7
+
8
+ /// An employee who can be assigned to shifts.
9
+ #[problem_fact]
10
+ #[derive(Serialize, Deserialize)]
11
+ pub struct Employee {
12
+ /// Index of this employee in `EmployeeSchedule.employees` for O(1) join matching.
13
+ pub index: usize,
14
+ pub name: String,
15
+ pub skills: HashSet<String>,
16
+ #[serde(rename = "unavailableDates", default)]
17
+ pub unavailable_dates: HashSet<NaiveDate>,
18
+ #[serde(rename = "undesiredDates", default)]
19
+ pub undesired_dates: HashSet<NaiveDate>,
20
+ #[serde(rename = "desiredDates", default)]
21
+ pub desired_dates: HashSet<NaiveDate>,
22
+ /// Sorted unavailable dates for `flatten_last` compatibility.
23
+ /// Populated by `finalize()` from `unavailable_dates` HashSet.
24
+ #[serde(skip)]
25
+ pub unavailable_days: Vec<NaiveDate>,
26
+ /// Sorted undesired dates for `flatten_last` compatibility.
27
+ #[serde(skip)]
28
+ pub undesired_days: Vec<NaiveDate>,
29
+ /// Sorted desired dates for `flatten_last` compatibility.
30
+ #[serde(skip)]
31
+ pub desired_days: Vec<NaiveDate>,
32
+ }
33
+
34
+ impl Employee {
35
+ pub fn new(index: usize, name: impl Into<String>) -> Self {
36
+ Self {
37
+ index,
38
+ name: name.into(),
39
+ skills: HashSet::new(),
40
+ unavailable_dates: HashSet::new(),
41
+ undesired_dates: HashSet::new(),
42
+ desired_dates: HashSet::new(),
43
+ unavailable_days: Vec::new(),
44
+ undesired_days: Vec::new(),
45
+ desired_days: Vec::new(),
46
+ }
47
+ }
48
+
49
+ /// Populates derived Vec fields from HashSets for zero-erasure stream compatibility.
50
+ /// Must be called after all dates have been added to HashSets.
51
+ pub fn finalize(&mut self) {
52
+ self.unavailable_days = self.unavailable_dates.iter().copied().collect();
53
+ self.unavailable_days.sort();
54
+ self.undesired_days = self.undesired_dates.iter().copied().collect();
55
+ self.undesired_days.sort();
56
+ self.desired_days = self.desired_dates.iter().copied().collect();
57
+ self.desired_days.sort();
58
+ }
59
+
60
+ pub fn with_skill(mut self, skill: impl Into<String>) -> Self {
61
+ self.skills.insert(skill.into());
62
+ self
63
+ }
64
+
65
+ pub fn with_skills(mut self, skills: impl IntoIterator<Item = impl Into<String>>) -> Self {
66
+ for skill in skills {
67
+ self.skills.insert(skill.into());
68
+ }
69
+ self
70
+ }
71
+
72
+ pub fn with_unavailable_date(mut self, date: NaiveDate) -> Self {
73
+ self.unavailable_dates.insert(date);
74
+ self
75
+ }
76
+
77
+ pub fn with_undesired_date(mut self, date: NaiveDate) -> Self {
78
+ self.undesired_dates.insert(date);
79
+ self
80
+ }
81
+
82
+ pub fn with_desired_date(mut self, date: NaiveDate) -> Self {
83
+ self.desired_dates.insert(date);
84
+ self
85
+ }
86
+ }
87
+
88
+ /// A shift that needs to be staffed by an employee.
89
+ #[planning_entity]
90
+ #[derive(Serialize, Deserialize)]
91
+ pub struct Shift {
92
+ #[planning_id]
93
+ pub id: String,
94
+ pub start: NaiveDateTime,
95
+ pub end: NaiveDateTime,
96
+ pub location: String,
97
+ #[serde(rename = "requiredSkill")]
98
+ pub required_skill: String,
99
+ /// Index into `EmployeeSchedule.employees` (O(1) lookup, no String cloning).
100
+ #[planning_variable(allows_unassigned = true)]
101
+ pub employee_idx: Option<usize>,
102
+ }
103
+
104
+ impl Shift {
105
+ pub fn new(
106
+ id: impl Into<String>,
107
+ start: NaiveDateTime,
108
+ end: NaiveDateTime,
109
+ location: impl Into<String>,
110
+ required_skill: impl Into<String>,
111
+ ) -> Self {
112
+ Self {
113
+ id: id.into(),
114
+ start,
115
+ end,
116
+ location: location.into(),
117
+ required_skill: required_skill.into(),
118
+ employee_idx: None,
119
+ }
120
+ }
121
+
122
+ /// Returns the date of the shift start.
123
+ pub fn date(&self) -> NaiveDate {
124
+ self.start.date()
125
+ }
126
+
127
+ /// Returns the duration in hours.
128
+ pub fn duration_hours(&self) -> f64 {
129
+ (self.end - self.start).num_minutes() as f64 / 60.0
130
+ }
131
+ }
132
+
133
+ /// The employee scheduling solution.
134
+ #[planning_solution]
135
+ #[basic_variable_config(
136
+ entity_collection = "shifts",
137
+ variable_field = "employee_idx",
138
+ variable_type = "usize",
139
+ value_range = "employees"
140
+ )]
141
+ #[solverforge_constraints_path = "crate::constraints::create_fluent_constraints"]
142
+ #[derive(Serialize, Deserialize)]
143
+ pub struct EmployeeSchedule {
144
+ #[problem_fact_collection]
145
+ pub employees: Vec<Employee>,
146
+ #[planning_entity_collection]
147
+ pub shifts: Vec<Shift>,
148
+ #[planning_score]
149
+ pub score: Option<HardSoftDecimalScore>,
150
+ #[serde(rename = "solverStatus", skip_serializing_if = "Option::is_none")]
151
+ pub solver_status: Option<String>,
152
+ }
153
+
154
+ impl EmployeeSchedule {
155
+ pub fn new(employees: Vec<Employee>, shifts: Vec<Shift>) -> Self {
156
+ Self {
157
+ employees,
158
+ shifts,
159
+ score: None,
160
+ solver_status: None,
161
+ }
162
+ }
163
+
164
+ /// Gets an Employee by index (O(1)).
165
+ #[inline]
166
+ pub fn get_employee(&self, idx: usize) -> Option<&Employee> {
167
+ self.employees.get(idx)
168
+ }
169
+
170
+ /// Returns the number of employees.
171
+ #[inline]
172
+ pub fn employee_count(&self) -> usize {
173
+ self.employees.len()
174
+ }
175
+ }
src/lib.rs ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Employee Scheduling Quickstart for SolverForge
2
+ //!
3
+ //! This library provides the domain model, typed constraints, and solver for
4
+ //! employee scheduling optimization.
5
+ //!
6
+ //! Uses zero-erasure typed constraints via `TypedScoreDirector`.
7
+
8
+ pub mod api;
9
+ pub mod constraints;
10
+ pub mod demo_data;
11
+ pub mod domain;
src/main.rs ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Employee Scheduling Quickstart - Axum Server
2
+ //!
3
+ //! Run with: cargo run -p employee-scheduling
4
+ //! Then open: http://localhost:7860
5
+
6
+ use std::net::SocketAddr;
7
+ use std::path::PathBuf;
8
+ use std::sync::Arc;
9
+ use tower_http::cors::{Any, CorsLayer};
10
+ use tower_http::services::ServeDir;
11
+
12
+ use employee_scheduling::api;
13
+
14
+ #[tokio::main]
15
+ async fn main() {
16
+ solverforge::console::init();
17
+
18
+ let state = Arc::new(api::AppState::new());
19
+
20
+ let cors = CorsLayer::new()
21
+ .allow_origin(Any)
22
+ .allow_methods(Any)
23
+ .allow_headers(Any);
24
+
25
+ let static_path = if PathBuf::from("examples/employee-scheduling/static").exists() {
26
+ "examples/employee-scheduling/static"
27
+ } else {
28
+ "static"
29
+ };
30
+
31
+ let app = api::router(state)
32
+ .fallback_service(ServeDir::new(static_path))
33
+ .layer(cors);
34
+
35
+ let addr = SocketAddr::from(([0, 0, 0, 0], 7860));
36
+
37
+ let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
38
+ println!("Server running at http://localhost:{}", addr.port());
39
+ axum::serve(listener, app).await.unwrap();
40
+ }
src/solver.rs ADDED
@@ -0,0 +1,562 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Solver service for Employee Scheduling.
2
+ //!
3
+ //! Uses Late Acceptance local search with change moves.
4
+ //! Incremental scoring via TypedScoreDirector for O(1) move evaluation.
5
+
6
+ use parking_lot::RwLock;
7
+ use rand::Rng;
8
+ use solverforge::prelude::*;
9
+ use solverforge::TypedScoreDirector;
10
+ use std::collections::HashMap;
11
+ use std::sync::Arc;
12
+ use std::time::{Duration, Instant};
13
+ use tokio::sync::oneshot;
14
+ use tracing::{debug, info};
15
+
16
+ use crate::console::{self, PhaseTimer};
17
+ use crate::constraints::create_fluent_constraints;
18
+ use crate::domain::EmployeeSchedule;
19
+
20
+ /// Default solving time: 30 seconds.
21
+ const DEFAULT_TIME_LIMIT_SECS: u64 = 30;
22
+
23
+ /// Late acceptance history size.
24
+ const LATE_ACCEPTANCE_SIZE: usize = 400;
25
+
26
+ /// Solver configuration with termination criteria.
27
+ #[derive(Debug, Clone, Default)]
28
+ pub struct SolverConfig {
29
+ /// Stop after this duration.
30
+ pub time_limit: Option<Duration>,
31
+ /// Stop after this duration without improvement.
32
+ pub unimproved_time_limit: Option<Duration>,
33
+ /// Stop after this many steps.
34
+ pub step_limit: Option<u64>,
35
+ /// Stop after this many steps without improvement.
36
+ pub unimproved_step_limit: Option<u64>,
37
+ }
38
+
39
+ impl SolverConfig {
40
+ /// Creates a config with default 30-second time limit.
41
+ pub fn default_config() -> Self {
42
+ Self {
43
+ time_limit: Some(Duration::from_secs(DEFAULT_TIME_LIMIT_SECS)),
44
+ ..Default::default()
45
+ }
46
+ }
47
+
48
+ /// Checks if any termination condition is met.
49
+ fn should_terminate(
50
+ &self,
51
+ elapsed: Duration,
52
+ steps: u64,
53
+ time_since_improvement: Duration,
54
+ steps_since_improvement: u64,
55
+ ) -> bool {
56
+ if let Some(limit) = self.time_limit {
57
+ if elapsed >= limit {
58
+ return true;
59
+ }
60
+ }
61
+ if let Some(limit) = self.unimproved_time_limit {
62
+ if time_since_improvement >= limit {
63
+ return true;
64
+ }
65
+ }
66
+ if let Some(limit) = self.step_limit {
67
+ if steps >= limit {
68
+ return true;
69
+ }
70
+ }
71
+ if let Some(limit) = self.unimproved_step_limit {
72
+ if steps_since_improvement >= limit {
73
+ return true;
74
+ }
75
+ }
76
+ false
77
+ }
78
+ }
79
+
80
+ /// Status of a solving job.
81
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
82
+ #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
83
+ pub enum SolverStatus {
84
+ /// Not currently solving.
85
+ NotSolving,
86
+ /// Actively solving.
87
+ Solving,
88
+ }
89
+
90
+ impl SolverStatus {
91
+ /// Returns the status as a SCREAMING_SNAKE_CASE string for API responses.
92
+ ///
93
+ /// ```
94
+ /// use employee_scheduling::solver::SolverStatus;
95
+ ///
96
+ /// assert_eq!(SolverStatus::NotSolving.as_str(), "NOT_SOLVING");
97
+ /// assert_eq!(SolverStatus::Solving.as_str(), "SOLVING");
98
+ /// ```
99
+ pub fn as_str(self) -> &'static str {
100
+ match self {
101
+ SolverStatus::NotSolving => "NOT_SOLVING",
102
+ SolverStatus::Solving => "SOLVING",
103
+ }
104
+ }
105
+ }
106
+
107
+ /// A solving job with current state.
108
+ pub struct SolveJob {
109
+ /// Unique job identifier.
110
+ pub id: String,
111
+ /// Current status.
112
+ pub status: SolverStatus,
113
+ /// Current best schedule.
114
+ pub schedule: EmployeeSchedule,
115
+ /// Solver configuration.
116
+ pub config: SolverConfig,
117
+ /// Stop signal sender.
118
+ stop_signal: Option<oneshot::Sender<()>>,
119
+ }
120
+
121
+ impl SolveJob {
122
+ /// Creates a new solve job with default config.
123
+ pub fn new(id: String, schedule: EmployeeSchedule) -> Self {
124
+ Self {
125
+ id,
126
+ status: SolverStatus::NotSolving,
127
+ schedule,
128
+ config: SolverConfig::default_config(),
129
+ stop_signal: None,
130
+ }
131
+ }
132
+
133
+ /// Creates a new solve job with custom config.
134
+ pub fn with_config(id: String, schedule: EmployeeSchedule, config: SolverConfig) -> Self {
135
+ Self {
136
+ id,
137
+ status: SolverStatus::NotSolving,
138
+ schedule,
139
+ config,
140
+ stop_signal: None,
141
+ }
142
+ }
143
+ }
144
+
145
+ /// Manages Employee Scheduling solving jobs.
146
+ ///
147
+ /// # Examples
148
+ ///
149
+ /// ```
150
+ /// use employee_scheduling::solver::SolverService;
151
+ /// use employee_scheduling::demo_data::{generate, DemoData};
152
+ ///
153
+ /// let service = SolverService::new();
154
+ /// let schedule = generate(DemoData::Small);
155
+ ///
156
+ /// // Create a job (doesn't start solving yet)
157
+ /// let job = service.create_job("test-1".to_string(), schedule);
158
+ /// assert_eq!(job.read().status, employee_scheduling::solver::SolverStatus::NotSolving);
159
+ /// ```
160
+ pub struct SolverService {
161
+ jobs: RwLock<HashMap<String, Arc<RwLock<SolveJob>>>>,
162
+ }
163
+
164
+ impl SolverService {
165
+ /// Creates a new solver service.
166
+ pub fn new() -> Self {
167
+ Self {
168
+ jobs: RwLock::new(HashMap::new()),
169
+ }
170
+ }
171
+
172
+ /// Creates a new job for the given schedule with default config.
173
+ pub fn create_job(&self, id: String, schedule: EmployeeSchedule) -> Arc<RwLock<SolveJob>> {
174
+ let job = Arc::new(RwLock::new(SolveJob::new(id.clone(), schedule)));
175
+ self.jobs.write().insert(id, job.clone());
176
+ job
177
+ }
178
+
179
+ /// Creates a new job with custom config.
180
+ pub fn create_job_with_config(
181
+ &self,
182
+ id: String,
183
+ schedule: EmployeeSchedule,
184
+ config: SolverConfig,
185
+ ) -> Arc<RwLock<SolveJob>> {
186
+ let job = Arc::new(RwLock::new(SolveJob::with_config(id.clone(), schedule, config)));
187
+ self.jobs.write().insert(id, job.clone());
188
+ job
189
+ }
190
+
191
+ /// Gets a job by ID.
192
+ pub fn get_job(&self, id: &str) -> Option<Arc<RwLock<SolveJob>>> {
193
+ self.jobs.read().get(id).cloned()
194
+ }
195
+
196
+ /// Lists all job IDs.
197
+ pub fn list_jobs(&self) -> Vec<String> {
198
+ self.jobs.read().keys().cloned().collect()
199
+ }
200
+
201
+ /// Removes a job by ID.
202
+ pub fn remove_job(&self, id: &str) -> Option<Arc<RwLock<SolveJob>>> {
203
+ self.jobs.write().remove(id)
204
+ }
205
+
206
+ /// Starts solving a job in the background.
207
+ pub fn start_solving(&self, job: Arc<RwLock<SolveJob>>) {
208
+ let (tx, rx) = oneshot::channel();
209
+ let config = job.read().config.clone();
210
+
211
+ {
212
+ let mut job_guard = job.write();
213
+ job_guard.status = SolverStatus::Solving;
214
+ job_guard.stop_signal = Some(tx);
215
+ }
216
+
217
+ let job_clone = job.clone();
218
+
219
+ tokio::task::spawn_blocking(move || {
220
+ solve_blocking(job_clone, rx, config);
221
+ });
222
+ }
223
+
224
+ /// Stops a solving job.
225
+ pub fn stop_solving(&self, id: &str) -> bool {
226
+ if let Some(job) = self.get_job(id) {
227
+ let mut job_guard = job.write();
228
+ if let Some(stop_signal) = job_guard.stop_signal.take() {
229
+ let _ = stop_signal.send(());
230
+ job_guard.status = SolverStatus::NotSolving;
231
+ return true;
232
+ }
233
+ }
234
+ false
235
+ }
236
+ }
237
+
238
+ impl Default for SolverService {
239
+ fn default() -> Self {
240
+ Self::new()
241
+ }
242
+ }
243
+
244
+ /// Runs the solver in a blocking context.
245
+ fn solve_blocking(
246
+ job: Arc<RwLock<SolveJob>>,
247
+ mut stop_rx: oneshot::Receiver<()>,
248
+ config: SolverConfig,
249
+ ) {
250
+ let initial_schedule = job.read().schedule.clone();
251
+ let job_id = job.read().id.clone();
252
+ let solve_start = Instant::now();
253
+
254
+ // Print problem configuration
255
+ console::print_config(
256
+ initial_schedule.shifts.len(),
257
+ initial_schedule.employees.len(),
258
+ );
259
+
260
+ info!(
261
+ job_id = %job_id,
262
+ shifts = initial_schedule.shifts.len(),
263
+ employees = initial_schedule.employees.len(),
264
+ "Starting Employee Scheduling solver"
265
+ );
266
+
267
+ // Create typed constraints and score director
268
+ let constraints = create_fluent_constraints();
269
+ let mut director = TypedScoreDirector::new(initial_schedule.clone(), constraints);
270
+
271
+ // Phase 1: Construction heuristic (round-robin)
272
+ let mut ch_timer = PhaseTimer::start("ConstructionHeuristic", 0);
273
+ let mut current_score = construction_heuristic(&mut director, &mut ch_timer);
274
+ ch_timer.finish();
275
+
276
+ // Print solving started after construction
277
+ console::print_solving_started(
278
+ solve_start.elapsed().as_millis() as u64,
279
+ &current_score.to_string(),
280
+ initial_schedule.shifts.len(),
281
+ initial_schedule.shifts.len(),
282
+ initial_schedule.employees.len(),
283
+ );
284
+
285
+ // Update job with constructed solution
286
+ update_job(&job, &director, current_score);
287
+
288
+ // Phase 2: Late Acceptance local search
289
+ let n_employees = director.working_solution().employees.len();
290
+ if n_employees == 0 {
291
+ info!("No employees to optimize");
292
+ console::print_solving_ended(
293
+ solve_start.elapsed(),
294
+ 0,
295
+ 1,
296
+ &current_score.to_string(),
297
+ current_score.is_feasible(),
298
+ );
299
+ finish_job(&job, &director, current_score);
300
+ return;
301
+ }
302
+
303
+ let mut ls_timer = PhaseTimer::start("LateAcceptance", 1);
304
+ let mut late_scores = vec![current_score; LATE_ACCEPTANCE_SIZE];
305
+ let mut step: u64 = 0;
306
+ let mut rng = rand::thread_rng();
307
+
308
+ // Track best score and improvement times
309
+ let mut best_score = current_score;
310
+ let mut last_improvement_time = solve_start;
311
+ let mut last_improvement_step: u64 = 0;
312
+
313
+ loop {
314
+ // Check termination conditions
315
+ let elapsed = solve_start.elapsed();
316
+ let time_since_improvement = last_improvement_time.elapsed();
317
+ let steps_since_improvement = step - last_improvement_step;
318
+
319
+ if config.should_terminate(elapsed, step, time_since_improvement, steps_since_improvement) {
320
+ debug!("Termination condition met");
321
+ break;
322
+ }
323
+
324
+ // Check for stop signal
325
+ if stop_rx.try_recv().is_ok() {
326
+ info!("Solving terminated early by user");
327
+ break;
328
+ }
329
+
330
+ // Generate random change move
331
+ if let Some((shift_idx, new_employee_idx)) = generate_move(&director, &mut rng) {
332
+ ls_timer.record_move();
333
+
334
+ // Try the move
335
+ let old_score = current_score;
336
+ let old_employee_idx = apply_move(&mut director, shift_idx, new_employee_idx);
337
+ let new_score = director.get_score();
338
+
339
+ // Late acceptance criterion
340
+ let late_idx = (step as usize) % LATE_ACCEPTANCE_SIZE;
341
+ let late_score = late_scores[late_idx];
342
+
343
+ if new_score >= old_score || new_score >= late_score {
344
+ // Accept
345
+ ls_timer.record_accepted(&current_score.to_string());
346
+ current_score = new_score;
347
+ late_scores[late_idx] = new_score;
348
+
349
+ // Track improvements
350
+ if new_score > best_score {
351
+ best_score = new_score;
352
+ last_improvement_time = Instant::now();
353
+ last_improvement_step = step;
354
+ }
355
+
356
+ // Periodic update
357
+ if ls_timer.steps_accepted().is_multiple_of(1000) {
358
+ update_job(&job, &director, current_score);
359
+ debug!(
360
+ step,
361
+ moves_accepted = ls_timer.steps_accepted(),
362
+ score = %current_score,
363
+ elapsed_secs = solve_start.elapsed().as_secs(),
364
+ "Progress update"
365
+ );
366
+ }
367
+
368
+ // Periodic console progress (every 10000 moves)
369
+ if ls_timer.moves_evaluated().is_multiple_of(10000) {
370
+ console::print_step_progress(
371
+ ls_timer.steps_accepted(),
372
+ ls_timer.elapsed(),
373
+ ls_timer.moves_evaluated(),
374
+ &current_score.to_string(),
375
+ );
376
+ }
377
+ } else {
378
+ // Reject - undo
379
+ undo_move(&mut director, shift_idx, old_employee_idx);
380
+ }
381
+
382
+ step += 1;
383
+ }
384
+ }
385
+
386
+ ls_timer.finish();
387
+
388
+ let total_duration = solve_start.elapsed();
389
+
390
+ info!(
391
+ job_id = %job_id,
392
+ duration_secs = total_duration.as_secs_f64(),
393
+ steps = step,
394
+ score = %current_score,
395
+ feasible = current_score.is_feasible(),
396
+ "Solving complete"
397
+ );
398
+
399
+ console::print_solving_ended(
400
+ total_duration,
401
+ step,
402
+ 2,
403
+ &current_score.to_string(),
404
+ current_score.is_feasible(),
405
+ );
406
+
407
+ finish_job(&job, &director, current_score);
408
+ }
409
+
410
+ /// Construction heuristic: round-robin employee assignment.
411
+ fn construction_heuristic(
412
+ director: &mut TypedScoreDirector<EmployeeSchedule, impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore>>,
413
+ timer: &mut PhaseTimer,
414
+ ) -> HardSoftDecimalScore {
415
+ // Initialize score
416
+ let _ = director.calculate_score();
417
+
418
+ let n_shifts = director.working_solution().shifts.len();
419
+ let n_employees = director.working_solution().employees.len();
420
+
421
+ if n_employees == 0 || n_shifts == 0 {
422
+ return director.get_score();
423
+ }
424
+
425
+ // Count already-assigned shifts
426
+ let assigned_count = director
427
+ .working_solution()
428
+ .shifts
429
+ .iter()
430
+ .filter(|s| s.employee_idx.is_some())
431
+ .count();
432
+
433
+ // If all shifts already assigned, skip construction
434
+ if assigned_count == n_shifts {
435
+ info!("All shifts already assigned, skipping construction heuristic");
436
+ return director.get_score();
437
+ }
438
+
439
+ // Round-robin assignment for unassigned shifts only
440
+ let mut employee_idx = 0;
441
+ for shift_idx in 0..n_shifts {
442
+ if director.working_solution().shifts[shift_idx].employee_idx.is_some() {
443
+ continue;
444
+ }
445
+
446
+ timer.record_move();
447
+ director.before_variable_changed(shift_idx);
448
+ director.working_solution_mut().shifts[shift_idx].employee_idx = Some(employee_idx);
449
+ director.after_variable_changed(shift_idx);
450
+
451
+ let score = director.get_score();
452
+ timer.record_accepted(&score.to_string());
453
+
454
+ employee_idx = (employee_idx + 1) % n_employees;
455
+ }
456
+
457
+ director.get_score()
458
+ }
459
+
460
+ /// Generates a random change move (assign a different employee to a shift).
461
+ fn generate_move<R: Rng>(
462
+ director: &TypedScoreDirector<EmployeeSchedule, impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore>>,
463
+ rng: &mut R,
464
+ ) -> Option<(usize, Option<usize>)> {
465
+ let solution = director.working_solution();
466
+ let n_shifts = solution.shifts.len();
467
+ let n_employees = solution.employees.len();
468
+
469
+ if n_shifts == 0 || n_employees == 0 {
470
+ return None;
471
+ }
472
+
473
+ // Pick random shift
474
+ let shift_idx = rng.gen_range(0..n_shifts);
475
+ let current_employee = solution.shifts[shift_idx].employee_idx;
476
+
477
+ // Pick random new employee (different from current)
478
+ let new_employee_idx = rng.gen_range(0..n_employees);
479
+
480
+ // Skip no-op moves
481
+ if current_employee == Some(new_employee_idx) {
482
+ return None;
483
+ }
484
+
485
+ Some((shift_idx, Some(new_employee_idx)))
486
+ }
487
+
488
+ /// Applies a change move, returns the old employee index.
489
+ fn apply_move(
490
+ director: &mut TypedScoreDirector<EmployeeSchedule, impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore>>,
491
+ shift_idx: usize,
492
+ new_employee_idx: Option<usize>,
493
+ ) -> Option<usize> {
494
+ let old_employee_idx = director.working_solution().shifts[shift_idx].employee_idx;
495
+
496
+ director.before_variable_changed(shift_idx);
497
+ director.working_solution_mut().shifts[shift_idx].employee_idx = new_employee_idx;
498
+ director.after_variable_changed(shift_idx);
499
+
500
+ old_employee_idx
501
+ }
502
+
503
+ /// Undoes a change move.
504
+ fn undo_move(
505
+ director: &mut TypedScoreDirector<EmployeeSchedule, impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore>>,
506
+ shift_idx: usize,
507
+ old_employee_idx: Option<usize>,
508
+ ) {
509
+ director.before_variable_changed(shift_idx);
510
+ director.working_solution_mut().shifts[shift_idx].employee_idx = old_employee_idx;
511
+ director.after_variable_changed(shift_idx);
512
+ }
513
+
514
+ /// Updates job with current solution.
515
+ fn update_job(
516
+ job: &Arc<RwLock<SolveJob>>,
517
+ director: &TypedScoreDirector<EmployeeSchedule, impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore>>,
518
+ score: HardSoftDecimalScore,
519
+ ) {
520
+ let mut job_guard = job.write();
521
+ job_guard.schedule = director.clone_working_solution();
522
+ job_guard.schedule.score = Some(score);
523
+ }
524
+
525
+ /// Finishes job and sets status.
526
+ fn finish_job(
527
+ job: &Arc<RwLock<SolveJob>>,
528
+ director: &TypedScoreDirector<EmployeeSchedule, impl ConstraintSet<EmployeeSchedule, HardSoftDecimalScore>>,
529
+ score: HardSoftDecimalScore,
530
+ ) {
531
+ let mut job_guard = job.write();
532
+ job_guard.schedule = director.clone_working_solution();
533
+ job_guard.schedule.score = Some(score);
534
+ job_guard.status = SolverStatus::NotSolving;
535
+ }
536
+
537
+ #[cfg(test)]
538
+ mod tests {
539
+ use super::*;
540
+ use crate::demo_data::{generate, DemoData};
541
+
542
+ #[test]
543
+ fn test_construction_heuristic() {
544
+ let schedule = generate(DemoData::Small);
545
+ let constraints = create_fluent_constraints();
546
+ let mut director = TypedScoreDirector::new(schedule, constraints);
547
+
548
+ let mut timer = PhaseTimer::start("ConstructionHeuristic", 0);
549
+ let score = construction_heuristic(&mut director, &mut timer);
550
+
551
+ // All shifts should be assigned
552
+ let assigned_count = director
553
+ .working_solution()
554
+ .shifts
555
+ .iter()
556
+ .filter(|s| s.employee_idx.is_some())
557
+ .count();
558
+ let total_shifts = director.working_solution().shifts.len();
559
+ assert_eq!(assigned_count, total_shifts);
560
+ assert!(score.hard_scaled() <= 0); // May have some violations
561
+ }
562
+ }
static/app.js ADDED
@@ -0,0 +1,522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let autoRefreshIntervalId = null;
2
+ const zoomMin = 2 * 1000 * 60 * 60 * 24 // 2 day in milliseconds
3
+ const zoomMax = 4 * 7 * 1000 * 60 * 60 * 24 // 4 weeks in milliseconds
4
+
5
+ const UNAVAILABLE_COLOR = '#ef2929' // Tango Scarlet Red
6
+ const UNDESIRED_COLOR = '#f57900' // Tango Orange
7
+ const DESIRED_COLOR = '#73d216' // Tango Chameleon
8
+
9
+ let demoDataId = null;
10
+ let scheduleId = null;
11
+ let loadedSchedule = null;
12
+
13
+ const byEmployeePanel = document.getElementById("byEmployeePanel");
14
+ const byEmployeeTimelineOptions = {
15
+ timeAxis: {scale: "hour", step: 6},
16
+ orientation: {axis: "top"},
17
+ stack: false,
18
+ xss: {disabled: true}, // Items are XSS safe through JQuery
19
+ zoomMin: zoomMin,
20
+ zoomMax: zoomMax,
21
+ };
22
+ let byEmployeeGroupDataSet = new vis.DataSet();
23
+ let byEmployeeItemDataSet = new vis.DataSet();
24
+ let byEmployeeTimeline = new vis.Timeline(byEmployeePanel, byEmployeeItemDataSet, byEmployeeGroupDataSet, byEmployeeTimelineOptions);
25
+
26
+ const byLocationPanel = document.getElementById("byLocationPanel");
27
+ const byLocationTimelineOptions = {
28
+ timeAxis: {scale: "hour", step: 6},
29
+ orientation: {axis: "top"},
30
+ xss: {disabled: true}, // Items are XSS safe through JQuery
31
+ zoomMin: zoomMin,
32
+ zoomMax: zoomMax,
33
+ };
34
+ let byLocationGroupDataSet = new vis.DataSet();
35
+ let byLocationItemDataSet = new vis.DataSet();
36
+ let byLocationTimeline = new vis.Timeline(byLocationPanel, byLocationItemDataSet, byLocationGroupDataSet, byLocationTimelineOptions);
37
+
38
+ let windowStart = JSJoda.LocalDate.now().toString();
39
+ let windowEnd = JSJoda.LocalDate.parse(windowStart).plusDays(7).toString();
40
+
41
+ $(document).ready(function () {
42
+ let initialized = false;
43
+
44
+ function safeInitialize() {
45
+ if (!initialized) {
46
+ initialized = true;
47
+ initializeApp();
48
+ }
49
+ }
50
+
51
+ // Ensure all resources are loaded before initializing
52
+ $(window).on('load', safeInitialize);
53
+
54
+ // Fallback if window load event doesn't fire
55
+ setTimeout(safeInitialize, 100);
56
+ });
57
+
58
+ function initializeApp() {
59
+ replaceQuickstartSolverForgeAutoHeaderFooter();
60
+
61
+ $("#solveButton").click(function () {
62
+ solve();
63
+ });
64
+ $("#stopSolvingButton").click(function () {
65
+ stopSolving();
66
+ });
67
+ $("#analyzeButton").click(function () {
68
+ analyze();
69
+ });
70
+ // HACK to allow vis-timeline to work within Bootstrap tabs
71
+ $("#byEmployeeTab").on('shown.bs.tab', function (event) {
72
+ byEmployeeTimeline.redraw();
73
+ })
74
+ $("#byLocationTab").on('shown.bs.tab', function (event) {
75
+ byLocationTimeline.redraw();
76
+ })
77
+
78
+ setupAjax();
79
+ fetchDemoData();
80
+ }
81
+
82
+ function setupAjax() {
83
+ $.ajaxSetup({
84
+ headers: {
85
+ 'Content-Type': 'application/json',
86
+ 'Accept': 'application/json,text/plain', // plain text is required by solve() returning UUID of the solver job
87
+ }
88
+ });
89
+ // Extend jQuery to support $.put() and $.delete()
90
+ jQuery.each(["put", "delete"], function (i, method) {
91
+ jQuery[method] = function (url, data, callback, type) {
92
+ if (jQuery.isFunction(data)) {
93
+ type = type || callback;
94
+ callback = data;
95
+ data = undefined;
96
+ }
97
+ return jQuery.ajax({
98
+ url: url,
99
+ type: method,
100
+ dataType: type,
101
+ data: data,
102
+ success: callback
103
+ });
104
+ };
105
+ });
106
+ }
107
+
108
+ function fetchDemoData() {
109
+ $.get("/demo-data", function (data) {
110
+ data.forEach(item => {
111
+ $("#testDataButton").append($('<a id="' + item + 'TestData" class="dropdown-item" href="#">' + item + '</a>'));
112
+ $("#" + item + "TestData").click(function () {
113
+ switchDataDropDownItemActive(item);
114
+ scheduleId = null;
115
+ demoDataId = item;
116
+
117
+ refreshSchedule();
118
+ });
119
+ });
120
+ demoDataId = data[0];
121
+ switchDataDropDownItemActive(demoDataId);
122
+ refreshSchedule();
123
+ }).fail(function (xhr, ajaxOptions, thrownError) {
124
+ // disable this page as there is no data
125
+ let $demo = $("#demo");
126
+ $demo.empty();
127
+ $demo.html("<h1><p align=\"center\">No test data available</p></h1>")
128
+ });
129
+ }
130
+
131
+ function switchDataDropDownItemActive(newItem) {
132
+ activeCssClass = "active";
133
+ $("#testDataButton > a." + activeCssClass).removeClass(activeCssClass);
134
+ $("#" + newItem + "TestData").addClass(activeCssClass);
135
+ }
136
+
137
+ function getShiftColor(shift, employee) {
138
+ const shiftStart = JSJoda.LocalDateTime.parse(shift.start);
139
+ const shiftStartDateString = shiftStart.toLocalDate().toString();
140
+ const shiftEnd = JSJoda.LocalDateTime.parse(shift.end);
141
+ const shiftEndDateString = shiftEnd.toLocalDate().toString();
142
+ if (employee.unavailableDates.includes(shiftStartDateString) ||
143
+ // The contains() check is ignored for a shift end at midnight (00:00:00).
144
+ (shiftEnd.isAfter(shiftStart.toLocalDate().plusDays(1).atStartOfDay()) &&
145
+ employee.unavailableDates.includes(shiftEndDateString))) {
146
+ return UNAVAILABLE_COLOR
147
+ } else if (employee.undesiredDates.includes(shiftStartDateString) ||
148
+ // The contains() check is ignored for a shift end at midnight (00:00:00).
149
+ (shiftEnd.isAfter(shiftStart.toLocalDate().plusDays(1).atStartOfDay()) &&
150
+ employee.undesiredDates.includes(shiftEndDateString))) {
151
+ return UNDESIRED_COLOR
152
+ } else if (employee.desiredDates.includes(shiftStartDateString) ||
153
+ // The contains() check is ignored for a shift end at midnight (00:00:00).
154
+ (shiftEnd.isAfter(shiftStart.toLocalDate().plusDays(1).atStartOfDay()) &&
155
+ employee.desiredDates.includes(shiftEndDateString))) {
156
+ return DESIRED_COLOR
157
+ } else {
158
+ return " #729fcf"; // Tango Sky Blue
159
+ }
160
+ }
161
+
162
+ function refreshSchedule() {
163
+ let path = "/schedules/" + scheduleId;
164
+ if (scheduleId === null) {
165
+ if (demoDataId === null) {
166
+ alert("Please select a test data set.");
167
+ return;
168
+ }
169
+
170
+ path = "/demo-data/" + demoDataId;
171
+ }
172
+ $.getJSON(path, function (schedule) {
173
+ loadedSchedule = schedule;
174
+ renderSchedule(schedule);
175
+ })
176
+ .fail(function (xhr, ajaxOptions, thrownError) {
177
+ showError("Getting the schedule has failed.", xhr);
178
+ refreshSolvingButtons(false);
179
+ });
180
+ }
181
+
182
+ function renderSchedule(schedule) {
183
+ console.log('Rendering schedule:', schedule);
184
+
185
+ if (!schedule) {
186
+ console.error('No schedule data provided to renderSchedule');
187
+ return;
188
+ }
189
+
190
+ refreshSolvingButtons(schedule.solverStatus != null && schedule.solverStatus !== "NOT_SOLVING");
191
+ $("#score").text("Score: " + (schedule.score == null ? "?" : schedule.score));
192
+
193
+ const unassignedShifts = $("#unassignedShifts");
194
+ const groups = [];
195
+
196
+ // Check if schedule.shifts exists and is an array
197
+ if (!schedule.shifts || !Array.isArray(schedule.shifts) || schedule.shifts.length === 0) {
198
+ console.warn('No shifts data available in schedule');
199
+ return;
200
+ }
201
+
202
+ // Show only first 7 days of draft
203
+ const scheduleStart = schedule.shifts.map(shift => JSJoda.LocalDateTime.parse(shift.start).toLocalDate()).sort()[0].toString();
204
+ const scheduleEnd = JSJoda.LocalDate.parse(scheduleStart).plusDays(7).toString();
205
+
206
+ windowStart = scheduleStart;
207
+ windowEnd = scheduleEnd;
208
+
209
+ unassignedShifts.children().remove();
210
+ let unassignedShiftsCount = 0;
211
+ byEmployeeGroupDataSet.clear();
212
+ byLocationGroupDataSet.clear();
213
+
214
+ byEmployeeItemDataSet.clear();
215
+ byLocationItemDataSet.clear();
216
+
217
+ // Check if schedule.employees exists and is an array
218
+ if (!schedule.employees || !Array.isArray(schedule.employees)) {
219
+ console.warn('No employees data available in schedule');
220
+ return;
221
+ }
222
+
223
+ schedule.employees.forEach((employee, index) => {
224
+ const employeeGroupElement = $('<div class="card-body p-2"/>')
225
+ .append($(`<h5 class="card-title mb-2"/>)`)
226
+ .append(employee.name))
227
+ .append($('<div/>')
228
+ .append($(employee.skills.map(skill => `<span class="badge me-1 mt-1" style="background-color:#d3d7cf">${skill}</span>`).join(''))));
229
+ byEmployeeGroupDataSet.add({id: employee.name, content: employeeGroupElement.html()});
230
+
231
+ employee.unavailableDates.forEach((rawDate, dateIndex) => {
232
+ const date = JSJoda.LocalDate.parse(rawDate)
233
+ const start = date.atStartOfDay().toString();
234
+ const end = date.plusDays(1).atStartOfDay().toString();
235
+ const byEmployeeShiftElement = $(`<div/>`)
236
+ .append($(`<h5 class="card-title mb-1"/>`).text("Unavailable"));
237
+ byEmployeeItemDataSet.add({
238
+ id: "employee-" + index + "-unavailability-" + dateIndex, group: employee.name,
239
+ content: byEmployeeShiftElement.html(),
240
+ start: start, end: end,
241
+ type: "background",
242
+ style: "opacity: 0.5; background-color: " + UNAVAILABLE_COLOR,
243
+ });
244
+ });
245
+ employee.undesiredDates.forEach((rawDate, dateIndex) => {
246
+ const date = JSJoda.LocalDate.parse(rawDate)
247
+ const start = date.atStartOfDay().toString();
248
+ const end = date.plusDays(1).atStartOfDay().toString();
249
+ const byEmployeeShiftElement = $(`<div/>`)
250
+ .append($(`<h5 class="card-title mb-1"/>`).text("Undesired"));
251
+ byEmployeeItemDataSet.add({
252
+ id: "employee-" + index + "-undesired-" + dateIndex, group: employee.name,
253
+ content: byEmployeeShiftElement.html(),
254
+ start: start, end: end,
255
+ type: "background",
256
+ style: "opacity: 0.5; background-color: " + UNDESIRED_COLOR,
257
+ });
258
+ });
259
+ employee.desiredDates.forEach((rawDate, dateIndex) => {
260
+ const date = JSJoda.LocalDate.parse(rawDate)
261
+ const start = date.atStartOfDay().toString();
262
+ const end = date.plusDays(1).atStartOfDay().toString();
263
+ const byEmployeeShiftElement = $(`<div/>`)
264
+ .append($(`<h5 class="card-title mb-1"/>`).text("Desired"));
265
+ byEmployeeItemDataSet.add({
266
+ id: "employee-" + index + "-desired-" + dateIndex, group: employee.name,
267
+ content: byEmployeeShiftElement.html(),
268
+ start: start, end: end,
269
+ type: "background",
270
+ style: "opacity: 0.5; background-color: " + DESIRED_COLOR,
271
+ });
272
+ });
273
+ });
274
+
275
+ schedule.shifts.forEach((shift, index) => {
276
+ if (groups.indexOf(shift.location) === -1) {
277
+ groups.push(shift.location);
278
+ byLocationGroupDataSet.add({
279
+ id: shift.location,
280
+ content: shift.location,
281
+ });
282
+ }
283
+
284
+ if (shift.employee == null) {
285
+ unassignedShiftsCount++;
286
+
287
+ const byLocationShiftElement = $('<div class="card-body p-2"/>')
288
+ .append($(`<h5 class="card-title mb-2"/>)`)
289
+ .append("Unassigned"))
290
+ .append($('<div/>')
291
+ .append($(`<span class="badge me-1 mt-1" style="background-color:#d3d7cf">${shift.requiredSkill}</span>`)));
292
+
293
+ byLocationItemDataSet.add({
294
+ id: 'shift-' + index, group: shift.location,
295
+ content: byLocationShiftElement.html(),
296
+ start: shift.start, end: shift.end,
297
+ style: "background-color: #EF292999"
298
+ });
299
+ } else {
300
+ const skillColor = (shift.employee.skills.indexOf(shift.requiredSkill) === -1 ? '#ef2929' : '#8ae234');
301
+ const byEmployeeShiftElement = $('<div class="card-body p-2"/>')
302
+ .append($(`<h5 class="card-title mb-2"/>)`)
303
+ .append(shift.location))
304
+ .append($('<div/>')
305
+ .append($(`<span class="badge me-1 mt-1" style="background-color:${skillColor}">${shift.requiredSkill}</span>`)));
306
+ const byLocationShiftElement = $('<div class="card-body p-2"/>')
307
+ .append($(`<h5 class="card-title mb-2"/>)`)
308
+ .append(shift.employee.name))
309
+ .append($('<div/>')
310
+ .append($(`<span class="badge me-1 mt-1" style="background-color:${skillColor}">${shift.requiredSkill}</span>`)));
311
+
312
+ const shiftColor = getShiftColor(shift, shift.employee);
313
+ byEmployeeItemDataSet.add({
314
+ id: 'shift-' + index, group: shift.employee.name,
315
+ content: byEmployeeShiftElement.html(),
316
+ start: shift.start, end: shift.end,
317
+ style: "background-color: " + shiftColor
318
+ });
319
+ byLocationItemDataSet.add({
320
+ id: 'shift-' + index, group: shift.location,
321
+ content: byLocationShiftElement.html(),
322
+ start: shift.start, end: shift.end,
323
+ style: "background-color: " + shiftColor
324
+ });
325
+ }
326
+ });
327
+
328
+
329
+ if (unassignedShiftsCount === 0) {
330
+ unassignedShifts.append($(`<p/>`).text(`There are no unassigned shifts.`));
331
+ } else {
332
+ unassignedShifts.append($(`<p/>`).text(`There are ${unassignedShiftsCount} unassigned shifts.`));
333
+ }
334
+ byEmployeeTimeline.setWindow(scheduleStart, scheduleEnd);
335
+ byLocationTimeline.setWindow(scheduleStart, scheduleEnd);
336
+ }
337
+
338
+ function solve() {
339
+ if (!loadedSchedule) {
340
+ showError("No schedule data loaded. Please wait for the data to load or refresh the page.");
341
+ return;
342
+ }
343
+
344
+ console.log('Sending schedule data for solving:', loadedSchedule);
345
+ $.post("/schedules", JSON.stringify(loadedSchedule), function (data) {
346
+ scheduleId = data;
347
+ refreshSolvingButtons(true);
348
+ }).fail(function (xhr, ajaxOptions, thrownError) {
349
+ showError("Start solving failed.", xhr);
350
+ refreshSolvingButtons(false);
351
+ },
352
+ "text");
353
+ }
354
+
355
+ function analyze() {
356
+ new bootstrap.Modal("#scoreAnalysisModal").show()
357
+ const scoreAnalysisModalContent = $("#scoreAnalysisModalContent");
358
+ scoreAnalysisModalContent.children().remove();
359
+ if (loadedSchedule.score == null) {
360
+ scoreAnalysisModalContent.text("No score to analyze yet, please first press the 'solve' button.");
361
+ } else {
362
+ $('#scoreAnalysisScoreLabel').text(`(${loadedSchedule.score})`);
363
+ $.put("/schedules/analyze", JSON.stringify(loadedSchedule), function (scoreAnalysis) {
364
+ let constraints = scoreAnalysis.constraints;
365
+ constraints.sort((a, b) => {
366
+ let aComponents = getScoreComponents(a.score), bComponents = getScoreComponents(b.score);
367
+ if (aComponents.hard < 0 && bComponents.hard > 0) return -1;
368
+ if (aComponents.hard > 0 && bComponents.soft < 0) return 1;
369
+ if (Math.abs(aComponents.hard) > Math.abs(bComponents.hard)) {
370
+ return -1;
371
+ } else {
372
+ if (aComponents.medium < 0 && bComponents.medium > 0) return -1;
373
+ if (aComponents.medium > 0 && bComponents.medium < 0) return 1;
374
+ if (Math.abs(aComponents.medium) > Math.abs(bComponents.medium)) {
375
+ return -1;
376
+ } else {
377
+ if (aComponents.soft < 0 && bComponents.soft > 0) return -1;
378
+ if (aComponents.soft > 0 && bComponents.soft < 0) return 1;
379
+
380
+ return Math.abs(bComponents.soft) - Math.abs(aComponents.soft);
381
+ }
382
+ }
383
+ });
384
+ constraints.map((e) => {
385
+ let components = getScoreComponents(e.weight);
386
+ e.type = components.hard != 0 ? 'hard' : (components.medium != 0 ? 'medium' : 'soft');
387
+ e.weight = components[e.type];
388
+ let scores = getScoreComponents(e.score);
389
+ e.implicitScore = scores.hard != 0 ? scores.hard : (scores.medium != 0 ? scores.medium : scores.soft);
390
+ });
391
+ scoreAnalysis.constraints = constraints;
392
+
393
+ scoreAnalysisModalContent.children().remove();
394
+ scoreAnalysisModalContent.text("");
395
+
396
+ const analysisTable = $(`<table class="table"/>`).css({textAlign: 'center'});
397
+ const analysisTHead = $(`<thead/>`).append($(`<tr/>`)
398
+ .append($(`<th></th>`))
399
+ .append($(`<th>Constraint</th>`).css({textAlign: 'left'}))
400
+ .append($(`<th>Type</th>`))
401
+ .append($(`<th># Matches</th>`))
402
+ .append($(`<th>Weight</th>`))
403
+ .append($(`<th>Score</th>`))
404
+ .append($(`<th></th>`)));
405
+ analysisTable.append(analysisTHead);
406
+ const analysisTBody = $(`<tbody/>`)
407
+ $.each(scoreAnalysis.constraints, (index, constraintAnalysis) => {
408
+ let icon = constraintAnalysis.type == "hard" && constraintAnalysis.implicitScore < 0 ? '<span class="fas fa-exclamation-triangle" style="color: red"></span>' : '';
409
+ if (!icon) icon = constraintAnalysis.matches.length == 0 ? '<span class="fas fa-check-circle" style="color: green"></span>' : '';
410
+
411
+ let row = $(`<tr/>`);
412
+ row.append($(`<td/>`).html(icon))
413
+ .append($(`<td/>`).text(constraintAnalysis.name).css({textAlign: 'left'}))
414
+ .append($(`<td/>`).text(constraintAnalysis.type))
415
+ .append($(`<td/>`).html(`<b>${constraintAnalysis.matches.length}</b>`))
416
+ .append($(`<td/>`).text(constraintAnalysis.weight))
417
+ .append($(`<td/>`).text(constraintAnalysis.implicitScore));
418
+ analysisTBody.append(row);
419
+ row.append($(`<td/>`));
420
+ });
421
+ analysisTable.append(analysisTBody);
422
+ scoreAnalysisModalContent.append(analysisTable);
423
+ }).fail(function (xhr, ajaxOptions, thrownError) {
424
+ showError("Analyze failed.", xhr);
425
+ }, "text");
426
+ }
427
+ }
428
+
429
+ function getScoreComponents(score) {
430
+ let components = {hard: 0, medium: 0, soft: 0};
431
+
432
+ $.each([...score.matchAll(/(-?\d*(\.\d+)?)(hard|medium|soft)/g)], (i, parts) => {
433
+ components[parts[3]] = parseFloat(parts[1], 10);
434
+ });
435
+
436
+ return components;
437
+ }
438
+
439
+ function refreshSolvingButtons(solving) {
440
+ if (solving) {
441
+ $("#solveButton").hide();
442
+ $("#stopSolvingButton").show();
443
+ $("#solvingSpinner").addClass("active");
444
+ if (autoRefreshIntervalId == null) {
445
+ autoRefreshIntervalId = setInterval(refreshSchedule, 2000);
446
+ }
447
+ } else {
448
+ $("#solveButton").show();
449
+ $("#stopSolvingButton").hide();
450
+ $("#solvingSpinner").removeClass("active");
451
+ if (autoRefreshIntervalId != null) {
452
+ clearInterval(autoRefreshIntervalId);
453
+ autoRefreshIntervalId = null;
454
+ }
455
+ }
456
+ }
457
+
458
+ function stopSolving() {
459
+ $.delete(`/schedules/${scheduleId}`, function () {
460
+ refreshSolvingButtons(false);
461
+ refreshSchedule();
462
+ }).fail(function (xhr, ajaxOptions, thrownError) {
463
+ showError("Stop solving failed.", xhr);
464
+ });
465
+ }
466
+
467
+ function replaceQuickstartSolverForgeAutoHeaderFooter() {
468
+ const solverforgeHeader = $("header#solverforge-auto-header");
469
+ if (solverforgeHeader != null) {
470
+ solverforgeHeader.css("background-color", "#ffffff");
471
+ solverforgeHeader.append(
472
+ $(`<div class="container-fluid">
473
+ <nav class="navbar sticky-top navbar-expand-lg shadow-sm mb-3" style="background-color: #ffffff;">
474
+ <a class="navbar-brand" href="https://www.solverforge.org">
475
+ <img src="/webjars/solverforge/img/solverforge-horizontal.svg" alt="SolverForge logo" width="400">
476
+ </a>
477
+ <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
478
+ <span class="navbar-toggler-icon"></span>
479
+ </button>
480
+ <div class="collapse navbar-collapse" id="navbarNav">
481
+ <ul class="nav nav-pills">
482
+ <li class="nav-item active" id="navUIItem">
483
+ <button class="nav-link active" id="navUI" data-bs-toggle="pill" data-bs-target="#demo" type="button" style="color: #1f2937;">Demo UI</button>
484
+ </li>
485
+ <li class="nav-item" id="navRestItem">
486
+ <button class="nav-link" id="navRest" data-bs-toggle="pill" data-bs-target="#rest" type="button" style="color: #1f2937;">Guide</button>
487
+ </li>
488
+ <li class="nav-item" id="navOpenApiItem">
489
+ <button class="nav-link" id="navOpenApi" data-bs-toggle="pill" data-bs-target="#openapi" type="button" style="color: #1f2937;">REST API</button>
490
+ </li>
491
+ </ul>
492
+ </div>
493
+ <div class="ms-auto">
494
+ <div class="dropdown">
495
+ <button class="btn dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" style="background-color: #10b981; color: #ffffff; border-color: #10b981;">
496
+ Data
497
+ </button>
498
+ <div id="testDataButton" class="dropdown-menu" aria-labelledby="dropdownMenuButton"></div>
499
+ </div>
500
+ </div>
501
+ </nav>
502
+ </div>`));
503
+ }
504
+
505
+ const solverforgeFooter = $("footer#solverforge-auto-footer");
506
+ if (solverforgeFooter != null) {
507
+ solverforgeFooter.append(
508
+ $(`<footer class="bg-black text-white-50">
509
+ <div class="container">
510
+ <div class="hstack gap-3 p-4">
511
+ <div class="ms-auto"><a class="text-white" href="https://www.solverforge.org">SolverForge</a></div>
512
+ <div class="vr"></div>
513
+ <div><a class="text-white" href="https://www.solverforge.org/docs">Documentation</a></div>
514
+ <div class="vr"></div>
515
+ <div><a class="text-white" href="https://github.com/SolverForge/solverforge-legacy">Code</a></div>
516
+ <div class="vr"></div>
517
+ <div class="me-auto"><a class="text-white" href="mailto:info@solverforge.org">Support</a></div>
518
+ </div>
519
+ </div>
520
+ </footer>`));
521
+ }
522
+ }
static/index.html ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <html lang="en">
2
+ <head>
3
+ <meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
4
+ <meta content="width=device-width, initial-scale=1" name="viewport">
5
+ <title>Employee scheduling - SolverForge for Python</title>
6
+
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/styles/vis-timeline-graph2d.min.css"
8
+ integrity="sha256-svzNasPg1yR5gvEaRei2jg+n4Pc3sVyMUWeS6xRAh6U=" crossorigin="anonymous">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css"/>
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"/>
11
+ <link rel="stylesheet" href="/webjars/solverforge/css/solverforge-webui.css"/>
12
+ <style>
13
+ .vis-time-axis .vis-grid.vis-saturday,
14
+ .vis-time-axis .vis-grid.vis-sunday {
15
+ background: #D3D7CFFF;
16
+ }
17
+
18
+ /* Solving spinner */
19
+ #solvingSpinner {
20
+ display: none;
21
+ width: 1.25rem;
22
+ height: 1.25rem;
23
+ border: 2px solid #10b981;
24
+ border-top-color: transparent;
25
+ border-radius: 50%;
26
+ animation: spin 0.75s linear infinite;
27
+ vertical-align: middle;
28
+ }
29
+ #solvingSpinner.active {
30
+ display: inline-block;
31
+ }
32
+ @keyframes spin {
33
+ to { transform: rotate(360deg); }
34
+ }
35
+ </style>
36
+ <link rel="icon" href="/webjars/solverforge/img/solverforge-favicon.svg" type="image/svg+xml">
37
+ </head>
38
+
39
+ <body>
40
+ <header id="solverforge-auto-header">
41
+ <!-- Filled in by app.js -->
42
+ </header>
43
+ <div class="tab-content">
44
+ <div id="demo" class="tab-pane fade show active container-fluid">
45
+ <div class="sticky-top d-flex justify-content-center align-items-center" aria-live="polite"
46
+ aria-atomic="true">
47
+ <div id="notificationPanel" style="position: absolute; top: .5rem;"></div>
48
+ </div>
49
+ <h1>Employee scheduling solver</h1>
50
+ <p>Generate the optimal schedule for your employees.</p>
51
+
52
+ <div class="mb-4">
53
+ <button id="solveButton" type="button" class="btn btn-success">
54
+ <span class="fas fa-play"></span> Solve
55
+ </button>
56
+ <button id="stopSolvingButton" type="button" class="btn btn-danger">
57
+ <span class="fas fa-stop"></span> Stop solving
58
+ </button>
59
+ <span id="solvingSpinner" class="ms-2"></span>
60
+ <span id="unassignedShifts" class="ms-2 align-middle fw-bold"></span>
61
+ <span id="score" class="score ms-2 align-middle fw-bold">Score: ?</span>
62
+ <button id="analyzeButton" type="button" class="ms-2 btn btn-secondary">
63
+ <span class="fas fa-question"></span>
64
+ </button>
65
+
66
+ <div class="float-end">
67
+ <ul class="nav nav-pills" role="tablist">
68
+ <li class="nav-item" role="presentation">
69
+ <button class="nav-link active" id="byLocationTab" data-bs-toggle="tab"
70
+ data-bs-target="#byLocationPanel" type="button" role="tab"
71
+ aria-controls="byLocationPanel" aria-selected="true">By location
72
+ </button>
73
+ </li>
74
+ <li class="nav-item" role="presentation">
75
+ <button class="nav-link" id="byEmployeeTab" data-bs-toggle="tab"
76
+ data-bs-target="#byEmployeePanel" type="button" role="tab"
77
+ aria-controls="byEmployeePanel" aria-selected="false">By employee
78
+ </button>
79
+ </li>
80
+ </ul>
81
+ </div>
82
+ </div>
83
+ <div class="mb-4 tab-content">
84
+ <div class="tab-pane fade show active" id="byLocationPanel" role="tabpanel"
85
+ aria-labelledby="byLocationTab">
86
+ <div id="locationVisualization"></div>
87
+ </div>
88
+ <div class="tab-pane fade" id="byEmployeePanel" role="tabpanel" aria-labelledby="byEmployeeTab">
89
+ <div id="employeeVisualization"></div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ <div id="rest" class="tab-pane fade container-fluid">
95
+ <h1>REST API Guide</h1>
96
+
97
+ <h2>Employee Scheduling solver integration via cURL</h2>
98
+
99
+ <h3>1. Download demo data</h3>
100
+ <pre>
101
+ <button class="btn btn-outline-dark btn-sm float-end"
102
+ onclick="copyTextToClipboard('curl1')">Copy</button>
103
+ <code id="curl1">curl -X GET -H 'Accept:application/json' http://localhost:7860/demo-data/SMALL -o sample.json</code>
104
+ </pre>
105
+
106
+ <h3>2. Post the sample data for solving</h3>
107
+ <p>The POST operation returns a <code>jobId</code> that should be used in subsequent commands.</p>
108
+ <pre>
109
+ <button class="btn btn-outline-dark btn-sm float-end"
110
+ onclick="copyTextToClipboard('curl2')">Copy</button>
111
+ <code id="curl2">curl -X POST -H 'Content-Type:application/json' http://localhost:7860/schedules -d@sample.json</code>
112
+ </pre>
113
+
114
+ <h3>3. Get the current status and score</h3>
115
+ <pre>
116
+ <button class="btn btn-outline-dark btn-sm float-end"
117
+ onclick="copyTextToClipboard('curl3')">Copy</button>
118
+ <code id="curl3">curl -X GET -H 'Accept:application/json' http://localhost:7860/schedules/{jobId}/status</code>
119
+ </pre>
120
+
121
+ <h3>4. Get the complete solution</h3>
122
+ <pre>
123
+ <button class="btn btn-outline-dark btn-sm float-end"
124
+ onclick="copyTextToClipboard('curl4')">Copy</button>
125
+ <code id="curl4">curl -X GET -H 'Accept:application/json' http://localhost:7860/schedules/{jobId}</code>
126
+ </pre>
127
+
128
+ <h3>5. Terminate solving early</h3>
129
+ <pre>
130
+ <button class="btn btn-outline-dark btn-sm float-end"
131
+ onclick="copyTextToClipboard('curl5')">Copy</button>
132
+ <code id="curl5">curl -X DELETE -H 'Accept:application/json' http://localhost:7860/schedules/{id}</code>
133
+ </pre>
134
+ </div>
135
+
136
+ <div id="openapi" class="tab-pane fade container-fluid">
137
+ <h1>REST API Reference</h1>
138
+ <div class="ratio ratio-1x1">
139
+ <!-- "scrolling" attribute is obsolete, but e.g. Chrome does not support "overflow:hidden" -->
140
+ <iframe src="/q/swagger-ui" style="overflow:hidden;" scrolling="no"></iframe>
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <div class="modal fadebd-example-modal-lg" id="scoreAnalysisModal" tabindex="-1" aria-labelledby="scoreAnalysisModalLabel" aria-hidden="true">
146
+ <div class="modal-dialog modal-lg modal-dialog-scrollable">
147
+ <div class="modal-content">
148
+ <div class="modal-header">
149
+ <h1 class="modal-title fs-5" id="scoreAnalysisModalLabel">Score analysis <span id="scoreAnalysisScoreLabel"></span></h1>
150
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
151
+ </div>
152
+ <div class="modal-body" id="scoreAnalysisModalContent">
153
+ <!-- Filled in by app.js -->
154
+ </div>
155
+ <div class="modal-footer">
156
+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ <footer id="solverforge-auto-footer"></footer>
163
+
164
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js"></script>
165
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.min.js"></script>
166
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
167
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/js-joda/1.11.0/js-joda.min.js"></script>
168
+ <script src="/webjars/solverforge/js/solverforge-webui.js"></script>
169
+ <script src="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/standalone/umd/vis-timeline-graph2d.min.js"
170
+ integrity="sha256-Jy2+UO7rZ2Dgik50z3XrrNpnc5+2PAx9MhL2CicodME=" crossorigin="anonymous"></script>
171
+ <script src="/app.js"></script>
172
+ </body>
173
+ </html>
static/webjars/solverforge/css/solverforge-webui.css ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ /* Keep in sync with .navbar height on a large screen. */
3
+ --ts-navbar-height: 109px;
4
+
5
+ --ts-green-1-rgb: #10b981;
6
+ --ts-green-2-rgb: #059669;
7
+ --ts-violet-1-rgb: #3E00FF;
8
+ --ts-violet-2-rgb: #3423A6;
9
+ --ts-violet-3-rgb: #2E1760;
10
+ --ts-violet-4-rgb: #200F4F;
11
+ --ts-violet-5-rgb: #000000; /* TODO FIXME */
12
+ --ts-violet-dark-1-rgb: #b6adfd;
13
+ --ts-violet-dark-2-rgb: #c1bbfd;
14
+ --ts-gray-rgb: #666666;
15
+ --ts-white-rgb: #FFFFFF;
16
+ --ts-light-rgb: #F2F2F2;
17
+ --ts-gray-border: #c5c5c5;
18
+
19
+ --tf-light-rgb-transparent: rgb(242,242,242,0.5); /* #F2F2F2 = rgb(242,242,242) */
20
+ --bs-body-bg: var(--ts-light-rgb); /* link to html bg */
21
+ --bs-link-color: var(--ts-violet-1-rgb);
22
+ --bs-link-hover-color: var(--ts-violet-2-rgb);
23
+
24
+ --bs-navbar-color: var(--ts-white-rgb);
25
+ --bs-navbar-hover-color: var(--ts-white-rgb);
26
+ --bs-nav-link-font-size: 18px;
27
+ --bs-nav-link-font-weight: 400;
28
+ --bs-nav-link-color: var(--ts-white-rgb);
29
+ --ts-nav-link-hover-border-color: var(--ts-violet-1-rgb);
30
+ }
31
+ .btn {
32
+ --bs-btn-border-radius: 1.5rem;
33
+ }
34
+ .btn-primary {
35
+ --bs-btn-bg: var(--ts-violet-1-rgb);
36
+ --bs-btn-border-color: var(--ts-violet-1-rgb);
37
+ --bs-btn-hover-bg: var(--ts-violet-2-rgb);
38
+ --bs-btn-hover-border-color: var(--ts-violet-2-rgb);
39
+ --bs-btn-active-bg: var(--ts-violet-2-rgb);
40
+ --bs-btn-active-border-bg: var(--ts-violet-2-rgb);
41
+ --bs-btn-disabled-bg: var(--ts-violet-1-rgb);
42
+ --bs-btn-disabled-border-color: var(--ts-violet-1-rgb);
43
+ }
44
+ .btn-outline-primary {
45
+ --bs-btn-color: var(--ts-violet-1-rgb);
46
+ --bs-btn-border-color: var(--ts-violet-1-rgb);
47
+ --bs-btn-hover-bg: var(--ts-violet-1-rgb);
48
+ --bs-btn-hover-border-color: var(--ts-violet-1-rgb);
49
+ --bs-btn-active-bg: var(--ts-violet-1-rgb);
50
+ --bs-btn-active-border-color: var(--ts-violet-1-rgb);
51
+ --bs-btn-disabled-color: var(--ts-violet-1-rgb);
52
+ --bs-btn-disabled-border-color: var(--ts-violet-1-rgb);
53
+ }
54
+ .navbar-dark {
55
+ --bs-link-color: var(--ts-violet-dark-1-rgb);
56
+ --bs-link-hover-color: var(--ts-violet-dark-2-rgb);
57
+ --bs-navbar-color: var(--ts-white-rgb);
58
+ --bs-navbar-hover-color: var(--ts-white-rgb);
59
+ }
60
+ .nav-pills {
61
+ --bs-nav-pills-link-active-bg: var(--ts-green-1-rgb);
62
+ }
63
+ .nav-pills .nav-link:hover {
64
+ color: var(--ts-green-1-rgb);
65
+ }
66
+ .nav-pills .nav-link.active:hover {
67
+ color: var(--ts-white-rgb);
68
+ }
static/webjars/solverforge/img/solverforge-favicon.svg ADDED
static/webjars/solverforge/img/solverforge-horizontal-white.svg ADDED
static/webjars/solverforge/img/solverforge-horizontal.svg ADDED
static/webjars/solverforge/img/solverforge-logo-stacked.svg ADDED
static/webjars/solverforge/js/solverforge-webui.js ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function replaceSolverForgeAutoHeaderFooter() {
2
+ const solverforgeHeader = $("header#solverforge-auto-header");
3
+ if (solverforgeHeader != null) {
4
+ solverforgeHeader.addClass("bg-black")
5
+ solverforgeHeader.append(
6
+ $(`<div class="container-fluid">
7
+ <nav class="navbar sticky-top navbar-expand-lg navbar-dark shadow mb-3">
8
+ <a class="navbar-brand" href="https://www.solverforge.org">
9
+ <img src="/solverforge/img/solverforge-horizontal-white.svg" alt="SolverForge logo" width="200">
10
+ </a>
11
+ </nav>
12
+ </div>`));
13
+ }
14
+ const solverforgeFooter = $("footer#solverforge-auto-footer");
15
+ if (solverforgeFooter != null) {
16
+ solverforgeFooter.append(
17
+ $(`<footer class="bg-black text-white-50">
18
+ <div class="container">
19
+ <div class="hstack gap-3 p-4">
20
+ <div class="ms-auto"><a class="text-white" href="https://www.solverforge.org">SolverForge</a></div>
21
+ <div class="vr"></div>
22
+ <div><a class="text-white" href="https://www.solverforge.org/docs">Documentation</a></div>
23
+ <div class="vr"></div>
24
+ <div><a class="text-white" href="https://github.com/SolverForge/solverforge-legacy">Code</a></div>
25
+ <div class="vr"></div>
26
+ <div class="me-auto"><a class="text-white" href="mailto:info@solverforge.org">Support</a></div>
27
+ </div>
28
+ </div>
29
+ <div id="applicationInfo" class="container text-center"></div>
30
+ </footer>`));
31
+
32
+ applicationInfo();
33
+ }
34
+
35
+ }
36
+
37
+ function showSimpleError(title) {
38
+ const notification = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="min-width: 50rem"/>`)
39
+ .append($(`<div class="toast-header bg-danger">
40
+ <strong class="me-auto text-dark">Error</strong>
41
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
42
+ </div>`))
43
+ .append($(`<div class="toast-body"/>`)
44
+ .append($(`<p/>`).text(title))
45
+ );
46
+ $("#notificationPanel").append(notification);
47
+ notification.toast({delay: 30000});
48
+ notification.toast('show');
49
+ }
50
+
51
+ function showError(title, xhr) {
52
+ var serverErrorMessage = !xhr.responseJSON ? `${xhr.status}: ${xhr.statusText}` : xhr.responseJSON.message;
53
+ var serverErrorCode = !xhr.responseJSON ? `unknown` : xhr.responseJSON.code;
54
+ var serverErrorId = !xhr.responseJSON ? `----` : xhr.responseJSON.id;
55
+ var serverErrorDetails = !xhr.responseJSON ? `no details provided` : xhr.responseJSON.details;
56
+
57
+ if (xhr.responseJSON && !serverErrorMessage) {
58
+ serverErrorMessage = JSON.stringify(xhr.responseJSON);
59
+ serverErrorCode = xhr.statusText + '(' + xhr.status + ')';
60
+ serverErrorId = `----`;
61
+ }
62
+
63
+ console.error(title + "\n" + serverErrorMessage + " : " + serverErrorDetails);
64
+ const notification = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="min-width: 50rem"/>`)
65
+ .append($(`<div class="toast-header bg-danger">
66
+ <strong class="me-auto text-dark">Error</strong>
67
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
68
+ </div>`))
69
+ .append($(`<div class="toast-body"/>`)
70
+ .append($(`<p/>`).text(title))
71
+ .append($(`<pre/>`)
72
+ .append($(`<code/>`).text(serverErrorMessage + "\n\nCode: " + serverErrorCode + "\nError id: " + serverErrorId))
73
+ )
74
+ );
75
+ $("#notificationPanel").append(notification);
76
+ notification.toast({delay: 30000});
77
+ notification.toast('show');
78
+ }
79
+
80
+ // ****************************************************************************
81
+ // Application info
82
+ // ****************************************************************************
83
+
84
+ function applicationInfo() {
85
+ $.getJSON("info", function (info) {
86
+ $("#applicationInfo").append("<small>" + info.application + " (version: " + info.version + ", built at: " + info.built + ")</small>");
87
+ }).fail(function (xhr, ajaxOptions, thrownError) {
88
+ console.warn("Unable to collect application information");
89
+ });
90
+ }
91
+
92
+ // ****************************************************************************
93
+ // TangoColorFactory
94
+ // ****************************************************************************
95
+
96
+ const SEQUENCE_1 = [0x8AE234, 0xFCE94F, 0x729FCF, 0xE9B96E, 0xAD7FA8];
97
+ const SEQUENCE_2 = [0x73D216, 0xEDD400, 0x3465A4, 0xC17D11, 0x75507B];
98
+
99
+ var colorMap = new Map;
100
+ var nextColorCount = 0;
101
+
102
+ function pickColor(object) {
103
+ let color = colorMap[object];
104
+ if (color !== undefined) {
105
+ return color;
106
+ }
107
+ color = nextColor();
108
+ colorMap[object] = color;
109
+ return color;
110
+ }
111
+
112
+ function nextColor() {
113
+ let color;
114
+ let colorIndex = nextColorCount % SEQUENCE_1.length;
115
+ let shadeIndex = Math.floor(nextColorCount / SEQUENCE_1.length);
116
+ if (shadeIndex === 0) {
117
+ color = SEQUENCE_1[colorIndex];
118
+ } else if (shadeIndex === 1) {
119
+ color = SEQUENCE_2[colorIndex];
120
+ } else {
121
+ shadeIndex -= 3;
122
+ let floorColor = SEQUENCE_2[colorIndex];
123
+ let ceilColor = SEQUENCE_1[colorIndex];
124
+ let base = Math.floor((shadeIndex / 2) + 1);
125
+ let divisor = 2;
126
+ while (base >= divisor) {
127
+ divisor *= 2;
128
+ }
129
+ base = (base * 2) - divisor + 1;
130
+ let shadePercentage = base / divisor;
131
+ color = buildPercentageColor(floorColor, ceilColor, shadePercentage);
132
+ }
133
+ nextColorCount++;
134
+ return "#" + color.toString(16);
135
+ }
136
+
137
+ function buildPercentageColor(floorColor, ceilColor, shadePercentage) {
138
+ let red = (floorColor & 0xFF0000) + Math.floor(shadePercentage * ((ceilColor & 0xFF0000) - (floorColor & 0xFF0000))) & 0xFF0000;
139
+ let green = (floorColor & 0x00FF00) + Math.floor(shadePercentage * ((ceilColor & 0x00FF00) - (floorColor & 0x00FF00))) & 0x00FF00;
140
+ let blue = (floorColor & 0x0000FF) + Math.floor(shadePercentage * ((ceilColor & 0x0000FF) - (floorColor & 0x0000FF))) & 0x0000FF;
141
+ return red | green | blue;
142
+ }