julee 0.1.2__py3-none-any.whl → 0.1.3__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/api/app.py +9 -8
- julee/api/dependencies.py +15 -15
- julee/api/requests.py +10 -9
- julee/api/responses.py +2 -1
- julee/api/routers/__init__.py +5 -5
- julee/api/routers/assembly_specifications.py +5 -4
- julee/api/routers/documents.py +1 -1
- julee/api/routers/knowledge_service_configs.py +4 -3
- julee/api/routers/knowledge_service_queries.py +7 -6
- julee/api/routers/system.py +4 -3
- julee/api/routers/workflows.py +4 -5
- julee/api/services/system_initialization.py +6 -6
- julee/api/tests/routers/test_assembly_specifications.py +4 -3
- julee/api/tests/routers/test_documents.py +5 -4
- julee/api/tests/routers/test_knowledge_service_configs.py +7 -6
- julee/api/tests/routers/test_knowledge_service_queries.py +4 -3
- julee/api/tests/routers/test_system.py +5 -4
- julee/api/tests/routers/test_workflows.py +5 -4
- julee/api/tests/test_app.py +5 -4
- julee/api/tests/test_dependencies.py +3 -2
- julee/api/tests/test_requests.py +2 -1
- julee/contrib/__init__.py +15 -0
- julee/contrib/polling/__init__.py +47 -0
- julee/contrib/polling/domain/__init__.py +17 -0
- julee/contrib/polling/domain/models/__init__.py +13 -0
- julee/contrib/polling/domain/models/polling_config.py +39 -0
- julee/contrib/polling/domain/services/__init__.py +11 -0
- julee/contrib/polling/domain/services/poller.py +39 -0
- julee/contrib/polling/infrastructure/__init__.py +15 -0
- julee/contrib/polling/infrastructure/services/__init__.py +12 -0
- julee/contrib/polling/infrastructure/services/polling/__init__.py +12 -0
- julee/contrib/polling/infrastructure/services/polling/http/__init__.py +12 -0
- julee/contrib/polling/infrastructure/services/polling/http/http_poller_service.py +80 -0
- julee/contrib/polling/infrastructure/temporal/__init__.py +20 -0
- julee/contrib/polling/infrastructure/temporal/activities.py +42 -0
- julee/contrib/polling/infrastructure/temporal/activity_names.py +20 -0
- julee/contrib/polling/infrastructure/temporal/proxies.py +45 -0
- julee/contrib/polling/tests/__init__.py +6 -0
- julee/contrib/polling/tests/unit/__init__.py +6 -0
- julee/contrib/polling/tests/unit/infrastructure/__init__.py +7 -0
- julee/contrib/polling/tests/unit/infrastructure/services/__init__.py +6 -0
- julee/contrib/polling/tests/unit/infrastructure/services/polling/__init__.py +6 -0
- julee/contrib/polling/tests/unit/infrastructure/services/polling/http/__init__.py +7 -0
- julee/contrib/polling/tests/unit/infrastructure/services/polling/http/test_http_poller_service.py +163 -0
- julee/docs/__init__.py +5 -0
- julee/docs/sphinx_hcd/__init__.py +82 -0
- julee/docs/sphinx_hcd/accelerators.py +1078 -0
- julee/docs/sphinx_hcd/apps.py +499 -0
- julee/docs/sphinx_hcd/config.py +148 -0
- julee/docs/sphinx_hcd/epics.py +448 -0
- julee/docs/sphinx_hcd/integrations.py +306 -0
- julee/docs/sphinx_hcd/journeys.py +783 -0
- julee/docs/sphinx_hcd/personas.py +435 -0
- julee/docs/sphinx_hcd/stories.py +932 -0
- julee/docs/sphinx_hcd/utils.py +180 -0
- julee/domain/models/__init__.py +5 -6
- julee/domain/models/assembly/assembly.py +7 -7
- julee/domain/models/assembly/tests/factories.py +2 -1
- julee/domain/models/assembly/tests/test_assembly.py +16 -13
- julee/domain/models/assembly_specification/assembly_specification.py +11 -10
- julee/domain/models/assembly_specification/knowledge_service_query.py +7 -6
- julee/domain/models/assembly_specification/tests/factories.py +2 -1
- julee/domain/models/assembly_specification/tests/test_assembly_specification.py +9 -6
- julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +3 -1
- julee/domain/models/custom_fields/content_stream.py +3 -2
- julee/domain/models/custom_fields/tests/test_custom_fields.py +2 -1
- julee/domain/models/document/document.py +12 -10
- julee/domain/models/document/tests/factories.py +3 -2
- julee/domain/models/document/tests/test_document.py +6 -3
- julee/domain/models/knowledge_service_config/knowledge_service_config.py +4 -4
- julee/domain/models/policy/__init__.py +4 -4
- julee/domain/models/policy/document_policy_validation.py +17 -17
- julee/domain/models/policy/policy.py +10 -10
- julee/domain/models/policy/tests/factories.py +2 -1
- julee/domain/models/policy/tests/test_document_policy_validation.py +3 -1
- julee/domain/models/policy/tests/test_policy.py +2 -1
- julee/domain/repositories/__init__.py +3 -3
- julee/domain/repositories/assembly.py +3 -1
- julee/domain/repositories/assembly_specification.py +2 -0
- julee/domain/repositories/base.py +5 -4
- julee/domain/repositories/document.py +3 -1
- julee/domain/repositories/document_policy_validation.py +3 -1
- julee/domain/repositories/knowledge_service_config.py +2 -0
- julee/domain/repositories/knowledge_service_query.py +1 -0
- julee/domain/repositories/policy.py +3 -1
- julee/domain/use_cases/decorators.py +3 -2
- julee/domain/use_cases/extract_assemble_data.py +13 -12
- julee/domain/use_cases/initialize_system_data.py +13 -13
- julee/domain/use_cases/tests/test_extract_assemble_data.py +10 -10
- julee/domain/use_cases/tests/test_initialize_system_data.py +2 -2
- julee/domain/use_cases/tests/test_validate_document.py +11 -11
- julee/domain/use_cases/validate_document.py +14 -14
- julee/maintenance/__init__.py +1 -0
- julee/maintenance/release.py +188 -0
- julee/repositories/memory/assembly.py +6 -5
- julee/repositories/memory/assembly_specification.py +8 -9
- julee/repositories/memory/base.py +12 -11
- julee/repositories/memory/document.py +8 -7
- julee/repositories/memory/document_policy_validation.py +7 -6
- julee/repositories/memory/knowledge_service_config.py +8 -7
- julee/repositories/memory/knowledge_service_query.py +8 -7
- julee/repositories/memory/policy.py +6 -5
- julee/repositories/memory/tests/test_document.py +6 -4
- julee/repositories/memory/tests/test_document_policy_validation.py +2 -1
- julee/repositories/memory/tests/test_policy.py +2 -1
- julee/repositories/minio/assembly.py +4 -4
- julee/repositories/minio/assembly_specification.py +6 -8
- julee/repositories/minio/client.py +22 -25
- julee/repositories/minio/document.py +11 -11
- julee/repositories/minio/document_policy_validation.py +5 -5
- julee/repositories/minio/knowledge_service_config.py +6 -6
- julee/repositories/minio/knowledge_service_query.py +6 -9
- julee/repositories/minio/policy.py +4 -4
- julee/repositories/minio/tests/fake_client.py +11 -9
- julee/repositories/minio/tests/test_assembly.py +3 -1
- julee/repositories/minio/tests/test_assembly_specification.py +2 -1
- julee/repositories/minio/tests/test_client_protocol.py +5 -5
- julee/repositories/minio/tests/test_document.py +7 -6
- julee/repositories/minio/tests/test_document_policy_validation.py +3 -1
- julee/repositories/minio/tests/test_knowledge_service_config.py +4 -2
- julee/repositories/minio/tests/test_knowledge_service_query.py +3 -2
- julee/repositories/minio/tests/test_policy.py +3 -1
- julee/repositories/temporal/activities.py +5 -5
- julee/repositories/temporal/proxies.py +5 -5
- julee/services/knowledge_service/__init__.py +1 -2
- julee/services/knowledge_service/anthropic/knowledge_service.py +8 -7
- julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +11 -10
- julee/services/knowledge_service/factory.py +8 -8
- julee/services/knowledge_service/knowledge_service.py +12 -14
- julee/services/knowledge_service/memory/knowledge_service.py +13 -12
- julee/services/knowledge_service/memory/test_knowledge_service.py +10 -7
- julee/services/knowledge_service/test_factory.py +11 -10
- julee/services/temporal/activities.py +10 -10
- julee/services/temporal/proxies.py +2 -2
- julee/util/domain.py +6 -6
- julee/util/repos/minio/file_storage.py +8 -9
- julee/util/repos/temporal/client_proxies/file_storage.py +3 -4
- julee/util/repos/temporal/data_converter.py +6 -6
- julee/util/repos/temporal/minio_file_storage.py +1 -1
- julee/util/repos/temporal/proxies/file_storage.py +2 -3
- julee/util/repositories.py +4 -3
- julee/util/temporal/decorators.py +20 -18
- julee/util/tests/test_decorators.py +13 -15
- julee/util/validation/repository.py +3 -3
- julee/util/validation/type_guards.py +12 -11
- julee/worker.py +9 -8
- julee/workflows/__init__.py +2 -2
- julee/workflows/extract_assemble.py +2 -1
- julee/workflows/validate_document.py +3 -2
- {julee-0.1.2.dist-info → julee-0.1.3.dist-info}/METADATA +2 -1
- julee-0.1.3.dist-info/RECORD +197 -0
- julee-0.1.2.dist-info/RECORD +0 -161
- {julee-0.1.2.dist-info → julee-0.1.3.dist-info}/WHEEL +0 -0
- {julee-0.1.2.dist-info → julee-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {julee-0.1.2.dist-info → julee-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""Sphinx extension for persona diagrams.
|
|
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
|
+
from collections import defaultdict
|
|
11
|
+
from docutils import nodes
|
|
12
|
+
from sphinx.util.docutils import SphinxDirective
|
|
13
|
+
from sphinx.util import logging
|
|
14
|
+
|
|
15
|
+
from .config import get_config
|
|
16
|
+
from .utils import normalize_name, slugify
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_epics_for_persona(persona_name: str, epic_registry: dict, story_registry: list) -> list[tuple[str, dict]]:
|
|
22
|
+
"""Get epics that contain stories for a given persona.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
persona_name: The persona name to match
|
|
26
|
+
epic_registry: Dict of epic_slug -> epic_data
|
|
27
|
+
story_registry: List of story dicts
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of (epic_slug, epic_data) tuples for matching epics
|
|
31
|
+
"""
|
|
32
|
+
persona_normalized = normalize_name(persona_name)
|
|
33
|
+
|
|
34
|
+
# Build lookup of story title -> persona
|
|
35
|
+
story_personas = {}
|
|
36
|
+
for story in story_registry:
|
|
37
|
+
story_personas[normalize_name(story['feature'])] = story['persona_normalized']
|
|
38
|
+
|
|
39
|
+
matching_epics = []
|
|
40
|
+
for slug, epic in epic_registry.items():
|
|
41
|
+
# Check if any story in this epic belongs to the persona
|
|
42
|
+
for story_title in epic.get('stories', []):
|
|
43
|
+
story_normalized = normalize_name(story_title)
|
|
44
|
+
if story_personas.get(story_normalized) == persona_normalized:
|
|
45
|
+
matching_epics.append((slug, epic))
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
return sorted(matching_epics, key=lambda x: x[0])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_apps_for_epic(epic: dict, story_registry: list) -> set[str]:
|
|
52
|
+
"""Get the set of app slugs used by stories in an epic.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
epic: Epic data dict with 'stories' list
|
|
56
|
+
story_registry: List of story dicts
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Set of app slug strings
|
|
60
|
+
"""
|
|
61
|
+
apps = set()
|
|
62
|
+
|
|
63
|
+
# Build lookup of story title -> app
|
|
64
|
+
story_apps = {}
|
|
65
|
+
for story in story_registry:
|
|
66
|
+
story_apps[normalize_name(story['feature'])] = story['app']
|
|
67
|
+
|
|
68
|
+
for story_title in epic.get('stories', []):
|
|
69
|
+
story_normalized = normalize_name(story_title)
|
|
70
|
+
if story_normalized in story_apps:
|
|
71
|
+
apps.add(story_apps[story_normalized])
|
|
72
|
+
|
|
73
|
+
return apps
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_apps_for_persona(persona_name: str, story_registry: list) -> set[str]:
|
|
77
|
+
"""Get the set of app slugs used by a persona.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
persona_name: The persona name to match
|
|
81
|
+
story_registry: List of story dicts
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Set of app slug strings
|
|
85
|
+
"""
|
|
86
|
+
persona_normalized = normalize_name(persona_name)
|
|
87
|
+
apps = set()
|
|
88
|
+
|
|
89
|
+
for story in story_registry:
|
|
90
|
+
if story['persona_normalized'] == persona_normalized:
|
|
91
|
+
apps.add(story['app'])
|
|
92
|
+
|
|
93
|
+
return apps
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def generate_persona_plantuml(persona_name: str, epics: list[tuple[str, dict]],
|
|
97
|
+
story_registry: list, app_registry: dict) -> str:
|
|
98
|
+
"""Generate PlantUML for a single persona's use case diagram.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
persona_name: Display name of the persona
|
|
102
|
+
epics: List of (epic_slug, epic_data) tuples
|
|
103
|
+
story_registry: List of story dicts
|
|
104
|
+
app_registry: Dict of app_slug -> app_data
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
PlantUML source string
|
|
108
|
+
"""
|
|
109
|
+
persona_id = slugify(persona_name).replace('-', '_')
|
|
110
|
+
|
|
111
|
+
lines = [
|
|
112
|
+
f"@startuml persona-{slugify(persona_name)}",
|
|
113
|
+
"left to right direction",
|
|
114
|
+
"skinparam actorStyle awesome",
|
|
115
|
+
"",
|
|
116
|
+
f'actor "{persona_name}" as {persona_id}',
|
|
117
|
+
"",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
# Collect all apps used by this persona's epics
|
|
121
|
+
all_apps = set()
|
|
122
|
+
epic_apps = {} # epic_slug -> set of apps
|
|
123
|
+
|
|
124
|
+
for epic_slug, epic in epics:
|
|
125
|
+
apps = get_apps_for_epic(epic, story_registry)
|
|
126
|
+
epic_apps[epic_slug] = apps
|
|
127
|
+
all_apps.update(apps)
|
|
128
|
+
|
|
129
|
+
# Generate component declarations for apps
|
|
130
|
+
for app_slug in sorted(all_apps):
|
|
131
|
+
app_id = app_slug.replace('-', '_')
|
|
132
|
+
app_name = app_registry.get(app_slug, {}).get('name', app_slug.replace('-', ' ').title())
|
|
133
|
+
lines.append(f'component "{app_name}" as {app_id}')
|
|
134
|
+
|
|
135
|
+
lines.append("")
|
|
136
|
+
|
|
137
|
+
# Generate usecase declarations for epics
|
|
138
|
+
for epic_slug, epic in epics:
|
|
139
|
+
epic_id = epic_slug.replace('-', '_')
|
|
140
|
+
epic_name = epic_slug.replace('-', ' ').title()
|
|
141
|
+
lines.append(f'usecase "{epic_name}" as {epic_id}')
|
|
142
|
+
|
|
143
|
+
lines.append("")
|
|
144
|
+
|
|
145
|
+
# Generate persona -> epic connections
|
|
146
|
+
for epic_slug, epic in epics:
|
|
147
|
+
epic_id = epic_slug.replace('-', '_')
|
|
148
|
+
lines.append(f"{persona_id} --> {epic_id}")
|
|
149
|
+
|
|
150
|
+
lines.append("")
|
|
151
|
+
|
|
152
|
+
# Generate epic -> app connections
|
|
153
|
+
for epic_slug, epic in epics:
|
|
154
|
+
epic_id = epic_slug.replace('-', '_')
|
|
155
|
+
for app_slug in sorted(epic_apps.get(epic_slug, [])):
|
|
156
|
+
app_id = app_slug.replace('-', '_')
|
|
157
|
+
lines.append(f"{epic_id} --> {app_id}")
|
|
158
|
+
|
|
159
|
+
lines.append("")
|
|
160
|
+
lines.append("@enduml")
|
|
161
|
+
|
|
162
|
+
return "\n".join(lines)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def generate_persona_index_plantuml(persona_type: str, personas: list[str],
|
|
166
|
+
epic_registry: dict, story_registry: list,
|
|
167
|
+
app_registry: dict) -> str:
|
|
168
|
+
"""Generate PlantUML for a group of personas (staff or external).
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
persona_type: 'staff' or 'external'
|
|
172
|
+
personas: List of persona names in this group
|
|
173
|
+
epic_registry: Dict of epic_slug -> epic_data
|
|
174
|
+
story_registry: List of story dicts
|
|
175
|
+
app_registry: Dict of app_slug -> app_data
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
PlantUML source string
|
|
179
|
+
"""
|
|
180
|
+
lines = [
|
|
181
|
+
f"@startuml persona-{persona_type}",
|
|
182
|
+
"left to right direction",
|
|
183
|
+
"skinparam actorStyle awesome",
|
|
184
|
+
"",
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
# Collect data for all personas
|
|
188
|
+
persona_epics = {} # persona -> list of epic slugs
|
|
189
|
+
all_apps = set()
|
|
190
|
+
epic_app_map = {} # epic_slug -> set of apps
|
|
191
|
+
|
|
192
|
+
for persona_name in personas:
|
|
193
|
+
epics = get_epics_for_persona(persona_name, epic_registry, story_registry)
|
|
194
|
+
persona_epics[persona_name] = [slug for slug, _ in epics]
|
|
195
|
+
|
|
196
|
+
for epic_slug, epic in epics:
|
|
197
|
+
if epic_slug not in epic_app_map:
|
|
198
|
+
apps = get_apps_for_epic(epic, story_registry)
|
|
199
|
+
epic_app_map[epic_slug] = apps
|
|
200
|
+
all_apps.update(apps)
|
|
201
|
+
|
|
202
|
+
# Generate actor declarations
|
|
203
|
+
for persona_name in sorted(personas):
|
|
204
|
+
persona_id = slugify(persona_name).replace('-', '_')
|
|
205
|
+
lines.append(f'actor "{persona_name}" as {persona_id}')
|
|
206
|
+
|
|
207
|
+
lines.append("")
|
|
208
|
+
|
|
209
|
+
# Generate component declarations for apps
|
|
210
|
+
for app_slug in sorted(all_apps):
|
|
211
|
+
app_id = app_slug.replace('-', '_')
|
|
212
|
+
app_name = app_registry.get(app_slug, {}).get('name', app_slug.replace('-', ' ').title())
|
|
213
|
+
lines.append(f'component "{app_name}" as {app_id}')
|
|
214
|
+
|
|
215
|
+
lines.append("")
|
|
216
|
+
|
|
217
|
+
# Collect unique epics across all personas in this group
|
|
218
|
+
all_epics = set()
|
|
219
|
+
for epic_slugs in persona_epics.values():
|
|
220
|
+
all_epics.update(epic_slugs)
|
|
221
|
+
|
|
222
|
+
# Generate usecase declarations for epics
|
|
223
|
+
for epic_slug in sorted(all_epics):
|
|
224
|
+
epic_id = epic_slug.replace('-', '_')
|
|
225
|
+
epic_name = epic_slug.replace('-', ' ').title()
|
|
226
|
+
lines.append(f'usecase "{epic_name}" as {epic_id}')
|
|
227
|
+
|
|
228
|
+
lines.append("")
|
|
229
|
+
|
|
230
|
+
# Generate persona -> epic connections
|
|
231
|
+
for persona_name in sorted(personas):
|
|
232
|
+
persona_id = slugify(persona_name).replace('-', '_')
|
|
233
|
+
for epic_slug in sorted(persona_epics.get(persona_name, [])):
|
|
234
|
+
epic_id = epic_slug.replace('-', '_')
|
|
235
|
+
lines.append(f"{persona_id} --> {epic_id}")
|
|
236
|
+
|
|
237
|
+
lines.append("")
|
|
238
|
+
|
|
239
|
+
# Generate epic -> app connections
|
|
240
|
+
for epic_slug in sorted(all_epics):
|
|
241
|
+
epic_id = epic_slug.replace('-', '_')
|
|
242
|
+
for app_slug in sorted(epic_app_map.get(epic_slug, [])):
|
|
243
|
+
app_id = app_slug.replace('-', '_')
|
|
244
|
+
lines.append(f"{epic_id} --> {app_id}")
|
|
245
|
+
|
|
246
|
+
lines.append("")
|
|
247
|
+
lines.append("@enduml")
|
|
248
|
+
|
|
249
|
+
return "\n".join(lines)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class PersonaDiagramDirective(SphinxDirective):
|
|
253
|
+
"""Generate PlantUML use case diagram for a single persona.
|
|
254
|
+
|
|
255
|
+
Usage::
|
|
256
|
+
|
|
257
|
+
.. persona-diagram:: Pilot Manager
|
|
258
|
+
|
|
259
|
+
Generates a diagram showing:
|
|
260
|
+
- The persona as an actor
|
|
261
|
+
- Epics they participate in as use cases
|
|
262
|
+
- Apps they interact with as components
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
required_arguments = 1
|
|
266
|
+
final_argument_whitespace = True
|
|
267
|
+
|
|
268
|
+
def run(self):
|
|
269
|
+
persona_name = self.arguments[0]
|
|
270
|
+
|
|
271
|
+
# Return placeholder - actual rendering in doctree-resolved
|
|
272
|
+
node = PersonaDiagramPlaceholder()
|
|
273
|
+
node['persona'] = persona_name
|
|
274
|
+
return [node]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class PersonaDiagramPlaceholder(nodes.General, nodes.Element):
|
|
278
|
+
"""Placeholder node for persona-diagram, replaced at doctree-resolved."""
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class PersonaIndexDiagramDirective(SphinxDirective):
|
|
283
|
+
"""Generate PlantUML diagram for a group of personas.
|
|
284
|
+
|
|
285
|
+
Usage::
|
|
286
|
+
|
|
287
|
+
.. persona-index-diagram:: staff
|
|
288
|
+
.. persona-index-diagram:: customers
|
|
289
|
+
.. persona-index-diagram:: vendors
|
|
290
|
+
|
|
291
|
+
Groups are determined by the type field from app.yaml manifests.
|
|
292
|
+
Any value is accepted - the directive filters personas to those
|
|
293
|
+
using apps with a matching type.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
required_arguments = 1
|
|
297
|
+
option_spec = {}
|
|
298
|
+
|
|
299
|
+
def run(self):
|
|
300
|
+
group_type = self.arguments[0].lower()
|
|
301
|
+
|
|
302
|
+
# Return placeholder - actual rendering in doctree-resolved
|
|
303
|
+
node = PersonaIndexDiagramPlaceholder()
|
|
304
|
+
node['group_type'] = group_type
|
|
305
|
+
return [node]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class PersonaIndexDiagramPlaceholder(nodes.General, nodes.Element):
|
|
309
|
+
"""Placeholder node for persona-index-diagram, replaced at doctree-resolved."""
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def get_personas_by_app_type(story_registry: list, app_registry: dict) -> dict[str, set[str]]:
|
|
314
|
+
"""Group personas by the type of apps they use.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
story_registry: List of story dicts
|
|
318
|
+
app_registry: Dict of app_slug -> app_data
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Dict mapping app type strings to sets of persona names
|
|
322
|
+
"""
|
|
323
|
+
personas_by_type = defaultdict(set)
|
|
324
|
+
|
|
325
|
+
for story in story_registry:
|
|
326
|
+
app_slug = story['app']
|
|
327
|
+
persona = story['persona']
|
|
328
|
+
|
|
329
|
+
if persona == 'unknown':
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
app_data = app_registry.get(app_slug, {})
|
|
333
|
+
app_type = app_data.get('type', 'unknown').lower()
|
|
334
|
+
|
|
335
|
+
personas_by_type[app_type].add(persona)
|
|
336
|
+
|
|
337
|
+
return personas_by_type
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def build_persona_diagram(persona_name: str, env, docname: str):
|
|
341
|
+
"""Build the PlantUML diagram for a single persona."""
|
|
342
|
+
from . import stories, epics, apps
|
|
343
|
+
from sphinxcontrib.plantuml import plantuml
|
|
344
|
+
import os
|
|
345
|
+
|
|
346
|
+
story_registry = stories.get_story_registry()
|
|
347
|
+
epic_registry = epics.get_epic_registry(env)
|
|
348
|
+
app_registry = apps.get_app_registry()
|
|
349
|
+
|
|
350
|
+
# Get epics for this persona
|
|
351
|
+
persona_epics = get_epics_for_persona(persona_name, epic_registry, story_registry)
|
|
352
|
+
|
|
353
|
+
if not persona_epics:
|
|
354
|
+
para = nodes.paragraph()
|
|
355
|
+
para += nodes.emphasis(text=f"No epics found for persona '{persona_name}'")
|
|
356
|
+
return [para]
|
|
357
|
+
|
|
358
|
+
# Generate PlantUML
|
|
359
|
+
puml_source = generate_persona_plantuml(
|
|
360
|
+
persona_name, persona_epics, story_registry, app_registry
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Create plantuml node with required attributes
|
|
364
|
+
node = plantuml(puml_source)
|
|
365
|
+
node['uml'] = puml_source
|
|
366
|
+
node['incdir'] = os.path.dirname(docname)
|
|
367
|
+
node['filename'] = os.path.basename(docname)
|
|
368
|
+
|
|
369
|
+
return [node]
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def build_persona_index_diagram(group_type: str, env, docname: str):
|
|
373
|
+
"""Build the PlantUML diagram for a persona group."""
|
|
374
|
+
from . import stories, epics, apps
|
|
375
|
+
from sphinxcontrib.plantuml import plantuml
|
|
376
|
+
import os
|
|
377
|
+
|
|
378
|
+
story_registry = stories.get_story_registry()
|
|
379
|
+
epic_registry = epics.get_epic_registry(env)
|
|
380
|
+
app_registry = apps.get_app_registry()
|
|
381
|
+
|
|
382
|
+
# Get personas for this group type
|
|
383
|
+
personas_by_type = get_personas_by_app_type(story_registry, app_registry)
|
|
384
|
+
personas = sorted(personas_by_type.get(group_type, set()))
|
|
385
|
+
|
|
386
|
+
if not personas:
|
|
387
|
+
para = nodes.paragraph()
|
|
388
|
+
para += nodes.emphasis(text=f"No {group_type} personas found")
|
|
389
|
+
return [para]
|
|
390
|
+
|
|
391
|
+
# Generate PlantUML
|
|
392
|
+
puml_source = generate_persona_index_plantuml(
|
|
393
|
+
group_type, personas, epic_registry, story_registry, app_registry
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Create plantuml node with required attributes
|
|
397
|
+
node = plantuml(puml_source)
|
|
398
|
+
node['uml'] = puml_source
|
|
399
|
+
node['incdir'] = os.path.dirname(docname)
|
|
400
|
+
node['filename'] = os.path.basename(docname)
|
|
401
|
+
|
|
402
|
+
return [node]
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def process_persona_placeholders(app, doctree, docname):
|
|
406
|
+
"""Replace persona diagram placeholders with rendered content."""
|
|
407
|
+
env = app.env
|
|
408
|
+
|
|
409
|
+
# Process persona-diagram placeholders
|
|
410
|
+
for node in doctree.traverse(PersonaDiagramPlaceholder):
|
|
411
|
+
persona = node['persona']
|
|
412
|
+
content = build_persona_diagram(persona, env, docname)
|
|
413
|
+
node.replace_self(content)
|
|
414
|
+
|
|
415
|
+
# Process persona-index-diagram placeholders
|
|
416
|
+
for node in doctree.traverse(PersonaIndexDiagramPlaceholder):
|
|
417
|
+
group_type = node['group_type']
|
|
418
|
+
content = build_persona_index_diagram(group_type, env, docname)
|
|
419
|
+
node.replace_self(content)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def setup(app):
|
|
423
|
+
app.connect("doctree-resolved", process_persona_placeholders)
|
|
424
|
+
|
|
425
|
+
app.add_directive("persona-diagram", PersonaDiagramDirective)
|
|
426
|
+
app.add_directive("persona-index-diagram", PersonaIndexDiagramDirective)
|
|
427
|
+
|
|
428
|
+
app.add_node(PersonaDiagramPlaceholder)
|
|
429
|
+
app.add_node(PersonaIndexDiagramPlaceholder)
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
"version": "1.0",
|
|
433
|
+
"parallel_read_safe": False,
|
|
434
|
+
"parallel_write_safe": True,
|
|
435
|
+
}
|