julee 0.1.5__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.
- julee/docs/sphinx_hcd/__init__.py +146 -13
- julee/docs/sphinx_hcd/domain/__init__.py +5 -0
- julee/docs/sphinx_hcd/domain/models/__init__.py +32 -0
- julee/docs/sphinx_hcd/domain/models/accelerator.py +152 -0
- julee/docs/sphinx_hcd/domain/models/app.py +151 -0
- julee/docs/sphinx_hcd/domain/models/code_info.py +121 -0
- julee/docs/sphinx_hcd/domain/models/epic.py +79 -0
- julee/docs/sphinx_hcd/domain/models/integration.py +230 -0
- julee/docs/sphinx_hcd/domain/models/journey.py +222 -0
- julee/docs/sphinx_hcd/domain/models/persona.py +106 -0
- julee/docs/sphinx_hcd/domain/models/story.py +128 -0
- julee/docs/sphinx_hcd/domain/repositories/__init__.py +25 -0
- julee/docs/sphinx_hcd/domain/repositories/accelerator.py +98 -0
- julee/docs/sphinx_hcd/domain/repositories/app.py +57 -0
- julee/docs/sphinx_hcd/domain/repositories/base.py +89 -0
- julee/docs/sphinx_hcd/domain/repositories/code_info.py +69 -0
- julee/docs/sphinx_hcd/domain/repositories/epic.py +62 -0
- julee/docs/sphinx_hcd/domain/repositories/integration.py +79 -0
- julee/docs/sphinx_hcd/domain/repositories/journey.py +106 -0
- julee/docs/sphinx_hcd/domain/repositories/story.py +68 -0
- julee/docs/sphinx_hcd/domain/use_cases/__init__.py +64 -0
- julee/docs/sphinx_hcd/domain/use_cases/derive_personas.py +166 -0
- julee/docs/sphinx_hcd/domain/use_cases/resolve_accelerator_references.py +236 -0
- julee/docs/sphinx_hcd/domain/use_cases/resolve_app_references.py +144 -0
- julee/docs/sphinx_hcd/domain/use_cases/resolve_story_references.py +121 -0
- julee/docs/sphinx_hcd/parsers/__init__.py +48 -0
- julee/docs/sphinx_hcd/parsers/ast.py +150 -0
- julee/docs/sphinx_hcd/parsers/gherkin.py +155 -0
- julee/docs/sphinx_hcd/parsers/yaml.py +184 -0
- julee/docs/sphinx_hcd/repositories/__init__.py +4 -0
- julee/docs/sphinx_hcd/repositories/memory/__init__.py +25 -0
- julee/docs/sphinx_hcd/repositories/memory/accelerator.py +86 -0
- julee/docs/sphinx_hcd/repositories/memory/app.py +45 -0
- julee/docs/sphinx_hcd/repositories/memory/base.py +106 -0
- julee/docs/sphinx_hcd/repositories/memory/code_info.py +59 -0
- julee/docs/sphinx_hcd/repositories/memory/epic.py +54 -0
- julee/docs/sphinx_hcd/repositories/memory/integration.py +70 -0
- julee/docs/sphinx_hcd/repositories/memory/journey.py +96 -0
- julee/docs/sphinx_hcd/repositories/memory/story.py +63 -0
- julee/docs/sphinx_hcd/sphinx/__init__.py +28 -0
- julee/docs/sphinx_hcd/sphinx/adapters.py +116 -0
- julee/docs/sphinx_hcd/sphinx/context.py +163 -0
- julee/docs/sphinx_hcd/sphinx/directives/__init__.py +160 -0
- julee/docs/sphinx_hcd/sphinx/directives/accelerator.py +576 -0
- julee/docs/sphinx_hcd/sphinx/directives/app.py +349 -0
- julee/docs/sphinx_hcd/sphinx/directives/base.py +211 -0
- julee/docs/sphinx_hcd/sphinx/directives/epic.py +434 -0
- julee/docs/sphinx_hcd/sphinx/directives/integration.py +220 -0
- julee/docs/sphinx_hcd/sphinx/directives/journey.py +642 -0
- julee/docs/sphinx_hcd/sphinx/directives/persona.py +345 -0
- julee/docs/sphinx_hcd/sphinx/directives/story.py +575 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/__init__.py +16 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/builder_inited.py +31 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_read.py +27 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_resolved.py +43 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/env_purge_doc.py +42 -0
- julee/docs/sphinx_hcd/sphinx/initialization.py +139 -0
- julee/docs/sphinx_hcd/tests/__init__.py +9 -0
- julee/docs/sphinx_hcd/tests/conftest.py +6 -0
- julee/docs/sphinx_hcd/tests/domain/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/domain/models/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_accelerator.py +266 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_app.py +258 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_code_info.py +231 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_epic.py +163 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_integration.py +327 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_journey.py +249 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_persona.py +172 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_story.py +216 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_derive_personas.py +314 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_accelerator_references.py +476 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_app_references.py +265 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_story_references.py +229 -0
- julee/docs/sphinx_hcd/tests/integration/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/parsers/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/parsers/test_ast.py +298 -0
- julee/docs/sphinx_hcd/tests/parsers/test_gherkin.py +282 -0
- julee/docs/sphinx_hcd/tests/parsers/test_yaml.py +496 -0
- julee/docs/sphinx_hcd/tests/repositories/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/repositories/test_accelerator.py +298 -0
- julee/docs/sphinx_hcd/tests/repositories/test_app.py +218 -0
- julee/docs/sphinx_hcd/tests/repositories/test_base.py +151 -0
- julee/docs/sphinx_hcd/tests/repositories/test_code_info.py +253 -0
- julee/docs/sphinx_hcd/tests/repositories/test_epic.py +237 -0
- julee/docs/sphinx_hcd/tests/repositories/test_integration.py +268 -0
- julee/docs/sphinx_hcd/tests/repositories/test_journey.py +294 -0
- julee/docs/sphinx_hcd/tests/repositories/test_story.py +236 -0
- julee/docs/sphinx_hcd/tests/sphinx/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/sphinx/directives/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/sphinx/directives/test_base.py +160 -0
- julee/docs/sphinx_hcd/tests/sphinx/test_adapters.py +176 -0
- julee/docs/sphinx_hcd/tests/sphinx/test_context.py +257 -0
- {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/METADATA +2 -1
- {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/RECORD +98 -13
- julee/docs/sphinx_hcd/accelerators.py +0 -1175
- julee/docs/sphinx_hcd/apps.py +0 -518
- julee/docs/sphinx_hcd/epics.py +0 -453
- julee/docs/sphinx_hcd/integrations.py +0 -310
- julee/docs/sphinx_hcd/journeys.py +0 -797
- julee/docs/sphinx_hcd/personas.py +0 -457
- julee/docs/sphinx_hcd/stories.py +0 -960
- {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/WHEEL +0 -0
- {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
"""Journey directives for sphinx_hcd.
|
|
2
|
+
|
|
3
|
+
A journey represents a persona's path through the system to achieve a goal.
|
|
4
|
+
Each journey captures the user's motivation, the value they receive, and
|
|
5
|
+
the sequence of steps (stories/epics) they follow.
|
|
6
|
+
|
|
7
|
+
Provides directives:
|
|
8
|
+
- define-journey: Define a journey with persona, intent, outcome, and steps
|
|
9
|
+
- step-story: Reference a story as a journey step
|
|
10
|
+
- step-epic: Reference an epic as a journey step
|
|
11
|
+
- step-phase: Optional grouping label for steps
|
|
12
|
+
- journey-index: Render index of all journeys
|
|
13
|
+
- journey-dependency-graph: Generate PlantUML graph of journey dependencies
|
|
14
|
+
- journeys-for-persona: List journeys for a specific persona
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from docutils import nodes
|
|
18
|
+
from docutils.parsers.rst import directives
|
|
19
|
+
|
|
20
|
+
from ...domain.models.journey import Journey, JourneyStep
|
|
21
|
+
from ...utils import (
|
|
22
|
+
normalize_name,
|
|
23
|
+
parse_csv_option,
|
|
24
|
+
parse_list_option,
|
|
25
|
+
path_to_root,
|
|
26
|
+
)
|
|
27
|
+
from .base import HCDDirective
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class JourneyDependencyGraphPlaceholder(nodes.General, nodes.Element):
|
|
31
|
+
"""Placeholder node for journey dependency graph, replaced at doctree-resolved."""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DefineJourneyDirective(HCDDirective):
|
|
37
|
+
"""Define a journey with persona, intent, outcome, and metadata.
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
:persona: (required) The persona undertaking this journey.
|
|
41
|
+
:intent: The user's underlying motivation.
|
|
42
|
+
:outcome: The business value delivered when the journey succeeds.
|
|
43
|
+
:depends-on: Comma-separated list of journey slugs.
|
|
44
|
+
:preconditions: Bullet list of conditions that must be true before starting.
|
|
45
|
+
:postconditions: Bullet list of conditions that will be true after completing.
|
|
46
|
+
|
|
47
|
+
Example::
|
|
48
|
+
|
|
49
|
+
.. define-journey:: build-vocabulary
|
|
50
|
+
:persona: Knowledge Curator
|
|
51
|
+
:intent: Ensure consistent terminology across RBA programs
|
|
52
|
+
:outcome: Semantic interoperability enabling automated compliance mapping
|
|
53
|
+
:depends-on: operate-pipelines
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
required_arguments = 1 # journey slug
|
|
57
|
+
has_content = True
|
|
58
|
+
option_spec = {
|
|
59
|
+
"persona": directives.unchanged_required,
|
|
60
|
+
"intent": directives.unchanged,
|
|
61
|
+
"outcome": directives.unchanged,
|
|
62
|
+
"depends-on": directives.unchanged,
|
|
63
|
+
"preconditions": directives.unchanged,
|
|
64
|
+
"postconditions": directives.unchanged,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def run(self):
|
|
68
|
+
journey_slug = self.arguments[0]
|
|
69
|
+
docname = self.env.docname
|
|
70
|
+
|
|
71
|
+
# Parse options
|
|
72
|
+
persona = self.options.get("persona", "").strip()
|
|
73
|
+
intent = self.options.get("intent", "").strip()
|
|
74
|
+
outcome = self.options.get("outcome", "").strip()
|
|
75
|
+
depends_on = parse_csv_option(self.options.get("depends-on", ""))
|
|
76
|
+
preconditions = parse_list_option(self.options.get("preconditions", ""))
|
|
77
|
+
postconditions = parse_list_option(self.options.get("postconditions", ""))
|
|
78
|
+
goal = "\n".join(self.content).strip()
|
|
79
|
+
|
|
80
|
+
# Create and register journey entity
|
|
81
|
+
journey = Journey(
|
|
82
|
+
slug=journey_slug,
|
|
83
|
+
persona=persona,
|
|
84
|
+
intent=intent,
|
|
85
|
+
outcome=outcome,
|
|
86
|
+
goal=goal,
|
|
87
|
+
depends_on=depends_on,
|
|
88
|
+
preconditions=preconditions,
|
|
89
|
+
postconditions=postconditions,
|
|
90
|
+
steps=[], # Will be populated by step directives
|
|
91
|
+
docname=docname,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Add to repository
|
|
95
|
+
self.hcd_context.journey_repo.save(journey)
|
|
96
|
+
|
|
97
|
+
# Track current journey in environment for step directives
|
|
98
|
+
if not hasattr(self.env, "journey_current"):
|
|
99
|
+
self.env.journey_current = {}
|
|
100
|
+
self.env.journey_current[docname] = journey_slug
|
|
101
|
+
|
|
102
|
+
# Build output nodes
|
|
103
|
+
result_nodes = []
|
|
104
|
+
|
|
105
|
+
# Intent and outcome paragraph
|
|
106
|
+
if persona and intent:
|
|
107
|
+
intro_para = nodes.paragraph()
|
|
108
|
+
intro_para += nodes.Text("The ")
|
|
109
|
+
intro_para += self.make_persona_link(persona)
|
|
110
|
+
intro_para += nodes.Text(" wants to ")
|
|
111
|
+
intent_text = intent[0].lower() + intent[1:] if intent else ""
|
|
112
|
+
intro_para += nodes.Text(intent_text + ".")
|
|
113
|
+
|
|
114
|
+
if outcome:
|
|
115
|
+
intro_para += nodes.Text(" Success means ")
|
|
116
|
+
outcome_text = outcome[0].lower() + outcome[1:] if outcome else ""
|
|
117
|
+
intro_para += nodes.Text(outcome_text + ".")
|
|
118
|
+
|
|
119
|
+
result_nodes.append(intro_para)
|
|
120
|
+
|
|
121
|
+
# Goal paragraph (backward compat for when intent not provided)
|
|
122
|
+
if goal and not intent:
|
|
123
|
+
intro_para = nodes.paragraph()
|
|
124
|
+
intro_para += nodes.Text("The goal of the ")
|
|
125
|
+
intro_para += self.make_persona_link(persona)
|
|
126
|
+
intro_para += nodes.Text(" is to ")
|
|
127
|
+
goal_text = goal[0].lower() + goal[1:] if goal else ""
|
|
128
|
+
intro_para += nodes.Text(goal_text)
|
|
129
|
+
result_nodes.append(intro_para)
|
|
130
|
+
elif goal and intent:
|
|
131
|
+
goal_para = nodes.paragraph()
|
|
132
|
+
goal_para += nodes.Text(goal)
|
|
133
|
+
result_nodes.append(goal_para)
|
|
134
|
+
|
|
135
|
+
# Placeholder for steps (filled in doctree-read)
|
|
136
|
+
steps_placeholder = nodes.container()
|
|
137
|
+
steps_placeholder["classes"].append("journey-steps-placeholder")
|
|
138
|
+
steps_placeholder["journey_slug"] = journey_slug
|
|
139
|
+
result_nodes.append(steps_placeholder)
|
|
140
|
+
|
|
141
|
+
return result_nodes
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class StepStoryDirective(HCDDirective):
|
|
145
|
+
"""Reference a story as a journey step.
|
|
146
|
+
|
|
147
|
+
Usage::
|
|
148
|
+
|
|
149
|
+
.. step-story:: Upload Scheme Documentation
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
required_arguments = 1
|
|
153
|
+
final_argument_whitespace = True
|
|
154
|
+
|
|
155
|
+
def run(self):
|
|
156
|
+
story_title = self.arguments[0]
|
|
157
|
+
docname = self.env.docname
|
|
158
|
+
|
|
159
|
+
# Get current journey and add step
|
|
160
|
+
journey_current = getattr(self.env, "journey_current", {})
|
|
161
|
+
journey_slug = journey_current.get(docname)
|
|
162
|
+
|
|
163
|
+
if journey_slug:
|
|
164
|
+
journey = self.hcd_context.journey_repo.get(journey_slug)
|
|
165
|
+
if journey:
|
|
166
|
+
step = JourneyStep.story(story_title)
|
|
167
|
+
journey.steps.append(step)
|
|
168
|
+
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class StepEpicDirective(HCDDirective):
|
|
173
|
+
"""Reference an epic as a journey step.
|
|
174
|
+
|
|
175
|
+
Usage::
|
|
176
|
+
|
|
177
|
+
.. step-epic:: vocabulary-management
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
required_arguments = 1
|
|
181
|
+
|
|
182
|
+
def run(self):
|
|
183
|
+
epic_slug = self.arguments[0]
|
|
184
|
+
docname = self.env.docname
|
|
185
|
+
|
|
186
|
+
journey_current = getattr(self.env, "journey_current", {})
|
|
187
|
+
journey_slug = journey_current.get(docname)
|
|
188
|
+
|
|
189
|
+
if journey_slug:
|
|
190
|
+
journey = self.hcd_context.journey_repo.get(journey_slug)
|
|
191
|
+
if journey:
|
|
192
|
+
step = JourneyStep.epic(epic_slug)
|
|
193
|
+
journey.steps.append(step)
|
|
194
|
+
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class StepPhaseDirective(HCDDirective):
|
|
199
|
+
"""Optional grouping label for journey steps.
|
|
200
|
+
|
|
201
|
+
Usage::
|
|
202
|
+
|
|
203
|
+
.. step-phase:: Upload Sources
|
|
204
|
+
|
|
205
|
+
Add reference materials to the knowledge base.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
required_arguments = 1
|
|
209
|
+
final_argument_whitespace = True
|
|
210
|
+
has_content = True
|
|
211
|
+
|
|
212
|
+
def run(self):
|
|
213
|
+
phase_title = self.arguments[0]
|
|
214
|
+
docname = self.env.docname
|
|
215
|
+
description = "\n".join(self.content).strip()
|
|
216
|
+
|
|
217
|
+
journey_current = getattr(self.env, "journey_current", {})
|
|
218
|
+
journey_slug = journey_current.get(docname)
|
|
219
|
+
|
|
220
|
+
if journey_slug:
|
|
221
|
+
journey = self.hcd_context.journey_repo.get(journey_slug)
|
|
222
|
+
if journey:
|
|
223
|
+
step = JourneyStep.phase(phase_title, description)
|
|
224
|
+
journey.steps.append(step)
|
|
225
|
+
|
|
226
|
+
return []
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class JourneyIndexDirective(HCDDirective):
|
|
230
|
+
"""Render index of all journeys.
|
|
231
|
+
|
|
232
|
+
Usage::
|
|
233
|
+
|
|
234
|
+
.. journey-index::
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
def run(self):
|
|
238
|
+
all_journeys = self.hcd_context.journey_repo.list_all()
|
|
239
|
+
|
|
240
|
+
if not all_journeys:
|
|
241
|
+
return self.empty_result("No journeys defined")
|
|
242
|
+
|
|
243
|
+
bullet_list = nodes.bullet_list()
|
|
244
|
+
|
|
245
|
+
for journey in sorted(all_journeys, key=lambda j: j.slug):
|
|
246
|
+
item = nodes.list_item()
|
|
247
|
+
para = nodes.paragraph()
|
|
248
|
+
|
|
249
|
+
# Link to journey
|
|
250
|
+
journey_path = f"{journey.slug}.html"
|
|
251
|
+
journey_ref = nodes.reference("", "", refuri=journey_path)
|
|
252
|
+
journey_ref += nodes.strong(text=journey.slug.replace("-", " ").title())
|
|
253
|
+
para += journey_ref
|
|
254
|
+
|
|
255
|
+
# Persona in parentheses
|
|
256
|
+
if journey.persona:
|
|
257
|
+
para += nodes.Text(f" ({journey.persona})")
|
|
258
|
+
|
|
259
|
+
item += para
|
|
260
|
+
|
|
261
|
+
# Intent as sub-paragraph
|
|
262
|
+
display_text = journey.intent or journey.goal or ""
|
|
263
|
+
if display_text:
|
|
264
|
+
desc_para = nodes.paragraph()
|
|
265
|
+
if len(display_text) > 100:
|
|
266
|
+
display_text = display_text[:100] + "..."
|
|
267
|
+
desc_para += nodes.Text(display_text)
|
|
268
|
+
item += desc_para
|
|
269
|
+
|
|
270
|
+
bullet_list += item
|
|
271
|
+
|
|
272
|
+
return [bullet_list]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class JourneyDependencyGraphDirective(HCDDirective):
|
|
276
|
+
"""Generate a PlantUML dependency graph from journey dependencies.
|
|
277
|
+
|
|
278
|
+
Usage::
|
|
279
|
+
|
|
280
|
+
.. journey-dependency-graph::
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
def run(self):
|
|
284
|
+
# Return placeholder - rendered in doctree-resolved
|
|
285
|
+
node = JourneyDependencyGraphPlaceholder()
|
|
286
|
+
return [node]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class JourneysForPersonaDirective(HCDDirective):
|
|
290
|
+
"""List journeys for a specific persona.
|
|
291
|
+
|
|
292
|
+
Usage::
|
|
293
|
+
|
|
294
|
+
.. journeys-for-persona:: Analyst
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
required_arguments = 1
|
|
298
|
+
final_argument_whitespace = True
|
|
299
|
+
|
|
300
|
+
def run(self):
|
|
301
|
+
persona_arg = self.arguments[0]
|
|
302
|
+
persona_normalized = normalize_name(persona_arg)
|
|
303
|
+
|
|
304
|
+
all_journeys = self.hcd_context.journey_repo.list_all()
|
|
305
|
+
|
|
306
|
+
# Find journeys for this persona
|
|
307
|
+
journeys = [
|
|
308
|
+
j for j in all_journeys if normalize_name(j.persona) == persona_normalized
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
if not journeys:
|
|
312
|
+
return self.empty_result(f"No journeys found for persona '{persona_arg}'")
|
|
313
|
+
|
|
314
|
+
bullet_list = nodes.bullet_list()
|
|
315
|
+
|
|
316
|
+
for journey in sorted(journeys, key=lambda j: j.slug):
|
|
317
|
+
item = nodes.list_item()
|
|
318
|
+
para = nodes.paragraph()
|
|
319
|
+
para += self.make_journey_link(journey.slug)
|
|
320
|
+
item += para
|
|
321
|
+
bullet_list += item
|
|
322
|
+
|
|
323
|
+
return [bullet_list]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def build_story_node(story_title: str, docname: str, hcd_context):
|
|
327
|
+
"""Build a paragraph node for a story reference."""
|
|
328
|
+
from ...config import get_config
|
|
329
|
+
|
|
330
|
+
config = get_config()
|
|
331
|
+
all_stories = hcd_context.story_repo.list_all()
|
|
332
|
+
all_apps = hcd_context.app_repo.list_all()
|
|
333
|
+
known_apps = {normalize_name(a.name) for a in all_apps}
|
|
334
|
+
prefix = path_to_root(docname)
|
|
335
|
+
|
|
336
|
+
# Find the story
|
|
337
|
+
story_normalized = normalize_name(story_title)
|
|
338
|
+
story = None
|
|
339
|
+
for s in all_stories:
|
|
340
|
+
if normalize_name(s.feature_title) == story_normalized:
|
|
341
|
+
story = s
|
|
342
|
+
break
|
|
343
|
+
|
|
344
|
+
para = nodes.paragraph()
|
|
345
|
+
|
|
346
|
+
if story:
|
|
347
|
+
# Story link
|
|
348
|
+
story_doc = f"{config.get_doc_path('stories')}/{story.app_slug}"
|
|
349
|
+
story_ref_uri = _build_relative_uri(docname, story_doc, story.slug)
|
|
350
|
+
story_ref = nodes.reference("", "", refuri=story_ref_uri)
|
|
351
|
+
story_ref += nodes.Text(story.feature_title)
|
|
352
|
+
para += story_ref
|
|
353
|
+
|
|
354
|
+
# App in parentheses
|
|
355
|
+
para += nodes.Text(" (")
|
|
356
|
+
app_path = (
|
|
357
|
+
f"{prefix}{config.get_doc_path('applications')}/{story.app_slug}.html"
|
|
358
|
+
)
|
|
359
|
+
app_valid = story.app_normalized in known_apps
|
|
360
|
+
|
|
361
|
+
if app_valid:
|
|
362
|
+
app_ref = nodes.reference("", "", refuri=app_path)
|
|
363
|
+
app_ref += nodes.Text(story.app_slug.replace("-", " ").title())
|
|
364
|
+
para += app_ref
|
|
365
|
+
else:
|
|
366
|
+
para += nodes.Text(story.app_slug.replace("-", " ").title())
|
|
367
|
+
para += nodes.Text(")")
|
|
368
|
+
else:
|
|
369
|
+
para += nodes.problematic(text=f"{story_title} [not found]")
|
|
370
|
+
|
|
371
|
+
return para
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def build_epic_node(epic_slug: str, docname: str):
|
|
375
|
+
"""Build a paragraph node for an epic reference."""
|
|
376
|
+
from ...config import get_config
|
|
377
|
+
|
|
378
|
+
config = get_config()
|
|
379
|
+
prefix = path_to_root(docname)
|
|
380
|
+
epic_path = f"{prefix}{config.get_doc_path('epics')}/{epic_slug}.html"
|
|
381
|
+
|
|
382
|
+
para = nodes.paragraph()
|
|
383
|
+
epic_ref = nodes.reference("", "", refuri=epic_path)
|
|
384
|
+
epic_ref += nodes.Text(epic_slug.replace("-", " ").title())
|
|
385
|
+
para += epic_ref
|
|
386
|
+
para += nodes.Text(" (epic)")
|
|
387
|
+
|
|
388
|
+
return para
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _build_relative_uri(from_docname: str, target_doc: str, anchor: str = None) -> str:
|
|
392
|
+
"""Build a relative URI from one doc to another."""
|
|
393
|
+
from_parts = from_docname.split("/")
|
|
394
|
+
target_parts = target_doc.split("/")
|
|
395
|
+
|
|
396
|
+
common = 0
|
|
397
|
+
for i in range(min(len(from_parts), len(target_parts))):
|
|
398
|
+
if from_parts[i] == target_parts[i]:
|
|
399
|
+
common += 1
|
|
400
|
+
else:
|
|
401
|
+
break
|
|
402
|
+
|
|
403
|
+
up_levels = len(from_parts) - common - 1
|
|
404
|
+
down_path = "/".join(target_parts[common:])
|
|
405
|
+
|
|
406
|
+
if up_levels > 0:
|
|
407
|
+
rel_path = "../" * up_levels + down_path + ".html"
|
|
408
|
+
else:
|
|
409
|
+
rel_path = down_path + ".html"
|
|
410
|
+
|
|
411
|
+
if anchor:
|
|
412
|
+
return f"{rel_path}#{anchor}"
|
|
413
|
+
return rel_path
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def render_journey_steps(journey: Journey, docname: str, hcd_context):
|
|
417
|
+
"""Render journey steps as a numbered list with phases grouping stories."""
|
|
418
|
+
steps = journey.steps
|
|
419
|
+
if not steps:
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
# Group steps by phase
|
|
423
|
+
phases = []
|
|
424
|
+
current_phase = None
|
|
425
|
+
|
|
426
|
+
for step in steps:
|
|
427
|
+
if step.step_type.value == "phase":
|
|
428
|
+
current_phase = {
|
|
429
|
+
"title": step.ref,
|
|
430
|
+
"description": step.description,
|
|
431
|
+
"items": [],
|
|
432
|
+
}
|
|
433
|
+
phases.append(current_phase)
|
|
434
|
+
elif step.step_type.value in ("story", "epic"):
|
|
435
|
+
if current_phase is None:
|
|
436
|
+
current_phase = {"title": None, "description": "", "items": []}
|
|
437
|
+
phases.append(current_phase)
|
|
438
|
+
current_phase["items"].append(step)
|
|
439
|
+
|
|
440
|
+
# Build enumerated list
|
|
441
|
+
enum_list = nodes.enumerated_list()
|
|
442
|
+
enum_list["enumtype"] = "arabic"
|
|
443
|
+
|
|
444
|
+
for phase in phases:
|
|
445
|
+
list_item = nodes.list_item()
|
|
446
|
+
|
|
447
|
+
if phase["title"]:
|
|
448
|
+
header_para = nodes.paragraph()
|
|
449
|
+
header_para += nodes.strong(text=phase["title"])
|
|
450
|
+
if phase["description"]:
|
|
451
|
+
header_para += nodes.Text(" — ")
|
|
452
|
+
header_para += nodes.Text(phase["description"])
|
|
453
|
+
list_item += header_para
|
|
454
|
+
|
|
455
|
+
if phase["items"]:
|
|
456
|
+
bullet_list = nodes.bullet_list()
|
|
457
|
+
for item in phase["items"]:
|
|
458
|
+
bullet_item = nodes.list_item()
|
|
459
|
+
if item.step_type.value == "story":
|
|
460
|
+
bullet_item += build_story_node(item.ref, docname, hcd_context)
|
|
461
|
+
elif item.step_type.value == "epic":
|
|
462
|
+
bullet_item += build_epic_node(item.ref, docname)
|
|
463
|
+
bullet_list += bullet_item
|
|
464
|
+
list_item += bullet_list
|
|
465
|
+
|
|
466
|
+
enum_list += list_item
|
|
467
|
+
|
|
468
|
+
return enum_list
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def make_labelled_list(
|
|
472
|
+
term: str, items: list, hcd_context, docname: str = None, item_type: str = "text"
|
|
473
|
+
):
|
|
474
|
+
"""Create a labelled bullet list with term as heading."""
|
|
475
|
+
container = nodes.container()
|
|
476
|
+
|
|
477
|
+
term_para = nodes.paragraph()
|
|
478
|
+
term_para += nodes.strong(text=term)
|
|
479
|
+
container += term_para
|
|
480
|
+
|
|
481
|
+
bullet_list = nodes.bullet_list()
|
|
482
|
+
all_journeys = hcd_context.journey_repo.list_all()
|
|
483
|
+
journey_slugs = {j.slug for j in all_journeys}
|
|
484
|
+
|
|
485
|
+
for item in items:
|
|
486
|
+
list_item = nodes.list_item()
|
|
487
|
+
inline = nodes.inline()
|
|
488
|
+
|
|
489
|
+
if item_type == "journey":
|
|
490
|
+
related_slug = item
|
|
491
|
+
related_path = f"{related_slug}.html"
|
|
492
|
+
if related_slug in journey_slugs:
|
|
493
|
+
ref = nodes.reference("", "", refuri=related_path)
|
|
494
|
+
ref += nodes.Text(related_slug.replace("-", " ").title())
|
|
495
|
+
inline += ref
|
|
496
|
+
else:
|
|
497
|
+
inline += nodes.Text(related_slug.replace("-", " ").title())
|
|
498
|
+
inline += nodes.emphasis(text=" [not found]")
|
|
499
|
+
else:
|
|
500
|
+
inline += nodes.Text(item)
|
|
501
|
+
|
|
502
|
+
list_item += inline
|
|
503
|
+
bullet_list += list_item
|
|
504
|
+
|
|
505
|
+
container += bullet_list
|
|
506
|
+
return container
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def clear_journey_state(app, env, docname):
|
|
510
|
+
"""Clear journey state when a document is re-read."""
|
|
511
|
+
from ..context import get_hcd_context
|
|
512
|
+
|
|
513
|
+
# Clear current journey tracker
|
|
514
|
+
if hasattr(env, "journey_current") and docname in env.journey_current:
|
|
515
|
+
del env.journey_current[docname]
|
|
516
|
+
|
|
517
|
+
# Clear journeys from this document via repository
|
|
518
|
+
hcd_context = get_hcd_context(app)
|
|
519
|
+
hcd_context.journey_repo.run_async(
|
|
520
|
+
hcd_context.journey_repo.async_repo.clear_by_docname(docname)
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def process_journey_steps(app, doctree):
|
|
525
|
+
"""Replace journey steps placeholder with rendered steps."""
|
|
526
|
+
from ..context import get_hcd_context
|
|
527
|
+
|
|
528
|
+
env = app.env
|
|
529
|
+
docname = env.docname
|
|
530
|
+
hcd_context = get_hcd_context(app)
|
|
531
|
+
journey_current = getattr(env, "journey_current", {})
|
|
532
|
+
|
|
533
|
+
journey_slug = journey_current.get(docname)
|
|
534
|
+
if not journey_slug:
|
|
535
|
+
return
|
|
536
|
+
|
|
537
|
+
journey = hcd_context.journey_repo.get(journey_slug)
|
|
538
|
+
if not journey:
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
# Replace steps placeholder
|
|
542
|
+
for node in doctree.traverse(nodes.container):
|
|
543
|
+
if "journey-steps-placeholder" in node.get("classes", []):
|
|
544
|
+
steps_node = render_journey_steps(journey, docname, hcd_context)
|
|
545
|
+
if steps_node:
|
|
546
|
+
node.replace_self(steps_node)
|
|
547
|
+
else:
|
|
548
|
+
node.replace_self([])
|
|
549
|
+
break
|
|
550
|
+
|
|
551
|
+
# Add preconditions
|
|
552
|
+
if journey.preconditions:
|
|
553
|
+
doctree += make_labelled_list(
|
|
554
|
+
"Preconditions", journey.preconditions, hcd_context
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Add postconditions
|
|
558
|
+
if journey.postconditions:
|
|
559
|
+
doctree += make_labelled_list(
|
|
560
|
+
"Postconditions", journey.postconditions, hcd_context
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Add depends-on
|
|
564
|
+
if journey.depends_on:
|
|
565
|
+
doctree += make_labelled_list(
|
|
566
|
+
"Depends On", journey.depends_on, hcd_context, docname, item_type="journey"
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Add depended-on-by (inferred)
|
|
570
|
+
all_journeys = hcd_context.journey_repo.list_all()
|
|
571
|
+
depended_on_by = [j.slug for j in all_journeys if journey_slug in j.depends_on]
|
|
572
|
+
if depended_on_by:
|
|
573
|
+
doctree += make_labelled_list(
|
|
574
|
+
"Depended On By",
|
|
575
|
+
sorted(depended_on_by),
|
|
576
|
+
hcd_context,
|
|
577
|
+
docname,
|
|
578
|
+
item_type="journey",
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def build_dependency_graph_node(env, hcd_context):
|
|
583
|
+
"""Build the PlantUML node for the journey dependency graph."""
|
|
584
|
+
try:
|
|
585
|
+
from sphinxcontrib.plantuml import plantuml
|
|
586
|
+
except ImportError:
|
|
587
|
+
para = nodes.paragraph()
|
|
588
|
+
para += nodes.emphasis(text="PlantUML extension not available")
|
|
589
|
+
return para
|
|
590
|
+
|
|
591
|
+
all_journeys = hcd_context.journey_repo.list_all()
|
|
592
|
+
|
|
593
|
+
if not all_journeys:
|
|
594
|
+
para = nodes.paragraph()
|
|
595
|
+
para += nodes.emphasis(text="No journeys defined")
|
|
596
|
+
return para
|
|
597
|
+
|
|
598
|
+
# Build PlantUML content
|
|
599
|
+
lines = [
|
|
600
|
+
"@startuml",
|
|
601
|
+
"skinparam componentStyle rectangle",
|
|
602
|
+
"skinparam defaultTextAlignment center",
|
|
603
|
+
"",
|
|
604
|
+
]
|
|
605
|
+
|
|
606
|
+
journey_slugs = {j.slug for j in all_journeys}
|
|
607
|
+
|
|
608
|
+
for journey in sorted(all_journeys, key=lambda j: j.slug):
|
|
609
|
+
title = journey.slug.replace("-", " ").title()
|
|
610
|
+
lines.append(f"[{title}] as {journey.slug.replace('-', '_')}")
|
|
611
|
+
|
|
612
|
+
lines.append("")
|
|
613
|
+
|
|
614
|
+
for journey in sorted(all_journeys, key=lambda j: j.slug):
|
|
615
|
+
for dep in journey.depends_on:
|
|
616
|
+
if dep in journey_slugs:
|
|
617
|
+
from_id = journey.slug.replace("-", "_")
|
|
618
|
+
to_id = dep.replace("-", "_")
|
|
619
|
+
lines.append(f"{from_id} --> {to_id}")
|
|
620
|
+
|
|
621
|
+
lines.append("")
|
|
622
|
+
lines.append("@enduml")
|
|
623
|
+
|
|
624
|
+
puml_content = "\n".join(lines)
|
|
625
|
+
|
|
626
|
+
puml_node = plantuml(puml_content)
|
|
627
|
+
puml_node["uml"] = puml_content
|
|
628
|
+
puml_node["incdir"] = ""
|
|
629
|
+
puml_node["filename"] = "journey-dependency-graph"
|
|
630
|
+
|
|
631
|
+
return puml_node
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def process_dependency_graph_placeholder(app, doctree, docname):
|
|
635
|
+
"""Replace dependency graph placeholder with actual PlantUML node."""
|
|
636
|
+
from ..context import get_hcd_context
|
|
637
|
+
|
|
638
|
+
hcd_context = get_hcd_context(app)
|
|
639
|
+
|
|
640
|
+
for node in doctree.traverse(JourneyDependencyGraphPlaceholder):
|
|
641
|
+
puml_node = build_dependency_graph_node(app.env, hcd_context)
|
|
642
|
+
node.replace_self(puml_node)
|