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,448 @@
|
|
|
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.docutils import SphinxDirective
|
|
12
|
+
from sphinx.util import logging
|
|
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
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class EpicsForPersonaDirective(SphinxDirective):
|
|
133
|
+
"""List epics for a specific persona (derived from stories).
|
|
134
|
+
|
|
135
|
+
Usage::
|
|
136
|
+
|
|
137
|
+
.. epics-for-persona:: Member Implementer
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
required_arguments = 1
|
|
141
|
+
final_argument_whitespace = True
|
|
142
|
+
|
|
143
|
+
def run(self):
|
|
144
|
+
# Return placeholder - actual rendering in doctree-resolved
|
|
145
|
+
node = EpicsForPersonaPlaceholder()
|
|
146
|
+
node['persona'] = self.arguments[0]
|
|
147
|
+
return [node]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class EpicsForPersonaPlaceholder(nodes.General, nodes.Element):
|
|
151
|
+
"""Placeholder node for epics-for-persona, replaced at doctree-resolved."""
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def clear_epic_state(app, env, docname):
|
|
156
|
+
"""Clear epic state when a document is re-read."""
|
|
157
|
+
current_epic = get_current_epic(env)
|
|
158
|
+
epic_registry = get_epic_registry(env)
|
|
159
|
+
|
|
160
|
+
if docname in current_epic:
|
|
161
|
+
del current_epic[docname]
|
|
162
|
+
|
|
163
|
+
# Remove epics defined in this document
|
|
164
|
+
to_remove = [slug for slug, e in epic_registry.items()
|
|
165
|
+
if e['docname'] == docname]
|
|
166
|
+
for slug in to_remove:
|
|
167
|
+
del epic_registry[slug]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def validate_epics(app, env):
|
|
171
|
+
"""Validate epic references after all documents are read."""
|
|
172
|
+
from . import stories
|
|
173
|
+
|
|
174
|
+
epic_registry = get_epic_registry(env)
|
|
175
|
+
_story_registry = stories.get_story_registry()
|
|
176
|
+
story_titles = {normalize_name(s['feature']) for s in _story_registry}
|
|
177
|
+
|
|
178
|
+
for slug, epic in epic_registry.items():
|
|
179
|
+
# Validate story references
|
|
180
|
+
for story_title in epic['stories']:
|
|
181
|
+
if normalize_name(story_title) not in story_titles:
|
|
182
|
+
logger.warning(
|
|
183
|
+
f"Epic '{slug}' references unknown story: '{story_title}'"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_personas_for_epic(epic: dict, story_registry: list) -> set[str]:
|
|
188
|
+
"""Get the set of personas for an epic based on its stories."""
|
|
189
|
+
personas = set()
|
|
190
|
+
for story_title in epic['stories']:
|
|
191
|
+
story_normalized = normalize_name(story_title)
|
|
192
|
+
for story in story_registry:
|
|
193
|
+
if normalize_name(story['feature']) == story_normalized:
|
|
194
|
+
personas.add(story['persona'])
|
|
195
|
+
break
|
|
196
|
+
return personas
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def render_epic_stories(epic: dict, docname: str, story_registry: list, known_personas: set):
|
|
200
|
+
"""Render epic stories as a simple bullet list."""
|
|
201
|
+
from . import stories
|
|
202
|
+
|
|
203
|
+
config = get_config()
|
|
204
|
+
_known_apps = stories.get_known_apps()
|
|
205
|
+
|
|
206
|
+
stories_data = []
|
|
207
|
+
for story_title in epic['stories']:
|
|
208
|
+
story_normalized = normalize_name(story_title)
|
|
209
|
+
for story in story_registry:
|
|
210
|
+
if normalize_name(story['feature']) == story_normalized:
|
|
211
|
+
stories_data.append(story)
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
if not stories_data:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
# Calculate paths
|
|
218
|
+
prefix = path_to_root(docname)
|
|
219
|
+
|
|
220
|
+
result_nodes = []
|
|
221
|
+
|
|
222
|
+
# Stories heading
|
|
223
|
+
stories_heading = nodes.paragraph()
|
|
224
|
+
stories_heading += nodes.strong(text="Stories")
|
|
225
|
+
result_nodes.append(stories_heading)
|
|
226
|
+
|
|
227
|
+
# Simple bullet list: "story name (App Name)"
|
|
228
|
+
story_list = nodes.bullet_list()
|
|
229
|
+
|
|
230
|
+
for story in sorted(stories_data, key=lambda s: s['feature'].lower()):
|
|
231
|
+
story_item = nodes.list_item()
|
|
232
|
+
story_para = nodes.paragraph()
|
|
233
|
+
|
|
234
|
+
# Story link
|
|
235
|
+
story_para += stories.make_story_reference(story, docname)
|
|
236
|
+
|
|
237
|
+
# App in parentheses
|
|
238
|
+
story_para += nodes.Text(" (")
|
|
239
|
+
app_path = f"{prefix}{config.get_doc_path('applications')}/{story['app']}.html"
|
|
240
|
+
app_valid = story['app_normalized'] in _known_apps
|
|
241
|
+
|
|
242
|
+
if app_valid:
|
|
243
|
+
app_ref = nodes.reference("", "", refuri=app_path)
|
|
244
|
+
app_ref += nodes.Text(story['app'].replace("-", " ").title())
|
|
245
|
+
story_para += app_ref
|
|
246
|
+
else:
|
|
247
|
+
story_para += nodes.Text(story['app'].replace("-", " ").title())
|
|
248
|
+
|
|
249
|
+
story_para += nodes.Text(")")
|
|
250
|
+
|
|
251
|
+
story_item += story_para
|
|
252
|
+
story_list += story_item
|
|
253
|
+
|
|
254
|
+
result_nodes.append(story_list)
|
|
255
|
+
|
|
256
|
+
return result_nodes
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def process_epic_placeholders(app, doctree, docname):
|
|
260
|
+
"""Replace epic placeholders with rendered content."""
|
|
261
|
+
from . import stories
|
|
262
|
+
|
|
263
|
+
config = get_config()
|
|
264
|
+
env = app.env
|
|
265
|
+
epic_registry = get_epic_registry(env)
|
|
266
|
+
current_epic = get_current_epic(env)
|
|
267
|
+
_story_registry = stories.get_story_registry()
|
|
268
|
+
_known_personas = stories.get_known_personas()
|
|
269
|
+
|
|
270
|
+
# Process epic stories placeholder
|
|
271
|
+
epic_slug = current_epic.get(docname)
|
|
272
|
+
if epic_slug and epic_slug in epic_registry:
|
|
273
|
+
epic = epic_registry[epic_slug]
|
|
274
|
+
|
|
275
|
+
for node in doctree.traverse(nodes.container):
|
|
276
|
+
if 'epic-stories-placeholder' in node.get('classes', []):
|
|
277
|
+
stories_nodes = render_epic_stories(
|
|
278
|
+
epic, docname, _story_registry, _known_personas
|
|
279
|
+
)
|
|
280
|
+
if stories_nodes:
|
|
281
|
+
node.replace_self(stories_nodes)
|
|
282
|
+
else:
|
|
283
|
+
node.replace_self([])
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
# Process epic index placeholder
|
|
287
|
+
for node in doctree.traverse(EpicIndexPlaceholder):
|
|
288
|
+
index_node = build_epic_index(env, docname, _story_registry)
|
|
289
|
+
node.replace_self(index_node)
|
|
290
|
+
|
|
291
|
+
# Process epics-for-persona placeholder
|
|
292
|
+
for node in doctree.traverse(EpicsForPersonaPlaceholder):
|
|
293
|
+
persona = node['persona']
|
|
294
|
+
epics_node = build_epics_for_persona(
|
|
295
|
+
env, docname, persona, _story_registry
|
|
296
|
+
)
|
|
297
|
+
node.replace_self(epics_node)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def build_epic_index(env, docname: str, story_registry: list):
|
|
301
|
+
"""Build the epic index listing all epics, plus unassigned stories."""
|
|
302
|
+
from . import stories
|
|
303
|
+
|
|
304
|
+
config = get_config()
|
|
305
|
+
epic_registry = get_epic_registry(env)
|
|
306
|
+
_known_apps = stories.get_known_apps()
|
|
307
|
+
|
|
308
|
+
if not epic_registry:
|
|
309
|
+
para = nodes.paragraph()
|
|
310
|
+
para += nodes.emphasis(text="No epics defined")
|
|
311
|
+
return [para]
|
|
312
|
+
|
|
313
|
+
result_nodes = []
|
|
314
|
+
bullet_list = nodes.bullet_list()
|
|
315
|
+
|
|
316
|
+
# Collect all stories assigned to epics
|
|
317
|
+
assigned_stories = set()
|
|
318
|
+
for epic in epic_registry.values():
|
|
319
|
+
for story_title in epic['stories']:
|
|
320
|
+
assigned_stories.add(normalize_name(story_title))
|
|
321
|
+
|
|
322
|
+
for slug in sorted(epic_registry.keys()):
|
|
323
|
+
epic = epic_registry[slug]
|
|
324
|
+
|
|
325
|
+
item = nodes.list_item()
|
|
326
|
+
para = nodes.paragraph()
|
|
327
|
+
|
|
328
|
+
# Link to epic
|
|
329
|
+
epic_path = f"{slug}.html"
|
|
330
|
+
epic_ref = nodes.reference("", "", refuri=epic_path)
|
|
331
|
+
epic_ref += nodes.Text(slug.replace("-", " ").title())
|
|
332
|
+
para += epic_ref
|
|
333
|
+
|
|
334
|
+
# Story count
|
|
335
|
+
story_count = len(epic['stories'])
|
|
336
|
+
para += nodes.Text(f" ({story_count} stories)")
|
|
337
|
+
|
|
338
|
+
item += para
|
|
339
|
+
bullet_list += item
|
|
340
|
+
|
|
341
|
+
result_nodes.append(bullet_list)
|
|
342
|
+
|
|
343
|
+
# Find unassigned stories
|
|
344
|
+
unassigned_stories = []
|
|
345
|
+
for story in story_registry:
|
|
346
|
+
if normalize_name(story['feature']) not in assigned_stories:
|
|
347
|
+
unassigned_stories.append(story)
|
|
348
|
+
|
|
349
|
+
if unassigned_stories:
|
|
350
|
+
# Calculate paths
|
|
351
|
+
prefix = path_to_root(docname)
|
|
352
|
+
|
|
353
|
+
# Add section heading
|
|
354
|
+
heading = nodes.paragraph()
|
|
355
|
+
heading += nodes.strong(text="Unassigned Stories")
|
|
356
|
+
result_nodes.append(heading)
|
|
357
|
+
|
|
358
|
+
intro = nodes.paragraph()
|
|
359
|
+
intro += nodes.Text(f"{len(unassigned_stories)} stories not yet assigned to an epic:")
|
|
360
|
+
result_nodes.append(intro)
|
|
361
|
+
|
|
362
|
+
# List unassigned stories
|
|
363
|
+
unassigned_list = nodes.bullet_list()
|
|
364
|
+
for story in sorted(unassigned_stories, key=lambda s: s['feature'].lower()):
|
|
365
|
+
item = nodes.list_item()
|
|
366
|
+
para = nodes.paragraph()
|
|
367
|
+
|
|
368
|
+
# Story link
|
|
369
|
+
para += stories.make_story_reference(story, docname)
|
|
370
|
+
|
|
371
|
+
# App in parentheses
|
|
372
|
+
para += nodes.Text(" (")
|
|
373
|
+
app_path = f"{prefix}{config.get_doc_path('applications')}/{story['app']}.html"
|
|
374
|
+
app_valid = story['app_normalized'] in _known_apps
|
|
375
|
+
|
|
376
|
+
if app_valid:
|
|
377
|
+
app_ref = nodes.reference("", "", refuri=app_path)
|
|
378
|
+
app_ref += nodes.Text(story['app'].replace("-", " ").title())
|
|
379
|
+
para += app_ref
|
|
380
|
+
else:
|
|
381
|
+
para += nodes.Text(story['app'].replace("-", " ").title())
|
|
382
|
+
|
|
383
|
+
para += nodes.Text(")")
|
|
384
|
+
|
|
385
|
+
item += para
|
|
386
|
+
unassigned_list += item
|
|
387
|
+
|
|
388
|
+
result_nodes.append(unassigned_list)
|
|
389
|
+
|
|
390
|
+
return result_nodes
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def build_epics_for_persona(env, docname: str, persona_arg: str, story_registry: list):
|
|
394
|
+
"""Build list of epics for a persona."""
|
|
395
|
+
config = get_config()
|
|
396
|
+
epic_registry = get_epic_registry(env)
|
|
397
|
+
persona_normalized = normalize_name(persona_arg)
|
|
398
|
+
|
|
399
|
+
prefix = path_to_root(docname)
|
|
400
|
+
|
|
401
|
+
# Find epics that contain stories for this persona
|
|
402
|
+
matching_epics = []
|
|
403
|
+
for slug, epic in epic_registry.items():
|
|
404
|
+
personas = get_personas_for_epic(epic, story_registry)
|
|
405
|
+
persona_names_normalized = {normalize_name(p) for p in personas}
|
|
406
|
+
if persona_normalized in persona_names_normalized:
|
|
407
|
+
matching_epics.append((slug, epic))
|
|
408
|
+
|
|
409
|
+
if not matching_epics:
|
|
410
|
+
para = nodes.paragraph()
|
|
411
|
+
para += nodes.emphasis(text=f"No epics found for persona '{persona_arg}'")
|
|
412
|
+
return [para]
|
|
413
|
+
|
|
414
|
+
bullet_list = nodes.bullet_list()
|
|
415
|
+
|
|
416
|
+
for slug, epic in sorted(matching_epics, key=lambda x: x[0]):
|
|
417
|
+
item = nodes.list_item()
|
|
418
|
+
para = nodes.paragraph()
|
|
419
|
+
|
|
420
|
+
epic_path = f"{prefix}{config.get_doc_path('epics')}/{slug}.html"
|
|
421
|
+
epic_ref = nodes.reference("", "", refuri=epic_path)
|
|
422
|
+
epic_ref += nodes.Text(slug.replace("-", " ").title())
|
|
423
|
+
para += epic_ref
|
|
424
|
+
|
|
425
|
+
item += para
|
|
426
|
+
bullet_list += item
|
|
427
|
+
|
|
428
|
+
return [bullet_list]
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def setup(app):
|
|
432
|
+
app.connect("env-purge-doc", clear_epic_state)
|
|
433
|
+
app.connect("env-check-consistency", validate_epics)
|
|
434
|
+
app.connect("doctree-resolved", process_epic_placeholders)
|
|
435
|
+
|
|
436
|
+
app.add_directive("define-epic", DefineEpicDirective)
|
|
437
|
+
app.add_directive("epic-story", EpicStoryDirective)
|
|
438
|
+
app.add_directive("epic-index", EpicIndexDirective)
|
|
439
|
+
app.add_directive("epics-for-persona", EpicsForPersonaDirective)
|
|
440
|
+
|
|
441
|
+
app.add_node(EpicIndexPlaceholder)
|
|
442
|
+
app.add_node(EpicsForPersonaPlaceholder)
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
"version": "1.0",
|
|
446
|
+
"parallel_read_safe": False, # Uses global state
|
|
447
|
+
"parallel_write_safe": True,
|
|
448
|
+
}
|