Skip to content

Commit dbf52a4

Browse files
committed
Add warning to use ids if release name contains spaces
1 parent 90eb149 commit dbf52a4

File tree

5 files changed

+93
-54
lines changed

5 files changed

+93
-54
lines changed

README.md

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -86,33 +86,50 @@ Section names are case-insensitive.
8686
### Assigning stories to releases
8787

8888
Use the `[release:: name]` field on each story to assign it to a release
89-
swimlane. The name must match a release defined in the `# Releases` section.
90-
Stories without a `[release::]` field are parsed but not shown in any swimlane.
89+
swimlane. The value must match a release name (or id, see below) defined in
90+
`# Releases`. Stories without a `[release::]` field are parsed but not shown
91+
in any swimlane.
9192

9293
```markdown
9394
### Authentication
9495
#### Sign in [status:: done] [release:: MVP]
95-
#### Remember me [status:: done] [release:: MVP]
9696
#### SSO [status:: not-started] [release:: Beta]
97-
#### SAML [status:: not-started] [release:: GA]
9897
```
9998

100-
This is more explicit than positional separators — each story carries its own
101-
release assignment regardless of order in the file.
99+
### Release identifiers
100+
101+
If a release has a long display name, add an `[id:: short-name]` field to the
102+
release heading. Stories then use the short id instead of the full name.
103+
storymap will warn if a release name contains spaces and has no `[id::]`.
104+
105+
```markdown
106+
# Releases
107+
## Minimum Viable Product [id:: mvp]
108+
## Private Beta [id:: beta]
109+
110+
# Map
111+
## User Management
112+
### Authentication
113+
#### Sign in [status:: done] [release:: mvp]
114+
#### SSO [status:: not-started] [release:: beta]
115+
```
116+
117+
This decouples the display name from the identifier — you can rename the
118+
release heading freely without updating every story.
102119

103120
### Story fields
104121

105122
Stories support optional inline fields using `[key:: value]` syntax.
106123
Fields appear as badges on the rendered story card.
107124

108125
```markdown
109-
#### Story name [status:: done] [persona:: Margie the Manager] [deadline:: 2026-03-01] [release:: MVP]
126+
#### Story name [status:: done] [persona:: Margie the Manager] [deadline:: 2026-03-01] [release:: mvp]
110127
```
111128

112129
| Field | Values | Default |
113130
|---|---|---|
114131
| `status` | `not-started`, `in-progress`, `done`, `blocked` | `not-started` |
115-
| `release` | Release name from `# Releases` section ||
132+
| `release` | Release name or id from `# Releases` section ||
116133
| `persona` | Any string matching a persona name ||
117134
| `deadline` | ISO date `YYYY-MM-DD` ||
118135

@@ -125,24 +142,13 @@ is treated as the story description. Descriptions support standard markdown:
125142
bold, italics, links, lists, and images.
126143

127144
The first paragraph is always visible on the story card. Additional paragraphs
128-
(separated by a blank line) are shown only in Detail zoom level.
129-
130-
```markdown
131-
#### Sign in [status:: done] [release:: MVP]
132-
User can log in with email and password.
133-
134-
**Acceptance criteria:**
135-
- Given valid credentials, user is redirected to dashboard
136-
- Given invalid credentials, an error message is shown
137-
138-
![wireframe](./screens/sign-in.png)
139-
```
145+
are shown only in Detail zoom level.
140146

141147
### Images
142148

143-
Images in story descriptions and persona descriptions are embedded as base64
144-
data URIs in the output HTML, making the file fully self-contained. Paths are
145-
resolved relative to the source `.md` file. Remote URLs are left as-is.
149+
Images in story and persona descriptions are embedded as base64 data URIs in
150+
the output HTML, making the file fully self-contained. Paths are resolved
151+
relative to the source `.md` file. Remote URLs are left as-is.
146152

147153
## Interactive HTML features
148154

@@ -151,15 +157,14 @@ The rendered HTML includes controls for navigating large maps:
151157
**Zoom levels** — three buttons in the sticky header:
152158
- **Overview** — story names only, compact layout
153159
- **Map** — story names, status badges, and first-paragraph descriptions
154-
- **Detail** — full descriptions and acceptance criteria expanded
160+
- **Detail** — full descriptions expanded
155161

156162
**Release focus** — a dropdown to highlight one release swimlane and dim
157163
the rest. Works independently of the zoom level.
158164

159165
**Story Lens** — a toggle that enables click-to-zoom on individual story cards.
160-
When active, clicking a card expands it to ~40% of the viewport width with full
161-
details visible. Clicking outside collapses it. Stories in the focused release
162-
(if any) are the only ones that can be expanded.
166+
When active, clicking a card expands it to ~40% of the viewport width. Clicking
167+
outside collapses it.
163168

164169
## CLI reference
165170

