@elaraai/e3-ui 1.0.93 → 1.0.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/dist/src/allocation/adapters.d.ts +159 -0
  2. package/dist/src/allocation/adapters.d.ts.map +1 -0
  3. package/dist/src/allocation/adapters.js +241 -0
  4. package/dist/src/allocation/adapters.js.map +1 -0
  5. package/dist/src/allocation/functions.d.ts +664 -0
  6. package/dist/src/allocation/functions.d.ts.map +1 -0
  7. package/dist/src/allocation/functions.js +782 -0
  8. package/dist/src/allocation/functions.js.map +1 -0
  9. package/dist/src/allocation/index.d.ts +2985 -0
  10. package/dist/src/allocation/index.d.ts.map +1 -0
  11. package/dist/src/allocation/index.js +499 -0
  12. package/dist/src/allocation/index.js.map +1 -0
  13. package/dist/src/allocation/types.d.ts +832 -0
  14. package/dist/src/allocation/types.d.ts.map +1 -0
  15. package/dist/src/allocation/types.js +360 -0
  16. package/dist/src/allocation/types.js.map +1 -0
  17. package/dist/src/bind/func.d.ts +14 -0
  18. package/dist/src/bind/func.d.ts.map +1 -1
  19. package/dist/src/bind/func.js +22 -1
  20. package/dist/src/bind/func.js.map +1 -1
  21. package/dist/src/calendar/index.d.ts +510 -1639
  22. package/dist/src/calendar/index.d.ts.map +1 -1
  23. package/dist/src/calendar/index.js +56 -29
  24. package/dist/src/calendar/index.js.map +1 -1
  25. package/dist/src/flowchart/canvas.d.ts +9 -21
  26. package/dist/src/flowchart/canvas.d.ts.map +1 -1
  27. package/dist/src/flowchart/canvas.js +10 -22
  28. package/dist/src/flowchart/canvas.js.map +1 -1
  29. package/dist/src/flowchart/flowchart.d.ts +19 -13
  30. package/dist/src/flowchart/flowchart.d.ts.map +1 -1
  31. package/dist/src/flowchart/flowchart.js.map +1 -1
  32. package/dist/src/flowchart/index.d.ts +44 -21
  33. package/dist/src/flowchart/index.d.ts.map +1 -1
  34. package/dist/src/flowchart/index.js +43 -20
  35. package/dist/src/flowchart/index.js.map +1 -1
  36. package/dist/src/flowchart/library.d.ts +166 -81
  37. package/dist/src/flowchart/library.d.ts.map +1 -1
  38. package/dist/src/flowchart/library.js +232 -105
  39. package/dist/src/flowchart/library.js.map +1 -1
  40. package/dist/src/flowchart/over.d.ts +9 -16
  41. package/dist/src/flowchart/over.d.ts.map +1 -1
  42. package/dist/src/flowchart/over.js +9 -16
  43. package/dist/src/flowchart/over.js.map +1 -1
  44. package/dist/src/flowchart/payload.d.ts +710 -508
  45. package/dist/src/flowchart/payload.d.ts.map +1 -1
  46. package/dist/src/flowchart/payload.js +128 -74
  47. package/dist/src/flowchart/payload.js.map +1 -1
  48. package/dist/src/flowchart/window.d.ts +132 -347
  49. package/dist/src/flowchart/window.d.ts.map +1 -1
  50. package/dist/src/flowchart/window.js +48 -55
  51. package/dist/src/flowchart/window.js.map +1 -1
  52. package/dist/src/index.d.ts +11 -7
  53. package/dist/src/index.d.ts.map +1 -1
  54. package/dist/src/index.js +14 -6
  55. package/dist/src/index.js.map +1 -1
  56. package/dist/src/internal.d.ts +18 -7
  57. package/dist/src/internal.d.ts.map +1 -1
  58. package/dist/src/internal.js +28 -9
  59. package/dist/src/internal.js.map +1 -1
  60. package/dist/src/plan/event-rows.d.ts +26 -10
  61. package/dist/src/plan/event-rows.d.ts.map +1 -1
  62. package/dist/src/plan/event-rows.js +39 -13
  63. package/dist/src/plan/event-rows.js.map +1 -1
  64. package/dist/src/plan/ir.d.ts +244 -815
  65. package/dist/src/plan/ir.d.ts.map +1 -1
  66. package/dist/src/plan/ir.js +8 -6
  67. package/dist/src/plan/ir.js.map +1 -1
  68. package/dist/src/plan/library.d.ts +3 -0
  69. package/dist/src/plan/library.d.ts.map +1 -1
  70. package/dist/src/plan/library.js +1 -1
  71. package/dist/src/plan/library.js.map +1 -1
  72. package/dist/src/plan/ordered-source.d.ts +1 -0
  73. package/dist/src/plan/ordered-source.d.ts.map +1 -1
  74. package/dist/src/plan/plan.d.ts +559 -1667
  75. package/dist/src/plan/plan.d.ts.map +1 -1
  76. package/dist/src/plan/plan.js +123 -45
  77. package/dist/src/plan/plan.js.map +1 -1
  78. package/dist/src/plan/root.d.ts +16 -20
  79. package/dist/src/plan/root.d.ts.map +1 -1
  80. package/dist/src/plan/root.js +23 -33
  81. package/dist/src/plan/root.js.map +1 -1
  82. package/dist/src/roster/coverage.d.ts +17 -231
  83. package/dist/src/roster/coverage.d.ts.map +1 -1
  84. package/dist/src/roster/coverage.js +94 -90
  85. package/dist/src/roster/coverage.js.map +1 -1
  86. package/dist/src/roster/fit.d.ts +59 -0
  87. package/dist/src/roster/fit.d.ts.map +1 -0
  88. package/dist/src/roster/fit.js +155 -0
  89. package/dist/src/roster/fit.js.map +1 -0
  90. package/dist/src/roster/index.d.ts +2862 -3662
  91. package/dist/src/roster/index.d.ts.map +1 -1
  92. package/dist/src/roster/index.js +383 -81
  93. package/dist/src/roster/index.js.map +1 -1
  94. package/dist/src/roster/library.d.ts +37 -92
  95. package/dist/src/roster/library.d.ts.map +1 -1
  96. package/dist/src/roster/library.js +25 -64
  97. package/dist/src/roster/library.js.map +1 -1
  98. package/dist/src/roster/lists.d.ts +195 -0
  99. package/dist/src/roster/lists.d.ts.map +1 -0
  100. package/dist/src/roster/lists.js +233 -0
  101. package/dist/src/roster/lists.js.map +1 -0
  102. package/dist/src/roster/rules.d.ts +17 -260
  103. package/dist/src/roster/rules.d.ts.map +1 -1
  104. package/dist/src/roster/rules.js +135 -132
  105. package/dist/src/roster/rules.js.map +1 -1
  106. package/dist/src/roster/targets.d.ts +115 -0
  107. package/dist/src/roster/targets.d.ts.map +1 -0
  108. package/dist/src/roster/targets.js +213 -0
  109. package/dist/src/roster/targets.js.map +1 -0
  110. package/dist/src/roster/times.d.ts +251 -0
  111. package/dist/src/roster/times.d.ts.map +1 -0
  112. package/dist/src/roster/times.js +174 -0
  113. package/dist/src/roster/times.js.map +1 -0
  114. package/dist/src/roster/types.d.ts +828 -183
  115. package/dist/src/roster/types.d.ts.map +1 -1
  116. package/dist/src/roster/types.js +224 -97
  117. package/dist/src/roster/types.js.map +1 -1
  118. package/dist/src/schedule/accessor.d.ts +28 -0
  119. package/dist/src/schedule/accessor.d.ts.map +1 -0
  120. package/dist/src/schedule/accessor.js +39 -0
  121. package/dist/src/schedule/accessor.js.map +1 -0
  122. package/dist/src/schedule/events.d.ts +170 -42
  123. package/dist/src/schedule/events.d.ts.map +1 -1
  124. package/dist/src/schedule/events.js +431 -171
  125. package/dist/src/schedule/events.js.map +1 -1
  126. package/dist/src/schedule/index.d.ts +41 -4
  127. package/dist/src/schedule/index.d.ts.map +1 -1
  128. package/dist/src/schedule/index.js +35 -3
  129. package/dist/src/schedule/index.js.map +1 -1
  130. package/dist/src/schedule/library.d.ts +13 -3
  131. package/dist/src/schedule/library.d.ts.map +1 -1
  132. package/dist/src/schedule/library.js +11 -4
  133. package/dist/src/schedule/library.js.map +1 -1
  134. package/dist/src/schedule/periods.d.ts +115 -0
  135. package/dist/src/schedule/periods.d.ts.map +1 -0
  136. package/dist/src/schedule/periods.js +215 -0
  137. package/dist/src/schedule/periods.js.map +1 -0
  138. package/dist/src/schedule/record-window.d.ts +25 -3
  139. package/dist/src/schedule/record-window.d.ts.map +1 -1
  140. package/dist/src/schedule/record-window.js +31 -18
  141. package/dist/src/schedule/record-window.js.map +1 -1
  142. package/dist/src/schedule/resources.d.ts +82 -4
  143. package/dist/src/schedule/resources.d.ts.map +1 -1
  144. package/dist/src/schedule/resources.js +44 -14
  145. package/dist/src/schedule/resources.js.map +1 -1
  146. package/dist/src/schedule/session.d.ts +69 -0
  147. package/dist/src/schedule/session.d.ts.map +1 -0
  148. package/dist/src/schedule/session.js +121 -0
  149. package/dist/src/schedule/session.js.map +1 -0
  150. package/dist/src/schedule/templates.d.ts +14 -2
  151. package/dist/src/schedule/templates.d.ts.map +1 -1
  152. package/dist/src/schedule/templates.js +5 -3
  153. package/dist/src/schedule/templates.js.map +1 -1
  154. package/dist/src/schedule/types.d.ts +1671 -37
  155. package/dist/src/schedule/types.d.ts.map +1 -1
  156. package/dist/src/schedule/types.js +196 -5
  157. package/dist/src/schedule/types.js.map +1 -1
  158. package/dist/src/schedule/window.d.ts +14 -0
  159. package/dist/src/schedule/window.d.ts.map +1 -1
  160. package/dist/src/schedule/window.js +14 -4
  161. package/dist/src/schedule/window.js.map +1 -1
  162. package/dist/src/schedule/words.d.ts +22 -0
  163. package/dist/src/schedule/words.d.ts.map +1 -0
  164. package/dist/src/schedule/words.js +25 -0
  165. package/dist/src/schedule/words.js.map +1 -0
  166. package/dist/src/sheet/index.d.ts +61 -27
  167. package/dist/src/sheet/index.d.ts.map +1 -1
  168. package/dist/src/sheet/index.js +59 -25
  169. package/dist/src/sheet/index.js.map +1 -1
  170. package/dist/src/sheet/library.d.ts +21 -3
  171. package/dist/src/sheet/library.d.ts.map +1 -1
  172. package/dist/src/sheet/library.js +23 -6
  173. package/dist/src/sheet/library.js.map +1 -1
  174. package/dist/src/sheet/registers.d.ts +100 -85
  175. package/dist/src/sheet/registers.d.ts.map +1 -1
  176. package/dist/src/sheet/registers.js +100 -85
  177. package/dist/src/sheet/registers.js.map +1 -1
  178. package/dist/src/sheet/root.d.ts +8 -12
  179. package/dist/src/sheet/root.d.ts.map +1 -1
  180. package/dist/src/sheet/root.js +6 -40
  181. package/dist/src/sheet/root.js.map +1 -1
  182. package/dist/src/sheet/sheet.d.ts +598 -2349
  183. package/dist/src/sheet/sheet.d.ts.map +1 -1
  184. package/dist/src/sheet/sheet.js +124 -37
  185. package/dist/src/sheet/sheet.js.map +1 -1
  186. package/dist/src/sheet/sub-rows.d.ts +74 -28
  187. package/dist/src/sheet/sub-rows.d.ts.map +1 -1
  188. package/dist/src/sheet/sub-rows.js +74 -28
  189. package/dist/src/sheet/sub-rows.js.map +1 -1
  190. package/dist/src/sheet/templates.d.ts +98 -34
  191. package/dist/src/sheet/templates.d.ts.map +1 -1
  192. package/dist/src/sheet/templates.js +102 -58
  193. package/dist/src/sheet/templates.js.map +1 -1
  194. package/dist/src/sheet/types.d.ts +289 -1359
  195. package/dist/src/sheet/types.d.ts.map +1 -1
  196. package/dist/src/sheet/types.js +20 -26
  197. package/dist/src/sheet/types.js.map +1 -1
  198. package/dist/src/sheet/views.d.ts +10 -11
  199. package/dist/src/sheet/views.d.ts.map +1 -1
  200. package/dist/src/sheet/views.js +15 -20
  201. package/dist/src/sheet/views.js.map +1 -1
  202. package/dist/src/{roster/visibility.d.ts → utils/key-visibility.d.ts} +42 -7
  203. package/dist/src/utils/key-visibility.d.ts.map +1 -0
  204. package/dist/src/utils/key-visibility.js +36 -0
  205. package/dist/src/utils/key-visibility.js.map +1 -0
  206. package/dist/src/utils/ordered-source.d.ts +1 -0
  207. package/dist/src/utils/ordered-source.d.ts.map +1 -1
  208. package/dist/src/utils/ordered-source.js +8 -3
  209. package/dist/src/utils/ordered-source.js.map +1 -1
  210. package/dist/src/utils/record-sources.d.ts +107 -0
  211. package/dist/src/utils/record-sources.d.ts.map +1 -0
  212. package/dist/src/utils/record-sources.js +132 -0
  213. package/dist/src/utils/record-sources.js.map +1 -0
  214. package/dist/test/allocation/allocation.examples.d.ts +2902 -0
  215. package/dist/test/allocation/allocation.examples.d.ts.map +1 -0
  216. package/dist/test/allocation/allocation.examples.js +643 -0
  217. package/dist/test/allocation/allocation.examples.js.map +1 -0
  218. package/dist/test/allocation/fit.examples.d.ts +546 -0
  219. package/dist/test/allocation/fit.examples.d.ts.map +1 -0
  220. package/dist/test/allocation/fit.examples.js +171 -0
  221. package/dist/test/allocation/fit.examples.js.map +1 -0
  222. package/dist/test/allocation/kiln-yard.d.ts +181 -0
  223. package/dist/test/allocation/kiln-yard.d.ts.map +1 -0
  224. package/dist/test/allocation/kiln-yard.js +228 -0
  225. package/dist/test/allocation/kiln-yard.js.map +1 -0
  226. package/dist/test/bind/data/data.examples.d.ts +651 -1
  227. package/dist/test/bind/data/data.examples.d.ts.map +1 -1
  228. package/dist/test/bind/data/data.examples.js +140 -2
  229. package/dist/test/bind/data/data.examples.js.map +1 -1
  230. package/dist/test/bind/func/func.examples.d.ts +10 -1
  231. package/dist/test/bind/func/func.examples.d.ts.map +1 -1
  232. package/dist/test/bind/func/func.examples.js +8 -1
  233. package/dist/test/bind/func/func.examples.js.map +1 -1
  234. package/dist/test/bind/record/record.examples.d.ts +633 -1
  235. package/dist/test/bind/record/record.examples.d.ts.map +1 -1
  236. package/dist/test/bind/record/record.examples.js +26 -2
  237. package/dist/test/bind/record/record.examples.js.map +1 -1
  238. package/dist/test/calendar/calendar-records.examples.d.ts.map +1 -1
  239. package/dist/test/calendar/calendar-records.examples.js +18 -14
  240. package/dist/test/calendar/calendar-records.examples.js.map +1 -1
  241. package/dist/test/calendar/calendar.examples.d.ts +521 -16
  242. package/dist/test/calendar/calendar.examples.d.ts.map +1 -1
  243. package/dist/test/calendar/calendar.examples.js +182 -135
  244. package/dist/test/calendar/calendar.examples.js.map +1 -1
  245. package/dist/test/examples-package.d.ts +75 -0
  246. package/dist/test/examples-package.d.ts.map +1 -0
  247. package/dist/test/examples-package.js +263 -0
  248. package/dist/test/examples-package.js.map +1 -0
  249. package/dist/test/flowchart/flowchart.examples.d.ts +211 -19
  250. package/dist/test/flowchart/flowchart.examples.d.ts.map +1 -1
  251. package/dist/test/flowchart/flowchart.examples.js +102 -75
  252. package/dist/test/flowchart/flowchart.examples.js.map +1 -1
  253. package/dist/test/plan/plan-events.examples.d.ts +1439 -209
  254. package/dist/test/plan/plan-events.examples.d.ts.map +1 -1
  255. package/dist/test/plan/plan-events.examples.js +199 -59
  256. package/dist/test/plan/plan-events.examples.js.map +1 -1
  257. package/dist/test/plan/plan-records.examples.d.ts +420 -0
  258. package/dist/test/plan/plan-records.examples.d.ts.map +1 -1
  259. package/dist/test/plan/plan-records.examples.js +70 -19
  260. package/dist/test/plan/plan-records.examples.js.map +1 -1
  261. package/dist/test/plan/plan-windows.examples.d.ts +636 -55
  262. package/dist/test/plan/plan-windows.examples.d.ts.map +1 -1
  263. package/dist/test/plan/plan-windows.examples.js +69 -33
  264. package/dist/test/plan/plan-windows.examples.js.map +1 -1
  265. package/dist/test/plan/plan.examples.d.ts +153 -31
  266. package/dist/test/plan/plan.examples.d.ts.map +1 -1
  267. package/dist/test/plan/plan.examples.js +178 -101
  268. package/dist/test/plan/plan.examples.js.map +1 -1
  269. package/dist/test/roster/domain.examples.d.ts +467 -250
  270. package/dist/test/roster/domain.examples.d.ts.map +1 -1
  271. package/dist/test/roster/domain.examples.js +92 -29
  272. package/dist/test/roster/domain.examples.js.map +1 -1
  273. package/dist/test/roster/roster-records.examples.d.ts +430 -586
  274. package/dist/test/roster/roster-records.examples.d.ts.map +1 -1
  275. package/dist/test/roster/roster-records.examples.js +102 -43
  276. package/dist/test/roster/roster-records.examples.js.map +1 -1
  277. package/dist/test/roster/roster.examples.d.ts +2319 -606
  278. package/dist/test/roster/roster.examples.d.ts.map +1 -1
  279. package/dist/test/roster/roster.examples.js +456 -148
  280. package/dist/test/roster/roster.examples.js.map +1 -1
  281. package/dist/test/roster/warehouse.d.ts +172 -63
  282. package/dist/test/roster/warehouse.d.ts.map +1 -1
  283. package/dist/test/roster/warehouse.js +172 -39
  284. package/dist/test/roster/warehouse.js.map +1 -1
  285. package/dist/test/schedule/schedule.examples.d.ts +3499 -0
  286. package/dist/test/schedule/schedule.examples.d.ts.map +1 -0
  287. package/dist/test/schedule/schedule.examples.js +192 -0
  288. package/dist/test/schedule/schedule.examples.js.map +1 -0
  289. package/dist/test/sheet/sheet-records.examples.d.ts.map +1 -1
  290. package/dist/test/sheet/sheet-records.examples.js +22 -18
  291. package/dist/test/sheet/sheet-records.examples.js.map +1 -1
  292. package/dist/test/sheet/sheet.examples.d.ts +715 -26
  293. package/dist/test/sheet/sheet.examples.d.ts.map +1 -1
  294. package/dist/test/sheet/sheet.examples.js +204 -130
  295. package/dist/test/sheet/sheet.examples.js.map +1 -1
  296. package/package.json +17 -10
  297. package/dist/src/roster/people.d.ts +0 -50
  298. package/dist/src/roster/people.d.ts.map +0 -1
  299. package/dist/src/roster/people.js +0 -32
  300. package/dist/src/roster/people.js.map +0 -1
  301. package/dist/src/roster/visibility.d.ts.map +0 -1
  302. package/dist/src/roster/visibility.js +0 -25
  303. package/dist/src/roster/visibility.js.map +0 -1
