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
@@ -1,453 +0,0 @@
1
- """Sphinx extension for defining and cross-referencing epics.
2
-
3
- Provides directives:
4
- - define-epic: Define an epic with description
5
- - epic-story: Reference a story as part of the epic
6
- - epic-index: Render index of all epics
7
- - epics-for-persona: List epics for a persona (derived from stories)
8
- """
9
-
10
- from docutils import nodes
11
- from sphinx.util import logging
12
- from sphinx.util.docutils import SphinxDirective
13
-
14
- from .config import get_config
15
- from .utils import normalize_name, path_to_root
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- def get_epic_registry(env):
21
- """Get or create the epic registry on the environment."""
22
- if not hasattr(env, "epic_registry"):
23
- env.epic_registry = {}
24
- return env.epic_registry
25
-
26
-
27
- def get_current_epic(env):
28
- """Get or create the current epic tracker on the environment."""
29
- if not hasattr(env, "epic_current"):
30
- env.epic_current = {}
31
- return env.epic_current
32
-
33
-
34
- class DefineEpicDirective(SphinxDirective):
35
- """Define an epic with description.
36
-
37
- Usage::
38
-
39
- .. define-epic:: credential-creation
40
-
41
- Covers the creation, attachment, and verification of UNTP-compliant
42
- credentials including DPPs, DFRs, and DCCs.
43
- """
44
-
45
- required_arguments = 1 # epic slug
46
- has_content = True
47
- option_spec = {}
48
-
49
- def run(self):
50
- epic_slug = self.arguments[0]
51
- docname = self.env.docname
52
-
53
- # Description is the directive content
54
- description = "\n".join(self.content).strip()
55
-
56
- # Register the epic in environment
57
- epic_registry = get_epic_registry(self.env)
58
- current_epic = get_current_epic(self.env)
59
-
60
- epic_data = {
61
- "slug": epic_slug,
62
- "description": description,
63
- "stories": [], # Will be populated by epic-story
64
- "docname": docname,
65
- }
66
- epic_registry[epic_slug] = epic_data
67
- current_epic[docname] = epic_slug
68
-
69
- # Build output nodes
70
- result_nodes = []
71
-
72
- # Description paragraph
73
- if description:
74
- desc_para = nodes.paragraph(text=description)
75
- result_nodes.append(desc_para)
76
-
77
- # Add a placeholder for stories (will be filled in doctree-resolved)
78
- stories_placeholder = nodes.container()
79
- stories_placeholder["classes"].append("epic-stories-placeholder")
80
- stories_placeholder["epic_slug"] = epic_slug
81
- result_nodes.append(stories_placeholder)
82
-
83
- return result_nodes
84
-
85
-
86
- class EpicStoryDirective(SphinxDirective):
87
- """Reference a story as part of the epic.
88
-
89
- Usage::
90
-
91
- .. epic-story:: Create DPP from Product Sheet
92
- """
93
-
94
- required_arguments = 1
95
- final_argument_whitespace = True
96
-
97
- def run(self):
98
- story_title = self.arguments[0]
99
- docname = self.env.docname
100
-
101
- # Add to current epic's stories
102
- epic_registry = get_epic_registry(self.env)
103
- current_epic = get_current_epic(self.env)
104
-
105
- epic_slug = current_epic.get(docname)
106
- if epic_slug and epic_slug in epic_registry:
107
- epic_registry[epic_slug]["stories"].append(story_title)
108
-
109
- # Return empty - rendering happens in doctree-resolved
110
- return []
111
-
112
-
113
- class EpicIndexDirective(SphinxDirective):
114
- """Render index of all epics.
115
-
116
- Usage::
117
-
118
- .. epic-index::
119
- """
120
-
121
- def run(self):
122
- # Return placeholder - actual rendering in doctree-resolved
123
- node = EpicIndexPlaceholder()
124
- return [node]
125
-
126
-
127
- class EpicIndexPlaceholder(nodes.General, nodes.Element):
128
- """Placeholder node for epic index, replaced at doctree-resolved."""
129
-
130
- pass
131
-
132
-
133
- class EpicsForPersonaDirective(SphinxDirective):
134
- """List epics for a specific persona (derived from stories).
135
-
136
- Usage::
137
-
138
- .. epics-for-persona:: Member Implementer
139
- """
140
-
141
- required_arguments = 1
142
- final_argument_whitespace = True
143
-
144
- def run(self):
145
- # Return placeholder - actual rendering in doctree-resolved
146
- node = EpicsForPersonaPlaceholder()
147
- node["persona"] = self.arguments[0]
148
- return [node]
149
-
150
-
151
- class EpicsForPersonaPlaceholder(nodes.General, nodes.Element):
152
- """Placeholder node for epics-for-persona, replaced at doctree-resolved."""
153
-
154
- pass
155
-
156
-
157
- def clear_epic_state(app, env, docname):
158
- """Clear epic state when a document is re-read."""
159
- current_epic = get_current_epic(env)
160
- epic_registry = get_epic_registry(env)
161
-
162
- if docname in current_epic:
163
- del current_epic[docname]
164
-
165
- # Remove epics defined in this document
166
- to_remove = [slug for slug, e in epic_registry.items() if e["docname"] == docname]
167
- for slug in to_remove:
168
- del epic_registry[slug]
169
-
170
-
171
- def validate_epics(app, env):
172
- """Validate epic references after all documents are read."""
173
- from . import stories
174
-
175
- epic_registry = get_epic_registry(env)
176
- _story_registry = stories.get_story_registry()
177
- story_titles = {normalize_name(s["feature"]) for s in _story_registry}
178
-
179
- for slug, epic in epic_registry.items():
180
- # Validate story references
181
- for story_title in epic["stories"]:
182
- if normalize_name(story_title) not in story_titles:
183
- logger.warning(
184
- f"Epic '{slug}' references unknown story: '{story_title}'"
185
- )
186
-
187
-
188
- def get_personas_for_epic(epic: dict, story_registry: list) -> set[str]:
189
- """Get the set of personas for an epic based on its stories."""
190
- personas = set()
191
- for story_title in epic["stories"]:
192
- story_normalized = normalize_name(story_title)
193
- for story in story_registry:
194
- if normalize_name(story["feature"]) == story_normalized:
195
- personas.add(story["persona"])
196
- break
197
- return personas
198
-
199
-
200
- def render_epic_stories(
201
- epic: dict, docname: str, story_registry: list, known_personas: set
202
- ):
203
- """Render epic stories as a simple bullet list."""
204
- from . import stories
205
-
206
- config = get_config()
207
- _known_apps = stories.get_known_apps()
208
-
209
- stories_data = []
210
- for story_title in epic["stories"]:
211
- story_normalized = normalize_name(story_title)
212
- for story in story_registry:
213
- if normalize_name(story["feature"]) == story_normalized:
214
- stories_data.append(story)
215
- break
216
-
217
- if not stories_data:
218
- return None
219
-
220
- # Calculate paths
221
- prefix = path_to_root(docname)
222
-
223
- result_nodes = []
224
-
225
- # Stories heading
226
- stories_heading = nodes.paragraph()
227
- stories_heading += nodes.strong(text="Stories")
228
- result_nodes.append(stories_heading)
229
-
230
- # Simple bullet list: "story name (App Name)"
231
- story_list = nodes.bullet_list()
232
-
233
- for story in sorted(stories_data, key=lambda s: s["feature"].lower()):
234
- story_item = nodes.list_item()
235
- story_para = nodes.paragraph()
236
-
237
- # Story link
238
- story_para += stories.make_story_reference(story, docname)
239
-
240
- # App in parentheses
241
- story_para += nodes.Text(" (")
242
- app_path = f"{prefix}{config.get_doc_path('applications')}/{story['app']}.html"
243
- app_valid = story["app_normalized"] in _known_apps
244
-
245
- if app_valid:
246
- app_ref = nodes.reference("", "", refuri=app_path)
247
- app_ref += nodes.Text(story["app"].replace("-", " ").title())
248
- story_para += app_ref
249
- else:
250
- story_para += nodes.Text(story["app"].replace("-", " ").title())
251
-
252
- story_para += nodes.Text(")")
253
-
254
- story_item += story_para
255
- story_list += story_item
256
-
257
- result_nodes.append(story_list)
258
-
259
- return result_nodes
260
-
261
-
262
- def process_epic_placeholders(app, doctree, docname):
263
- """Replace epic placeholders with rendered content."""
264
- from . import stories
265
-
266
- get_config()
267
- env = app.env
268
- epic_registry = get_epic_registry(env)
269
- current_epic = get_current_epic(env)
270
- _story_registry = stories.get_story_registry()
271
- _known_personas = stories.get_known_personas()
272
-
273
- # Process epic stories placeholder
274
- epic_slug = current_epic.get(docname)
275
- if epic_slug and epic_slug in epic_registry:
276
- epic = epic_registry[epic_slug]
277
-
278
- for node in doctree.traverse(nodes.container):
279
- if "epic-stories-placeholder" in node.get("classes", []):
280
- stories_nodes = render_epic_stories(
281
- epic, docname, _story_registry, _known_personas
282
- )
283
- if stories_nodes:
284
- node.replace_self(stories_nodes)
285
- else:
286
- node.replace_self([])
287
- break
288
-
289
- # Process epic index placeholder
290
- for node in doctree.traverse(EpicIndexPlaceholder):
291
- index_node = build_epic_index(env, docname, _story_registry)
292
- node.replace_self(index_node)
293
-
294
- # Process epics-for-persona placeholder
295
- for node in doctree.traverse(EpicsForPersonaPlaceholder):
296
- persona = node["persona"]
297
- epics_node = build_epics_for_persona(env, docname, persona, _story_registry)
298
- node.replace_self(epics_node)
299
-
300
-
301
- def build_epic_index(env, docname: str, story_registry: list):
302
- """Build the epic index listing all epics, plus unassigned stories."""
303
- from . import stories
304
-
305
- config = get_config()
306
- epic_registry = get_epic_registry(env)
307
- _known_apps = stories.get_known_apps()
308
-
309
- if not epic_registry:
310
- para = nodes.paragraph()
311
- para += nodes.emphasis(text="No epics defined")
312
- return [para]
313
-
314
- result_nodes = []
315
- bullet_list = nodes.bullet_list()
316
-
317
- # Collect all stories assigned to epics
318
- assigned_stories = set()
319
- for epic in epic_registry.values():
320
- for story_title in epic["stories"]:
321
- assigned_stories.add(normalize_name(story_title))
322
-
323
- for slug in sorted(epic_registry.keys()):
324
- epic = epic_registry[slug]
325
-
326
- item = nodes.list_item()
327
- para = nodes.paragraph()
328
-
329
- # Link to epic
330
- epic_path = f"{slug}.html"
331
- epic_ref = nodes.reference("", "", refuri=epic_path)
332
- epic_ref += nodes.Text(slug.replace("-", " ").title())
333
- para += epic_ref
334
-
335
- # Story count
336
- story_count = len(epic["stories"])
337
- para += nodes.Text(f" ({story_count} stories)")
338
-
339
- item += para
340
- bullet_list += item
341
-
342
- result_nodes.append(bullet_list)
343
-
344
- # Find unassigned stories
345
- unassigned_stories = []
346
- for story in story_registry:
347
- if normalize_name(story["feature"]) not in assigned_stories:
348
- unassigned_stories.append(story)
349
-
350
- if unassigned_stories:
351
- # Calculate paths
352
- prefix = path_to_root(docname)
353
-
354
- # Add section heading
355
- heading = nodes.paragraph()
356
- heading += nodes.strong(text="Unassigned Stories")
357
- result_nodes.append(heading)
358
-
359
- intro = nodes.paragraph()
360
- intro += nodes.Text(
361
- f"{len(unassigned_stories)} stories not yet assigned to an epic:"
362
- )
363
- result_nodes.append(intro)
364
-
365
- # List unassigned stories
366
- unassigned_list = nodes.bullet_list()
367
- for story in sorted(unassigned_stories, key=lambda s: s["feature"].lower()):
368
- item = nodes.list_item()
369
- para = nodes.paragraph()
370
-
371
- # Story link
372
- para += stories.make_story_reference(story, docname)
373
-
374
- # App in parentheses
375
- para += nodes.Text(" (")
376
- app_path = (
377
- f"{prefix}{config.get_doc_path('applications')}/{story['app']}.html"
378
- )
379
- app_valid = story["app_normalized"] in _known_apps
380
-
381
- if app_valid:
382
- app_ref = nodes.reference("", "", refuri=app_path)
383
- app_ref += nodes.Text(story["app"].replace("-", " ").title())
384
- para += app_ref
385
- else:
386
- para += nodes.Text(story["app"].replace("-", " ").title())
387
-
388
- para += nodes.Text(")")
389
-
390
- item += para
391
- unassigned_list += item
392
-
393
- result_nodes.append(unassigned_list)
394
-
395
- return result_nodes
396
-
397
-
398
- def build_epics_for_persona(env, docname: str, persona_arg: str, story_registry: list):
399
- """Build list of epics for a persona."""
400
- config = get_config()
401
- epic_registry = get_epic_registry(env)
402
- persona_normalized = normalize_name(persona_arg)
403
-
404
- prefix = path_to_root(docname)
405
-
406
- # Find epics that contain stories for this persona
407
- matching_epics = []
408
- for slug, epic in epic_registry.items():
409
- personas = get_personas_for_epic(epic, story_registry)
410
- persona_names_normalized = {normalize_name(p) for p in personas}
411
- if persona_normalized in persona_names_normalized:
412
- matching_epics.append((slug, epic))
413
-
414
- if not matching_epics:
415
- para = nodes.paragraph()
416
- para += nodes.emphasis(text=f"No epics found for persona '{persona_arg}'")
417
- return [para]
418
-
419
- bullet_list = nodes.bullet_list()
420
-
421
- for slug, _epic in sorted(matching_epics, key=lambda x: x[0]):
422
- item = nodes.list_item()
423
- para = nodes.paragraph()
424
-
425
- epic_path = f"{prefix}{config.get_doc_path('epics')}/{slug}.html"
426
- epic_ref = nodes.reference("", "", refuri=epic_path)
427
- epic_ref += nodes.Text(slug.replace("-", " ").title())
428
- para += epic_ref
429
-
430
- item += para
431
- bullet_list += item
432
-
433
- return [bullet_list]
434
-
435
-
436
- def setup(app):
437
- app.connect("env-purge-doc", clear_epic_state)
438
- app.connect("env-check-consistency", validate_epics)
439
- app.connect("doctree-resolved", process_epic_placeholders)
440
-
441
- app.add_directive("define-epic", DefineEpicDirective)
442
- app.add_directive("epic-story", EpicStoryDirective)
443
- app.add_directive("epic-index", EpicIndexDirective)
444
- app.add_directive("epics-for-persona", EpicsForPersonaDirective)
445
-
446
- app.add_node(EpicIndexPlaceholder)
447
- app.add_node(EpicsForPersonaPlaceholder)
448
-
449
- return {
450
- "version": "1.0",
451
- "parallel_read_safe": False, # Uses global state
452
- "parallel_write_safe": True,
453
- }