@nowline/layout 0.8.6 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/dist/axis-columns.d.ts +70 -0
  2. package/dist/axis-columns.d.ts.map +1 -0
  3. package/dist/axis-columns.js +104 -0
  4. package/dist/axis-columns.js.map +1 -0
  5. package/dist/calendar-resolver.d.ts +27 -0
  6. package/dist/calendar-resolver.d.ts.map +1 -0
  7. package/dist/calendar-resolver.js +132 -0
  8. package/dist/calendar-resolver.js.map +1 -0
  9. package/dist/calendar.d.ts +0 -19
  10. package/dist/calendar.d.ts.map +1 -1
  11. package/dist/calendar.js +4 -44
  12. package/dist/calendar.js.map +1 -1
  13. package/dist/canvas-width.d.ts +23 -0
  14. package/dist/canvas-width.d.ts.map +1 -0
  15. package/dist/canvas-width.js +26 -0
  16. package/dist/canvas-width.js.map +1 -0
  17. package/dist/capacity.d.ts +28 -0
  18. package/dist/capacity.d.ts.map +1 -1
  19. package/dist/capacity.js +64 -29
  20. package/dist/capacity.js.map +1 -1
  21. package/dist/chart-translation.d.ts +22 -0
  22. package/dist/chart-translation.d.ts.map +1 -0
  23. package/dist/chart-translation.js +83 -0
  24. package/dist/chart-translation.js.map +1 -0
  25. package/dist/container-header-geometry.d.ts +64 -0
  26. package/dist/container-header-geometry.d.ts.map +1 -0
  27. package/dist/container-header-geometry.js +89 -0
  28. package/dist/container-header-geometry.js.map +1 -0
  29. package/dist/dsl-utils.d.ts +3 -4
  30. package/dist/dsl-utils.d.ts.map +1 -1
  31. package/dist/dsl-utils.js +8 -25
  32. package/dist/dsl-utils.js.map +1 -1
  33. package/dist/edge-routing.d.ts +69 -78
  34. package/dist/edge-routing.d.ts.map +1 -1
  35. package/dist/edge-routing.js +271 -401
  36. package/dist/edge-routing.js.map +1 -1
  37. package/dist/frame-tab-geometry.d.ts +24 -14
  38. package/dist/frame-tab-geometry.d.ts.map +1 -1
  39. package/dist/frame-tab-geometry.js +69 -23
  40. package/dist/frame-tab-geometry.js.map +1 -1
  41. package/dist/header-card-geometry.d.ts +12 -0
  42. package/dist/header-card-geometry.d.ts.map +1 -1
  43. package/dist/header-card-geometry.js +14 -2
  44. package/dist/header-card-geometry.js.map +1 -1
  45. package/dist/header-logo.d.ts +28 -0
  46. package/dist/header-logo.d.ts.map +1 -0
  47. package/dist/header-logo.js +51 -0
  48. package/dist/header-logo.js.map +1 -0
  49. package/dist/i18n.d.ts +11 -0
  50. package/dist/i18n.d.ts.map +1 -1
  51. package/dist/i18n.js +11 -1
  52. package/dist/i18n.js.map +1 -1
  53. package/dist/index.d.ts +20 -8
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +17 -7
  56. package/dist/index.js.map +1 -1
  57. package/dist/inline-date-pin-geometry.d.ts +19 -11
  58. package/dist/inline-date-pin-geometry.d.ts.map +1 -1
  59. package/dist/inline-date-pin-geometry.js +45 -46
  60. package/dist/inline-date-pin-geometry.js.map +1 -1
  61. package/dist/item-bar-geometry.d.ts +192 -4
  62. package/dist/item-bar-geometry.d.ts.map +1 -1
  63. package/dist/item-bar-geometry.js +258 -5
  64. package/dist/item-bar-geometry.js.map +1 -1
  65. package/dist/item-port-geometry.d.ts +13 -0
  66. package/dist/item-port-geometry.d.ts.map +1 -0
  67. package/dist/item-port-geometry.js +63 -0
  68. package/dist/item-port-geometry.js.map +1 -0
  69. package/dist/layout-context.d.ts +115 -51
  70. package/dist/layout-context.d.ts.map +1 -1
  71. package/dist/layout-context.js +96 -5
  72. package/dist/layout-context.js.map +1 -1
  73. package/dist/layout-insights.d.ts.map +1 -1
  74. package/dist/layout-insights.js +73 -1
  75. package/dist/layout-insights.js.map +1 -1
  76. package/dist/layout.d.ts +35 -0
  77. package/dist/layout.d.ts.map +1 -1
  78. package/dist/layout.js +18 -1315
  79. package/dist/layout.js.map +1 -1
  80. package/dist/nodes/anchor-node.d.ts.map +1 -1
  81. package/dist/nodes/anchor-node.js +11 -4
  82. package/dist/nodes/anchor-node.js.map +1 -1
  83. package/dist/nodes/content-extent.d.ts +24 -0
  84. package/dist/nodes/content-extent.d.ts.map +1 -0
  85. package/dist/nodes/content-extent.js +100 -0
  86. package/dist/nodes/content-extent.js.map +1 -0
  87. package/dist/nodes/dependency-edges.d.ts +6 -0
  88. package/dist/nodes/dependency-edges.d.ts.map +1 -0
  89. package/dist/nodes/dependency-edges.js +148 -0
  90. package/dist/nodes/dependency-edges.js.map +1 -0
  91. package/dist/nodes/footnote-node.d.ts +24 -3
  92. package/dist/nodes/footnote-node.d.ts.map +1 -1
  93. package/dist/nodes/footnote-node.js +129 -25
  94. package/dist/nodes/footnote-node.js.map +1 -1
  95. package/dist/nodes/group-node.d.ts +10 -21
  96. package/dist/nodes/group-node.d.ts.map +1 -1
  97. package/dist/nodes/group-node.js +79 -152
  98. package/dist/nodes/group-node.js.map +1 -1
  99. package/dist/nodes/header-node.d.ts +56 -0
  100. package/dist/nodes/header-node.d.ts.map +1 -0
  101. package/dist/nodes/header-node.js +163 -0
  102. package/dist/nodes/header-node.js.map +1 -0
  103. package/dist/nodes/include-node.d.ts +22 -12
  104. package/dist/nodes/include-node.d.ts.map +1 -1
  105. package/dist/nodes/include-node.js +95 -62
  106. package/dist/nodes/include-node.js.map +1 -1
  107. package/dist/nodes/item-measure.d.ts +135 -0
  108. package/dist/nodes/item-measure.d.ts.map +1 -0
  109. package/dist/nodes/item-measure.js +514 -0
  110. package/dist/nodes/item-measure.js.map +1 -0
  111. package/dist/nodes/item-node.d.ts +162 -9
  112. package/dist/nodes/item-node.d.ts.map +1 -1
  113. package/dist/nodes/item-node.js +256 -69
  114. package/dist/nodes/item-node.js.map +1 -1
  115. package/dist/nodes/marker-band.d.ts +118 -0
  116. package/dist/nodes/marker-band.d.ts.map +1 -0
  117. package/dist/nodes/marker-band.js +303 -0
  118. package/dist/nodes/marker-band.js.map +1 -0
  119. package/dist/nodes/marker-geometry.js +1 -1
  120. package/dist/nodes/marker-geometry.js.map +1 -1
  121. package/dist/nodes/milestone-node.d.ts +112 -15
  122. package/dist/nodes/milestone-node.d.ts.map +1 -1
  123. package/dist/nodes/milestone-node.js +246 -95
  124. package/dist/nodes/milestone-node.js.map +1 -1
  125. package/dist/nodes/nowline-node.d.ts +8 -0
  126. package/dist/nodes/nowline-node.d.ts.map +1 -0
  127. package/dist/nodes/nowline-node.js +74 -0
  128. package/dist/nodes/nowline-node.js.map +1 -0
  129. package/dist/nodes/parallel-node.d.ts +7 -12
  130. package/dist/nodes/parallel-node.d.ts.map +1 -1
  131. package/dist/nodes/parallel-node.js +66 -30
  132. package/dist/nodes/parallel-node.js.map +1 -1
  133. package/dist/nodes/region-markers.d.ts +41 -0
  134. package/dist/nodes/region-markers.d.ts.map +1 -0
  135. package/dist/nodes/region-markers.js +130 -0
  136. package/dist/nodes/region-markers.js.map +1 -0
  137. package/dist/nodes/roadmap-node.d.ts +9 -72
  138. package/dist/nodes/roadmap-node.d.ts.map +1 -1
  139. package/dist/nodes/roadmap-node.js +38 -788
  140. package/dist/nodes/roadmap-node.js.map +1 -1
  141. package/dist/nodes/row-track.d.ts +33 -0
  142. package/dist/nodes/row-track.d.ts.map +1 -0
  143. package/dist/nodes/row-track.js +170 -0
  144. package/dist/nodes/row-track.js.map +1 -0
  145. package/dist/nodes/sequence-item.d.ts +25 -0
  146. package/dist/nodes/sequence-item.d.ts.map +1 -0
  147. package/dist/nodes/sequence-item.js +396 -0
  148. package/dist/nodes/sequence-item.js.map +1 -0
  149. package/dist/nodes/sequence-one.d.ts +5 -0
  150. package/dist/nodes/sequence-one.d.ts.map +1 -0
  151. package/dist/nodes/sequence-one.js +26 -0
  152. package/dist/nodes/sequence-one.js.map +1 -0
  153. package/dist/nodes/swimlane-node.d.ts +5 -17
  154. package/dist/nodes/swimlane-node.d.ts.map +1 -1
  155. package/dist/nodes/swimlane-node.js +62 -145
  156. package/dist/nodes/swimlane-node.js.map +1 -1
  157. package/dist/nodes/wave-node.d.ts +52 -0
  158. package/dist/nodes/wave-node.d.ts.map +1 -0
  159. package/dist/nodes/wave-node.js +483 -0
  160. package/dist/nodes/wave-node.js.map +1 -0
  161. package/dist/non-working-display.d.ts +14 -0
  162. package/dist/non-working-display.d.ts.map +1 -0
  163. package/dist/non-working-display.js +28 -0
  164. package/dist/non-working-display.js.map +1 -0
  165. package/dist/phases/date-window.d.ts +48 -0
  166. package/dist/phases/date-window.d.ts.map +1 -0
  167. package/dist/phases/date-window.js +130 -0
  168. package/dist/phases/date-window.js.map +1 -0
  169. package/dist/phases/finalize.d.ts +198 -0
  170. package/dist/phases/finalize.d.ts.map +1 -0
  171. package/dist/phases/finalize.js +1058 -0
  172. package/dist/phases/finalize.js.map +1 -0
  173. package/dist/phases/measure.d.ts +77 -0
  174. package/dist/phases/measure.d.ts.map +1 -0
  175. package/dist/phases/measure.js +222 -0
  176. package/dist/phases/measure.js.map +1 -0
  177. package/dist/phases/place.d.ts +28 -0
  178. package/dist/phases/place.d.ts.map +1 -0
  179. package/dist/phases/place.js +225 -0
  180. package/dist/phases/place.js.map +1 -0
  181. package/dist/renderable.d.ts.map +1 -1
  182. package/dist/renderable.js +23 -3
  183. package/dist/renderable.js.map +1 -1
  184. package/dist/resolve-today.d.ts +11 -1
  185. package/dist/resolve-today.d.ts.map +1 -1
  186. package/dist/resolve-today.js +26 -12
  187. package/dist/resolve-today.js.map +1 -1
  188. package/dist/row-packer.d.ts +42 -16
  189. package/dist/row-packer.d.ts.map +1 -1
  190. package/dist/row-packer.js +51 -78
  191. package/dist/row-packer.js.map +1 -1
  192. package/dist/schedule.d.ts +58 -1
  193. package/dist/schedule.d.ts.map +1 -1
  194. package/dist/schedule.js +280 -106
  195. package/dist/schedule.js.map +1 -1
  196. package/dist/start-rules.d.ts +27 -0
  197. package/dist/start-rules.d.ts.map +1 -0
  198. package/dist/start-rules.js +91 -0
  199. package/dist/start-rules.js.map +1 -0
  200. package/dist/start-terms.d.ts +23 -0
  201. package/dist/start-terms.d.ts.map +1 -0
  202. package/dist/start-terms.js +27 -0
  203. package/dist/start-terms.js.map +1 -0
  204. package/dist/status-dot.d.ts +46 -0
  205. package/dist/status-dot.d.ts.map +1 -0
  206. package/dist/status-dot.js +78 -0
  207. package/dist/status-dot.js.map +1 -0
  208. package/dist/style-resolution.d.ts.map +1 -1
  209. package/dist/style-resolution.js +23 -43
  210. package/dist/style-resolution.js.map +1 -1
  211. package/dist/text-measure.d.ts +28 -0
  212. package/dist/text-measure.d.ts.map +1 -0
  213. package/dist/text-measure.js +70 -0
  214. package/dist/text-measure.js.map +1 -0
  215. package/dist/themes/contrast.d.ts +4 -0
  216. package/dist/themes/contrast.d.ts.map +1 -0
  217. package/dist/themes/contrast.js +32 -0
  218. package/dist/themes/contrast.js.map +1 -0
  219. package/dist/themes/dark.d.ts.map +1 -1
  220. package/dist/themes/dark.js +28 -1
  221. package/dist/themes/dark.js.map +1 -1
  222. package/dist/themes/grayscale.d.ts.map +1 -1
  223. package/dist/themes/grayscale.js +28 -1
  224. package/dist/themes/grayscale.js.map +1 -1
  225. package/dist/themes/index.d.ts +4 -11
  226. package/dist/themes/index.d.ts.map +1 -1
  227. package/dist/themes/index.js +6 -4
  228. package/dist/themes/index.js.map +1 -1
  229. package/dist/themes/light.d.ts.map +1 -1
  230. package/dist/themes/light.js +28 -1
  231. package/dist/themes/light.js.map +1 -1
  232. package/dist/themes/shape.d.ts +31 -0
  233. package/dist/themes/shape.d.ts.map +1 -1
  234. package/dist/themes/shared.d.ts +44 -50
  235. package/dist/themes/shared.d.ts.map +1 -1
  236. package/dist/themes/shared.js +79 -83
  237. package/dist/themes/shared.js.map +1 -1
  238. package/dist/time-scale.d.ts +42 -6
  239. package/dist/time-scale.d.ts.map +1 -1
  240. package/dist/time-scale.js +109 -17
  241. package/dist/time-scale.js.map +1 -1
  242. package/dist/types.d.ts +528 -11
  243. package/dist/types.d.ts.map +1 -1
  244. package/dist/view-preset.d.ts +61 -6
  245. package/dist/view-preset.d.ts.map +1 -1
  246. package/dist/view-preset.js +439 -24
  247. package/dist/view-preset.js.map +1 -1
  248. package/dist/wave-barrier.d.ts +107 -0
  249. package/dist/wave-barrier.d.ts.map +1 -0
  250. package/dist/wave-barrier.js +174 -0
  251. package/dist/wave-barrier.js.map +1 -0
  252. package/dist/wave-hatch.d.ts +4 -0
  253. package/dist/wave-hatch.d.ts.map +1 -0
  254. package/dist/wave-hatch.js +21 -0
  255. package/dist/wave-hatch.js.map +1 -0
  256. package/dist/wave-layout.d.ts +99 -0
  257. package/dist/wave-layout.d.ts.map +1 -0
  258. package/dist/wave-layout.js +130 -0
  259. package/dist/wave-layout.js.map +1 -0
  260. package/dist/working-calendar.d.ts +128 -4
  261. package/dist/working-calendar.d.ts.map +1 -1
  262. package/dist/working-calendar.js +725 -51
  263. package/dist/working-calendar.js.map +1 -1
  264. package/dist/working-span.d.ts +11 -0
  265. package/dist/working-span.d.ts.map +1 -0
  266. package/dist/working-span.js +18 -0
  267. package/dist/working-span.js.map +1 -0
  268. package/package.json +4 -4
  269. package/src/axis-columns.ts +140 -0
  270. package/src/calendar-resolver.ts +161 -0
  271. package/src/calendar.ts +4 -45
  272. package/src/canvas-width.ts +36 -0
  273. package/src/capacity.ts +71 -30
  274. package/src/chart-translation.ts +88 -0
  275. package/src/container-header-geometry.ts +116 -0
  276. package/src/dsl-utils.ts +8 -26
  277. package/src/edge-routing.ts +347 -491
  278. package/src/frame-tab-geometry.ts +78 -25
  279. package/src/header-card-geometry.ts +17 -2
  280. package/src/header-logo.ts +67 -0
  281. package/src/i18n.ts +22 -1
  282. package/src/index.ts +97 -12
  283. package/src/inline-date-pin-geometry.ts +55 -58
  284. package/src/item-bar-geometry.ts +341 -5
  285. package/src/item-port-geometry.ts +69 -0
  286. package/src/layout-context.ts +210 -72
  287. package/src/layout-insights.ts +109 -2
  288. package/src/layout.ts +57 -1530
  289. package/src/nodes/anchor-node.ts +11 -4
  290. package/src/nodes/content-extent.ts +107 -0
  291. package/src/nodes/dependency-edges.ts +171 -0
  292. package/src/nodes/footnote-node.ts +171 -31
  293. package/src/nodes/group-node.ts +92 -214
  294. package/src/nodes/header-node.ts +255 -0
  295. package/src/nodes/include-node.ts +124 -110
  296. package/src/nodes/item-measure.ts +723 -0
  297. package/src/nodes/item-node.ts +358 -80
  298. package/src/nodes/marker-band.ts +406 -0
  299. package/src/nodes/marker-geometry.ts +1 -1
  300. package/src/nodes/milestone-node.ts +321 -136
  301. package/src/nodes/nowline-node.ts +87 -0
  302. package/src/nodes/parallel-node.ts +69 -38
  303. package/src/nodes/region-markers.ts +194 -0
  304. package/src/nodes/roadmap-node.ts +46 -950
  305. package/src/nodes/row-track.ts +217 -0
  306. package/src/nodes/sequence-item.ts +433 -0
  307. package/src/nodes/sequence-one.ts +40 -0
  308. package/src/nodes/swimlane-node.ts +66 -205
  309. package/src/nodes/wave-node.ts +609 -0
  310. package/src/non-working-display.ts +32 -0
  311. package/src/phases/date-window.ts +191 -0
  312. package/src/phases/finalize.ts +1303 -0
  313. package/src/phases/measure.ts +329 -0
  314. package/src/phases/place.ts +318 -0
  315. package/src/renderable.ts +23 -3
  316. package/src/resolve-today.ts +28 -25
  317. package/src/row-packer.ts +85 -84
  318. package/src/schedule.ts +362 -114
  319. package/src/start-rules.ts +113 -0
  320. package/src/start-terms.ts +38 -0
  321. package/src/status-dot.ts +81 -0
  322. package/src/style-resolution.ts +27 -57
  323. package/src/text-measure.ts +71 -0
  324. package/src/themes/contrast.ts +34 -0
  325. package/src/themes/dark.ts +28 -1
  326. package/src/themes/grayscale.ts +28 -1
  327. package/src/themes/index.ts +10 -7
  328. package/src/themes/light.ts +28 -1
  329. package/src/themes/shape.ts +38 -0
  330. package/src/themes/shared.ts +85 -93
  331. package/src/time-scale.ts +117 -19
  332. package/src/types.ts +529 -20
  333. package/src/view-preset.ts +520 -27
  334. package/src/wave-barrier.ts +242 -0
  335. package/src/wave-hatch.ts +25 -0
  336. package/src/wave-layout.ts +203 -0
  337. package/src/working-calendar.ts +875 -57
  338. package/src/working-span.ts +19 -0