@@ -0,0 +1,782 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
4
+ */
5
+ /**
6
+ * The Allocation's pure functions (`Allocation Spec.md` §5.1, §12): a day's
7
+ * loads, a fit's candidates, the built-in rules, an answer to a fit's
8
+ * question, a greedy look-ahead, the built-in checks and the day's slots, and
9
+ * a bin as the board reads it from the app's row. Tasks, the app's
10
+ * `e3.function` fit and the renderer run these same East functions, so the
11
+ * screen and e3 agree on every figure and every word.
12
+ *
13
+ * Those over a day take the app's bin row type (#1371, `Builder Interfaces
14
+ * Spec.md` §6.3), as `Editing.apply` takes its source type:
15
+ * `Allocation.loads(KilnBin)` is an East function over
16
+ * `Allocation.Types.Day(KilnBin)`. They read each bin's roles, never its row,
17
+ * which they carry for the app's own functions. `Allocation.fit` reads one
18
+ * candidate, so it needs none.
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+ import { ArrayType, BooleanType, DateTimeType, DictType, East, FloatType, FunctionType, IntegerType, OptionType, SetType, StringType, StructType, none, some, variant, } from "@elaraai/east";
23
+ import { StatusTokenType, TimeStepType } from "@elaraai/east-ui";
24
+ import { ScheduleAllocationRolesType } from "../schedule/types.js";
25
+ import { scheduleNumberText } from "../schedule/words.js";
26
+ import { AllocationArrivalType, AllocationBinTypeFor, AllocationCandidateType, AllocationContextType, AllocationDayTypeFor, AllocationFitRequestTypeFor, AllocationFitResultType, AllocationFitType, AllocationFixType, AllocationIntervalType, AllocationIssueType, AllocationJobType, AllocationLoadType, AllocationResourceLineType, AllocationResourceType, AllocationRulesType, AllocationSlotType, } from "./types.js";
27
+ /** A day, in milliseconds. */
28
+ const DAY_MS = 86_400_000;
29
+ // ============================================================================
30
+ // Words and time
31
+ // ============================================================================
32
+ /**
33
+ * A duration in milliseconds — a month counts 30 days, as Schedule's do.
34
+ *
35
+ * @param step - The duration
36
+ * @returns Its length in milliseconds
37
+ */
38
+ export const allocationMilliseconds = East.function([TimeStepType], IntegerType, (_$, step) => East.Float.roundHalf(step.match({
39
+ minutes: (_$2, n) => n.multiply(60_000.0),
40
+ hours: (_$2, n) => n.multiply(3_600_000.0),
41
+ days: (_$2, n) => n.multiply(86_400_000.0),
42
+ weeks: (_$2, n) => n.multiply(604_800_000.0),
43
+ months: (_$2, n) => n.multiply(2_592_000_000.0),
44
+ })));
45
+ /**
46
+ * A quantity and its unit.
47
+ *
48
+ * @param value - The quantity
49
+ * @param unit - The unit, or empty
50
+ * @returns `21 m³`, or `21` with no unit
51
+ */
52
+ export const allocationQuantityText = East.function([FloatType, StringType], StringType, ($, value, unit) => {
53
+ const num = $.const(scheduleNumberText);
54
+ return East.equal(unit, "").ifElse(() => num(value), () => East.str `${num(value)} ${unit}`);
55
+ });
56
+ /**
57
+ * A day as the board names it.
58
+ *
59
+ * @param at - An instant on the day
60
+ * @returns `Wed 19`
61
+ */
62
+ export const allocationDayText = East.function([DateTimeType], StringType, (_$, at) => at.printFormatted("ddd D"));
63
+ /**
64
+ * A resource's load, naming the limit it breaks (A19).
65
+ *
66
+ * @param fill - What its bins put on it
67
+ * @param resource - The resource
68
+ * @param unit - The quantity's unit
69
+ * @returns `11 m³ · max 9 · 2 over`, `16 m³ · min 40 · 24 short`, or `45 m³ · takes 26–48`
70
+ */
71
+ export const allocationLoadText = East.function([FloatType, AllocationResourceType, StringType], StringType, ($, fill, resource, unit) => {
72
+ const num = $.const(scheduleNumberText);
73
+ const qty = $.const(allocationQuantityText);
74
+ const f = $.const(East.Float.roundHalf(fill).toFloat());
75
+ const out = $.let(East.str `${qty(f, unit)} · takes ${num(resource.min)}–${num(resource.max)}`);
76
+ $.if(East.greater(f, resource.max), ($2) => {
77
+ $2.assign(out, East.str `${qty(f, unit)} · max ${num(resource.max)} · ${num(f.subtract(resource.max))} over`);
78
+ }).else(($2) => {
79
+ $2.if(East.greater(f, 0.0).and(() => East.less(f, resource.min)), ($3) => {
80
+ $3.assign(out, East.str `${qty(f, unit)} · min ${num(resource.min)} · ${num(resource.min.subtract(f))} short`);
81
+ });
82
+ });
83
+ return out;
84
+ });
85
+ /**
86
+ * The job that makes a resource busy on a day: one covering the day's start,
87
+ * the latest to end of them.
88
+ *
89
+ * @param resource - The resource
90
+ * @param day - The plan day
91
+ * @returns The job, or `none`
92
+ */
93
+ export const allocationBusyJob = East.function([AllocationResourceType, DateTimeType], OptionType(AllocationJobType), ($, resource, day) => {
94
+ const found = $.let(none, OptionType(AllocationJobType));
95
+ $.for(resource.jobs, ($2, job) => {
96
+ $2.if(East.lessEqual(job.start, day).and(() => East.greater(job.end, day)), ($3) => {
97
+ $3.match(found, {
98
+ none: ($4) => { $4.assign(found, some(job)); },
99
+ some: ($4, current) => { $4.if(East.greater(job.end, current.end), ($5) => { $5.assign(found, some(job)); }); },
100
+ });
101
+ });
102
+ });
103
+ return found;
104
+ });
105
+ /**
106
+ * A resource's next job after a day's start: the first to start.
107
+ *
108
+ * @param resource - The resource
109
+ * @param day - The plan day
110
+ * @returns The job, or `none`
111
+ */
112
+ export const allocationNextJob = East.function([AllocationResourceType, DateTimeType], OptionType(AllocationJobType), ($, resource, day) => {
113
+ const found = $.let(none, OptionType(AllocationJobType));
114
+ $.for(resource.jobs, ($2, job) => {
115
+ $2.if(East.greater(job.start, day), ($3) => {
116
+ $3.match(found, {
117
+ none: ($4) => { $4.assign(found, some(job)); },
118
+ some: ($4, current) => { $4.if(East.less(job.start, current.start), ($5) => { $5.assign(found, some(job)); }); },
119
+ });
120
+ });
121
+ });
122
+ return found;
123
+ });
124
+ /**
125
+ * A resource's line on the plan day (A19), and the marks its bins' cards carry
126
+ * (A16).
127
+ *
128
+ * @param day - The plan day
129
+ * @param resource - The resource
130
+ * @param load - Its load
131
+ * @param unit - The quantity's unit
132
+ * @returns Its line, its tone, whether it is over its maximum, under its minimum, or held into its next job, and that job
133
+ *
134
+ * @remarks
135
+ * Loading, the line is its load (`45 m³ · takes 26–48`; `reserved · takes
136
+ * 26–48` with only reservations) and `· 2 d into next job` when its hold runs
137
+ * into the job; busy, `busy until Fri 14`; free, its meta (or its group) and
138
+ * what it takes (`Hall B · 40 m³ · takes 20–40 m³`).
139
+ */
140
+ export const allocationResourceLine = East.function([DateTimeType, AllocationResourceType, AllocationLoadType, StringType], AllocationResourceLineType, ($, day, resource, load, unit) => {
141
+ const busyOf = $.const(allocationBusyJob);
142
+ const nextOf = $.const(allocationNextJob);
143
+ const dayText = $.const(allocationDayText);
144
+ const num = $.const(scheduleNumberText);
145
+ const qty = $.const(allocationQuantityText);
146
+ const loadText = $.const(allocationLoadText);
147
+ const over = $.const(East.greater(load.fill, resource.max));
148
+ const under = $.const(East.greater(load.fill, 0.0).and(() => East.less(load.fill, resource.min)));
149
+ const next = $.const(nextOf(resource, day));
150
+ const overlap = $.let(false, BooleanType);
151
+ const into = $.let(0.0, FloatType);
152
+ $.match(load.hold, {
153
+ some: ($2, hold) => {
154
+ $2.match(next, {
155
+ some: ($3, job) => {
156
+ $3.if(East.greater(hold.to, job.start), ($4) => {
157
+ $4.assign(overlap, true);
158
+ $4.assign(into, job.start.durationMilliseconds(hold.to).toFloat().divide(DAY_MS));
159
+ });
160
+ },
161
+ });
162
+ },
163
+ });
164
+ const text = $.let("", StringType);
165
+ $.match(load.state, {
166
+ loading: ($2) => {
167
+ const held = $2.const(East.greater(load.fill, 0.0).ifElse(() => loadText(load.fill, resource, unit), () => East.str `reserved · takes ${num(resource.min)}–${num(resource.max)}`));
168
+ $2.assign(text, overlap.ifElse(() => East.str `${held} · ${num(into)} d into next job`, () => held));
169
+ },
170
+ busy: ($2) => {
171
+ $2.assign(text, busyOf(resource, day).match({
172
+ some: (_$3, job) => East.str `busy until ${dayText(job.end)}`,
173
+ none: (_$3) => East.str `busy`,
174
+ }));
175
+ },
176
+ free: ($2) => {
177
+ const takes = $2.const(East.str `takes ${num(resource.min)}–${qty(resource.max, unit)}`);
178
+ $2.assign(text, resource.meta.match({
179
+ some: (_$3, meta) => East.str `${meta} · ${takes}`,
180
+ none: (_$3) => resource.group.match({ some: (_$4, group) => East.str `${group} · ${takes}`, none: (_$4) => takes }),
181
+ }));
182
+ },
183
+ });
184
+ return {
185
+ text,
186
+ tone: over.ifElse(() => East.value(variant("danger", null), StatusTokenType), () => overlap.or(() => under).ifElse(() => East.value(variant("warning", null), StatusTokenType), () => East.value(variant("neutral", null), StatusTokenType))),
187
+ over, under, overlap, next,
188
+ };
189
+ });
190
+ // ============================================================================
191
+ // A bin, as the board reads it from the app's row
192
+ // ============================================================================
193
+ /**
194
+ * One bin, as the board reads it on its plan day (#1371, B12): its kind's
195
+ * roles beside its row — a stored bin's roles as the kind's `rows` gives them,
196
+ * a drafted one's as its `read` does.
197
+ *
198
+ * @typeParam R - The app's bin row type
199
+ * @param row - The app's bin row type
200
+ * @returns An East function `(roles, row, day) → Option<Bin(R)>`: the bin, when its `at` falls in the day `[day, day + 1 d)`; `none` for a bin of another day
201
+ *
202
+ * @remarks
203
+ * A bin's UTC day is its plan day (`Builder Interfaces Spec.md` §6.2), so the
204
+ * board's day holds the bins whose `at` falls in it, `day` its UTC midnight.
205
+ * Each bin's `at` is read as it is, so a batch's bins, booked a minute apart,
206
+ * read back a minute apart, in one slot (`Allocation.slots`).
207
+ * @internal
208
+ */
209
+ export function allocationBinOf(row) {
210
+ const Bin = AllocationBinTypeFor(row);
211
+ return East.function([ScheduleAllocationRolesType, row, DateTimeType], OptionType(Bin), ($, roles, value, day) => {
212
+ $.if(East.less(roles.at, day).or(() => East.greaterEqual(roles.at, day.addDays(1n))), ($2) => { $2.return(none); });
213
+ return some({
214
+ lane: roles.lane, at: roles.at, arrival: roles.arrival, quantity: roles.quantity, kind: roles.kind, hold: roles.hold, parts: roles.parts,
215
+ row: value,
216
+ });
217
+ });
218
+ }
219
+ // ============================================================================
220
+ // Mates: what a resource already holds, without the app's rows
221
+ // ============================================================================
222
+ /**
223
+ * One bin's part on one resource, as loads and candidates read it.
224
+ *
225
+ * @property bin - The bin's id; empty for a placement the look-ahead adds
226
+ * @property quantity - Its part's quantity
227
+ * @property hold - Its hold, in milliseconds
228
+ * @property kind - Its kind
229
+ * @property reserve - Whether it is a reservation
230
+ * @internal
231
+ */
232
+ export const AllocationMateType = StructType({ bin: StringType, quantity: FloatType, hold: IntegerType, kind: OptionType(StringType), reserve: BooleanType });
233
+ /** The bins' parts on every resource, by the resource's key. @internal */
234
+ const MatesType = DictType(StringType, ArrayType(AllocationMateType));
235
+ /**
236
+ * Every resource's parts of the day's bins, an entry for every resource.
237
+ *
238
+ * @typeParam R - The app's bin row type
239
+ * @param row - The app's bin row type
240
+ * @returns An East function from the day and the context to the parts on each resource
241
+ * @internal
242
+ */
243
+ export function allocationMates(row) {
244
+ return East.function([AllocationDayTypeFor(row), AllocationContextType], MatesType, ($, plan, context) => {
245
+ const step = $.const(allocationMilliseconds);
246
+ const out = $.let(context.resources.map((_$2, _resource) => East.value([], ArrayType(AllocationMateType))));
247
+ $.for(plan.bins, ($2, bin, id) => {
248
+ const hold = $2.const(step(bin.hold));
249
+ const reserve = $2.const(bin.arrival.hasTag("none"));
250
+ $2.for(bin.parts, ($3, quantity, resource) => {
251
+ $3.if(out.has(resource), ($4) => {
252
+ $4(out.get(resource).pushLast({ bin: id, quantity, hold, kind: bin.kind, reserve }));
253
+ });
254
+ });
255
+ });
256
+ return out;
257
+ });
258
+ }
259
+ /**
260
+ * A resource's load from its parts.
261
+ *
262
+ * @param day - The plan day
263
+ * @param resource - The resource
264
+ * @param mates - Its parts
265
+ * @returns Its load
266
+ * @internal
267
+ */
268
+ export const allocationLoadOf = East.function([DateTimeType, AllocationResourceType, ArrayType(AllocationMateType)], AllocationLoadType, ($, day, resource, mates) => {
269
+ const step = $.const(allocationMilliseconds);
270
+ const busyOf = $.const(allocationBusyJob);
271
+ const fill = $.let(0.0, FloatType);
272
+ const longest = $.let(none, OptionType(IntegerType));
273
+ $.for(mates, ($2, mate) => {
274
+ $2.assign(fill, fill.add(mate.quantity));
275
+ $2.if(mate.reserve.not(), ($3) => {
276
+ $3.match(longest, {
277
+ none: ($4) => { $4.assign(longest, some(mate.hold)); },
278
+ some: ($4, held) => { $4.assign(longest, some(East.max(held, mate.hold))); },
279
+ });
280
+ });
281
+ });
282
+ // A resource holding only reservations is held for its own hold.
283
+ const length = $.const(longest.unwrap("some", () => step(resource.hold)));
284
+ const hold = $.let(none, OptionType(AllocationIntervalType));
285
+ $.if(East.greater(mates.size(), 0n), ($2) => { $2.assign(hold, some({ from: day, to: day.addMilliseconds(length) })); });
286
+ const busy = $.const(busyOf(resource, day).hasTag("some"));
287
+ return {
288
+ resource: resource.key, fill, bins: mates.size(), hold,
289
+ state: East.greater(mates.size(), 0n).ifElse(() => East.value(variant("loading", null), AllocationLoadType.fields.state), () => busy.ifElse(() => East.value(variant("busy", null), AllocationLoadType.fields.state), () => East.value(variant("free", null), AllocationLoadType.fields.state))),
290
+ };
291
+ });
292
+ /**
293
+ * One candidate: a quantity on a resource, against the parts it already holds.
294
+ *
295
+ * @param day - The plan day
296
+ * @param bin - The bin it is for, if any
297
+ * @param reserve - Whether it reserves a slot
298
+ * @param quantity - What the resource would hold of it
299
+ * @param arrival - Its arrival, if any
300
+ * @param kind - Its kind
301
+ * @param hold - Its hold, in milliseconds
302
+ * @param resource - The resource
303
+ * @param mates - The resource's other parts
304
+ * @param load - The resource's load
305
+ * @param rules - The built-in rules
306
+ * @param unit - The quantity's unit
307
+ * @returns The candidate
308
+ * @internal
309
+ */
310
+ export const allocationCandidateOf = East.function([
311
+ DateTimeType, OptionType(StringType), BooleanType, FloatType, OptionType(AllocationArrivalType), OptionType(StringType), IntegerType,
312
+ AllocationResourceType, ArrayType(AllocationMateType), AllocationLoadType, AllocationRulesType, StringType,
313
+ ], AllocationCandidateType, ($, day, bin, reserve, quantity, arrival, kind, hold, resource, mates, load, rules, unit) => {
314
+ const nextOf = $.const(allocationNextJob);
315
+ const fill = $.let(0.0, FloatType);
316
+ const length = $.let(hold, IntegerType);
317
+ const kindMates = $.let(false, BooleanType);
318
+ $.for(mates, ($2, mate) => {
319
+ $2.assign(fill, fill.add(mate.quantity));
320
+ $2.assign(length, East.max(length, mate.hold));
321
+ $2.if(kind.hasTag("some").and(() => East.equal(mate.kind, kind)), ($3) => { $3.assign(kindMates, true); });
322
+ });
323
+ const to = $.const(day.addMilliseconds(length));
324
+ const spare = $.let(none, OptionType(FloatType));
325
+ $.match(nextOf(resource, day), {
326
+ some: ($2, job) => { $2.assign(spare, some(to.durationMilliseconds(job.start).toFloat().divide(DAY_MS))); },
327
+ });
328
+ return {
329
+ day, bin, reserve, quantity, arrival, kind, resource, load, after: fill.add(quantity), hold: { from: day, to }, spare, kindMates,
330
+ otherMates: East.greater(mates.size(), 0n).and(() => kindMates.not()), rules, unit,
331
+ };
332
+ });
333
+ // ============================================================================
334
+ // The built-in rules
335
+ // ============================================================================
336
+ /**
337
+ * The built-in rules (§12.3): one verdict for one candidate.
338
+ *
339
+ * @param candidate - A quantity on a resource, against the drafted day
340
+ * @returns Its verdict
341
+ *
342
+ * @remarks
343
+ * In order: a job covering the day's start refuses it (`busy until Fri 14`);
344
+ * a hold running past the next job's start refuses it (`gap too short · next
345
+ * job Wed 19`); a fill over the maximum refuses it (`over max by 4 m³`); a
346
+ * reservation on a resource already at its maximum is refused (`full · …`).
347
+ * Otherwise it fits: its score is what it fills of the maximum, plus 0.25 when
348
+ * a bin of its kind is already there, less 0.1 when only other kinds are, less
349
+ * 0.5 under the minimum, less 0.04 a spare day up to six; its reason the spare
350
+ * days and the load it leaves; under the minimum it warns, when the rules ask.
351
+ */
352
+ export const allocationFit = East.function([AllocationCandidateType], AllocationFitType, ($, candidate) => {
353
+ const busyOf = $.const(allocationBusyJob);
354
+ const nextOf = $.const(allocationNextJob);
355
+ const dayText = $.const(allocationDayText);
356
+ const qty = $.const(allocationQuantityText);
357
+ const num = $.const(scheduleNumberText);
358
+ const loadText = $.const(allocationLoadText);
359
+ const resource = $.const(candidate.resource);
360
+ $.match(busyOf(resource, candidate.day), {
361
+ some: ($2, job) => {
362
+ $2.return(variant("refused", { group: "Busy", reason: East.str `busy until ${dayText(job.end)}`, measure: variant("day", { lead: "until", at: job.end }) }));
363
+ },
364
+ });
365
+ const next = $.const(nextOf(resource, candidate.day));
366
+ $.match(next, {
367
+ some: ($2, job) => {
368
+ $2.if(East.greater(candidate.hold.to, job.start), ($3) => {
369
+ $3.return(variant("refused", {
370
+ group: "Gap too short", reason: East.str `gap too short · next job ${dayText(job.start)}`,
371
+ measure: variant("day", { lead: "next job", at: job.start }),
372
+ }));
373
+ });
374
+ },
375
+ });
376
+ $.if(East.greater(candidate.after, resource.max), ($2) => {
377
+ const by = $2.const(East.Float.roundHalf(candidate.after.subtract(resource.max)).toFloat());
378
+ $2.return(variant("refused", { group: "Over max", reason: East.str `over max by ${qty(by, candidate.unit)}`, measure: variant("amount", { lead: "by", value: by }) }));
379
+ });
380
+ const fill = $.const(candidate.after.subtract(candidate.quantity));
381
+ $.if(candidate.reserve.and(() => East.greaterEqual(fill, resource.max)), ($2) => {
382
+ $2.return(variant("refused", { group: "Full", reason: East.str `full · ${loadText(fill, resource, candidate.unit)}`, measure: variant("none", null) }));
383
+ });
384
+ // The score, term for term in the mock's order, so equal fits tie exactly.
385
+ const spare = $.const(candidate.spare.unwrap("some", () => 9.0));
386
+ const score = $.const(candidate.after.divide(resource.max)
387
+ .add(candidate.kindMates.ifElse(() => 0.25, () => 0.0))
388
+ .subtract(candidate.otherMates.ifElse(() => 0.1, () => 0.0))
389
+ .subtract(East.less(candidate.after, resource.min).ifElse(() => 0.5, () => 0.0))
390
+ .subtract(East.min(spare, $.const(6.0, FloatType)).multiply(0.04)));
391
+ const gap = $.const(next.match({
392
+ some: (_$2, job) => East.str `${num(spare)} d spare to ${dayText(job.start)}`,
393
+ none: (_$2) => East.str `open gap`,
394
+ }));
395
+ return variant("fits", {
396
+ score, why: East.str `${gap} · leaves ${loadText(candidate.after, resource, candidate.unit)}`,
397
+ warn: East.less(candidate.after, resource.min).and(() => candidate.rules.under).ifElse(() => East.value(some("under min"), OptionType(StringType)), () => East.value(none, OptionType(StringType))),
398
+ });
399
+ });
400
+ // ============================================================================
401
+ // Loads, candidates and answers
402
+ // ============================================================================
403
+ /**
404
+ * Every resource's load on the plan day.
405
+ *
406
+ * @typeParam R - The app's bin row type
407
+ * @param row - The app's bin row type
408
+ * @returns An East function `(day, plan, context) → Dict<String, Load>`: an entry for every resource, by key
409
+ */
410
+ export function allocationLoads(row) {
411
+ const matesOf = allocationMates(row);
412
+ return East.function([DateTimeType, AllocationDayTypeFor(row), AllocationContextType], DictType(StringType, AllocationLoadType), ($, day, plan, context) => {
413
+ const parts = $.const(matesOf);
414
+ const loadOf = $.const(allocationLoadOf);
415
+ const mates = $.const(parts(plan, context));
416
+ return context.resources.map(($2, resource, key) => loadOf(day, resource, mates.get(key)));
417
+ });
418
+ }
419
+ /**
420
+ * The candidates for a fit's question.
421
+ *
422
+ * @typeParam R - The app's bin row type
423
+ * @param row - The app's bin row type
424
+ * @returns An East function `(request) → Array<Candidate>`: a place's subject on every resource, in the resources' key order; or for a take, every bin on its resource, then every arrival the day has not booked, then a reservation
425
+ *
426
+ * @remarks
427
+ * A bin is placed with its own kind and hold, an arrival with its, and a bin
428
+ * booking no arrival — a reservation — with no quantity and each resource's
429
+ * own hold (§12.3, rule 4); the resource's other parts are every bin's but
430
+ * the subject's. A take places each bin's unallocated quantity (with its part
431
+ * already there), or the whole bin when it has none; then each arrival no bin
432
+ * of the day books, whole, as a place of it would; and last a reservation with
433
+ * the resource's hold.
434
+ */
435
+ export function allocationCandidates(row) {
436
+ const matesOf = allocationMates(row);
437
+ return East.function([AllocationFitRequestTypeFor(row)], ArrayType(AllocationCandidateType), ($, request) => {
438
+ const parts = $.const(matesOf);
439
+ const loadOf = $.const(allocationLoadOf);
440
+ const candidateOf = $.const(allocationCandidateOf);
441
+ const step = $.const(allocationMilliseconds);
442
+ const context = $.const(request.context);
443
+ const day = $.const(request.day);
444
+ const mates = $.const(parts(request.plan, context));
445
+ const out = $.let([], ArrayType(AllocationCandidateType));
446
+ $.match(request.ask, {
447
+ place: ($2, place) => {
448
+ const bin = $2.let(none, OptionType(StringType));
449
+ const arrival = $2.let(none, OptionType(AllocationArrivalType));
450
+ const kind = $2.let(none, OptionType(StringType));
451
+ const hold = $2.let(0n, IntegerType);
452
+ const reserve = $2.let(false, BooleanType);
453
+ $2.match(place.subject, {
454
+ bin: ($3, id) => {
455
+ const subject = $3.const(request.plan.bins.get(id));
456
+ $3.assign(bin, some(id));
457
+ $3.match(subject.arrival, { some: ($4, key) => { $4.assign(arrival, context.arrivals.tryGet(key)); } });
458
+ $3.assign(kind, subject.kind);
459
+ $3.assign(hold, step(subject.hold));
460
+ $3.assign(reserve, subject.arrival.hasTag("none"));
461
+ },
462
+ arrival: ($3, key) => {
463
+ const subject = $3.const(context.arrivals.get(key));
464
+ $3.assign(arrival, some(subject));
465
+ $3.assign(kind, subject.kind);
466
+ $3.assign(hold, step(subject.hold));
467
+ },
468
+ });
469
+ const subject = $2.const(bin.unwrap("some", () => ""));
470
+ $2.for(context.resources, ($3, resource, key) => {
471
+ const all = $3.const(mates.get(key));
472
+ const others = $3.const(all.filter(($4, mate) => East.notEqual(mate.bin, subject)));
473
+ const length = $3.const(reserve.ifElse(() => step(resource.hold), () => hold));
474
+ $3(out.pushLast(candidateOf(day, bin, reserve, place.quantity, arrival, kind, length, resource, others, loadOf(day, resource, all), context.rules, context.unit)));
475
+ });
476
+ },
477
+ take: ($2, take) => {
478
+ const resource = $2.const(context.resources.get(take.resource));
479
+ const all = $2.const(mates.get(take.resource));
480
+ const load = $2.const(loadOf(day, resource, all));
481
+ $2.for(request.plan.bins, ($3, bin, id) => {
482
+ const allocated = $3.let(0.0, FloatType);
483
+ $3.for(bin.parts, ($4, part) => { $4.assign(allocated, allocated.add(part)); });
484
+ const reserve = $3.const(bin.arrival.hasTag("none"));
485
+ const quantity = $3.let(bin.quantity, FloatType);
486
+ $3.if(reserve, ($4) => { $4.assign(quantity, 0.0); }).else(($4) => {
487
+ $4.if(East.less(allocated, bin.quantity), ($5) => {
488
+ $5.assign(quantity, bin.quantity.subtract(allocated).add(bin.parts.get(take.resource, () => 0.0)));
489
+ });
490
+ });
491
+ const arrival = $3.let(none, OptionType(AllocationArrivalType));
492
+ $3.match(bin.arrival, { some: ($4, key) => { $4.assign(arrival, context.arrivals.tryGet(key)); } });
493
+ const others = $3.const(all.filter(($4, mate) => East.notEqual(mate.bin, id)));
494
+ $3(out.pushLast(candidateOf(day, some(id), reserve, quantity, arrival, bin.kind, step(bin.hold), resource, others, load, context.rules, context.unit)));
495
+ });
496
+ // Every arrival the day has not booked, whole on the resource.
497
+ const booked = $2.let(new Set(), SetType(StringType));
498
+ $2.for(request.plan.bins, ($3, bin) => {
499
+ $3.match(bin.arrival, { some: ($4, key) => { $4.if(booked.has(key).not(), ($5) => { $5(booked.insert(key)); }); } });
500
+ });
501
+ $2.for(context.arrivals, ($3, arrival, key) => {
502
+ $3.if(booked.has(key).not(), ($4) => {
503
+ $4(out.pushLast(candidateOf(day, none, false, arrival.quantity, some(arrival), arrival.kind, step(arrival.hold), resource, all, load, context.rules, context.unit)));
504
+ });
505
+ });
506
+ $2(out.pushLast(candidateOf(day, none, true, 0.0, none, none, step(resource.hold), resource, all, load, context.rules, context.unit)));
507
+ },
508
+ });
509
+ return out;
510
+ });
511
+ }
512
+ /**
513
+ * Answers a fit's question with a fit per candidate.
514
+ *
515
+ * @typeParam R - The app's bin row type
516
+ * @param row - The app's bin row type
517
+ * @returns An East function `(request, fit, basis) → FitResult`: a place's verdict for every resource, or a take's for every bin, every arrival the day has not booked, and reserving; `basis` says in words how the fit ranks (`ranked by load, kind and spare days`)
518
+ */
519
+ export function allocationAnswer(row) {
520
+ const candidatesOf = allocationCandidates(row);
521
+ return East.function([
522
+ AllocationFitRequestTypeFor(row), FunctionType([AllocationCandidateType], AllocationFitType), StringType,
523
+ ], AllocationFitResultType, ($, request, fit, basis) => {
524
+ const candidates = $.const(candidatesOf);
525
+ const resources = $.let(new Map(), DictType(StringType, AllocationFitType));
526
+ const bins = $.let(new Map(), DictType(StringType, AllocationFitType));
527
+ const arrivals = $.let(new Map(), DictType(StringType, AllocationFitType));
528
+ const reserve = $.let(none, OptionType(AllocationFitType));
529
+ $.for(candidates(request), ($2, candidate) => {
530
+ $2.match(request.ask, {
531
+ place: ($3) => { $3(resources.insert(candidate.resource.key, fit(candidate))); },
532
+ take: ($3) => {
533
+ $3.match(candidate.bin, {
534
+ some: ($4, id) => { $4(bins.insert(id, fit(candidate))); },
535
+ none: ($4) => {
536
+ $4.match(candidate.arrival, {
537
+ some: ($5, arrival) => { $5(arrivals.insert(arrival.key, fit(candidate))); },
538
+ none: ($5) => { $5.assign(reserve, some(fit(candidate))); },
539
+ });
540
+ },
541
+ });
542
+ },
543
+ });
544
+ });
545
+ return { ask: request.ask, resources, bins, arrivals, reserve, basis };
546
+ });
547
+ }
548
+ /**
549
+ * A greedy look-ahead over a fit (§12.4): what a placement leaves for the
550
+ * day's other arrivals.
551
+ *
552
+ * @typeParam R - The app's bin row type
553
+ * @param row - The app's bin row type
554
+ * @returns An East function `(request, candidate, fit) → Fit`
555
+ *
556
+ * @remarks
557
+ * A refusal is returned as `fit` gives it. A fit places the candidate — a bin
558
+ * moves whole onto the candidate's resource with the candidate's quantity —
559
+ * then each of the day's unbooked arrivals, largest first (ties in key
560
+ * order), on the resource `fit` scores best for it (ties in key order), and
561
+ * lowers the score by one for each arrival left with nowhere to go, adding
562
+ * `· leaves 1 load with no fit` to its reason.
563
+ */
564
+ export function allocationLookahead(row) {
565
+ const matesOf = allocationMates(row);
566
+ return East.function([
567
+ AllocationFitRequestTypeFor(row), AllocationCandidateType, FunctionType([AllocationCandidateType], AllocationFitType),
568
+ ], AllocationFitType, ($, request, candidate, fit) => {
569
+ const parts = $.const(matesOf);
570
+ const loadOf = $.const(allocationLoadOf);
571
+ const candidateOf = $.const(allocationCandidateOf);
572
+ const step = $.const(allocationMilliseconds);
573
+ const context = $.const(request.context);
574
+ const day = $.const(request.day);
575
+ const verdict = $.const(fit(candidate));
576
+ $.match(verdict, { refused: ($2) => { $2.return(verdict); } });
577
+ const fits = $.const(verdict.unwrap("fits"));
578
+ // The candidate placed: its bin off every resource, its quantity on its resource.
579
+ const subject = $.const(candidate.bin.unwrap("some", () => ""));
580
+ const placed = $.const(parts(request.plan, context).map(($2, list) => list.filter(($3, mate) => East.notEqual(mate.bin, subject))));
581
+ $(placed.get(candidate.resource.key).pushLast({
582
+ bin: "", quantity: candidate.quantity, hold: day.durationMilliseconds(candidate.hold.to), kind: candidate.kind, reserve: candidate.reserve,
583
+ }));
584
+ // The day's unbooked arrivals, largest first.
585
+ const booked = $.let(new Set(), SetType(StringType));
586
+ $.for(request.plan.bins, ($2, bin) => {
587
+ $2.match(bin.arrival, { some: ($3, key) => { $3.if(booked.has(key).not(), ($4) => { $4(booked.insert(key)); }); } });
588
+ });
589
+ $.match(candidate.arrival, { some: ($2, arrival) => { $2.if(booked.has(arrival.key).not(), ($3) => { $3(booked.insert(arrival.key)); }); } });
590
+ const waiting = $.const(context.arrivals.filter(($2, _arrival, key) => booked.has(key).not()).toArray(($2, arrival) => arrival)
591
+ .sort(($2, arrival) => ({ quantity: arrival.quantity.negate(), key: arrival.key })));
592
+ const left = $.let(0n, IntegerType);
593
+ $.for(waiting, ($2, arrival) => {
594
+ const hold = $2.const(step(arrival.hold));
595
+ const best = $2.let(none, OptionType(StringType));
596
+ const bestScore = $2.let(0.0, FloatType);
597
+ $2.for(context.resources, ($3, resource, key) => {
598
+ const mates = $3.const(placed.get(key));
599
+ const there = $3.const(candidateOf(day, none, false, arrival.quantity, some(arrival), arrival.kind, hold, resource, mates, loadOf(day, resource, mates), context.rules, context.unit));
600
+ $3.match(fit(there), {
601
+ fits: ($4, it) => {
602
+ $4.if(best.hasTag("none").or(() => East.greater(it.score, bestScore)), ($5) => {
603
+ $5.assign(best, some(key));
604
+ $5.assign(bestScore, it.score);
605
+ });
606
+ },
607
+ });
608
+ });
609
+ $2.match(best, {
610
+ some: ($3, key) => { $3(placed.get(key).pushLast({ bin: "", quantity: arrival.quantity, hold, kind: arrival.kind, reserve: false })); },
611
+ none: ($3) => { $3.assign(left, left.add(1n)); },
612
+ });
613
+ });
614
+ const leaves = $.const(East.equal(left, 1n).ifElse(() => East.str ` · leaves 1 load with no fit`, () => East.str ` · leaves ${left} loads with no fit`));
615
+ return East.greater(left, 0n).ifElse(() => East.value(variant("fits", { score: fits.score.subtract(left.toFloat()), why: East.str `${fits.why}${leaves}`, warn: fits.warn }), AllocationFitType), () => verdict);
616
+ });
617
+ }
618
+ // ============================================================================
619
+ // The built-in checks
620
+ // ============================================================================
621
+ /**
622
+ * The built-in checks (§12.3), over committed and drafted bins alike.
623
+ *
624
+ * @typeParam R - The app's bin row type
625
+ * @param row - The app's bin row type
626
+ * @returns An East function `(day, plan, context) → Array<Issue>`: bins not linked, then partly allocated, then resources whose hold overlaps their next job, over their maximum, and under their minimum when the rules ask
627
+ *
628
+ * @remarks
629
+ * Issues never block Save or Publish. A resource's issue offers to move its
630
+ * latest bin; the app's `check` follows these wherever issues show.
631
+ */
632
+ export function allocationCheck(row) {
633
+ const loadsOf = allocationLoads(row);
634
+ return East.function([DateTimeType, AllocationDayTypeFor(row), AllocationContextType], ArrayType(AllocationIssueType), ($, day, plan, context) => {
635
+ const loadsFor = $.const(loadsOf);
636
+ const nextOf = $.const(allocationNextJob);
637
+ const qty = $.const(allocationQuantityText);
638
+ const num = $.const(scheduleNumberText);
639
+ const dayText = $.const(allocationDayText);
640
+ const loadText = $.const(allocationLoadText);
641
+ const loads = $.const(loadsFor(day, plan, context));
642
+ const lanes = $.const(context.lanes.toDict(($2, lane) => lane.key, ($2, lane) => lane.label));
643
+ const issues = $.let([], ArrayType(AllocationIssueType));
644
+ const where = $.const(East.function([StringType, DateTimeType], StringType, ($2, lane, at) => East.str `${lanes.get(lane, () => lane)} ${at.printFormatted("HH:mm")}`));
645
+ const labelOf = $.const(East.function([StringType], StringType, ($2, key) => context.arrivals.tryGet(key).match({
646
+ some: (_$3, arrival) => arrival.label, none: (_$3) => key,
647
+ })));
648
+ // Bins: not linked, then partly allocated.
649
+ $.for(plan.bins, ($2, bin, id) => {
650
+ $2.match(bin.arrival, {
651
+ some: ($3, key) => {
652
+ $3.if(East.equal(bin.parts.size(), 0n), ($4) => {
653
+ $4(issues.pushLast({ kind: variant("unlinked", null), tone: variant("warning", null),
654
+ title: East.str `${labelOf(key)} · ${qty(bin.quantity, context.unit)}`, detail: East.str `${where(bin.lane, bin.at)} · no resource yet`,
655
+ bin: some(id), resource: none, flag: none, fix: some(variant("choose", null)) }));
656
+ });
657
+ },
658
+ });
659
+ });
660
+ $.for(plan.bins, ($2, bin, id) => {
661
+ $2.match(bin.arrival, {
662
+ some: ($3, key) => {
663
+ const allocated = $3.let(0.0, FloatType);
664
+ $3.for(bin.parts, ($4, part) => { $4.assign(allocated, allocated.add(part)); });
665
+ $3.if(East.greater(bin.parts.size(), 0n).and(() => East.less(allocated, bin.quantity)), ($4) => {
666
+ $4(issues.pushLast({ kind: variant("unallocated", null), tone: variant("warning", null),
667
+ title: East.str `${labelOf(key)} · ${qty(bin.quantity, context.unit)}`,
668
+ detail: East.str `${qty(bin.quantity.subtract(allocated), context.unit)} unallocated · ${where(bin.lane, bin.at)}`,
669
+ bin: some(id), resource: none, flag: none, fix: some(variant("find", null)) }));
670
+ });
671
+ },
672
+ });
673
+ });
674
+ // The latest bin booking an arrival on each resource: what its issues offer to move.
675
+ const lastOf = $.const(East.function([StringType], OptionType(StringType), ($2, resource) => {
676
+ const last = $2.let(none, OptionType(StringType));
677
+ const lastAt = $2.let(day, DateTimeType);
678
+ $2.for(plan.bins, ($3, bin, id) => {
679
+ $3.if(bin.arrival.hasTag("some").and(() => bin.parts.has(resource)), ($4) => {
680
+ $4.if(last.hasTag("none").or(() => East.greaterEqual(bin.at, lastAt)), ($5) => {
681
+ $5.assign(last, some(id));
682
+ $5.assign(lastAt, bin.at);
683
+ });
684
+ });
685
+ });
686
+ return last;
687
+ }));
688
+ const moveOf = $.const(East.function([StringType], OptionType(AllocationFixType), ($2, resource) => {
689
+ const fix = $2.let(none, OptionType(AllocationFixType));
690
+ $2.match(lastOf(resource), { some: ($3, id) => { $3.assign(fix, some(variant("move", id))); } });
691
+ return fix;
692
+ }));
693
+ $.for(context.resources, ($2, resource, key) => {
694
+ const load = $2.const(loads.get(key));
695
+ $2.match(load.hold, {
696
+ some: ($3, hold) => {
697
+ $3.match(nextOf(resource, day), {
698
+ some: ($4, job) => {
699
+ $4.if(East.greater(hold.to, job.start), ($5) => {
700
+ const length = $5.const(day.durationMilliseconds(hold.to).toFloat().divide(DAY_MS));
701
+ const into = $5.const(job.start.durationMilliseconds(hold.to).toFloat().divide(DAY_MS));
702
+ $5(issues.pushLast({ kind: variant("overlap", null), tone: variant("warning", null), title: resource.label,
703
+ detail: East.str `${num(length)}-day hold to ${dayText(hold.to.addMilliseconds(-1n))} · runs ${num(into)} d into the job planned ${dayText(job.start)}`,
704
+ bin: none, resource: some(key), flag: none, fix: moveOf(key) }));
705
+ });
706
+ },
707
+ });
708
+ },
709
+ });
710
+ });
711
+ $.for(context.resources, ($2, resource, key) => {
712
+ const load = $2.const(loads.get(key));
713
+ $2.if(East.greater(load.fill, resource.max), ($3) => {
714
+ $3(issues.pushLast({ kind: variant("over", null), tone: variant("danger", null), title: resource.label,
715
+ detail: East.str `${loadText(load.fill, resource, context.unit)} · over max`, bin: none, resource: some(key), flag: none, fix: moveOf(key) }));
716
+ });
717
+ });
718
+ $.for(context.resources, ($2, resource, key) => {
719
+ const load = $2.const(loads.get(key));
720
+ $2.if(context.rules.under.and(() => East.greater(load.fill, 0.0)).and(() => East.less(load.fill, resource.min))
721
+ .and(() => East.lessEqual(load.fill, resource.max)), ($3) => {
722
+ $3(issues.pushLast({ kind: variant("under", null), tone: variant("warning", null), title: resource.label,
723
+ detail: East.str `${loadText(load.fill, resource, context.unit)} · a warning, Save still works`,
724
+ bin: none, resource: some(key), flag: none, fix: moveOf(key) }));
725
+ });
726
+ });
727
+ return issues;
728
+ });
729
+ }
730
+ // ============================================================================
731
+ // Slots
732
+ // ============================================================================
733
+ /**
734
+ * The plan day's lane slots: one per lane per `every`, in time then lane order.
735
+ *
736
+ * @typeParam R - The app's bin row type
737
+ * @param row - The app's bin row type
738
+ * @returns An East function `(day, plan, context) → Array<Slot>`
739
+ *
740
+ * @remarks
741
+ * A bin is in the slot whose time it follows: from ten minutes before the
742
+ * slot's time to ten minutes before the next one's, as a batch follows its
743
+ * slot by a minute a bin. A slot holding a bin booking an arrival is booked,
744
+ * one holding only reservations reserved, an empty one free. A bin in a lane
745
+ * the context does not have is in no slot.
746
+ */
747
+ export function allocationSlots(row) {
748
+ return East.function([DateTimeType, AllocationDayTypeFor(row), AllocationContextType], ArrayType(AllocationSlotType), ($, day, plan, context) => {
749
+ const step = $.const(allocationMilliseconds);
750
+ const every = $.const(step(context.every));
751
+ $.if(East.lessEqual(every, 0n), ($2) => { $2.error("Allocation: `every` must be longer than no time"); });
752
+ const count = $.const(East.value(BigInt(DAY_MS)).divide(every));
753
+ const laneIndex = $.const(context.lanes.toDict(($2, lane) => lane.key, ($2, _lane, i) => i));
754
+ // Each lane's slots, each its bins' ids.
755
+ const held = $.let(context.lanes.map((_$2) => East.Array.generate(count, ArrayType(StringType), (_$3) => East.value([], ArrayType(StringType)))));
756
+ $.for(plan.bins, ($2, bin, id) => {
757
+ $2.match(laneIndex.tryGet(bin.lane), {
758
+ some: ($3, lane) => {
759
+ const offset = $3.const(step(context.lanes.get(lane).offset));
760
+ const since = $3.const(day.durationMilliseconds(bin.at).subtract(offset).add(600000n));
761
+ const index = $3.const(East.max(since, $3.const(0n, IntegerType)).divide(every).remainder(count));
762
+ $3(held.get(lane).get(index).pushLast(id));
763
+ },
764
+ });
765
+ });
766
+ const out = $.let([], ArrayType(AllocationSlotType));
767
+ $.for(context.lanes, ($2, lane, li) => {
768
+ const offset = $2.const(step(lane.offset));
769
+ $2.for(held.get(li), ($3, ids, k) => {
770
+ const sorted = $3.const(ids.sort(($4, id) => ({ at: plan.bins.get(id).at, id })));
771
+ const booked = $3.const(sorted.some(($4, id) => plan.bins.get(id).arrival.hasTag("some")));
772
+ $3(out.pushLast({
773
+ lane: lane.key, at: day.addMilliseconds(k.multiply(every).add(offset)),
774
+ status: booked.ifElse(() => East.value(variant("booked", null), AllocationSlotType.fields.status), () => East.greater(sorted.size(), 0n).ifElse(() => East.value(variant("reserved", null), AllocationSlotType.fields.status), () => East.value(variant("free", null), AllocationSlotType.fields.status))),
775
+ bins: sorted,
776
+ }));
777
+ });
778
+ });
779
+ return out.sort(($2, slot) => ({ at: slot.at, lane: laneIndex.get(slot.lane) }));
780
+ });
781
+ }
782
+ //# sourceMappingURL=functions.js.map