@diagrammo/dgmo 0.8.17 → 0.8.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +103 -103
- package/dist/editor.cjs +1 -1
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +1 -1
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +1 -1
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +1306 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -15
- package/dist/index.d.ts +120 -15
- package/dist/index.js +1325 -151
- package/dist/index.js.map +1 -1
- package/docs/guide/how-dgmo-thinks.md +277 -0
- package/docs/guide/registry.json +1 -0
- package/gallery/fixtures/gantt-sprints.dgmo +20 -0
- package/package.json +1 -1
- package/src/colors.ts +1 -1
- package/src/editor/dgmo.grammar +1 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/gantt/calculator.ts +120 -7
- package/src/gantt/parser.ts +98 -3
- package/src/gantt/renderer.ts +410 -95
- package/src/gantt/types.ts +23 -2
- package/src/index.ts +10 -2
- package/src/sequence/collapse.ts +169 -0
- package/src/sequence/parser.ts +14 -2
- package/src/sequence/renderer.ts +186 -49
- package/src/sharing.ts +86 -49
- package/src/utils/duration.ts +16 -2
- package/src/utils/legend-constants.ts +11 -0
- package/src/utils/legend-d3.ts +171 -0
- package/src/utils/legend-layout.ts +148 -17
- package/src/utils/legend-types.ts +45 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# How DGMO Thinks
|
|
2
|
+
|
|
3
|
+
A guide to the design principles behind the DGMO language. Understanding these themes will help you write diagrams faster and make better use of the language's features.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Indent, Don't Repeat
|
|
8
|
+
|
|
9
|
+
The most important pattern in DGMO: write the thing once, then indent what belongs to it beneath.
|
|
10
|
+
|
|
11
|
+
Instead of repeating the source on every line:
|
|
12
|
+
|
|
13
|
+
```dgmo-source
|
|
14
|
+
// Verbose — repeats "API" three times
|
|
15
|
+
API -routes-> UserService
|
|
16
|
+
API -routes-> ProductService
|
|
17
|
+
API -auth-> AuthService
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Indent edges under their source:
|
|
21
|
+
|
|
22
|
+
```dgmo-source
|
|
23
|
+
// Concise — declare once, indent connections
|
|
24
|
+
API
|
|
25
|
+
-routes-> UserService
|
|
26
|
+
-routes-> ProductService
|
|
27
|
+
-auth-> AuthService
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This isn't just a shorthand — it's how DGMO thinks about ownership and hierarchy. The pattern shows up everywhere:
|
|
31
|
+
|
|
32
|
+
- **Org charts and sitemaps** — indentation *is* the hierarchy
|
|
33
|
+
- **Kanban** — cards indented under columns
|
|
34
|
+
- **Gantt** — dependencies indented under tasks
|
|
35
|
+
- **ER** — columns and relationships indented under tables
|
|
36
|
+
- **Sequence** — participants indented under groups
|
|
37
|
+
|
|
38
|
+
When you see indentation in DGMO, read it as "belongs to."
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Meaning First, Color Second
|
|
43
|
+
|
|
44
|
+
When you want to make something red, your instinct might be to reach for a color suffix: `AuthService(red)`. That works for one-offs. But DGMO encourages a different approach for anything that carries meaning.
|
|
45
|
+
|
|
46
|
+
**Tags** let you separate *what something means* from *how it looks*:
|
|
47
|
+
|
|
48
|
+
```dgmo-source
|
|
49
|
+
tag Priority alias p
|
|
50
|
+
Critical(red)
|
|
51
|
+
Normal(green)
|
|
52
|
+
Low(gray)
|
|
53
|
+
|
|
54
|
+
API | p: Critical
|
|
55
|
+
Cache | p: Low
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Why go through this indirection?
|
|
59
|
+
|
|
60
|
+
- **Colors become meaningful.** Red means "Critical," not just red. The legend self-documents.
|
|
61
|
+
- **Palettes stay intact.** Switching from Nord to Dracula adjusts all your colors harmoniously. Direct hex codes would break this.
|
|
62
|
+
- **Filtering works.** Tags are structured metadata — you can sort, hide, and group by them.
|
|
63
|
+
- **One change updates everything.** Rename "Critical" to "Urgent" in one place, not fifty nodes.
|
|
64
|
+
|
|
65
|
+
Use color suffixes `(red)` for quick visual accents. Use tags when color carries meaning you'd want in a legend.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Name Things, Skip the Boilerplate
|
|
70
|
+
|
|
71
|
+
DGMO infers as much as it can from names you're already writing.
|
|
72
|
+
|
|
73
|
+
In sequence diagrams, the parser recognizes common names and gives them the right shape automatically:
|
|
74
|
+
|
|
75
|
+
```dgmo-source
|
|
76
|
+
Redis // cache (cylinder, dashed)
|
|
77
|
+
UserService // service (rounded rectangle)
|
|
78
|
+
Kafka // queue (horizontal cylinder)
|
|
79
|
+
User // actor (stick figure)
|
|
80
|
+
WebApp // frontend (monitor)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
You only need `is a` when the name doesn't match a pattern:
|
|
84
|
+
|
|
85
|
+
```dgmo-source
|
|
86
|
+
Payments is a service // "Payments" alone wouldn't infer
|
|
87
|
+
Vault is a database // "Vault" would infer as service
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
In flowcharts, arrow labels infer color:
|
|
91
|
+
|
|
92
|
+
```dgmo-source
|
|
93
|
+
<Valid?>
|
|
94
|
+
-yes-> [Process] // automatically green
|
|
95
|
+
-no-> [Show Error] // automatically red
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The first declared tag group auto-activates. Chart types are detected from the first line. Org hierarchy comes from indentation alone.
|
|
99
|
+
|
|
100
|
+
The principle: **name things sensibly and DGMO figures out the rest.** Override only when inference gets it wrong.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Palette-Aware Colors
|
|
105
|
+
|
|
106
|
+
When you write `(red)`, DGMO doesn't render `#ff0000`. It renders *the red from your active palette* — a shade that harmonizes with the other nine named colors in that palette.
|
|
107
|
+
|
|
108
|
+
The allowed color names are: `red`, `orange`, `yellow`, `green`, `blue`, `purple`, `teal`, `cyan`, `gray`, `black`, `white`.
|
|
109
|
+
|
|
110
|
+
That's the complete list. No hex codes, no CSS keywords, no custom colors. This is a deliberate constraint:
|
|
111
|
+
|
|
112
|
+
- Every palette (Nord, Dracula, Catppuccin, Gruvbox, etc.) defines its own version of these eleven names
|
|
113
|
+
- Switching palettes recolors your entire diagram coherently
|
|
114
|
+
- Diagrams always look good regardless of which palette or theme is active
|
|
115
|
+
- No one accidentally picks colors that clash or become invisible on certain backgrounds
|
|
116
|
+
|
|
117
|
+
DGMO prioritizes *beautiful by default* over *total control.*
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Brackets Mean "Container"
|
|
122
|
+
|
|
123
|
+
Wherever you see `[Name]`, something is being grouped:
|
|
124
|
+
|
|
125
|
+
```dgmo-source
|
|
126
|
+
[Backend] // group in infra, boxes-and-lines, C4
|
|
127
|
+
[To Do] // column in kanban
|
|
128
|
+
[Sprint 1] // swimlane in gantt
|
|
129
|
+
[Marketing] // container in sitemap
|
|
130
|
+
[Caribbean] // category in scatter charts
|
|
131
|
+
[Royal Navy] // group in timeline
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Content indented below the bracket line belongs to that group. Bracket grouping works in sequence, infra, flowchart, state, org, kanban, sitemap, gantt, boxes-and-lines, timeline, and scatter/bubble diagrams. When you see brackets, read them as "these things go together."
|
|
135
|
+
|
|
136
|
+
Groups can have color suffixes and pipe metadata:
|
|
137
|
+
|
|
138
|
+
```dgmo-source
|
|
139
|
+
[Backend](blue) | team: Platform
|
|
140
|
+
API
|
|
141
|
+
Database
|
|
142
|
+
Cache
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## One Arrow Vocabulary
|
|
148
|
+
|
|
149
|
+
DGMO uses the same small set of arrow patterns everywhere:
|
|
150
|
+
|
|
151
|
+
| Pattern | Meaning | Example |
|
|
152
|
+
|---------|---------|---------|
|
|
153
|
+
| `->` | Synchronous / directed | `API -> Database` |
|
|
154
|
+
| `~>` | Asynchronous | `API ~> Queue` |
|
|
155
|
+
| `-label->` | Labeled edge | `-routes-> UserService` |
|
|
156
|
+
| `~label~>` | Labeled async edge | `~notify~> Email` |
|
|
157
|
+
| `-(color)->` | Colored edge | `-(red)-> Fallback` |
|
|
158
|
+
| `<->` | Bidirectional | `A <-> B` |
|
|
159
|
+
|
|
160
|
+
The label goes between the dashes (or tildes). Color goes in parens on the label. This works identically in sequence, infra, flowchart, C4, ER, class, sitemap, and boxes-and-lines diagrams.
|
|
161
|
+
|
|
162
|
+
Learn it once, use it everywhere.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Pipe Metadata: The Universal "And Also..."
|
|
167
|
+
|
|
168
|
+
When you need to attach extra information to something, the pipe `|` syntax works on almost anything:
|
|
169
|
+
|
|
170
|
+
```dgmo-source
|
|
171
|
+
API | description: Main gateway, team: Platform
|
|
172
|
+
API -routes-> UserService | frequency: High
|
|
173
|
+
[Backend] | owner: Platform Team
|
|
174
|
+
10bd Database Schema | p: Foundation, 80%
|
|
175
|
+
1718-05 Blockade | p: Blackbeard
|
|
176
|
+
Card Title | priority: High, assignee: Alice
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Nodes, edges, groups, tasks, events, cards — if it exists in DGMO, you can probably pipe metadata onto it. The format is always `| key: value, key2: value2`.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Describe Relationships, Not Layouts
|
|
184
|
+
|
|
185
|
+
DGMO diagrams have no x/y coordinates, no manual positioning, no pixel-level control. You describe *what things are* and *how they connect*, and the layout engine handles placement.
|
|
186
|
+
|
|
187
|
+
```dgmo-source
|
|
188
|
+
// You write this
|
|
189
|
+
CEO
|
|
190
|
+
CTO
|
|
191
|
+
Engineering
|
|
192
|
+
DevOps
|
|
193
|
+
CFO
|
|
194
|
+
Finance
|
|
195
|
+
|
|
196
|
+
// DGMO handles the layout
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
This is a trade-off:
|
|
200
|
+
|
|
201
|
+
- Diagrams reflow cleanly when content changes — add a node and everything adjusts
|
|
202
|
+
- Version control diffs are meaningful (text changes, not coordinate noise)
|
|
203
|
+
- You spend time on content, not dragging boxes around
|
|
204
|
+
- But you don't get pixel-perfect placement
|
|
205
|
+
|
|
206
|
+
If you find yourself wanting to control exact positions, you're probably fighting the tool. Instead, use groups, ordering, and direction options (`direction-tb`, `direction-lr`) to guide the layout.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Options Are Simple Toggles
|
|
211
|
+
|
|
212
|
+
Configuration goes at the top of the diagram as plain keywords:
|
|
213
|
+
|
|
214
|
+
```dgmo-source
|
|
215
|
+
gantt Product Launch
|
|
216
|
+
start 2026-03-15
|
|
217
|
+
today-marker
|
|
218
|
+
critical-path
|
|
219
|
+
no-dependencies
|
|
220
|
+
active-tag Team
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- **Boolean options**: bare keyword turns it on, `no-` prefix turns it off (`activations` / `no-activations`)
|
|
224
|
+
- **Value options**: keyword followed by the value, space-separated (`start 2026-03-15`)
|
|
225
|
+
- No YAML, no JSON, no nested config blocks
|
|
226
|
+
- Options must appear before diagram content
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## The Colon Rule
|
|
231
|
+
|
|
232
|
+
Colons show up in exactly two situations:
|
|
233
|
+
|
|
234
|
+
**1. Open-ended metadata** — when you're defining freeform key-value pairs:
|
|
235
|
+
```dgmo-source
|
|
236
|
+
API | description: Main gateway // pipe metadata
|
|
237
|
+
role: Senior Engineer // org/C4 indented metadata
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**2. Type separators** — where both sides can contain spaces and a delimiter is needed:
|
|
241
|
+
```dgmo-source
|
|
242
|
+
+ name: string // class field type
|
|
243
|
+
+ sail(): void // class method return
|
|
244
|
+
Trajectory(blue): -0.001*x^2 + 0.27*x // function expression
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**No colons anywhere else** — declarations, options, tags, data rows, arrows, groups, and comments are all colon-free:
|
|
248
|
+
```dgmo-source
|
|
249
|
+
bar Revenue by Quarter // declaration: no colon
|
|
250
|
+
tag Team alias t // tag: no colon
|
|
251
|
+
start 2026-03-15 // option: no colon
|
|
252
|
+
Gold 3500 4200 5100 // data row: no colon
|
|
253
|
+
id int pk // ER column: no colon
|
|
254
|
+
latency-ms 50 // infra property: no colon
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The intuition: **if DGMO already knows what the fields are, spaces are enough. Colons appear only when you're defining something freeform or need an unambiguous separator.**
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## One Diagram Per File
|
|
262
|
+
|
|
263
|
+
The first line declares the chart type. There's no way to embed multiple diagrams in a single file. One file, one diagram, one clear purpose.
|
|
264
|
+
|
|
265
|
+
```dgmo-source
|
|
266
|
+
sequence Auth Flow
|
|
267
|
+
// everything below is this one diagram
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Comments Are Full-Line Only
|
|
271
|
+
|
|
272
|
+
```dgmo-source
|
|
273
|
+
// This is a comment
|
|
274
|
+
API -> Database // This is NOT a comment — it's part of the line
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Use `//` at the start of a line. There are no inline comments. `#` is not a comment character.
|
package/docs/guide/registry.json
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
{ "slug": "index", "title": "Welcome", "group": "getting-started", "file": "index.md" },
|
|
11
11
|
{ "slug": "keyboard-shortcuts", "title": "Keyboard Shortcuts", "group": "getting-started", "file": "keyboard-shortcuts.md" },
|
|
12
12
|
{ "slug": "colors", "title": "Colors", "group": "getting-started", "file": "colors.md" },
|
|
13
|
+
{ "slug": "how-dgmo-thinks", "title": "How DGMO Thinks", "group": "getting-started", "file": "how-dgmo-thinks.md" },
|
|
13
14
|
|
|
14
15
|
{ "slug": "chart-arc", "title": "Arc Diagram", "group": "software", "file": "chart-arc.md" },
|
|
15
16
|
{ "slug": "chart-boxes-and-lines", "title": "Boxes and Lines", "group": "software", "file": "chart-boxes-and-lines.md" },
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
gantt Sprint Planning
|
|
2
|
+
start 2026-03-01
|
|
3
|
+
sprint-length 2w
|
|
4
|
+
sprint-number 5
|
|
5
|
+
sprint-start 2026-01-05
|
|
6
|
+
|
|
7
|
+
[Design]
|
|
8
|
+
2s UX Research
|
|
9
|
+
1s Wireframes
|
|
10
|
+
1.5s Visual Design
|
|
11
|
+
|
|
12
|
+
[Engineering]
|
|
13
|
+
3s Backend API
|
|
14
|
+
2s Frontend Build
|
|
15
|
+
0.5s Performance Tuning
|
|
16
|
+
|
|
17
|
+
[QA]
|
|
18
|
+
1s Integration Testing
|
|
19
|
+
0.5s UAT
|
|
20
|
+
0d Release Candidate
|
package/package.json
CHANGED
package/src/colors.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function isRecognizedColorName(name: string): boolean {
|
|
|
70
70
|
/**
|
|
71
71
|
* Resolves a recognized color name to its hex value for the active palette
|
|
72
72
|
* (falling back to the built-in Nord defaults). Returns `null` for any
|
|
73
|
-
* unrecognized input — including hex codes, CSS keywords like `
|
|
73
|
+
* unrecognized input — including hex codes, CSS keywords like `pink`,
|
|
74
74
|
* and typos. Callers MUST treat `null` as a parse error and emit a
|
|
75
75
|
* diagnostic; do not silently fall back to the raw input.
|
|
76
76
|
*/
|
package/src/editor/dgmo.grammar
CHANGED
|
@@ -27,7 +27,7 @@ contentPart {
|
|
|
27
27
|
SyncArrow { "->" }
|
|
28
28
|
AsyncArrow { "~>" }
|
|
29
29
|
|
|
30
|
-
Duration { $[0-9]+ ("." $[0-9]+)? ("min" | "bd" | "h" | "d" | "w" | "m" | "q" | "y") "?"? }
|
|
30
|
+
Duration { $[0-9]+ ("." $[0-9]+)? ("min" | "bd" | "h" | "d" | "w" | "m" | "q" | "y" | "s") "?"? }
|
|
31
31
|
DateLiteral { $[0-9] $[0-9] $[0-9] $[0-9] "-" $[0-9] $[0-9] ("-" $[0-9] $[0-9])? }
|
|
32
32
|
Percentage { $[0-9]+ ("." $[0-9]+)? "%" }
|
|
33
33
|
Number { $[0-9]+ ("." $[0-9]+)? }
|
|
@@ -10,7 +10,7 @@ export const parser = LRParser.deserialize({
|
|
|
10
10
|
maxTerm: 40,
|
|
11
11
|
skippedNodes: [0],
|
|
12
12
|
repeatNodeCount: 2,
|
|
13
|
-
tokenData: "
|
|
13
|
+
tokenData: ":T~RxOX#oXY#tYZ$PZp#opq#tqt#oux#oxy$Uyz$tz{${{|%S|}%Z}!O%b!O!P#o!P!Q%q!Q![&b![!]-Y!]!^#o!^!_-a!_!`-h!`!a-u!a!b-|!b!c#o!c!}.T!}#O1Z#O#P#o#P#Q1`#Q#R#o#R#S.T#S#T#o#T#[.T#[#]1g#]#o.T#o#p#o#p#q9g#q#r#o#r#s9n#s;'S#o;'S;=`9}<%lO#o~#tOq~~#yQv~XY#tpq#t~$UOw~~$]Qc~q~}!O$c#T#o$c~$fRyz$o}!O$c#T#o$c~$tOg~~${Od~q~~%SOn~q~~%ZOk~q~~%bOj~q~~%iPl~q~!`!a%l~%qOX~~%vPq~!P!Q%y~&OSV~OY%yZ;'S%y;'S;=`&[<%lO%y~&_P;=`<%l%y~&iZ^~q~uv'[!O!P'a!QVZ^~uv'[!O!P'a!Q}Z^~uv'[!O!P'a!Q => (specializeKeyword(value, stack) << 1), external: specializeKeyword}],
|
package/src/gantt/calculator.ts
CHANGED
|
@@ -17,7 +17,10 @@ import type {
|
|
|
17
17
|
GanttTask,
|
|
18
18
|
GanttGroup,
|
|
19
19
|
GanttHolidays,
|
|
20
|
+
GanttOptions,
|
|
21
|
+
Duration,
|
|
20
22
|
ResolvedSchedule,
|
|
23
|
+
ResolvedSprint,
|
|
21
24
|
ResolvedGroup,
|
|
22
25
|
Offset,
|
|
23
26
|
} from './types';
|
|
@@ -52,6 +55,7 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
52
55
|
tagGroups: parsed.tagGroups,
|
|
53
56
|
eras: parsed.eras,
|
|
54
57
|
markers: parsed.markers,
|
|
58
|
+
sprints: [],
|
|
55
59
|
options: parsed.options,
|
|
56
60
|
diagnostics,
|
|
57
61
|
error: parsed.error,
|
|
@@ -85,6 +89,12 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
85
89
|
projectStart = new Date(2000, 0, 1);
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
// ── Sprint config ──────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
const sprintOpts = parsed.options.sprintLength
|
|
95
|
+
? { sprintLength: parsed.options.sprintLength }
|
|
96
|
+
: undefined;
|
|
97
|
+
|
|
88
98
|
// ── Dep offset storage ─────────────────────────────────
|
|
89
99
|
|
|
90
100
|
const depOffsetMap = new Map<string, Offset>();
|
|
@@ -208,7 +218,8 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
208
218
|
depOffset.duration,
|
|
209
219
|
parsed.holidays,
|
|
210
220
|
holidaySet,
|
|
211
|
-
depOffset.direction
|
|
221
|
+
depOffset.direction,
|
|
222
|
+
sprintOpts
|
|
212
223
|
);
|
|
213
224
|
}
|
|
214
225
|
|
|
@@ -225,7 +236,8 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
225
236
|
task.offset.duration,
|
|
226
237
|
parsed.holidays,
|
|
227
238
|
holidaySet,
|
|
228
|
-
task.offset.direction
|
|
239
|
+
task.offset.direction,
|
|
240
|
+
sprintOpts
|
|
229
241
|
);
|
|
230
242
|
if (start.getTime() < projectStart.getTime()) {
|
|
231
243
|
warn(
|
|
@@ -273,7 +285,9 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
273
285
|
start,
|
|
274
286
|
task.duration,
|
|
275
287
|
parsed.holidays,
|
|
276
|
-
holidaySet
|
|
288
|
+
holidaySet,
|
|
289
|
+
1,
|
|
290
|
+
sprintOpts
|
|
277
291
|
);
|
|
278
292
|
}
|
|
279
293
|
} else {
|
|
@@ -294,7 +308,8 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
294
308
|
taskMap,
|
|
295
309
|
depOffsetMap,
|
|
296
310
|
parsed.holidays,
|
|
297
|
-
holidaySet
|
|
311
|
+
holidaySet,
|
|
312
|
+
sprintOpts
|
|
298
313
|
)
|
|
299
314
|
: new Set<string>();
|
|
300
315
|
|
|
@@ -347,6 +362,29 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
|
347
362
|
result.endDate = maxDate;
|
|
348
363
|
}
|
|
349
364
|
|
|
365
|
+
// ── Generate sprint bands ──────────────────────────────
|
|
366
|
+
|
|
367
|
+
if (
|
|
368
|
+
parsed.options.sprintMode &&
|
|
369
|
+
parsed.options.sprintLength &&
|
|
370
|
+
result.tasks.length > 0
|
|
371
|
+
) {
|
|
372
|
+
result.sprints = generateSprintBands(
|
|
373
|
+
parsed.options,
|
|
374
|
+
result.startDate,
|
|
375
|
+
result.endDate,
|
|
376
|
+
projectStart
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// Extend chart range to include the last sprint's end so it doesn't clip
|
|
380
|
+
if (result.sprints.length > 0) {
|
|
381
|
+
const lastSprintEnd = result.sprints[result.sprints.length - 1].endDate;
|
|
382
|
+
if (lastSprintEnd.getTime() > result.endDate.getTime()) {
|
|
383
|
+
result.endDate = lastSprintEnd;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
350
388
|
// ── Warnings ────────────────────────────────────────────
|
|
351
389
|
|
|
352
390
|
// Missing parallel warning: 2+ top-level groups without parallel wrapper
|
|
@@ -613,7 +651,8 @@ function computeCriticalPath(
|
|
|
613
651
|
taskMap: Map<string, TaskNode>,
|
|
614
652
|
depOffsetMap: Map<string, Offset>,
|
|
615
653
|
holidays: GanttHolidays,
|
|
616
|
-
holidaySet: Set<string
|
|
654
|
+
holidaySet: Set<string>,
|
|
655
|
+
sprintOpts?: { sprintLength?: Duration }
|
|
617
656
|
): Set<string> {
|
|
618
657
|
if (sortedIds.length === 0) return new Set();
|
|
619
658
|
|
|
@@ -660,7 +699,8 @@ function computeCriticalPath(
|
|
|
660
699
|
succTask.offset.duration,
|
|
661
700
|
holidays,
|
|
662
701
|
holidaySet,
|
|
663
|
-
reverseDir
|
|
702
|
+
reverseDir,
|
|
703
|
+
sprintOpts
|
|
664
704
|
);
|
|
665
705
|
succLS = adjusted.getTime();
|
|
666
706
|
}
|
|
@@ -674,7 +714,8 @@ function computeCriticalPath(
|
|
|
674
714
|
depOffset.duration,
|
|
675
715
|
holidays,
|
|
676
716
|
holidaySet,
|
|
677
|
-
reverseDir
|
|
717
|
+
reverseDir,
|
|
718
|
+
sprintOpts
|
|
678
719
|
);
|
|
679
720
|
succLS = adjusted.getTime();
|
|
680
721
|
}
|
|
@@ -769,6 +810,78 @@ function buildResolvedGroups(
|
|
|
769
810
|
}
|
|
770
811
|
}
|
|
771
812
|
|
|
813
|
+
// ── Sprint band generation ──────────────────────────────────
|
|
814
|
+
|
|
815
|
+
function generateSprintBands(
|
|
816
|
+
options: GanttOptions,
|
|
817
|
+
chartStart: Date,
|
|
818
|
+
chartEnd: Date,
|
|
819
|
+
projectStart: Date
|
|
820
|
+
): ResolvedSprint[] {
|
|
821
|
+
const sprintLength = options.sprintLength!;
|
|
822
|
+
const sprintNumber = options.sprintNumber ?? 1;
|
|
823
|
+
|
|
824
|
+
// Determine anchor date: sprint-start or chart start or today
|
|
825
|
+
let anchorDate: Date;
|
|
826
|
+
if (options.sprintStart) {
|
|
827
|
+
anchorDate = new Date(options.sprintStart + 'T00:00:00');
|
|
828
|
+
} else if (options.start) {
|
|
829
|
+
anchorDate = new Date(projectStart);
|
|
830
|
+
} else {
|
|
831
|
+
const now = new Date();
|
|
832
|
+
anchorDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
833
|
+
}
|
|
834
|
+
anchorDate.setHours(0, 0, 0, 0);
|
|
835
|
+
|
|
836
|
+
// Sprint length in whole days (parser ensures only d/w with integer day result)
|
|
837
|
+
const sprintDays = Math.round(
|
|
838
|
+
sprintLength.amount * (sprintLength.unit === 'w' ? 7 : 1)
|
|
839
|
+
);
|
|
840
|
+
if (sprintDays <= 0) return []; // defensive guard
|
|
841
|
+
|
|
842
|
+
// Bail if anchor date is invalid (e.g. sprint-start 2026-13-45)
|
|
843
|
+
if (Number.isNaN(anchorDate.getTime())) return [];
|
|
844
|
+
|
|
845
|
+
// Calculate which sprint chartStart falls in, relative to the anchor
|
|
846
|
+
const chartStartTime = new Date(chartStart);
|
|
847
|
+
chartStartTime.setHours(0, 0, 0, 0);
|
|
848
|
+
const chartEndTime = new Date(chartEnd);
|
|
849
|
+
chartEndTime.setHours(0, 0, 0, 0);
|
|
850
|
+
|
|
851
|
+
const msPerDay = 86400000;
|
|
852
|
+
const dayDiff = Math.floor(
|
|
853
|
+
(chartStartTime.getTime() - anchorDate.getTime()) / msPerDay
|
|
854
|
+
);
|
|
855
|
+
// Floor division: which sprint index (from anchor) does chartStart fall in?
|
|
856
|
+
const startSprintIndex = Math.floor(dayDiff / sprintDays);
|
|
857
|
+
|
|
858
|
+
const sprints: ResolvedSprint[] = [];
|
|
859
|
+
const maxSprints = 1000; // safety guard against infinite loops
|
|
860
|
+
|
|
861
|
+
// Generate sprints that overlap with [chartStart, chartEnd]
|
|
862
|
+
// Sprint at index i (relative to anchor) has number: sprintNumber + i
|
|
863
|
+
for (let i = startSprintIndex; sprints.length < maxSprints; i++) {
|
|
864
|
+
const sprintStartDate = new Date(anchorDate);
|
|
865
|
+
sprintStartDate.setDate(sprintStartDate.getDate() + i * sprintDays);
|
|
866
|
+
sprintStartDate.setHours(0, 0, 0, 0);
|
|
867
|
+
|
|
868
|
+
const sprintEndDate = new Date(sprintStartDate);
|
|
869
|
+
sprintEndDate.setDate(sprintEndDate.getDate() + sprintDays);
|
|
870
|
+
sprintEndDate.setHours(0, 0, 0, 0);
|
|
871
|
+
|
|
872
|
+
// Stop when sprint start is past chart end
|
|
873
|
+
if (sprintStartDate.getTime() >= chartEndTime.getTime() + msPerDay) break;
|
|
874
|
+
|
|
875
|
+
sprints.push({
|
|
876
|
+
number: sprintNumber + i,
|
|
877
|
+
startDate: sprintStartDate,
|
|
878
|
+
endDate: sprintEndDate,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return sprints;
|
|
883
|
+
}
|
|
884
|
+
|
|
772
885
|
// ── Utility ─────────────────────────────────────────────────
|
|
773
886
|
|
|
774
887
|
function formatDate(d: Date): string {
|