package/src/schedule.ts CHANGED
@@ -1,40 +1,63 @@
1
1
  // scheduleRoadmap — compute the floating calendar start/end date for every
2
2
  // named entity (items, milestones, anchors) without running the full layout.
3
3
  //
4
- // The sequencing rules exactly mirror `computeContentEndDay` in `layout.ts`
5
- // plus the roadmap-start resolution in `RoadmapNode.place`:
6
- // - date: / start: — absolute pin (date: wins over start:)
7
- // - after:[id|DATE] — start after the maximum predecessor end
4
+ // This is the only day-level scheduler (specs/scheduler-unification.md): the
5
+ // date window reads its content day from `extent`, and the exporters read the
6
+ // dates. The pixel layout keeps its own placement and differs from it in the
7
+ // catalogued ways of `test/engine-divergences.json`. The rules, with the
8
+ // roadmap-start resolution of the date-window phase (`resolveDateWindow` in
9
+ // `phases/date-window.ts`):
8
10
  // - sequential default — start where the previous item in the lane ended
11
+ // - after:[id|DATE] — start no earlier than the maximum predecessor end
12
+ // (work has no fixed start pin: `date:` / `start:` on an item are
13
+ // unknown keys, specs/dsl.md § Item Properties); a parallel ends at its
14
+ // latest track end, a group at its last child's end
15
+ // - wave barriers (roadmaps with waves only) — every start is floored by
16
+ // its wave's start S_k, iterated to a fixpoint by `solveWaveBarriers`
17
+ // (specs/waves.md §8.5)
9
18
  //
