@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.
@@ -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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.8.17",
3
+ "version": "0.8.19",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
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 `black`,
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
  */
@@ -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: "9q~RxOX#oXY#tYZ$PZp#opq#tqt#oux#oxy$Uyz$tz{${{|%S|}%Z}!O%b!O!P#o!P!Q%q!Q![&b![!],v!]!^#o!^!_,}!_!`-U!`!a-c!a!b-j!b!c#o!c!}-q!}#O0w#O#P#o#P#Q0|#Q#R#o#R#S-q#S#T#o#T#[-q#[#]1T#]#o-q#o#p#o#p#q9T#q#r#o#r#s9[#s;'S#o;'S;=`9k<%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~&iY^~q~uv'X!O!P'^!Q![(z#U#V(U#W#X([#[#]([#a#b(i#e#f([#k#l([#m#n([~'^O]~~'aP!Q!['d~'iX^~uv'X!Q!['d#U#V(U#W#X([#[#]([#a#b(i#e#f([#k#l([#m#n([~(XP#W#X([~(aPZ~!a!b(d~(iOZ~~(nQZ~!a!b(d#]#^(t~(wP#b#c([~)PY^~uv'X!O!P'^!Q![)o#U#V(U#W#X([#[#]([#a#b(i#e#f([#k#l([#m#n([~)tY^~uv'X!O!P'^!Q![*d#U#V(U#W#X([#[#]([#a#b(i#e#f([#k#l([#m#n([~*iZ^~uv'X}!O+[!O!P'^!Q![,R#U#V(U#W#X([#[#]([#a#b(i#e#f([#k#l([#m#n([~+_P!Q![+b~+eP!Q![+h~+mP[~}!O+p~+sP!Q![+v~+yP!Q![+|~,RO[~~,WY^~uv'X!O!P'^!Q![,R#U#V(U#W#X([#[#]([#a#b(i#e#f([#k#l([#m#n([~,}Oi~q~~-UOe~q~~-ZPq~!_!`-^~-cO_~~-jOf~q~~-qOo~q~~-x_p~q~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#o.w~.|_p~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#o.w~0O]qr.wst.wvw.wwx.w{|.w!O!P.w!P!Q.w!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#o.w~0|Oa~~1TOb~q~~1[ap~q~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#h.w#h#i2a#i#o.w~2fap~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#h.w#h#i3k#i#o.w~3pap~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#d.w#d#e4u#e#o.w~4zbp~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w![!]6S!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#g.w#g#h7|#h#o.w~6VP!P!Q6Y~6]P!P!Q6`~6cYOX7RZp7Rqy7Rz|7R}!`7R!a#P7R#Q#p7R#q;'S7R;'S;=`7v<%lO7R~7WY`~OX7RZp7Rqy7Rz|7R}!`7R!a#P7R#Q#p7R#q;'S7R;'S;=`7v<%lO7R~7yP;=`<%l7R~8R`p~qr.wst.wvw.wwx.w{|.w}!O/{!O!P.w!P!Q.w!Q![.w![!]6S!_!`.w!a!b.w!b!c.w!c!}.w#R#S.w#T#o.w~9[Oh~q~~9cPm~q~!`!a9f~9kOY~~9nP;=`<%l#o",
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!Q![)Q#U#V([#W#X(b#[#](b#a#b(o#e#f(b#g#h(b#k#l(b#m#n(b~'aO]~~'dP!Q!['g~'lY^~uv'[!Q!['g#U#V([#W#X(b#[#](b#a#b(o#e#f(b#g#h(b#k#l(b#m#n(b~(_P#W#X(b~(gPZ~!a!b(j~(oOZ~~(tQZ~!a!b(j#]#^(z~(}P#b#c(b~)VZ^~uv'[!O!P'a!Q![)x#U#V([#W#X(b#[#](b#a#b(o#e#f(b#g#h(b#k#l(b#m#n(b~)}Z^~uv'[!O!P'a!Q![*p#U#V([#W#X(b#[#](b#a#b(o#e#f(b#g#h(b#k#l(b#m#n(b~*u[^~uv'[}!O+k!O!P'a!Q![,b#U#V([#W#X(b#[#](b#a#b(o#e#f(b#g#h(b#k#l(b#m#n(b~+nP!Q![+q~+tP!Q![+w~+|P[~}!O,P~,SP!Q![,V~,YP!Q![,]~,bO[~~,gZ^~uv'[!O!P'a!Q![,b#U#V([#W#X(b#[#](b#a#b(o#e#f(b#g#h(b#k#l(b#m#n(b~-aOi~q~~-hOe~q~~-mPq~!_!`-p~-uO_~~-|Of~q~~.TOo~q~~.[_p~q~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#o/Z~/`_p~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#o/Z~0b]qr/Zst/Zvw/Zwx/Z{|/Z!O!P/Z!P!Q/Z!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#o/Z~1`Oa~~1gOb~q~~1nap~q~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#h/Z#h#i2s#i#o/Z~2xap~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#h/Z#h#i3}#i#o/Z~4Sap~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#d/Z#d#e5X#e#o/Z~5^bp~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z![!]6f!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#g/Z#g#h8`#h#o/Z~6iP!P!Q6l~6oP!P!Q6r~6uYOX7eZp7eqy7ez|7e}!`7e!a#P7e#Q#p7e#q;'S7e;'S;=`8Y<%lO7e~7jY`~OX7eZp7eqy7ez|7e}!`7e!a#P7e#Q#p7e#q;'S7e;'S;=`8Y<%lO7e~8]P;=`<%l7e~8e`p~qr/Zst/Zvw/Zwx/Z{|/Z}!O0_!O!P/Z!P!Q/Z!Q![/Z![!]6f!_!`/Z!a!b/Z!b!c/Z!c!}/Z#R#S/Z#T#o/Z~9nOh~q~~9uPm~q~!`!a9x~9}OY~~:QP;=`<%l#o",
14
14
  tokenizers: [0],
15
15
  topRules: {"Document":[0,6]},
16
16
  specialized: [{term: 32, get: (value, stack) => (specializeKeyword(value, stack) << 1), external: specializeKeyword}],
@@ -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 {