SKILL.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ render them to HTML with `storymap render <file>`.
2323
Optional description.
2424
2525
# Releases ← required, defines swimlane labels
26-
## Release 1
27-
## Release 2
26+
## MVP
27+
## Private Beta [id:: beta] ← use [id::] when name has spaces
2828
2929
# Personas ← optional
3030
## Persona Name
@@ -33,19 +33,21 @@ Description with markdown.
3333
# Map ← required
3434
## Activity ← column group
3535
### Task ← column
36-
#### Story [status:: done] [persona:: Name] [release:: Release 1] [deadline:: YYYY-MM-DD]
36+
#### Story [status:: done] [persona:: Name] [release:: MVP] [deadline:: YYYY-MM-DD]
3737
Optional story description.
3838
39-
#### Story in second swimlane [status:: not-started] [release:: Release 2]
39+
#### Story in beta [status:: not-started] [release:: beta]
4040
```
4141

4242
## Rules
4343

4444
- `# Releases`, `# Personas`, `# Map` are reserved section names (case-insensitive)
4545
- Any other `#` heading becomes the document title (first one) or is passed through
4646
- Map hierarchy is strictly `##` Activity → `###` Task → `####` Story
47-
- Each story is assigned to a release swimlane via `[release:: Release Name]`
48-
- The `[release::]` value must exactly match a release name defined in `# Releases`
47+
- Each story is assigned to a release swimlane via `[release:: value]`
48+
- If a release has no `[id::]`, stories use the release name exactly
49+
- If a release has `[id:: slug]`, stories must use the slug — not the display name
50+
- Add `[id::]` whenever a release name contains spaces; omit it for short names like MVP
4951
- Stories without a `[release::]` field are parsed but not shown in any swimlane
5052
- Story names are short labels (a few words); put detail in the description body
5153
- Always add `[status:: ...]` to every story — it drives the card color in the HTML
@@ -60,6 +62,7 @@ Optional story description.
6062
4. If the user hasn't specified releases, default to two: `MVP` and `Next`
6163
5. If the user hasn't specified personas, omit the `# Personas` section
6264
6. Aim for 3–6 activities, 2–4 tasks per activity, 1–3 stories per task per release
65+
7. Use `[id::]` on releases with spaces in the name; skip it for short names
6366

6467
## Example
6568

@@ -71,8 +74,8 @@ A simple app for individuals to manage daily tasks.
7174
## MVP
7275
Core task management.
7376

74-
## Beta
75-
Collaboration features.
77+
## Private Beta [id:: beta]
78+
Collaboration features with selected users.
7679

7780
# Personas
7881
## Sam the Solo User
@@ -86,14 +89,14 @@ Uses the app daily for personal and work tasks.
8689
#### Add a task [status:: done] [persona:: Sam the Solo User] [release:: MVP]
8790
Title and optional due date.
8891

89-
#### Add subtasks [status:: not-started] [release:: Beta]
92+
#### Add subtasks [status:: not-started] [release:: beta]
9093

9194
### Complete Tasks
9295
#### Mark task as done [status:: done] [release:: MVP]
93-
#### Bulk complete [status:: not-started] [release:: Beta]
96+
#### Bulk complete [status:: not-started] [release:: beta]
9497

9598
## Sharing
9699
### Collaboration
97100
#### Share task list [status:: not-started] [release:: MVP]
98-
#### Assign tasks [status:: not-started] [release:: Beta]
101+
#### Assign tasks [status:: not-started] [release:: beta]
99102
```

SYSTEM_PROMPT.md

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,23 @@ Short description of what this product does and who it's for.
2828

2929
Defines the release swimlanes. Each `##` heading is a release.
3030

31+
For short release names (no spaces), no extra syntax is needed:
3132
```markdown
3233
# Releases
3334
## MVP
34-
Core functionality for the first public launch.
35-
3635
## Beta
37-
Invite-only beta with selected users.
3836
```
3937

38+
For longer names, add an `[id:: slug]` field. Stories then reference the slug
39+
instead of the full name — this decouples display name from identifier:
40+
```markdown
41+
# Releases
42+
## Minimum Viable Product [id:: mvp]
43+
## Private Beta [id:: beta]
44+
```
45+
46+
storymap will warn if a release name contains spaces and has no `[id::]`.
47+
4048
### Personas section (optional)
4149

4250
UX personas. Each `##` heading is a persona. Descriptions support full markdown.
@@ -60,22 +68,23 @@ The story map itself. Three levels of hierarchy:
6068
#### Story — card in a release swimlane
6169
```
6270

63-
Each story is assigned to a release swimlane using the `[release:: name]` field.
64-
The name must exactly match a release defined in `# Releases`.
65-
Stories without a `[release::]` field are parsed but not shown in any swimlane.
71+
Each story is assigned to a release swimlane using `[release:: value]`. The
72+
value must match the release name exactly (for releases without an id) or the
73+
release id (for releases with `[id::]`). Stories without `[release::]` are
74+
parsed but not shown in any swimlane.
6675