10
- // Used by the XLSX exporter to populate the "Start" / "End" date columns and
11
- // the milestone "Date" cell when no explicit `date:` is set. Keeping it
12
- // separate from `computeContentEndDay` avoids mutating the byte-stable snapshot
13
- // pipeline.
19
+ // Offsets are working-day indices from the start (specs/working-calendar.md
20
+ // §5): dates convert with `workingIndexOf`, so an `after:` date on a
21
+ // non-working day starts on the next working day. A start (and an after-only milestone) is a
22
+ // point, `dateAtWorkingIndex`; an exclusive end is `spanEndDate`, so a
23
+ // Mon–Fri item ends on Saturday. Anchors and dated milestones keep their own
24
+ // dates. A calendar with no non-working day keeps calendar-day arithmetic.
25
+ //
26
+ // The schedule is every exporter's source for the chart's calendar and
27
+ // durations (handoff m2p decision 16): `calendar` is the file's resolved
28
+ // calendar and `ScheduledItem.days` the duration engine C placed. The XLSX
29
+ // exporter also reads the Start / End dates, the milestone dates and the
30
+ // wave spans; the Mermaid exporter reads the wave spans.
14
31
 
15
32
  import type {
16
33
  GroupBlock,
17
34
  ItemDeclaration,
18
35
  MilestoneDeclaration,
19
36
  NowlineFile,
20
- ParallelBlock,
37
+ ResolvedContent,
21
38
  ResolveResult,
22
39
  SwimlaneDeclaration,
40
+ WavePlan,
23
41
  } from '@nowline/core';
