@nowline/layout 0.5.1 → 0.7.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 (39) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/layout-context.d.ts +8 -0
  6. package/dist/layout-context.d.ts.map +1 -1
  7. package/dist/layout.d.ts.map +1 -1
  8. package/dist/layout.js +68 -47
  9. package/dist/layout.js.map +1 -1
  10. package/dist/nodes/group-node.d.ts.map +1 -1
  11. package/dist/nodes/group-node.js +2 -1
  12. package/dist/nodes/group-node.js.map +1 -1
  13. package/dist/nodes/include-node.d.ts.map +1 -1
  14. package/dist/nodes/include-node.js +4 -0
  15. package/dist/nodes/include-node.js.map +1 -1
  16. package/dist/nodes/parallel-node.d.ts.map +1 -1
  17. package/dist/nodes/parallel-node.js +2 -1
  18. package/dist/nodes/parallel-node.js.map +1 -1
  19. package/dist/nodes/roadmap-node.d.ts.map +1 -1
  20. package/dist/nodes/roadmap-node.js +4 -0
  21. package/dist/nodes/roadmap-node.js.map +1 -1
  22. package/dist/resolve-today.d.ts +90 -0
  23. package/dist/resolve-today.d.ts.map +1 -0
  24. package/dist/resolve-today.js +197 -0
  25. package/dist/resolve-today.js.map +1 -0
  26. package/dist/schedule.d.ts +38 -0
  27. package/dist/schedule.d.ts.map +1 -0
  28. package/dist/schedule.js +158 -0
  29. package/dist/schedule.js.map +1 -0
  30. package/package.json +3 -3
  31. package/src/index.ts +10 -0
  32. package/src/layout-context.ts +8 -0
  33. package/src/layout.ts +69 -46
  34. package/src/nodes/group-node.ts +2 -1
  35. package/src/nodes/include-node.ts +4 -0
  36. package/src/nodes/parallel-node.ts +2 -1
  37. package/src/nodes/roadmap-node.ts +4 -0
  38. package/src/resolve-today.ts +266 -0
  39. package/src/schedule.ts +220 -0
@@ -0,0 +1,220 @@
1
+ // scheduleRoadmap — compute the floating calendar start/end date for every
2
+ // named entity (items, milestones, anchors) without running the full layout.
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
8
+ // - sequential default — start where the previous item in the lane ended
9
+ //
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.
14
+
15
+ import type {
16
+ GroupBlock,
17
+ ItemDeclaration,
18
+ MilestoneDeclaration,
19
+ NowlineFile,
20
+ ParallelBlock,
21
+ ResolveResult,
22
+ SwimlaneDeclaration,
23
+ } 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';
33
+
34
+ /** Per-item scheduled interval, keyed by item id (name). */
35
+ export interface ScheduledItem {
36
+ start: Date;
37
+ end: Date;
38
+ }
39
+
40
+ /**
41
+ * Result of scheduling a roadmap. All dates are UTC midnight.
42
+ *
43
+ * `items` is keyed by DSL id and only contains **named** items.
44
+ * `byNode` is keyed by AST node identity and contains **every** item,
45
+ * including anonymous ones — use this when you have the AST node in hand
46
+ * and want dates regardless of whether an id was declared.
47
+ */
48
+ export interface RoadmapSchedule {
49
+ /** Resolved roadmap start date (UTC midnight). */
50
+ startDate: Date;
51
+ /** Named items keyed by their DSL id (`name`). */
52
+ items: Map<string, ScheduledItem>;
53
+ /** Every item keyed by AST node identity (named and anonymous). */
54
+ byNode: WeakMap<ItemDeclaration, ScheduledItem>;
55
+ /** Named milestones keyed by their DSL id. */
56
+ milestones: Map<string, Date>;
57
+ /** Every milestone keyed by AST node identity (named and anonymous). */
58
+ milestoneByNode: WeakMap<MilestoneDeclaration, Date>;
59
+ /** Named anchors: their declared or computed date. */
60
+ anchors: Map<string, Date>;
61
+ }
62
+
63
+ export interface ScheduleOptions {
64
+ /** Passed as the reference date when the roadmap omits `start:`. */
65
+ today?: Date;
66
+ }
67
+
68
+ /**
69
+ * Compute the scheduled start/end date for every named entity in the roadmap.
70
+ * Does NOT run the full layout (no SVG geometry, no pixel coordinates).
71
+ */
72
+ export function scheduleRoadmap(
73
+ file: NowlineFile,
74
+ resolved: ResolveResult,
75
+ options: ScheduleOptions = {},
76
+ ): RoadmapSchedule {
77
+ const cal = resolveCalendar(file, resolved.config.calendar);
78
+ const sizes = resolveSizes(resolved.content.sizes, cal);
79
+
80
+ // Resolve roadmap start date — same precedence as RoadmapNode.place.
81
+ const startRaw = propValue(file.roadmapDecl?.properties ?? [], 'start');
82
+ const startDate = parseDate(startRaw) ?? utcMidnight(options.today ?? new Date());
83
+
84
+ // 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
87
+ const anchorEnd = new Map<string, number>(); // id → date day
88
+ const milestoneEnd = new Map<string, number>(); // id → date day
89
+
90
+ // Result maps (Date objects).
91
+ const itemResults = new Map<string, ScheduledItem>();
92
+ const itemByNode = new WeakMap<ItemDeclaration, ScheduledItem>();
93
+ const milestoneResults = new Map<string, Date>();
94
+ const milestoneByNode = new WeakMap<MilestoneDeclaration, Date>();
95
+ const anchorResults = new Map<string, Date>();
96
+
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
+ // Pre-seed named anchors (they may be referenced by item after: before we
108
+ // walk the lanes).
109
+ for (const [id, anchor] of resolved.content.anchors) {
110
+ const d = parseDate(propValue(anchor.properties, 'date'));
111
+ if (d) {
112
+ const day = daysBetween(startDate, d);
113
+ anchorEnd.set(id, day);
114
+ anchorResults.set(id, addDays(startDate, day));
115
+ }
116
+ }
117
+
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
+ };
127
+
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));
144
+ }
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);
154
+ }
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);
168
+ }
169
+ return parallelEnd;
170
+ }
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);
178
+ }
179
+ return prevEnd;
180
+ };
181
+
182
+ for (const lane of resolved.content.swimlanes.values()) {
183
+ walkLane(lane.content, 0);
184
+ }
185
+
186
+ // Milestones — same pass order as computeContentEndDay (after items so
187
+ // after: can resolve item end-days).
188
+ 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);
205
+ }
206
+ }
207
+
208
+ return {
209
+ startDate,
210
+ items: itemResults,
211
+ byNode: itemByNode,
212
+ milestones: milestoneResults,
213
+ milestoneByNode,
214
+ anchors: anchorResults,
215
+ };
216
+ }
217
+
218
+ function utcMidnight(d: Date): Date {
219
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
220
+ }