6776
```markdown
6877
## User Management
6978
### Authentication
70-
#### Sign in [status:: done] [persona:: Alice the User] [release:: MVP]
79+
#### Sign in [status:: done] [persona:: Alice the User] [release:: mvp]
7180
User can log in with email and password.
7281

73-
#### SSO login [status:: in-progress] [release:: Beta]
82+
#### SSO login [status:: in-progress] [release:: beta]
7483
Support Google and Microsoft OAuth.
7584

7685
### Profile
77-
#### Edit profile [status:: done] [release:: MVP]
78-
#### Upload avatar [status:: not-started] [release:: Beta]
86+
#### Edit profile [status:: done] [release:: mvp]
87+
#### Upload avatar [status:: not-started] [release:: beta]
7988
```
8089

8190
### Story fields
@@ -86,7 +95,7 @@ Fields appear as badges on the rendered card.
8695
| Field | Values |
8796
|---|---|
8897
| `status` | `not-started`, `in-progress`, `done`, `blocked` |
89-
| `release` | Release name from `# Releases` section |
98+
| `release` | Release name or id from `# Releases` section |
9099
| `persona` | Any string matching a persona name |
91100
| `deadline` | ISO date `YYYY-MM-DD` |
92101

@@ -96,7 +105,8 @@ Any other `[key:: value]` field is accepted and rendered as a badge.
96105

97106
- Every task must have at least one story
98107
- Every activity must have at least one task
99-
- Every story must have a `[release:: name]` field matching a defined release
108+
- Every story should have a `[release::]` field — stories without one are invisible in the map
109+
- Use `[id::]` on any release whose name contains spaces; omit it for short names like MVP
100110
- Story names should be short (a few words) — put detail in the description
101111
- Use the `status` field on every story so the map is readable at a glance
102112

@@ -110,7 +120,7 @@ A simple app for individuals to manage daily tasks.
110120
## MVP
111121
Core task management — create, complete, delete.
112122

113-
## Beta
123+
## Private Beta [id:: beta]
114124
Collaboration and sharing features.
115125

116126
# Personas
@@ -124,16 +134,16 @@ Uses the app daily to manage personal and work tasks.
124134
#### Add a task [status:: done] [persona:: Sam the Solo User] [release:: MVP]
125135
User can create a task with a title and optional due date.
126136

127-
#### Add subtasks [status:: not-started] [release:: Beta]
137+
#### Add subtasks [status:: not-started] [release:: beta]
128138
Break tasks into smaller steps.
129139

130140
### Complete Tasks
131141
#### Mark task as done [status:: done] [release:: MVP]
132-
#### Bulk complete [status:: not-started] [release:: Beta]
142+
#### Bulk complete [status:: not-started] [release:: beta]
133143
Select multiple tasks and mark them all done.
134144

135145
## Sharing
136146
### Collaboration
137147
#### Share task list [status:: not-started] [deadline:: 2026-09-01] [release:: MVP]
138-
#### Assign tasks to others [status:: not-started] [release:: Beta]
148+
#### Assign tasks to others [status:: not-started] [release:: beta]
139149
```

storymap/parser.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,12 @@ def _warn_unmatched_releases(doc: StorymapDocument) -> None:
229229
f"Story '{story.name}': release '{r}' not found in "
230230
"Releases section — story will not appear in any swimlane."
231231
)
232+
for release in doc.releases:
233+
if release.id is None and " " in release.name:
234+
doc.warnings.append(
235+
f"Release '{release.name}' has spaces in its name and no [id::] field. "
236+
"Consider adding [id:: short-name] to avoid issues when renaming."
237+
)
232238

233239

234240
def _heading_content(tokens: list, heading_open_index: int) -> str:

tests/test_parser.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,21 @@ def test_release_id_stripped_from_display_name(self):
211211
doc = StorymapParser().parse(src)
212212
assert "[id::" not in doc.releases[0].name
213213

214+
def test_release_name_with_spaces_and_no_id_warns(self):
215+
src = "# Releases\n## My Long Release\n\n# Map\n## A\n### T\n#### S\n"
216+
doc = StorymapParser().parse(src)
217+
assert any("My Long Release" in w and "id::" in w for w in doc.warnings)
218+
219+
def test_release_name_with_spaces_and_id_does_not_warn(self):
220+
src = "# Releases\n## My Long Release [id:: mlr]\n\n# Map\n## A\n### T\n#### S\n"
221+
doc = StorymapParser().parse(src)
222+
assert not any("id::" in w for w in doc.warnings)
223+
224+
def test_release_name_without_spaces_does_not_warn(self):
225+
src = "# Releases\n## MVP\n\n# Map\n## A\n### T\n#### S\n"
226+
doc = StorymapParser().parse(src)
227+
assert doc.warnings == []
228+
214229
def test_release_description_with_markdown(self):
215230
src = (
216231
"# Releases\n"

0 commit comments

Comments
 (0)