24
- import { isGroupBlock, isItemDeclaration, isParallelBlock } from '@nowline/core';
25
- import {
26
- addDays,
27
- daysBetween,
28
- deriveItemDurationDays,
29
- resolveCalendar,
30
- resolveSizes,
31
- } from './calendar.js';
32
- import { parseDate, propValue, propValues } from './dsl-utils.js';
42
+ import { buildWavePlan, isGroupBlock, isItemDeclaration, isParallelBlock } from '@nowline/core';
43
+ import { type TrackNode, trackChildren } from '@nowline/core/internal';
44
+ import { deriveItemDurationDays, resolveSizes } from './calendar.js';
45
+ import { type ResolvedCalendar, resolveWorkingCalendar } from './calendar-resolver.js';
46
+ import { parseIsoDate, propValue } from './dsl-utils.js';
47
+ import { type AfterTerm, startTermsOf } from './start-terms.js';
48
+ import { solveWaveBarriers, summarizeWaves, WavePass, waveFloorDays } from './wave-barrier.js';
49
+ import { spanEndDate } from './working-calendar.js';
33
50
 
34
51
  /** Per-item scheduled interval, keyed by item id (name). */
35
52
  export interface ScheduledItem {
36
53
  start: Date;
37
54
  end: Date;
55
+ /**
56
+ * Duration in working days: the `duration:` literal, or the size's effort
57
+ * divided by `capacity:`, under the file's calendar. 0 when the item has
58
+ * neither, or names an undeclared size.
59
+ */
60
+ days: number;
38
61
  }
