julee 0.1.5__py3-none-any.whl → 0.1.7__py3-none-any.whl
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.
- julee/__init__.py +1 -1
- julee/contrib/polling/apps/worker/pipelines.py +3 -1
- julee/contrib/polling/tests/unit/apps/worker/test_pipelines.py +3 -0
- julee/docs/sphinx_hcd/__init__.py +146 -13
- julee/docs/sphinx_hcd/domain/__init__.py +5 -0
- julee/docs/sphinx_hcd/domain/models/__init__.py +32 -0
- julee/docs/sphinx_hcd/domain/models/accelerator.py +152 -0
- julee/docs/sphinx_hcd/domain/models/app.py +151 -0
- julee/docs/sphinx_hcd/domain/models/code_info.py +121 -0
- julee/docs/sphinx_hcd/domain/models/epic.py +79 -0
- julee/docs/sphinx_hcd/domain/models/integration.py +230 -0
- julee/docs/sphinx_hcd/domain/models/journey.py +222 -0
- julee/docs/sphinx_hcd/domain/models/persona.py +106 -0
- julee/docs/sphinx_hcd/domain/models/story.py +128 -0
- julee/docs/sphinx_hcd/domain/repositories/__init__.py +25 -0
- julee/docs/sphinx_hcd/domain/repositories/accelerator.py +98 -0
- julee/docs/sphinx_hcd/domain/repositories/app.py +57 -0
- julee/docs/sphinx_hcd/domain/repositories/base.py +89 -0
- julee/docs/sphinx_hcd/domain/repositories/code_info.py +69 -0
- julee/docs/sphinx_hcd/domain/repositories/epic.py +62 -0
- julee/docs/sphinx_hcd/domain/repositories/integration.py +79 -0
- julee/docs/sphinx_hcd/domain/repositories/journey.py +106 -0
- julee/docs/sphinx_hcd/domain/repositories/story.py +68 -0
- julee/docs/sphinx_hcd/domain/use_cases/__init__.py +64 -0
- julee/docs/sphinx_hcd/domain/use_cases/derive_personas.py +166 -0
- julee/docs/sphinx_hcd/domain/use_cases/resolve_accelerator_references.py +236 -0
- julee/docs/sphinx_hcd/domain/use_cases/resolve_app_references.py +144 -0
- julee/docs/sphinx_hcd/domain/use_cases/resolve_story_references.py +121 -0
- julee/docs/sphinx_hcd/parsers/__init__.py +48 -0
- julee/docs/sphinx_hcd/parsers/ast.py +150 -0
- julee/docs/sphinx_hcd/parsers/gherkin.py +155 -0
- julee/docs/sphinx_hcd/parsers/yaml.py +184 -0
- julee/docs/sphinx_hcd/repositories/__init__.py +4 -0
- julee/docs/sphinx_hcd/repositories/memory/__init__.py +25 -0
- julee/docs/sphinx_hcd/repositories/memory/accelerator.py +86 -0
- julee/docs/sphinx_hcd/repositories/memory/app.py +45 -0
- julee/docs/sphinx_hcd/repositories/memory/base.py +106 -0
- julee/docs/sphinx_hcd/repositories/memory/code_info.py +59 -0
- julee/docs/sphinx_hcd/repositories/memory/epic.py +54 -0
- julee/docs/sphinx_hcd/repositories/memory/integration.py +70 -0
- julee/docs/sphinx_hcd/repositories/memory/journey.py +96 -0
- julee/docs/sphinx_hcd/repositories/memory/story.py +63 -0
- julee/docs/sphinx_hcd/sphinx/__init__.py +28 -0
- julee/docs/sphinx_hcd/sphinx/adapters.py +116 -0
- julee/docs/sphinx_hcd/sphinx/context.py +163 -0
- julee/docs/sphinx_hcd/sphinx/directives/__init__.py +160 -0
- julee/docs/sphinx_hcd/sphinx/directives/accelerator.py +576 -0
- julee/docs/sphinx_hcd/sphinx/directives/app.py +349 -0
- julee/docs/sphinx_hcd/sphinx/directives/base.py +211 -0
- julee/docs/sphinx_hcd/sphinx/directives/epic.py +434 -0
- julee/docs/sphinx_hcd/sphinx/directives/integration.py +220 -0
- julee/docs/sphinx_hcd/sphinx/directives/journey.py +642 -0
- julee/docs/sphinx_hcd/sphinx/directives/persona.py +345 -0
- julee/docs/sphinx_hcd/sphinx/directives/story.py +575 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/__init__.py +16 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/builder_inited.py +31 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_read.py +27 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_resolved.py +43 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/env_purge_doc.py +42 -0
- julee/docs/sphinx_hcd/sphinx/initialization.py +139 -0
- julee/docs/sphinx_hcd/tests/__init__.py +9 -0
- julee/docs/sphinx_hcd/tests/conftest.py +6 -0
- julee/docs/sphinx_hcd/tests/domain/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/domain/models/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_accelerator.py +266 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_app.py +258 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_code_info.py +231 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_epic.py +163 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_integration.py +327 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_journey.py +249 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_persona.py +172 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_story.py +216 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_derive_personas.py +314 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_accelerator_references.py +476 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_app_references.py +265 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_story_references.py +229 -0
- julee/docs/sphinx_hcd/tests/integration/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/parsers/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/parsers/test_ast.py +298 -0
- julee/docs/sphinx_hcd/tests/parsers/test_gherkin.py +282 -0
- julee/docs/sphinx_hcd/tests/parsers/test_yaml.py +496 -0
- julee/docs/sphinx_hcd/tests/repositories/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/repositories/test_accelerator.py +298 -0
- julee/docs/sphinx_hcd/tests/repositories/test_app.py +218 -0
- julee/docs/sphinx_hcd/tests/repositories/test_base.py +151 -0
- julee/docs/sphinx_hcd/tests/repositories/test_code_info.py +253 -0
- julee/docs/sphinx_hcd/tests/repositories/test_epic.py +237 -0
- julee/docs/sphinx_hcd/tests/repositories/test_integration.py +268 -0
- julee/docs/sphinx_hcd/tests/repositories/test_journey.py +294 -0
- julee/docs/sphinx_hcd/tests/repositories/test_story.py +236 -0
- julee/docs/sphinx_hcd/tests/sphinx/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/sphinx/directives/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/sphinx/directives/test_base.py +160 -0
- julee/docs/sphinx_hcd/tests/sphinx/test_adapters.py +176 -0
- julee/docs/sphinx_hcd/tests/sphinx/test_context.py +257 -0
- {julee-0.1.5.dist-info → julee-0.1.7.dist-info}/METADATA +2 -1
- {julee-0.1.5.dist-info → julee-0.1.7.dist-info}/RECORD +101 -16
- julee/docs/sphinx_hcd/accelerators.py +0 -1175
- julee/docs/sphinx_hcd/apps.py +0 -518
- julee/docs/sphinx_hcd/epics.py +0 -453
- julee/docs/sphinx_hcd/integrations.py +0 -310
- julee/docs/sphinx_hcd/journeys.py +0 -797
- julee/docs/sphinx_hcd/personas.py +0 -457
- julee/docs/sphinx_hcd/stories.py +0 -960
- {julee-0.1.5.dist-info → julee-0.1.7.dist-info}/WHEEL +0 -0
- {julee-0.1.5.dist-info → julee-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {julee-0.1.5.dist-info → julee-0.1.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
"""Story directives for sphinx_hcd.
|
|
2
|
+
|
|
3
|
+
Provides directives for rendering user stories from Gherkin feature files:
|
|
4
|
+
- story-app: Full rendering of stories for an app (with anchors)
|
|
5
|
+
- story-list-for-persona: List of stories for a persona
|
|
6
|
+
- story-list-for-app: List of stories for an app
|
|
7
|
+
- story-index: Toctree-style index of per-app story pages
|
|
8
|
+
- stories: Render specific stories by name
|
|
9
|
+
- story: Single story reference
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
|
|
14
|
+
from docutils import nodes
|
|
15
|
+
|
|
16
|
+
from ...domain.models.story import Story
|
|
17
|
+
from ...domain.use_cases import (
|
|
18
|
+
get_epics_for_story,
|
|
19
|
+
get_journeys_for_story,
|
|
20
|
+
)
|
|
21
|
+
from ...utils import normalize_name, slugify
|
|
22
|
+
from .base import HCDDirective, make_deprecated_directive
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StorySeeAlsoPlaceholder(nodes.General, nodes.Element):
|
|
26
|
+
"""Placeholder for story seealso block, replaced at doctree-read."""
|
|
27
|
+
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StoryAppDirective(HCDDirective):
|
|
32
|
+
"""Render all stories for an application with full details and anchors.
|
|
33
|
+
|
|
34
|
+
Usage::
|
|
35
|
+
|
|
36
|
+
.. story-app:: staff-portal
|
|
37
|
+
|
|
38
|
+
Renders stories grouped by persona, each with:
|
|
39
|
+
- Heading with anchor
|
|
40
|
+
- Gherkin snippet
|
|
41
|
+
- Feature file path
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
required_arguments = 1
|
|
45
|
+
|
|
46
|
+
def run(self):
|
|
47
|
+
app_arg = self.arguments[0]
|
|
48
|
+
app_normalized = normalize_name(app_arg)
|
|
49
|
+
|
|
50
|
+
# Get stories from repository
|
|
51
|
+
all_stories = self.hcd_context.story_repo.list_all()
|
|
52
|
+
stories = [s for s in all_stories if s.app_normalized == app_normalized]
|
|
53
|
+
|
|
54
|
+
if not stories:
|
|
55
|
+
return self.empty_result(f"No stories found for application '{app_arg}'")
|
|
56
|
+
|
|
57
|
+
# Get known apps and personas for validation
|
|
58
|
+
all_apps = self.hcd_context.app_repo.list_all()
|
|
59
|
+
known_apps = {normalize_name(a.name) for a in all_apps}
|
|
60
|
+
|
|
61
|
+
# Group stories by persona
|
|
62
|
+
by_persona: dict[str, list[Story]] = defaultdict(list)
|
|
63
|
+
for story in stories:
|
|
64
|
+
by_persona[story.persona].append(story)
|
|
65
|
+
|
|
66
|
+
result_nodes = []
|
|
67
|
+
|
|
68
|
+
# Build intro paragraph
|
|
69
|
+
persona_count = len(by_persona)
|
|
70
|
+
total_stories = len(stories)
|
|
71
|
+
app_display = app_arg.replace("-", " ").title()
|
|
72
|
+
app_valid = app_normalized in known_apps
|
|
73
|
+
|
|
74
|
+
intro_para = nodes.paragraph()
|
|
75
|
+
intro_para += nodes.Text("The ")
|
|
76
|
+
|
|
77
|
+
if app_valid:
|
|
78
|
+
intro_para += self.make_app_link(app_arg)
|
|
79
|
+
else:
|
|
80
|
+
intro_para += nodes.Text(app_display)
|
|
81
|
+
|
|
82
|
+
if total_stories == 1:
|
|
83
|
+
intro_para += nodes.Text(" has one story for ")
|
|
84
|
+
else:
|
|
85
|
+
intro_para += nodes.Text(f" has {total_stories} stories ")
|
|
86
|
+
|
|
87
|
+
if persona_count == 1:
|
|
88
|
+
persona = list(by_persona.keys())[0]
|
|
89
|
+
if total_stories != 1:
|
|
90
|
+
intro_para += nodes.Text("for ")
|
|
91
|
+
intro_para += self.make_persona_link(persona)
|
|
92
|
+
intro_para += nodes.Text(".")
|
|
93
|
+
else:
|
|
94
|
+
intro_para += nodes.Text(f"across {persona_count} personas: ")
|
|
95
|
+
sorted_personas = sorted(by_persona.keys())
|
|
96
|
+
for i, persona in enumerate(sorted_personas):
|
|
97
|
+
count = len(by_persona[persona])
|
|
98
|
+
intro_para += self.make_persona_link(persona)
|
|
99
|
+
intro_para += nodes.Text(f" ({count})")
|
|
100
|
+
if i < len(sorted_personas) - 1:
|
|
101
|
+
intro_para += nodes.Text(", ")
|
|
102
|
+
else:
|
|
103
|
+
intro_para += nodes.Text(".")
|
|
104
|
+
|
|
105
|
+
result_nodes.append(intro_para)
|
|
106
|
+
|
|
107
|
+
# Render stories grouped by persona
|
|
108
|
+
for persona in sorted(by_persona.keys()):
|
|
109
|
+
persona_stories = by_persona[persona]
|
|
110
|
+
persona_slug_id = slugify(persona)
|
|
111
|
+
|
|
112
|
+
persona_section = nodes.section(ids=[persona_slug_id])
|
|
113
|
+
persona_section += nodes.title(text=persona)
|
|
114
|
+
|
|
115
|
+
for story in sorted(persona_stories, key=lambda s: s.feature_title):
|
|
116
|
+
story_section = nodes.section(ids=[story.slug])
|
|
117
|
+
story_section += nodes.title(text=story.feature_title)
|
|
118
|
+
|
|
119
|
+
# Gherkin snippet
|
|
120
|
+
if story.gherkin_snippet:
|
|
121
|
+
snippet = nodes.literal_block(text=story.gherkin_snippet)
|
|
122
|
+
snippet["language"] = "gherkin"
|
|
123
|
+
story_section += snippet
|
|
124
|
+
|
|
125
|
+
# Feature file path
|
|
126
|
+
path_para = nodes.paragraph()
|
|
127
|
+
path_para += nodes.strong(text="Feature file: ")
|
|
128
|
+
path_para += nodes.literal(text=story.file_path)
|
|
129
|
+
story_section += path_para
|
|
130
|
+
|
|
131
|
+
# Placeholder for seealso (filled in doctree-read)
|
|
132
|
+
seealso_placeholder = StorySeeAlsoPlaceholder()
|
|
133
|
+
seealso_placeholder["story_feature"] = story.feature_title
|
|
134
|
+
seealso_placeholder["story_persona"] = story.persona
|
|
135
|
+
seealso_placeholder["story_app"] = story.app_slug
|
|
136
|
+
story_section += seealso_placeholder
|
|
137
|
+
|
|
138
|
+
persona_section += story_section
|
|
139
|
+
|
|
140
|
+
result_nodes.append(persona_section)
|
|
141
|
+
|
|
142
|
+
return result_nodes
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class StoryListForPersonaDirective(HCDDirective):
|
|
146
|
+
"""Render stories for a specific persona as a simple bullet list.
|
|
147
|
+
|
|
148
|
+
Usage::
|
|
149
|
+
|
|
150
|
+
.. story-list-for-persona:: Pilot Manager
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
required_arguments = 1
|
|
154
|
+
final_argument_whitespace = True
|
|
155
|
+
|
|
156
|
+
def run(self):
|
|
157
|
+
persona_arg = self.arguments[0]
|
|
158
|
+
persona_normalized = normalize_name(persona_arg)
|
|
159
|
+
|
|
160
|
+
# Get stories from repository
|
|
161
|
+
all_stories = self.hcd_context.story_repo.list_all()
|
|
162
|
+
stories = [s for s in all_stories if s.persona_normalized == persona_normalized]
|
|
163
|
+
|
|
164
|
+
if not stories:
|
|
165
|
+
return self.empty_result(f"No stories found for persona '{persona_arg}'")
|
|
166
|
+
|
|
167
|
+
# Get known apps for validation
|
|
168
|
+
all_apps = self.hcd_context.app_repo.list_all()
|
|
169
|
+
known_apps = {normalize_name(a.name) for a in all_apps}
|
|
170
|
+
|
|
171
|
+
story_list = nodes.bullet_list()
|
|
172
|
+
|
|
173
|
+
for story in sorted(stories, key=lambda s: s.feature_title.lower()):
|
|
174
|
+
story_item = nodes.list_item()
|
|
175
|
+
story_para = nodes.paragraph()
|
|
176
|
+
|
|
177
|
+
# Story link
|
|
178
|
+
story_para += self.make_story_link(story)
|
|
179
|
+
|
|
180
|
+
# App in parentheses
|
|
181
|
+
story_para += nodes.Text(" (")
|
|
182
|
+
app_valid = normalize_name(story.app_slug) in known_apps
|
|
183
|
+
if app_valid:
|
|
184
|
+
story_para += self.make_app_link(story.app_slug)
|
|
185
|
+
else:
|
|
186
|
+
story_para += nodes.Text(story.app_slug.replace("-", " ").title())
|
|
187
|
+
story_para += nodes.Text(")")
|
|
188
|
+
|
|
189
|
+
story_item += story_para
|
|
190
|
+
story_list += story_item
|
|
191
|
+
|
|
192
|
+
return [story_list]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class StoryListForAppDirective(HCDDirective):
|
|
196
|
+
"""Render stories for a specific application, grouped by persona then benefit.
|
|
197
|
+
|
|
198
|
+
Usage::
|
|
199
|
+
|
|
200
|
+
.. story-list-for-app:: staff-portal
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
required_arguments = 1
|
|
204
|
+
|
|
205
|
+
def run(self):
|
|
206
|
+
app_arg = self.arguments[0]
|
|
207
|
+
app_normalized = normalize_name(app_arg)
|
|
208
|
+
|
|
209
|
+
# Get stories from repository
|
|
210
|
+
all_stories = self.hcd_context.story_repo.list_all()
|
|
211
|
+
stories = [s for s in all_stories if s.app_normalized == app_normalized]
|
|
212
|
+
|
|
213
|
+
if not stories:
|
|
214
|
+
return self.empty_result(f"No stories found for application '{app_arg}'")
|
|
215
|
+
|
|
216
|
+
# Group by persona, then by benefit
|
|
217
|
+
by_persona: dict[str, dict[str, list[Story]]] = defaultdict(
|
|
218
|
+
lambda: defaultdict(list)
|
|
219
|
+
)
|
|
220
|
+
for story in stories:
|
|
221
|
+
by_persona[story.persona][story.so_that].append(story)
|
|
222
|
+
|
|
223
|
+
result_nodes = []
|
|
224
|
+
|
|
225
|
+
for persona in sorted(by_persona.keys()):
|
|
226
|
+
benefits = by_persona[persona]
|
|
227
|
+
|
|
228
|
+
# Persona heading
|
|
229
|
+
persona_heading = nodes.paragraph()
|
|
230
|
+
persona_ref = self.make_persona_link(persona)
|
|
231
|
+
persona_ref.children = [nodes.strong(text=persona)]
|
|
232
|
+
persona_heading += persona_ref
|
|
233
|
+
result_nodes.append(persona_heading)
|
|
234
|
+
|
|
235
|
+
# Outer bullet list for benefits
|
|
236
|
+
benefit_list = nodes.bullet_list()
|
|
237
|
+
|
|
238
|
+
for benefit in sorted(benefits.keys()):
|
|
239
|
+
benefit_stories = benefits[benefit]
|
|
240
|
+
benefit_item = nodes.list_item()
|
|
241
|
+
|
|
242
|
+
benefit_para = nodes.paragraph()
|
|
243
|
+
benefit_para += nodes.Text("So that ")
|
|
244
|
+
benefit_para += nodes.Text(benefit)
|
|
245
|
+
benefit_item += benefit_para
|
|
246
|
+
|
|
247
|
+
# Inner bullet list for features
|
|
248
|
+
feature_list = nodes.bullet_list()
|
|
249
|
+
|
|
250
|
+
for story in sorted(benefit_stories, key=lambda s: s.i_want):
|
|
251
|
+
feature_item = nodes.list_item()
|
|
252
|
+
feature_para = nodes.paragraph()
|
|
253
|
+
feature_para += nodes.Text("I need to ")
|
|
254
|
+
feature_para += self.make_story_link(story)
|
|
255
|
+
feature_item += feature_para
|
|
256
|
+
feature_list += feature_item
|
|
257
|
+
|
|
258
|
+
benefit_item += feature_list
|
|
259
|
+
benefit_list += benefit_item
|
|
260
|
+
|
|
261
|
+
result_nodes.append(benefit_list)
|
|
262
|
+
|
|
263
|
+
return result_nodes
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class StoryIndexDirective(HCDDirective):
|
|
267
|
+
"""Render index pointing to per-app story pages.
|
|
268
|
+
|
|
269
|
+
Usage::
|
|
270
|
+
|
|
271
|
+
.. story-index::
|
|
272
|
+
|
|
273
|
+
Renders a list of links to per-app story pages with story counts.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def run(self):
|
|
277
|
+
all_stories = self.hcd_context.story_repo.list_all()
|
|
278
|
+
|
|
279
|
+
if not all_stories:
|
|
280
|
+
return self.empty_result("No Gherkin stories found")
|
|
281
|
+
|
|
282
|
+
# Count stories per app
|
|
283
|
+
stories_per_app: dict[str, int] = defaultdict(int)
|
|
284
|
+
for story in all_stories:
|
|
285
|
+
stories_per_app[story.app_slug] += 1
|
|
286
|
+
|
|
287
|
+
app_list = nodes.bullet_list()
|
|
288
|
+
|
|
289
|
+
for app in sorted(stories_per_app.keys()):
|
|
290
|
+
count = stories_per_app[app]
|
|
291
|
+
app_item = nodes.list_item()
|
|
292
|
+
app_para = nodes.paragraph()
|
|
293
|
+
|
|
294
|
+
# Link to app's story page
|
|
295
|
+
app_ref = nodes.reference("", "", refuri=f"{app}.html")
|
|
296
|
+
app_ref += nodes.strong(
|
|
297
|
+
text=app.replace("-", " ").replace("_", " ").title()
|
|
298
|
+
)
|
|
299
|
+
app_para += app_ref
|
|
300
|
+
app_para += nodes.Text(f" ({count} stories)")
|
|
301
|
+
|
|
302
|
+
app_item += app_para
|
|
303
|
+
app_list += app_item
|
|
304
|
+
|
|
305
|
+
return [app_list]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class StoriesDirective(HCDDirective):
|
|
309
|
+
"""Render multiple stories grouped by persona and benefit.
|
|
310
|
+
|
|
311
|
+
Usage::
|
|
312
|
+
|
|
313
|
+
.. stories::
|
|
314
|
+
Upload Scheme Documentation
|
|
315
|
+
Add External Standard to Knowledge Base
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
has_content = True
|
|
319
|
+
|
|
320
|
+
def run(self):
|
|
321
|
+
# Parse feature names from content
|
|
322
|
+
feature_names = [line.strip() for line in self.content if line.strip()]
|
|
323
|
+
|
|
324
|
+
if not feature_names:
|
|
325
|
+
return self.empty_result("No stories specified")
|
|
326
|
+
|
|
327
|
+
# Get all stories for lookup
|
|
328
|
+
all_stories = self.hcd_context.story_repo.list_all()
|
|
329
|
+
story_lookup = {normalize_name(s.feature_title): s for s in all_stories}
|
|
330
|
+
|
|
331
|
+
# Get known apps for validation
|
|
332
|
+
all_apps = self.hcd_context.app_repo.list_all()
|
|
333
|
+
known_apps = {normalize_name(a.name) for a in all_apps}
|
|
334
|
+
|
|
335
|
+
# Look up stories
|
|
336
|
+
stories = []
|
|
337
|
+
not_found = []
|
|
338
|
+
for feature_name in feature_names:
|
|
339
|
+
feature_normalized = normalize_name(feature_name)
|
|
340
|
+
story = story_lookup.get(feature_normalized)
|
|
341
|
+
if story:
|
|
342
|
+
stories.append(story)
|
|
343
|
+
else:
|
|
344
|
+
not_found.append(feature_name)
|
|
345
|
+
|
|
346
|
+
# Group by persona, then by benefit
|
|
347
|
+
by_persona: dict[str, dict[str, list[Story]]] = defaultdict(
|
|
348
|
+
lambda: defaultdict(list)
|
|
349
|
+
)
|
|
350
|
+
for story in stories:
|
|
351
|
+
by_persona[story.persona][story.so_that].append(story)
|
|
352
|
+
|
|
353
|
+
result_nodes = []
|
|
354
|
+
|
|
355
|
+
for persona in sorted(by_persona.keys()):
|
|
356
|
+
benefits = by_persona[persona]
|
|
357
|
+
|
|
358
|
+
# Persona heading
|
|
359
|
+
persona_heading = nodes.paragraph()
|
|
360
|
+
persona_ref = self.make_persona_link(persona)
|
|
361
|
+
persona_ref.children = [nodes.strong(text=persona)]
|
|
362
|
+
persona_heading += persona_ref
|
|
363
|
+
result_nodes.append(persona_heading)
|
|
364
|
+
|
|
365
|
+
# Outer bullet list for benefits
|
|
366
|
+
benefit_list = nodes.bullet_list()
|
|
367
|
+
|
|
368
|
+
for benefit in sorted(benefits.keys()):
|
|
369
|
+
benefit_stories = benefits[benefit]
|
|
370
|
+
benefit_item = nodes.list_item()
|
|
371
|
+
|
|
372
|
+
benefit_para = nodes.paragraph()
|
|
373
|
+
benefit_para += nodes.Text("So that ")
|
|
374
|
+
benefit_para += nodes.Text(benefit)
|
|
375
|
+
benefit_item += benefit_para
|
|
376
|
+
|
|
377
|
+
# Inner bullet list for features
|
|
378
|
+
feature_list = nodes.bullet_list()
|
|
379
|
+
|
|
380
|
+
for story in sorted(benefit_stories, key=lambda s: s.i_want):
|
|
381
|
+
feature_item = nodes.list_item()
|
|
382
|
+
feature_para = nodes.paragraph()
|
|
383
|
+
feature_para += nodes.Text("I need to ")
|
|
384
|
+
feature_para += self.make_story_link(story)
|
|
385
|
+
|
|
386
|
+
# App in parentheses
|
|
387
|
+
feature_para += nodes.Text(" (")
|
|
388
|
+
app_valid = story.app_normalized in known_apps
|
|
389
|
+
if app_valid:
|
|
390
|
+
feature_para += self.make_app_link(story.app_slug)
|
|
391
|
+
else:
|
|
392
|
+
feature_para += nodes.Text(
|
|
393
|
+
story.app_slug.replace("-", " ").title()
|
|
394
|
+
)
|
|
395
|
+
feature_para += nodes.emphasis(text=" (?)")
|
|
396
|
+
feature_para += nodes.Text(")")
|
|
397
|
+
|
|
398
|
+
feature_item += feature_para
|
|
399
|
+
feature_list += feature_item
|
|
400
|
+
|
|
401
|
+
benefit_item += feature_list
|
|
402
|
+
benefit_list += benefit_item
|
|
403
|
+
|
|
404
|
+
result_nodes.append(benefit_list)
|
|
405
|
+
|
|
406
|
+
# Add warnings for not found stories
|
|
407
|
+
if not_found:
|
|
408
|
+
result_nodes.append(
|
|
409
|
+
self.warning_node(f"Stories not found: {', '.join(not_found)}")
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return result_nodes
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class StoryRefDirective(HCDDirective):
|
|
416
|
+
"""Render a single story reference.
|
|
417
|
+
|
|
418
|
+
Usage::
|
|
419
|
+
|
|
420
|
+
.. story:: Upload CMA Documents for Analysis
|
|
421
|
+
"""
|
|
422
|
+
|
|
423
|
+
required_arguments = 1
|
|
424
|
+
final_argument_whitespace = True
|
|
425
|
+
|
|
426
|
+
def run(self):
|
|
427
|
+
# Delegate to StoriesDirective with single story
|
|
428
|
+
directive = StoriesDirective(
|
|
429
|
+
self.name,
|
|
430
|
+
[], # arguments
|
|
431
|
+
{}, # options
|
|
432
|
+
[self.arguments[0]], # content - single feature name
|
|
433
|
+
self.lineno,
|
|
434
|
+
self.content_offset,
|
|
435
|
+
self.block_text,
|
|
436
|
+
self.state,
|
|
437
|
+
self.state_machine,
|
|
438
|
+
)
|
|
439
|
+
return directive.run()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def build_story_seealso(story, env, docname: str, hcd_context):
|
|
443
|
+
"""Build seealso block with links to related persona, app, epics, and journeys.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
story: Story entity or dict
|
|
447
|
+
env: Sphinx environment
|
|
448
|
+
docname: Current document name
|
|
449
|
+
hcd_context: HCDContext for accessing repositories
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Seealso admonition node or None if no links
|
|
453
|
+
"""
|
|
454
|
+
from ...config import get_config
|
|
455
|
+
from ...utils import path_to_root, slugify
|
|
456
|
+
|
|
457
|
+
config = get_config()
|
|
458
|
+
prefix = path_to_root(docname)
|
|
459
|
+
links = []
|
|
460
|
+
|
|
461
|
+
# Handle both Story entities and legacy dicts
|
|
462
|
+
if hasattr(story, "persona"):
|
|
463
|
+
persona = story.persona
|
|
464
|
+
app_slug = story.app_slug
|
|
465
|
+
feature_title = story.feature_title
|
|
466
|
+
else:
|
|
467
|
+
persona = story.get("persona")
|
|
468
|
+
app_slug = story.get("app")
|
|
469
|
+
feature_title = story.get("feature")
|
|
470
|
+
|
|
471
|
+
# Persona link
|
|
472
|
+
if persona and persona != "unknown":
|
|
473
|
+
persona_slug = slugify(persona)
|
|
474
|
+
persona_path = f"{prefix}{config.get_doc_path('personas')}/{persona_slug}.html"
|
|
475
|
+
links.append(("Persona", persona, persona_path))
|
|
476
|
+
|
|
477
|
+
# App link
|
|
478
|
+
if app_slug:
|
|
479
|
+
app_path = f"{prefix}{config.get_doc_path('applications')}/{app_slug}.html"
|
|
480
|
+
links.append(("App", app_slug.replace("-", " ").title(), app_path))
|
|
481
|
+
|
|
482
|
+
# Get story entity for use cases
|
|
483
|
+
all_stories = hcd_context.story_repo.list_all()
|
|
484
|
+
all_epics = hcd_context.epic_repo.list_all()
|
|
485
|
+
all_journeys = hcd_context.journey_repo.list_all()
|
|
486
|
+
|
|
487
|
+
story_entity = None
|
|
488
|
+
for s in all_stories:
|
|
489
|
+
if normalize_name(s.feature_title) == normalize_name(feature_title):
|
|
490
|
+
story_entity = s
|
|
491
|
+
break
|
|
492
|
+
|
|
493
|
+
if story_entity:
|
|
494
|
+
# Epic links via use case
|
|
495
|
+
epics = get_epics_for_story(story_entity, all_epics)
|
|
496
|
+
for epic in epics:
|
|
497
|
+
epic_title = epic.slug.replace("-", " ").title()
|
|
498
|
+
epic_path = f"{prefix}{config.get_doc_path('epics')}/{epic.slug}.html"
|
|
499
|
+
links.append(("Epic", epic_title, epic_path))
|
|
500
|
+
|
|
501
|
+
# Journey links via use case
|
|
502
|
+
journeys = get_journeys_for_story(story_entity, all_journeys)
|
|
503
|
+
for journey in journeys:
|
|
504
|
+
journey_title = journey.slug.replace("-", " ").title()
|
|
505
|
+
journey_path = (
|
|
506
|
+
f"{prefix}{config.get_doc_path('journeys')}/{journey.slug}.html"
|
|
507
|
+
)
|
|
508
|
+
links.append(("Journey", journey_title, journey_path))
|
|
509
|
+
|
|
510
|
+
if not links:
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
# Build seealso block
|
|
514
|
+
seealso = nodes.admonition(classes=["seealso"])
|
|
515
|
+
seealso += nodes.title(text="See also")
|
|
516
|
+
|
|
517
|
+
line_block = nodes.line_block()
|
|
518
|
+
for link_type, link_text, link_path in links:
|
|
519
|
+
line = nodes.line()
|
|
520
|
+
line += nodes.strong(text=f"{link_type}: ")
|
|
521
|
+
ref = nodes.reference("", "", refuri=link_path)
|
|
522
|
+
ref += nodes.Text(link_text)
|
|
523
|
+
line += ref
|
|
524
|
+
line_block += line
|
|
525
|
+
|
|
526
|
+
seealso += line_block
|
|
527
|
+
return seealso
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def process_story_seealso_placeholders(app, doctree):
|
|
531
|
+
"""Replace story seealso placeholders with actual content.
|
|
532
|
+
|
|
533
|
+
Uses doctree-read event so epic/journey registries are populated.
|
|
534
|
+
"""
|
|
535
|
+
from ..context import get_hcd_context
|
|
536
|
+
|
|
537
|
+
env = app.env
|
|
538
|
+
docname = env.docname
|
|
539
|
+
hcd_context = get_hcd_context(app)
|
|
540
|
+
|
|
541
|
+
for node in doctree.traverse(StorySeeAlsoPlaceholder):
|
|
542
|
+
story = {
|
|
543
|
+
"feature": node["story_feature"],
|
|
544
|
+
"persona": node["story_persona"],
|
|
545
|
+
"app": node.get("story_app"),
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
seealso = build_story_seealso(story, env, docname, hcd_context)
|
|
549
|
+
if seealso:
|
|
550
|
+
node.replace_self([seealso])
|
|
551
|
+
else:
|
|
552
|
+
node.replace_self([])
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
# Deprecated aliases
|
|
556
|
+
GherkinStoryDirective = make_deprecated_directive(
|
|
557
|
+
StoryRefDirective, "gherkin-story", "story"
|
|
558
|
+
)
|
|
559
|
+
GherkinStoriesDirective = make_deprecated_directive(
|
|
560
|
+
StoriesDirective, "gherkin-stories", "stories"
|
|
561
|
+
)
|
|
562
|
+
GherkinStoriesForPersonaDirective = make_deprecated_directive(
|
|
563
|
+
StoryListForPersonaDirective,
|
|
564
|
+
"gherkin-stories-for-persona",
|
|
565
|
+
"story-list-for-persona",
|
|
566
|
+
)
|
|
567
|
+
GherkinStoriesForAppDirective = make_deprecated_directive(
|
|
568
|
+
StoryListForAppDirective, "gherkin-stories-for-app", "story-list-for-app"
|
|
569
|
+
)
|
|
570
|
+
GherkinStoriesIndexDirective = make_deprecated_directive(
|
|
571
|
+
StoryIndexDirective, "gherkin-stories-index", "story-index"
|
|
572
|
+
)
|
|
573
|
+
GherkinAppStoriesDirective = make_deprecated_directive(
|
|
574
|
+
StoryAppDirective, "gherkin-app-stories", "story-app"
|
|
575
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Event handlers for sphinx_hcd.
|
|
2
|
+
|
|
3
|
+
Consolidates all Sphinx event handlers for the HCD extension.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .builder_inited import on_builder_inited
|
|
7
|
+
from .doctree_read import on_doctree_read
|
|
8
|
+
from .doctree_resolved import on_doctree_resolved
|
|
9
|
+
from .env_purge_doc import on_env_purge_doc
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"on_builder_inited",
|
|
13
|
+
"on_doctree_read",
|
|
14
|
+
"on_doctree_resolved",
|
|
15
|
+
"on_env_purge_doc",
|
|
16
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Builder-inited event handler for sphinx_hcd.
|
|
2
|
+
|
|
3
|
+
Initializes HCD context and scans source files at build start.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from sphinx.util import logging
|
|
7
|
+
|
|
8
|
+
from ..initialization import initialize_hcd_context
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def on_builder_inited(app):
|
|
14
|
+
"""Initialize HCD context when builder is initialized.
|
|
15
|
+
|
|
16
|
+
This handler:
|
|
17
|
+
1. Creates the HCDContext with all repositories
|
|
18
|
+
2. Scans feature files for stories
|
|
19
|
+
3. Scans app manifests
|
|
20
|
+
4. Scans integration manifests
|
|
21
|
+
5. Scans bounded contexts for code info
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
app: Sphinx application instance
|
|
25
|
+
"""
|
|
26
|
+
logger.info("Initializing HCD context...")
|
|
27
|
+
|
|
28
|
+
# Initialize the HCD context (creates repos, scans files)
|
|
29
|
+
initialize_hcd_context(app)
|
|
30
|
+
|
|
31
|
+
logger.info("HCD context initialized")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Doctree-read event handler for sphinx_hcd.
|
|
2
|
+
|
|
3
|
+
Processes placeholders that need to be replaced after all directives
|
|
4
|
+
in a document have been parsed but before the doctree is pickled.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..directives import (
|
|
8
|
+
process_journey_steps,
|
|
9
|
+
process_story_seealso_placeholders,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def on_doctree_read(app, doctree):
|
|
14
|
+
"""Process doctree after all directives are parsed.
|
|
15
|
+
|
|
16
|
+
This handler runs after a document is read but before it's pickled.
|
|
17
|
+
Used for placeholders that need to be resolved within a single document.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
app: Sphinx application instance
|
|
21
|
+
doctree: The document tree
|
|
22
|
+
"""
|
|
23
|
+
# Process story seealso placeholders
|
|
24
|
+
process_story_seealso_placeholders(app, doctree)
|
|
25
|
+
|
|
26
|
+
# Process journey steps placeholder
|
|
27
|
+
process_journey_steps(app, doctree)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Doctree-resolved event handler for sphinx_hcd.
|
|
2
|
+
|
|
3
|
+
Processes placeholders that need cross-document data (all documents read).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ..directives import (
|
|
7
|
+
process_accelerator_placeholders,
|
|
8
|
+
process_app_placeholders,
|
|
9
|
+
process_dependency_graph_placeholder,
|
|
10
|
+
process_epic_placeholders,
|
|
11
|
+
process_integration_placeholders,
|
|
12
|
+
process_persona_placeholders,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def on_doctree_resolved(app, doctree, docname):
|
|
17
|
+
"""Process doctree after all documents are read.
|
|
18
|
+
|
|
19
|
+
This handler runs after ALL documents have been read, allowing
|
|
20
|
+
cross-document references to be resolved.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
app: Sphinx application instance
|
|
24
|
+
doctree: The document tree
|
|
25
|
+
docname: The document name
|
|
26
|
+
"""
|
|
27
|
+
# Process app placeholders (need story/journey/epic registries)
|
|
28
|
+
process_app_placeholders(app, doctree, docname)
|
|
29
|
+
|
|
30
|
+
# Process epic placeholders (need story registry)
|
|
31
|
+
process_epic_placeholders(app, doctree, docname)
|
|
32
|
+
|
|
33
|
+
# Process accelerator placeholders (need many registries)
|
|
34
|
+
process_accelerator_placeholders(app, doctree, docname)
|
|
35
|
+
|
|
36
|
+
# Process integration placeholders
|
|
37
|
+
process_integration_placeholders(app, doctree, docname)
|
|
38
|
+
|
|
39
|
+
# Process persona diagram placeholders (need epic/story registries)
|
|
40
|
+
process_persona_placeholders(app, doctree, docname)
|
|
41
|
+
|
|
42
|
+
# Process journey dependency graph placeholder (needs all journeys)
|
|
43
|
+
process_dependency_graph_placeholder(app, doctree, docname)
|