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,345 @@
1
+ """Persona directives for sphinx_hcd.
2
+
3
+ Generates PlantUML use case diagrams dynamically from epic and story data.
4
+
5
+ Provides directives:
6
+ - persona-diagram: Generate UML diagram for a single persona showing their epics
7
+ - persona-index-diagram: Generate UML diagram for staff or external persona groups
8
+ """
9
+
10
+ import os
11
+
12
+ from docutils import nodes
13
+
14
+ from ...domain.use_cases import (
15
+ derive_personas,
16
+ derive_personas_by_app_type,
17
+ get_epics_for_persona,
18
+ )
19
+ from ...utils import normalize_name, slugify
20
+ from .base import HCDDirective
21
+
22
+
23
+ class PersonaDiagramPlaceholder(nodes.General, nodes.Element):
24
+ """Placeholder node for persona-diagram, replaced at doctree-resolved."""
25
+
26
+ pass
27
+
28
+
29
+ class PersonaIndexDiagramPlaceholder(nodes.General, nodes.Element):
30
+ """Placeholder node for persona-index-diagram, replaced at doctree-resolved."""
31
+
32
+ pass
33
+
34
+
35
+ class PersonaDiagramDirective(HCDDirective):
36
+ """Generate PlantUML use case diagram for a single persona.
37
+
38
+ Usage::
39
+
40
+ .. persona-diagram:: Pilot Manager
41
+
42
+ Generates a diagram showing:
43
+ - The persona as an actor
44
+ - Epics they participate in as use cases
45
+ - Apps they interact with as components
46
+ """
47
+
48
+ required_arguments = 1
49
+ final_argument_whitespace = True
50
+
51
+ def run(self):
52
+ persona_name = self.arguments[0]
53
+
54
+ # Return placeholder - rendering in doctree-resolved
55
+ node = PersonaDiagramPlaceholder()
56
+ node["persona"] = persona_name
57
+ return [node]
58
+
59
+
60
+ class PersonaIndexDiagramDirective(HCDDirective):
61
+ """Generate PlantUML diagram for a group of personas.
62
+
63
+ Usage::
64
+
65
+ .. persona-index-diagram:: staff
66
+ .. persona-index-diagram:: customers
67
+ .. persona-index-diagram:: vendors
68
+
69
+ Groups are determined by the type field from app.yaml manifests.
70
+ """
71
+
72
+ required_arguments = 1
73
+ option_spec = {}
74
+
75
+ def run(self):
76
+ group_type = self.arguments[0].lower()
77
+
78
+ # Return placeholder - rendering in doctree-resolved
79
+ node = PersonaIndexDiagramPlaceholder()
80
+ node["group_type"] = group_type
81
+ return [node]
82
+
83
+
84
+ def get_apps_for_epic(epic, all_stories) -> set[str]:
85
+ """Get the set of app slugs used by stories in an epic."""
86
+ apps = set()
87
+
88
+ # Build lookup of story title -> app
89
+ story_apps = {}
90
+ for story in all_stories:
91
+ story_apps[normalize_name(story.feature_title)] = story.app_slug
92
+
93
+ for story_title in epic.story_refs:
94
+ story_normalized = normalize_name(story_title)
95
+ if story_normalized in story_apps:
96
+ apps.add(story_apps[story_normalized])
97
+
98
+ return apps
99
+
100
+
101
+ def generate_persona_plantuml(persona, all_epics, all_stories, all_apps) -> str:
102
+ """Generate PlantUML for a single persona's use case diagram."""
103
+ persona_id = slugify(persona.name).replace("-", "_")
104
+ app_lookup = {a.slug: a for a in all_apps}
105
+
106
+ lines = [
107
+ f"@startuml persona-{slugify(persona.name)}",
108
+ "left to right direction",
109
+ "skinparam actorStyle awesome",
110
+ "",
111
+ f'actor "{persona.name}" as {persona_id}',
112
+ "",
113
+ ]
114
+
115
+ # Get epics for this persona
116
+ epics = get_epics_for_persona(persona, all_epics, all_stories)
117
+
118
+ # Collect all apps used by this persona's epics
119
+ all_epic_apps = set()
120
+ epic_apps_map = {}
121
+
122
+ for epic in epics:
123
+ apps = get_apps_for_epic(epic, all_stories)
124
+ epic_apps_map[epic.slug] = apps
125
+ all_epic_apps.update(apps)
126
+
127
+ # Generate component declarations for apps
128
+ for app_slug in sorted(all_epic_apps):
129
+ app_id = app_slug.replace("-", "_")
130
+ app = app_lookup.get(app_slug)
131
+ app_name = app.name if app else app_slug.replace("-", " ").title()
132
+ lines.append(f'component "{app_name}" as {app_id}')
133
+
134
+ lines.append("")
135
+
136
+ # Generate usecase declarations for epics
137
+ for epic in epics:
138
+ epic_id = epic.slug.replace("-", "_")
139
+ epic_name = epic.slug.replace("-", " ").title()
140
+ lines.append(f'usecase "{epic_name}" as {epic_id}')
141
+
142
+ lines.append("")
143
+
144
+ # Generate persona -> epic connections
145
+ for epic in epics:
146
+ epic_id = epic.slug.replace("-", "_")
147
+ lines.append(f"{persona_id} --> {epic_id}")
148
+
149
+ lines.append("")
150
+
151
+ # Generate epic -> app connections
152
+ for epic in epics:
153
+ epic_id = epic.slug.replace("-", "_")
154
+ for app_slug in sorted(epic_apps_map.get(epic.slug, [])):
155
+ app_id = app_slug.replace("-", "_")
156
+ lines.append(f"{epic_id} --> {app_id}")
157
+
158
+ lines.append("")
159
+ lines.append("@enduml")
160
+
161
+ return "\n".join(lines)
162
+
163
+
164
+ def generate_persona_index_plantuml(
165
+ group_type: str, personas, all_epics, all_stories, all_apps
166
+ ) -> str:
167
+ """Generate PlantUML for a group of personas."""
168
+ app_lookup = {a.slug: a for a in all_apps}
169
+
170
+ lines = [
171
+ f"@startuml persona-{group_type}",
172
+ "left to right direction",
173
+ "skinparam actorStyle awesome",
174
+ "",
175
+ ]
176
+
177
+ # Collect data for all personas
178
+ persona_epics_map = {}
179
+ all_epic_apps = set()
180
+ epic_apps_map = {}
181
+
182
+ for persona in personas:
183
+ epics = get_epics_for_persona(persona, all_epics, all_stories)
184
+ persona_epics_map[persona.name] = epics
185
+
186
+ for epic in epics:
187
+ if epic.slug not in epic_apps_map:
188
+ apps = get_apps_for_epic(epic, all_stories)
189
+ epic_apps_map[epic.slug] = apps
190
+ all_epic_apps.update(apps)
191
+
192
+ # Generate actor declarations
193
+ for persona in sorted(personas, key=lambda p: p.name):
194
+ persona_id = slugify(persona.name).replace("-", "_")
195
+ lines.append(f'actor "{persona.name}" as {persona_id}')
196
+
197
+ lines.append("")
198
+
199
+ # Generate component declarations for apps
200
+ for app_slug in sorted(all_epic_apps):
201
+ app_id = app_slug.replace("-", "_")
202
+ app = app_lookup.get(app_slug)
203
+ app_name = app.name if app else app_slug.replace("-", " ").title()
204
+ lines.append(f'component "{app_name}" as {app_id}')
205
+
206
+ lines.append("")
207
+
208
+ # Collect unique epics
209
+ all_group_epics = set()
210
+ for epics in persona_epics_map.values():
211
+ all_group_epics.update(e.slug for e in epics)
212
+
213
+ # Generate usecase declarations for epics
214
+ for epic_slug in sorted(all_group_epics):
215
+ epic_id = epic_slug.replace("-", "_")
216
+ epic_name = epic_slug.replace("-", " ").title()
217
+ lines.append(f'usecase "{epic_name}" as {epic_id}')
218
+
219
+ lines.append("")
220
+
221
+ # Generate persona -> epic connections
222
+ for persona in sorted(personas, key=lambda p: p.name):
223
+ persona_id = slugify(persona.name).replace("-", "_")
224
+ for epic in sorted(
225
+ persona_epics_map.get(persona.name, []), key=lambda e: e.slug
226
+ ):
227
+ epic_id = epic.slug.replace("-", "_")
228
+ lines.append(f"{persona_id} --> {epic_id}")
229
+
230
+ lines.append("")
231
+
232
+ # Generate epic -> app connections
233
+ for epic_slug in sorted(all_group_epics):
234
+ epic_id = epic_slug.replace("-", "_")
235
+ for app_slug in sorted(epic_apps_map.get(epic_slug, [])):
236
+ app_id = app_slug.replace("-", "_")
237
+ lines.append(f"{epic_id} --> {app_id}")
238
+
239
+ lines.append("")
240
+ lines.append("@enduml")
241
+
242
+ return "\n".join(lines)
243
+
244
+
245
+ def build_persona_diagram(persona_name: str, docname: str, hcd_context):
246
+ """Build the PlantUML diagram for a single persona."""
247
+ try:
248
+ from sphinxcontrib.plantuml import plantuml
249
+ except ImportError:
250
+ para = nodes.paragraph()
251
+ para += nodes.emphasis(text="PlantUML extension not available")
252
+ return [para]
253
+
254
+ all_stories = hcd_context.story_repo.list_all()
255
+ all_epics = hcd_context.epic_repo.list_all()
256
+ all_apps = hcd_context.app_repo.list_all()
257
+
258
+ # Derive personas
259
+ personas = derive_personas(all_stories, all_epics)
260
+ persona_normalized = normalize_name(persona_name)
261
+
262
+ # Find the persona
263
+ persona = None
264
+ for p in personas:
265
+ if p.normalized_name == persona_normalized:
266
+ persona = p
267
+ break
268
+
269
+ if not persona:
270
+ para = nodes.paragraph()
271
+ para += nodes.emphasis(text=f"No persona found: '{persona_name}'")
272
+ return [para]
273
+
274
+ # Check if persona has epics
275
+ epics = get_epics_for_persona(persona, all_epics, all_stories)
276
+ if not epics:
277
+ para = nodes.paragraph()
278
+ para += nodes.emphasis(text=f"No epics found for persona '{persona_name}'")
279
+ return [para]
280
+
281
+ # Generate PlantUML
282
+ puml_source = generate_persona_plantuml(persona, all_epics, all_stories, all_apps)
283
+
284
+ # Create plantuml node
285
+ node = plantuml(puml_source)
286
+ node["uml"] = puml_source
287
+ node["incdir"] = os.path.dirname(docname)
288
+ node["filename"] = os.path.basename(docname)
289
+
290
+ return [node]
291
+
292
+
293
+ def build_persona_index_diagram(group_type: str, docname: str, hcd_context):
294
+ """Build the PlantUML diagram for a persona group."""
295
+ try:
296
+ from sphinxcontrib.plantuml import plantuml
297
+ except ImportError:
298
+ para = nodes.paragraph()
299
+ para += nodes.emphasis(text="PlantUML extension not available")
300
+ return [para]
301
+
302
+ all_stories = hcd_context.story_repo.list_all()
303
+ all_epics = hcd_context.epic_repo.list_all()
304
+ all_apps = hcd_context.app_repo.list_all()
305
+
306
+ # Get personas grouped by app type
307
+ personas_by_type = derive_personas_by_app_type(all_stories, all_epics, all_apps)
308
+ personas = sorted(personas_by_type.get(group_type, []), key=lambda p: p.name)
309
+
310
+ if not personas:
311
+ para = nodes.paragraph()
312
+ para += nodes.emphasis(text=f"No {group_type} personas found")
313
+ return [para]
314
+
315
+ # Generate PlantUML
316
+ puml_source = generate_persona_index_plantuml(
317
+ group_type, personas, all_epics, all_stories, all_apps
318
+ )
319
+
320
+ # Create plantuml node
321
+ node = plantuml(puml_source)
322
+ node["uml"] = puml_source
323
+ node["incdir"] = os.path.dirname(docname)
324
+ node["filename"] = os.path.basename(docname)
325
+
326
+ return [node]
327
+
328
+
329
+ def process_persona_placeholders(app, doctree, docname):
330
+ """Replace persona diagram placeholders with rendered content."""
331
+ from ..context import get_hcd_context
332
+
333
+ hcd_context = get_hcd_context(app)
334
+
335
+ # Process persona-diagram placeholders
336
+ for node in doctree.traverse(PersonaDiagramPlaceholder):
337
+ persona = node["persona"]
338
+ content = build_persona_diagram(persona, docname, hcd_context)
339
+ node.replace_self(content)
340
+
341
+ # Process persona-index-diagram placeholders
342
+ for node in doctree.traverse(PersonaIndexDiagramPlaceholder):
343
+ group_type = node["group_type"]
344
+ content = build_persona_index_diagram(group_type, docname, hcd_context)
345
+ node.replace_self(content)