39
62
 
40
63
  /**
@@ -48,6 +71,8 @@ export interface ScheduledItem {
48
71
  export interface RoadmapSchedule {
49
72
  /** Resolved roadmap start date (UTC midnight). */
50
73
  startDate: Date;
74
+ /** The file's calendar, which every date and `days` here uses. */
75
+ calendar: ResolvedCalendar;
51
76
  /** Named items keyed by their DSL id (`name`). */
52
77
  items: Map<string, ScheduledItem>;
53
78
  /** Every item keyed by AST node identity (named and anonymous). */
@@ -58,11 +83,67 @@ export interface RoadmapSchedule {
58
83
  milestoneByNode: WeakMap<MilestoneDeclaration, Date>;
59
84
  /** Named anchors: their declared or computed date. */
60
85
  anchors: Map<string, Date>;
86
+ /**
87
+ * Waves by id, in declaration order (specs/waves.md §8.5). Present only
88
+ * when the roadmap declares waves.
89
+ */
90
+ waves?: Map<string, ScheduledWave>;
91
+ /** The wave plan the schedule ran with; undefined without waves. */
92
+ plan?: WavePlan;
93
+ /** The `ResolveResult` this schedule was built from. */
94
+ resolved: ResolveResult;
95
+ /** What the date window needs (specs/scheduler-unification.md §3.3). */
96
+ extent: ScheduleExtent;
97
+ }
98
+
99
+ /**
100
+ * True when some wave has a member (specs/waves.md §9.1). With none, the
101
+ * roadmap draws no waves: no strip, no hatch, no legend. The barrier still
102
+ * runs, so wave floors and `after:<wave>` keep their dates.
103
+ */
104
+ export function hasVisibleWaves(schedule: Pick<RoadmapSchedule, 'waves'>): boolean {
105
+ for (const wave of schedule.waves?.values() ?? []) {
106
+ if (wave.memberCount > 0) return true;
107
+ }
108
+ return false;
109
+ }
110
+
111
+ /** A wave's solved span (specs/waves.md §5.1). */
112
+ export interface ScheduledWave {
113
+ /** 1-based declaration order. */
114
+ index: number;
115
+ /** `S_k` (UTC midnight). */
116
+ start: Date;
117
+ /** `E_k`, exclusive (UTC midnight); equals `start` for an empty wave. */
118
+ end: Date;
119
+ /** Members placed in the main lanes and first-level isolated regions. */
120
+ memberCount: number;
121
+ /** The member that sets `E_k` (id ?? title); omitted when none ends past `S_k`. */
122
+ heldBy?: string;
123
+ /** The wave's `after:` element that set `S_k`, when it beat the previous wave's end. */
124
+ floorRef?: string;
61
125
  }
62
126
 
63
127
  export interface ScheduleOptions {
64
128
  /** Passed as the reference date when the roadmap omits `start:`. */
65
129
  today?: Date;
130
+ /**
131
+ * The roadmap's wave plan from `buildWavePlan(resolved)`, when the caller
132
+ * already built it; `null` means "built, no waves". Omitted: built here.
133
+ */
134
+ plan?: WavePlan | null;
135
+ }
136
+
137
+ /** What the date window needs from the schedule (specs/scheduler-unification.md §3.2). */
138
+ export interface ScheduleExtent {
139
+ /**
140
+ * Latest working-day offset from `startDate` reached by any lane cursor
141
+ * (item ends and container starts), any dated anchor, any milestone, and,
142
+ * with waves, every `S_k` and `E_n`. Isolated regions count too: without
143
+ * waves every region at any depth with its anchors and milestones, with
144
+ * waves the first-level regions' lanes. `today` is not included.
145
+ */
146
+ contentEndDay: number;
66
147
  }
67
148
 
