julee 0.1.4__py3-none-any.whl → 0.1.6__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.
Files changed (165) hide show
  1. julee/__init__.py +1 -1
  2. julee/api/tests/routers/test_assembly_specifications.py +2 -0
  3. julee/api/tests/routers/test_documents.py +2 -0
  4. julee/api/tests/routers/test_knowledge_service_configs.py +2 -0
  5. julee/api/tests/routers/test_knowledge_service_queries.py +2 -0
  6. julee/api/tests/routers/test_system.py +2 -0
  7. julee/api/tests/routers/test_workflows.py +2 -0
  8. julee/api/tests/test_app.py +2 -0
  9. julee/api/tests/test_dependencies.py +2 -0
  10. julee/api/tests/test_requests.py +2 -0
  11. julee/contrib/polling/__init__.py +22 -19
  12. julee/contrib/polling/apps/__init__.py +17 -0
  13. julee/contrib/polling/apps/worker/__init__.py +17 -0
  14. julee/contrib/polling/apps/worker/pipelines.py +288 -0
  15. julee/contrib/polling/domain/__init__.py +7 -9
  16. julee/contrib/polling/domain/models/__init__.py +6 -7
  17. julee/contrib/polling/domain/models/polling_config.py +18 -1
  18. julee/contrib/polling/domain/services/__init__.py +6 -5
  19. julee/contrib/polling/domain/services/poller.py +1 -1
  20. julee/contrib/polling/infrastructure/__init__.py +9 -8
  21. julee/contrib/polling/infrastructure/services/__init__.py +6 -5
  22. julee/contrib/polling/infrastructure/services/polling/__init__.py +6 -5
  23. julee/contrib/polling/infrastructure/services/polling/http/__init__.py +6 -5
  24. julee/contrib/polling/infrastructure/services/polling/http/http_poller_service.py +5 -2
  25. julee/contrib/polling/infrastructure/temporal/__init__.py +12 -12
  26. julee/contrib/polling/infrastructure/temporal/activities.py +1 -1
  27. julee/contrib/polling/infrastructure/temporal/manager.py +291 -0
  28. julee/contrib/polling/infrastructure/temporal/proxies.py +1 -1
  29. julee/contrib/polling/tests/unit/apps/worker/test_pipelines.py +580 -0
  30. julee/contrib/polling/tests/unit/infrastructure/services/polling/http/test_http_poller_service.py +40 -2
  31. julee/contrib/polling/tests/unit/infrastructure/temporal/__init__.py +7 -0
  32. julee/contrib/polling/tests/unit/infrastructure/temporal/test_manager.py +475 -0
  33. julee/docs/sphinx_hcd/__init__.py +146 -13
  34. julee/docs/sphinx_hcd/domain/__init__.py +5 -0
  35. julee/docs/sphinx_hcd/domain/models/__init__.py +32 -0
  36. julee/docs/sphinx_hcd/domain/models/accelerator.py +152 -0
  37. julee/docs/sphinx_hcd/domain/models/app.py +151 -0
  38. julee/docs/sphinx_hcd/domain/models/code_info.py +121 -0
  39. julee/docs/sphinx_hcd/domain/models/epic.py +79 -0
  40. julee/docs/sphinx_hcd/domain/models/integration.py +230 -0
  41. julee/docs/sphinx_hcd/domain/models/journey.py +222 -0
  42. julee/docs/sphinx_hcd/domain/models/persona.py +106 -0
  43. julee/docs/sphinx_hcd/domain/models/story.py +128 -0
  44. julee/docs/sphinx_hcd/domain/repositories/__init__.py +25 -0
  45. julee/docs/sphinx_hcd/domain/repositories/accelerator.py +98 -0
  46. julee/docs/sphinx_hcd/domain/repositories/app.py +57 -0
  47. julee/docs/sphinx_hcd/domain/repositories/base.py +89 -0
  48. julee/docs/sphinx_hcd/domain/repositories/code_info.py +69 -0
  49. julee/docs/sphinx_hcd/domain/repositories/epic.py +62 -0
  50. julee/docs/sphinx_hcd/domain/repositories/integration.py +79 -0
  51. julee/docs/sphinx_hcd/domain/repositories/journey.py +106 -0
  52. julee/docs/sphinx_hcd/domain/repositories/story.py +68 -0
  53. julee/docs/sphinx_hcd/domain/use_cases/__init__.py +64 -0
  54. julee/docs/sphinx_hcd/domain/use_cases/derive_personas.py +166 -0
  55. julee/docs/sphinx_hcd/domain/use_cases/resolve_accelerator_references.py +236 -0
  56. julee/docs/sphinx_hcd/domain/use_cases/resolve_app_references.py +144 -0
  57. julee/docs/sphinx_hcd/domain/use_cases/resolve_story_references.py +121 -0
  58. julee/docs/sphinx_hcd/parsers/__init__.py +48 -0
  59. julee/docs/sphinx_hcd/parsers/ast.py +150 -0
  60. julee/docs/sphinx_hcd/parsers/gherkin.py +155 -0
  61. julee/docs/sphinx_hcd/parsers/yaml.py +184 -0
  62. julee/docs/sphinx_hcd/repositories/__init__.py +4 -0
  63. julee/docs/sphinx_hcd/repositories/memory/__init__.py +25 -0
  64. julee/docs/sphinx_hcd/repositories/memory/accelerator.py +86 -0
  65. julee/docs/sphinx_hcd/repositories/memory/app.py +45 -0
  66. julee/docs/sphinx_hcd/repositories/memory/base.py +106 -0
  67. julee/docs/sphinx_hcd/repositories/memory/code_info.py +59 -0
  68. julee/docs/sphinx_hcd/repositories/memory/epic.py +54 -0
  69. julee/docs/sphinx_hcd/repositories/memory/integration.py +70 -0
  70. julee/docs/sphinx_hcd/repositories/memory/journey.py +96 -0
  71. julee/docs/sphinx_hcd/repositories/memory/story.py +63 -0
  72. julee/docs/sphinx_hcd/sphinx/__init__.py +28 -0
  73. julee/docs/sphinx_hcd/sphinx/adapters.py +116 -0
  74. julee/docs/sphinx_hcd/sphinx/context.py +163 -0
  75. julee/docs/sphinx_hcd/sphinx/directives/__init__.py +160 -0
  76. julee/docs/sphinx_hcd/sphinx/directives/accelerator.py +576 -0
  77. julee/docs/sphinx_hcd/sphinx/directives/app.py +349 -0
  78. julee/docs/sphinx_hcd/sphinx/directives/base.py +211 -0
  79. julee/docs/sphinx_hcd/sphinx/directives/epic.py +434 -0
  80. julee/docs/sphinx_hcd/sphinx/directives/integration.py +220 -0
  81. julee/docs/sphinx_hcd/sphinx/directives/journey.py +642 -0
  82. julee/docs/sphinx_hcd/sphinx/directives/persona.py +345 -0
  83. julee/docs/sphinx_hcd/sphinx/directives/story.py +575 -0
  84. julee/docs/sphinx_hcd/sphinx/event_handlers/__init__.py +16 -0
  85. julee/docs/sphinx_hcd/sphinx/event_handlers/builder_inited.py +31 -0
  86. julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_read.py +27 -0
  87. julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_resolved.py +43 -0
  88. julee/docs/sphinx_hcd/sphinx/event_handlers/env_purge_doc.py +42 -0
  89. julee/docs/sphinx_hcd/sphinx/initialization.py +139 -0
  90. julee/docs/sphinx_hcd/tests/__init__.py +9 -0
  91. julee/docs/sphinx_hcd/tests/conftest.py +6 -0
  92. julee/docs/sphinx_hcd/tests/domain/__init__.py +1 -0
  93. julee/docs/sphinx_hcd/tests/domain/models/__init__.py +1 -0
  94. julee/docs/sphinx_hcd/tests/domain/models/test_accelerator.py +266 -0
  95. julee/docs/sphinx_hcd/tests/domain/models/test_app.py +258 -0
  96. julee/docs/sphinx_hcd/tests/domain/models/test_code_info.py +231 -0
  97. julee/docs/sphinx_hcd/tests/domain/models/test_epic.py +163 -0
  98. julee/docs/sphinx_hcd/tests/domain/models/test_integration.py +327 -0
  99. julee/docs/sphinx_hcd/tests/domain/models/test_journey.py +249 -0
  100. julee/docs/sphinx_hcd/tests/domain/models/test_persona.py +172 -0
  101. julee/docs/sphinx_hcd/tests/domain/models/test_story.py +216 -0
  102. julee/docs/sphinx_hcd/tests/domain/use_cases/__init__.py +1 -0
  103. julee/docs/sphinx_hcd/tests/domain/use_cases/test_derive_personas.py +314 -0
  104. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_accelerator_references.py +476 -0
  105. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_app_references.py +265 -0
  106. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_story_references.py +229 -0
  107. julee/docs/sphinx_hcd/tests/integration/__init__.py +1 -0
  108. julee/docs/sphinx_hcd/tests/parsers/__init__.py +1 -0
  109. julee/docs/sphinx_hcd/tests/parsers/test_ast.py +298 -0
  110. julee/docs/sphinx_hcd/tests/parsers/test_gherkin.py +282 -0
  111. julee/docs/sphinx_hcd/tests/parsers/test_yaml.py +496 -0
  112. julee/docs/sphinx_hcd/tests/repositories/__init__.py +1 -0
  113. julee/docs/sphinx_hcd/tests/repositories/test_accelerator.py +298 -0
  114. julee/docs/sphinx_hcd/tests/repositories/test_app.py +218 -0
  115. julee/docs/sphinx_hcd/tests/repositories/test_base.py +151 -0
  116. julee/docs/sphinx_hcd/tests/repositories/test_code_info.py +253 -0
  117. julee/docs/sphinx_hcd/tests/repositories/test_epic.py +237 -0
  118. julee/docs/sphinx_hcd/tests/repositories/test_integration.py +268 -0
  119. julee/docs/sphinx_hcd/tests/repositories/test_journey.py +294 -0
  120. julee/docs/sphinx_hcd/tests/repositories/test_story.py +236 -0
  121. julee/docs/sphinx_hcd/tests/sphinx/__init__.py +1 -0
  122. julee/docs/sphinx_hcd/tests/sphinx/directives/__init__.py +1 -0
  123. julee/docs/sphinx_hcd/tests/sphinx/directives/test_base.py +160 -0
  124. julee/docs/sphinx_hcd/tests/sphinx/test_adapters.py +176 -0
  125. julee/docs/sphinx_hcd/tests/sphinx/test_context.py +257 -0
  126. julee/domain/models/assembly/tests/test_assembly.py +2 -0
  127. julee/domain/models/assembly_specification/tests/test_assembly_specification.py +2 -0
  128. julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +2 -0
  129. julee/domain/models/custom_fields/tests/test_custom_fields.py +2 -0
  130. julee/domain/models/document/tests/test_document.py +2 -0
  131. julee/domain/models/policy/tests/test_document_policy_validation.py +2 -0
  132. julee/domain/models/policy/tests/test_policy.py +2 -0
  133. julee/domain/use_cases/tests/test_extract_assemble_data.py +2 -0
  134. julee/domain/use_cases/tests/test_initialize_system_data.py +2 -0
  135. julee/domain/use_cases/tests/test_validate_document.py +2 -0
  136. julee/maintenance/release.py +10 -5
  137. julee/repositories/memory/tests/test_document.py +2 -0
  138. julee/repositories/memory/tests/test_document_policy_validation.py +2 -0
  139. julee/repositories/memory/tests/test_policy.py +2 -0
  140. julee/repositories/minio/tests/test_assembly.py +2 -0
  141. julee/repositories/minio/tests/test_assembly_specification.py +2 -0
  142. julee/repositories/minio/tests/test_client_protocol.py +3 -0
  143. julee/repositories/minio/tests/test_document.py +2 -0
  144. julee/repositories/minio/tests/test_document_policy_validation.py +2 -0
  145. julee/repositories/minio/tests/test_knowledge_service_config.py +2 -0
  146. julee/repositories/minio/tests/test_knowledge_service_query.py +2 -0
  147. julee/repositories/minio/tests/test_policy.py +2 -0
  148. julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +2 -0
  149. julee/services/knowledge_service/memory/test_knowledge_service.py +2 -0
  150. julee/services/knowledge_service/test_factory.py +2 -0
  151. julee/util/tests/test_decorators.py +2 -0
  152. julee-0.1.6.dist-info/METADATA +104 -0
  153. julee-0.1.6.dist-info/RECORD +288 -0
  154. julee/docs/sphinx_hcd/accelerators.py +0 -1175
  155. julee/docs/sphinx_hcd/apps.py +0 -518
  156. julee/docs/sphinx_hcd/epics.py +0 -453
  157. julee/docs/sphinx_hcd/integrations.py +0 -310
  158. julee/docs/sphinx_hcd/journeys.py +0 -797
  159. julee/docs/sphinx_hcd/personas.py +0 -457
  160. julee/docs/sphinx_hcd/stories.py +0 -960
  161. julee-0.1.4.dist-info/METADATA +0 -197
  162. julee-0.1.4.dist-info/RECORD +0 -196
  163. {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/WHEEL +0 -0
  164. {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/licenses/LICENSE +0 -0
  165. {julee-0.1.4.dist-info → julee-0.1.6.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)