68
149
  /**
@@ -74,144 +155,311 @@ export function scheduleRoadmap(
74
155
  resolved: ResolveResult,
75
156
  options: ScheduleOptions = {},
76
157
  ): RoadmapSchedule {
77
- const cal = resolveCalendar(file, resolved.config.calendar);
158
+ const resolvedCalendar = resolveWorkingCalendar(file, resolved);
159
+ const { config: cal, working: calendar } = resolvedCalendar;
78
160
  const sizes = resolveSizes(resolved.content.sizes, cal);
79
161
 
80
- // Resolve roadmap start date — same precedence as RoadmapNode.place.
162
+ // Resolve roadmap start date — same precedence as the date-window phase.
81
163
  const startRaw = propValue(file.roadmapDecl?.properties ?? [], 'start');
82
- const startDate = parseDate(startRaw) ?? utcMidnight(options.today ?? new Date());
164
+ const startDate = parseIsoDate(startRaw) ?? utcMidnight(options.today ?? new Date());
165
+ // Working-day index of a date, and back: a point and an exclusive end.
166
+ const dayOf = (date: Date): number => calendar.workingIndexOf(startDate, date);
167
+ const pointAt = (day: number): Date => calendar.dateAtWorkingIndex(startDate, day);
168
+ const endAt = (start: number, end: number): Date =>
169
+ spanEndDate(calendar, startDate, start, end);
170
+
171
+ // The latest day any lane cursor, anchor, milestone or wave edge reaches.
172
+ let contentEndDay = 0;
173
+ const reachMax = (day: number): void => {
174
+ contentEndDay = Math.max(contentEndDay, day);
175
+ };
83
176
 
84
177
  // These maps accumulate end-day offsets (from startDate) for cross-entity
85
- // `after:` resolution, matching computeContentEndDay exactly.
86
- const itemEnd = new Map<string, number>(); // id → end day
178
+ // `after:` resolution. In a roadmap with waves the item maps are rebuilt on
179
+ // every barrier pass.
180
+ let itemEnd = new Map<string, number>(); // item, group, parallel or wave id → end day
87
181
  const anchorEnd = new Map<string, number>(); // id → date day
88
182
  const milestoneEnd = new Map<string, number>(); // id → date day
89
183
 
90
184
  // Result maps (Date objects).
91
- const itemResults = new Map<string, ScheduledItem>();
92
- const itemByNode = new WeakMap<ItemDeclaration, ScheduledItem>();
185
+ let itemResults = new Map<string, ScheduledItem>();
186
+ let itemByNode = new WeakMap<ItemDeclaration, ScheduledItem>();
93
187
  const milestoneResults = new Map<string, Date>();
94
188
  const milestoneByNode = new WeakMap<MilestoneDeclaration, Date>();
95
189
  const anchorResults = new Map<string, Date>();
96
190
 
97
- // Resolve a single `after:` element to a day-offset.
98
- const resolveAfterDay = (ref: string): number => {
99
- const inlineDate = parseDate(ref);
100
- if (inlineDate) return daysBetween(startDate, inlineDate);
101
- if (itemEnd.has(ref)) return itemEnd.get(ref)!;
102
- if (anchorEnd.has(ref)) return anchorEnd.get(ref)!;
103
- if (milestoneEnd.has(ref)) return milestoneEnd.get(ref)!;
104
- return 0;
105
- };
106
-
107
191
  // Pre-seed named anchors (they may be referenced by item after: before we
108
192
  // walk the lanes).
109
193
  for (const [id, anchor] of resolved.content.anchors) {
110
- const d = parseDate(propValue(anchor.properties, 'date'));
194
+ const d = parseIsoDate(propValue(anchor.properties, 'date'));
111
195
  if (d) {
112
- const day = daysBetween(startDate, d);
196
+ const day = dayOf(d);
113
197
  anchorEnd.set(id, day);
114
- anchorResults.set(id, addDays(startDate, day));
198
+ anchorResults.set(id, d);
199
+ reachMax(day);
115
200
  }
116
201
  }
117
202
 
118
- // Walk a sequential lane, returning the end-day of the last child.
119
- const walkLane = (children: SwimlaneDeclaration['content'], baselineEnd: number): number => {
120
- let prevEnd = baselineEnd;
121
- for (const child of children) {
122
- if (child.$type === 'DescriptionDirective') continue;
123
- prevEnd = walkNode(child as ItemDeclaration | GroupBlock | ParallelBlock, prevEnd);
124
- }
125
- return prevEnd;
126
- };
203
+ // The id maps one walk resolves `after:` against: the main lanes share
204
+ // the maps above; an isolated region gets fresh maps.
205
+ interface WalkScope {
206
+ itemEnd: Map<string, number>;
207
+ anchorEnd: ReadonlyMap<string, number>;
208
+ milestoneEnd: ReadonlyMap<string, number>;
209
+ sizes: typeof sizes;
210
+ /** Id-keyed results; omitted for a region, whose ids stay invisible. */
211
+ itemResults?: Map<string, ScheduledItem>;
212
+ /** False for a nested region: its items count in the extent only. */
213
+ inByNode: boolean;
214
+ }
215
+ const mainScope = (): WalkScope => ({
216
+ itemEnd,
217
+ anchorEnd,
218
+ milestoneEnd,
219
+ sizes,
220
+ itemResults,
221
+ inByNode: true,
222
+ });
127
223
 
128
- const walkNode = (
129
- node: ItemDeclaration | GroupBlock | ParallelBlock,
130
- prevEnd: number,
131
- ): number => {
132
- if (isItemDeclaration(node)) {
133
- const dur = deriveItemDurationDays(node.properties, sizes, cal);
134
- const dateProp = parseDate(propValue(node.properties, 'date'));
135
- const startProp = parseDate(propValue(node.properties, 'start'));
136
- const afterRefs = propValues(node.properties, 'after');
137
- let start = prevEnd;
138
- if (dateProp) {
139
- start = daysBetween(startDate, dateProp);
140
- } else if (startProp) {
141
- start = daysBetween(startDate, startProp);
142
- } else if (afterRefs.length > 0) {
143
- start = Math.max(prevEnd, ...afterRefs.map(resolveAfterDay));
224
+ // `wave` is the current barrier pass, undefined when there are no waves;
225
+ // `reach` receives every lane cursor, so a container start that passes
226
+ // every child (an empty `group after:x`) still counts in the extent.
227
+ const makeWalker = (
228
+ scope: WalkScope,
229
+ wave: WavePass | undefined,
230
+ reach: (day: number) => void,
231
+ ) => {
232
+ // Resolve a single `after:` element to a day-offset.
233
+ const resolveAfterDay = (term: AfterTerm): number => {
234
+ if ('date' in term) return dayOf(term.date);
235
+ const ref = term.ref;
236
+ if (scope.itemEnd.has(ref)) return scope.itemEnd.get(ref)!;
237
+ if (scope.anchorEnd.has(ref)) return scope.anchorEnd.get(ref)!;
238
+ if (scope.milestoneEnd.has(ref)) return scope.milestoneEnd.get(ref)!;
239
+ return 0;
240
+ };
241
+
242
+ // Walk a sequential lane or group, returning the end-day of the last
243
+ // child. Core's `trackChildren` says what a child is; the recursion
244
+ // stays here because it carries the cursor and the wave pass.
245
+ const walkLane = (
246
+ container: SwimlaneDeclaration | GroupBlock,
247
+ baselineEnd: number,
248
+ ): number => {
249
+ let prevEnd = baselineEnd;
250
+ for (const child of trackChildren(container)) {
251
+ prevEnd = walkNode(child, prevEnd);
252
+ reach(prevEnd);
144
253
  }
145
- const end = start + dur;
146
- const scheduled: ScheduledItem = {
147
- start: addDays(startDate, start),
148
- end: addDays(startDate, end),
149
- };
150
- itemByNode.set(node, scheduled);
151
- if (node.name) {
152
- itemEnd.set(node.name, end);
153
- itemResults.set(node.name, scheduled);
254
+ return prevEnd;
255
+ };
256
+
257
+ const walkNode = (node: TrackNode, prevEnd: number): number => {
258
+ if (isItemDeclaration(node)) {
259
+ const dur = deriveItemDurationDays(node.properties, scope.sizes, cal);
260
+ const { after } = startTermsOf(node.properties);
261
+ let start =
262
+ after.length > 0 ? Math.max(prevEnd, ...after.map(resolveAfterDay)) : prevEnd;
263
+ // The barrier floor is one more term in the max.
264
+ if (wave) start = wave.apply(node, start);
265
+ const end = start + dur;
266
+ const scheduled: ScheduledItem = {
267
+ start: pointAt(start),
268
+ end: endAt(start, end),
269
+ days: dur,
270
+ };
271
+ if (scope.inByNode) itemByNode.set(node, scheduled);
272
+ if (node.name) {
273
+ scope.itemEnd.set(node.name, end);
274
+ scope.itemResults?.set(node.name, scheduled);
275
+ }
276
+ wave?.accumulate(node, end);
277
+ return end;
154
278
  }
155
- return end;
156
- }
157
- if (isParallelBlock(node)) {
158
- const afterRefs = propValues(node.properties, 'after');
159
- const containerStart =
160
- afterRefs.length > 0
161
- ? Math.max(prevEnd, ...afterRefs.map(resolveAfterDay))
162
- : prevEnd;
163
- let parallelEnd = containerStart;
164
- for (const child of node.content) {
165
- if (child.$type === 'DescriptionDirective') continue;
166
- const childEnd = walkNode(child as ItemDeclaration | GroupBlock, containerStart);
167
- parallelEnd = Math.max(parallelEnd, childEnd);
279
+ if (isParallelBlock(node)) {
280
+ const { after } = startTermsOf(node.properties);
281
+ let containerStart =
282
+ after.length > 0 ? Math.max(prevEnd, ...after.map(resolveAfterDay)) : prevEnd;
283
+ if (wave) containerStart = wave.apply(node, containerStart);
284
+ let parallelEnd = containerStart;
285
+ for (const child of trackChildren(node)) {
286
+ parallelEnd = Math.max(parallelEnd, walkNode(child, containerStart));
287
+ }
288
+ // `after:` a parallel waits for its latest track end.
289
+ if (node.name) scope.itemEnd.set(node.name, parallelEnd);
290
+ return parallelEnd;
291
+ }
292
+ if (isGroupBlock(node)) {
293
+ const { after } = startTermsOf(node.properties);
294
+ let containerStart =
295
+ after.length > 0 ? Math.max(prevEnd, ...after.map(resolveAfterDay)) : prevEnd;
296
+ if (wave) containerStart = wave.apply(node, containerStart);
297
+ // `after:` a group waits for its last child's end.
298
+ const groupEnd = walkLane(node, containerStart);
299
+ if (node.name) scope.itemEnd.set(node.name, groupEnd);
300
+ return groupEnd;
301
+ }
302
+ return prevEnd;
303
+ };
304
+
305
+ return { walkLane, resolveAfterDay };
306
+ };
307
+
308
+ // A milestone is a point: its `date:`, else its latest predecessor end.
309
+ // Returns the day, or undefined when it has neither.
310
+ const milestoneDay = (
311
+ ms: MilestoneDeclaration,
312
+ resolveAfterDay: (term: AfterTerm) => number,
313
+ ): number | undefined => {
314
+ const d = parseIsoDate(propValue(ms.properties, 'date'));
315
+ if (d) return dayOf(d);
316
+ const { after } = startTermsOf(ms.properties);
317
+ return after.length > 0 ? Math.max(0, ...after.map(resolveAfterDay)) : undefined;
318
+ };
319
+
320
+ // An isolated region in a roadmap without waves: fresh id maps, its own
321
+ // sizes (specs/scheduler-unification.md §8, decision 2), its anchors and
322
+ // milestones (resolved inside the region and counted in the extent, never
323
+ // in the results) and its nested regions.
324
+ // First-level region items join `byNode`; their ids stay out of `items`.
325
+ const scheduleRegion = (content: ResolvedContent, firstLevel: boolean): void => {
326
+ const regionItemEnd = new Map<string, number>();
327
+ const regionAnchorEnd = new Map<string, number>();
328
+ const regionMilestoneEnd = new Map<string, number>();
329
+ for (const [id, anchor] of content.anchors) {
330
+ const d = parseIsoDate(propValue(anchor.properties, 'date'));
331
+ if (d) {
332
+ const day = dayOf(d);
333
+ regionAnchorEnd.set(id, day);
334
+ reachMax(day);
168
335
  }
169
- return parallelEnd;
170
336
  }
171
- if (isGroupBlock(node)) {
172
- const afterRefs = propValues(node.properties, 'after');
173
- const containerStart =
174
- afterRefs.length > 0
175
- ? Math.max(prevEnd, ...afterRefs.map(resolveAfterDay))
176
- : prevEnd;
177
- return walkLane(node.content as SwimlaneDeclaration['content'], containerStart);
337
+ const { walkLane, resolveAfterDay } = makeWalker(
338
+ {
339
+ itemEnd: regionItemEnd,
340
+ anchorEnd: regionAnchorEnd,
341
+ milestoneEnd: regionMilestoneEnd,
342
+ sizes: resolveSizes(content.sizes, cal),
343
+ inByNode: firstLevel,
344
+ },
345
+ undefined,
346
+ reachMax,
347
+ );
348
+ for (const lane of content.swimlanes.values()) walkLane(lane, 0);
349
+ for (const [id, ms] of content.milestones) {
350
+ const day = milestoneDay(ms, resolveAfterDay);
351
+ if (day === undefined) continue;
352
+ regionMilestoneEnd.set(id, day);
353
+ reachMax(day);
178
354
  }
179
- return prevEnd;
355
+ for (const nested of content.isolatedRegions) scheduleRegion(nested.content, false);
180
356
  };
181
357
 
182
- for (const lane of resolved.content.swimlanes.values()) {
183
- walkLane(lane.content, 0);
358
+ const plan = options.plan !== undefined ? (options.plan ?? undefined) : buildWavePlan(resolved);
359
+ let waves: Map<string, ScheduledWave> | undefined;
360
+ if (!plan) {
361
+ const { walkLane } = makeWalker(mainScope(), undefined, reachMax);
362
+ for (const lane of resolved.content.swimlanes.values()) {
363
+ walkLane(lane, 0);
364
+ }
365
+ } else {
366
+ // Wave barriers (specs/waves.md §8.5): iterate the lane walk to the
367
+ // least fixpoint. The final pass's results are the schedule.
368
+ const ids = plan.waves.map((w) => w.name as string);
369
+ const floors = waveFloorDays(plan, startDate, calendar);
370
+ let finalPass: WavePass | undefined;
371
+ // The extent of the latest pass; the final one ran with the solved S/E.
372
+ let passMax = 0;
373
+ const reachPass = (day: number): void => {
374
+ passMax = Math.max(passMax, day);
375
+ };
376
+ const result = solveWaveBarriers(ids.length, 0, floors, (S, E) => {
377
+ // Reset the item maps, keep the anchors, and seed each wave id
378
+ // with E_k so `after:<wave>` resolves to the wave's end.
379
+ const seeds = ids.map((id, i): [string, number] => [id, E[i]]);
380
+ itemEnd = new Map(seeds);
381
+ itemResults = new Map();
382
+ itemByNode = new WeakMap();
383
+ passMax = 0;
384
+ const pass = new WavePass(plan, S);
385
+ const { walkLane } = makeWalker(mainScope(), pass, reachPass);
386
+ for (const lane of resolved.content.swimlanes.values()) {
387
+ walkLane(lane, 0);
388
+ }
389
+ // Exactly one level of isolated regions, each with fresh id maps
390
+ // seeded only with the wave edges. Region items join the barrier
391
+ // and `byNode`; their ids stay out of `items`. Region anchors,
392
+ // region milestones and nested regions are not walked: engine A
393
+ // draws none of them.
394
+ for (const region of resolved.content.isolatedRegions) {
395
+ const regionWalker = makeWalker(
396
+ {
397
+ itemEnd: new Map(seeds),
398
+ anchorEnd: new Map(),
399
+ milestoneEnd: new Map(),
400
+ sizes: resolveSizes(region.content.sizes, cal),
401
+ inByNode: true,
402
+ },
403
+ pass,
404
+ reachPass,
405
+ );
406
+ for (const lane of region.content.swimlanes.values()) {
407
+ regionWalker.walkLane(lane, 0);
408
+ }
409
+ }
410
+ finalPass = pass;
411
+ return pass;
412
+ });
413
+ // The content end covers the last wave's end and every wave start,
414
+ // so floors and empty trailing waves stay inside the window.
415
+ reachMax(Math.max(passMax, ...result.S, ...result.E.slice(-1)));
416
+ waves = new Map();
417
+ for (const span of summarizeWaves(plan, result, finalPass!, 0, floors)) {
418
+ waves.set(span.id, {
419
+ index: span.index,
420
+ start: pointAt(span.start),
421
+ end: endAt(span.start, span.end),
422
+ memberCount: span.memberCount,
423
+ ...(span.heldBy !== undefined ? { heldBy: span.heldBy } : {}),
424
+ ...(span.floorRef !== undefined ? { floorRef: span.floorRef } : {}),
425
+ });
426
+ }
184
427
  }
185
428
 
186
- // Milestones — same pass order as computeContentEndDay (after items so
187
- // after: can resolve item end-days).
429
+ // Milestones, after the items so after: can resolve item end-days, and
430
+ // wave ids their E_k.
431
+ const { resolveAfterDay } = makeWalker(mainScope(), undefined, reachMax);
188
432
  for (const [id, ms] of resolved.content.milestones) {
189
- const d = parseDate(propValue(ms.properties, 'date'));
190
- if (d) {
191
- const day = daysBetween(startDate, d);
192
- const resolved2 = addDays(startDate, day);
193
- milestoneEnd.set(id, day);
194
- milestoneResults.set(id, resolved2);
195
- milestoneByNode.set(ms, resolved2);
196
- continue;
197
- }
198
- const after = propValues(ms.properties, 'after');
199
- if (after.length > 0) {
200
- const day = Math.max(0, ...after.map(resolveAfterDay));
201
- const resolved2 = addDays(startDate, day);
202
- milestoneEnd.set(id, day);
203
- milestoneResults.set(id, resolved2);
204
- milestoneByNode.set(ms, resolved2);
433
+ const day = milestoneDay(ms, resolveAfterDay);
434
+ if (day === undefined) continue;
435
+ // A dated milestone keeps its own date; an after-only one is a point
436
+ // at the latest predecessor end: the next working day.
437
+ const date = parseIsoDate(propValue(ms.properties, 'date')) ?? pointAt(day);
438
+ milestoneEnd.set(id, day);
439
+ milestoneResults.set(id, date);
440
+ milestoneByNode.set(ms, date);
441
+ reachMax(day);
442
+ }
443
+
444
+ // Isolated regions without waves (with waves they joined the passes).
445
+ if (!plan) {
446
+ for (const region of resolved.content.isolatedRegions) {
447
+ scheduleRegion(region.content, true);
205
448
  }
206
449
  }
207
450
 
208
451
  return {
209
452
  startDate,
453
+ calendar: resolvedCalendar,
210
454
  items: itemResults,
211
455
  byNode: itemByNode,
212
456
  milestones: milestoneResults,
213
457
  milestoneByNode,
214
458
  anchors: anchorResults,
459
+ ...(waves ? { waves } : {}),
460
+ ...(plan ? { plan } : {}),
461
+ resolved,
462
+ extent: { contentEndDay },
215
463
  };
216
464
  }
217
465