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
julee/docs/sphinx_hcd/apps.py
DELETED
|
@@ -1,518 +0,0 @@
|
|
|
1
|
-
"""Sphinx extension for applications.
|
|
2
|
-
|
|
3
|
-
Scans apps/*/app.yaml for canonical app definitions and provides directives
|
|
4
|
-
to render app information with derived data (personas, journeys, epics, stories).
|
|
5
|
-
|
|
6
|
-
Provides directives:
|
|
7
|
-
- define-app: Render app info from YAML + derived data
|
|
8
|
-
- app-index: Generate index tables grouped by type
|
|
9
|
-
- apps-for-persona: List apps for a persona
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import yaml
|
|
13
|
-
from docutils import nodes
|
|
14
|
-
from sphinx.util import logging
|
|
15
|
-
from sphinx.util.docutils import SphinxDirective
|
|
16
|
-
|
|
17
|
-
from .config import get_config
|
|
18
|
-
from .utils import normalize_name, path_to_root, slugify
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
# Global registry populated at build init
|
|
23
|
-
_app_registry: dict = {}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def get_app_registry() -> dict:
|
|
27
|
-
"""Get the app registry."""
|
|
28
|
-
return _app_registry
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def get_documented_apps(env) -> set:
|
|
32
|
-
"""Get documented apps set from env, creating if needed."""
|
|
33
|
-
if not hasattr(env, "documented_apps"):
|
|
34
|
-
env.documented_apps = set()
|
|
35
|
-
return env.documented_apps
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def scan_app_manifests(app):
|
|
39
|
-
"""Scan apps/*/app.yaml and build the app registry."""
|
|
40
|
-
global _app_registry
|
|
41
|
-
_app_registry = {}
|
|
42
|
-
|
|
43
|
-
config = get_config()
|
|
44
|
-
apps_dir = config.get_path("app_manifests")
|
|
45
|
-
|
|
46
|
-
if not apps_dir.exists():
|
|
47
|
-
logger.info(
|
|
48
|
-
f"apps/ directory not found at {apps_dir} - no app manifests to index"
|
|
49
|
-
)
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
# Scan for app.yaml files
|
|
53
|
-
for app_dir in apps_dir.iterdir():
|
|
54
|
-
if not app_dir.is_dir():
|
|
55
|
-
continue
|
|
56
|
-
|
|
57
|
-
manifest_path = app_dir / "app.yaml"
|
|
58
|
-
if not manifest_path.exists():
|
|
59
|
-
continue
|
|
60
|
-
|
|
61
|
-
app_slug = app_dir.name
|
|
62
|
-
|
|
63
|
-
try:
|
|
64
|
-
with open(manifest_path) as f:
|
|
65
|
-
manifest = yaml.safe_load(f)
|
|
66
|
-
except Exception as e:
|
|
67
|
-
logger.warning(f"Could not read {manifest_path}: {e}")
|
|
68
|
-
continue
|
|
69
|
-
|
|
70
|
-
_app_registry[app_slug] = {
|
|
71
|
-
"slug": app_slug,
|
|
72
|
-
"name": manifest.get("name", app_slug.replace("-", " ").title()),
|
|
73
|
-
"type": manifest.get("type", "unknown"),
|
|
74
|
-
"status": manifest.get("status"),
|
|
75
|
-
"description": manifest.get("description", "").strip(),
|
|
76
|
-
"accelerators": manifest.get("accelerators", []),
|
|
77
|
-
"manifest_path": str(manifest_path),
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
logger.info(f"Indexed {len(_app_registry)} apps from manifests")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def validate_apps(app, env):
|
|
84
|
-
"""Validate app coverage after all documents are read."""
|
|
85
|
-
from . import stories
|
|
86
|
-
|
|
87
|
-
documented_apps = get_documented_apps(env)
|
|
88
|
-
_apps_with_stories = stories.get_apps_with_stories()
|
|
89
|
-
|
|
90
|
-
# Check for apps without documentation
|
|
91
|
-
for app_slug in _app_registry:
|
|
92
|
-
if app_slug not in documented_apps:
|
|
93
|
-
logger.warning(
|
|
94
|
-
f"App '{app_slug}' from apps/{app_slug}/app.yaml has no docs page. "
|
|
95
|
-
f"Create applications/{app_slug}.rst with '.. define-app:: {app_slug}' "
|
|
96
|
-
f"(or run 'make clean html' if the file exists)"
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# Check for apps without stories
|
|
100
|
-
for app_slug in _app_registry:
|
|
101
|
-
if app_slug not in _apps_with_stories:
|
|
102
|
-
logger.info(
|
|
103
|
-
f"App '{app_slug}' has no stories yet "
|
|
104
|
-
f"(add .feature files to tests/e2e/{app_slug}/features/)"
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
# Check for documented apps without manifests
|
|
108
|
-
for app_slug in documented_apps:
|
|
109
|
-
if app_slug not in _app_registry:
|
|
110
|
-
logger.warning(
|
|
111
|
-
f"App '{app_slug}' documented but has no manifest. "
|
|
112
|
-
f"Create apps/{app_slug}/app.yaml"
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def get_personas_for_app(app_slug: str, story_registry: list) -> list[str]:
|
|
117
|
-
"""Get personas that have stories for this app."""
|
|
118
|
-
personas = set()
|
|
119
|
-
app_normalized = normalize_name(app_slug)
|
|
120
|
-
for story in story_registry:
|
|
121
|
-
if story["app_normalized"] == app_normalized:
|
|
122
|
-
personas.add(story["persona"])
|
|
123
|
-
return sorted(personas)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def get_journeys_for_app(
|
|
127
|
-
app_slug: str, story_registry: list, journey_registry: dict
|
|
128
|
-
) -> list[str]:
|
|
129
|
-
"""Get journeys that include stories from this app."""
|
|
130
|
-
# Get story titles for this app
|
|
131
|
-
app_normalized = normalize_name(app_slug)
|
|
132
|
-
app_story_titles = {
|
|
133
|
-
normalize_name(s["feature"])
|
|
134
|
-
for s in story_registry
|
|
135
|
-
if s["app_normalized"] == app_normalized
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
# Find journeys that reference these stories
|
|
139
|
-
journeys = []
|
|
140
|
-
for slug, journey in journey_registry.items():
|
|
141
|
-
for step in journey.get("steps", []):
|
|
142
|
-
if step.get("type") == "story":
|
|
143
|
-
if normalize_name(step["ref"]) in app_story_titles:
|
|
144
|
-
journeys.append(slug)
|
|
145
|
-
break
|
|
146
|
-
|
|
147
|
-
return sorted(set(journeys))
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def get_epics_for_app(
|
|
151
|
-
app_slug: str, story_registry: list, epic_registry: dict
|
|
152
|
-
) -> list[str]:
|
|
153
|
-
"""Get epics that include stories from this app."""
|
|
154
|
-
# Get story titles for this app
|
|
155
|
-
app_normalized = normalize_name(app_slug)
|
|
156
|
-
app_story_titles = {
|
|
157
|
-
normalize_name(s["feature"])
|
|
158
|
-
for s in story_registry
|
|
159
|
-
if s["app_normalized"] == app_normalized
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
# Find epics that reference these stories
|
|
163
|
-
epics = []
|
|
164
|
-
for slug, epic in epic_registry.items():
|
|
165
|
-
for story_title in epic.get("stories", []):
|
|
166
|
-
if normalize_name(story_title) in app_story_titles:
|
|
167
|
-
epics.append(slug)
|
|
168
|
-
break
|
|
169
|
-
|
|
170
|
-
return sorted(set(epics))
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class DefineAppDirective(SphinxDirective):
|
|
174
|
-
"""Render app info from YAML manifest plus derived data.
|
|
175
|
-
|
|
176
|
-
Usage::
|
|
177
|
-
|
|
178
|
-
.. define-app:: credential-tool
|
|
179
|
-
"""
|
|
180
|
-
|
|
181
|
-
required_arguments = 1
|
|
182
|
-
|
|
183
|
-
def run(self):
|
|
184
|
-
app_slug = self.arguments[0]
|
|
185
|
-
|
|
186
|
-
# Register that this app is documented (env-based for incremental builds)
|
|
187
|
-
get_documented_apps(self.env).add(app_slug)
|
|
188
|
-
|
|
189
|
-
# Return placeholder - actual rendering in doctree-resolved
|
|
190
|
-
node = DefineAppPlaceholder()
|
|
191
|
-
node["app_slug"] = app_slug
|
|
192
|
-
return [node]
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
class DefineAppPlaceholder(nodes.General, nodes.Element):
|
|
196
|
-
"""Placeholder node for define-app, replaced at doctree-resolved."""
|
|
197
|
-
|
|
198
|
-
pass
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
class AppIndexDirective(SphinxDirective):
|
|
202
|
-
"""Generate index tables grouped by app type.
|
|
203
|
-
|
|
204
|
-
Usage::
|
|
205
|
-
|
|
206
|
-
.. app-index::
|
|
207
|
-
"""
|
|
208
|
-
|
|
209
|
-
def run(self):
|
|
210
|
-
node = AppIndexPlaceholder()
|
|
211
|
-
return [node]
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
class AppIndexPlaceholder(nodes.General, nodes.Element):
|
|
215
|
-
"""Placeholder node for app-index, replaced at doctree-resolved."""
|
|
216
|
-
|
|
217
|
-
pass
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
class AppsForPersonaDirective(SphinxDirective):
|
|
221
|
-
"""List apps for a specific persona.
|
|
222
|
-
|
|
223
|
-
Usage::
|
|
224
|
-
|
|
225
|
-
.. apps-for-persona:: Member Implementer
|
|
226
|
-
"""
|
|
227
|
-
|
|
228
|
-
required_arguments = 1
|
|
229
|
-
final_argument_whitespace = True
|
|
230
|
-
|
|
231
|
-
def run(self):
|
|
232
|
-
node = AppsForPersonaPlaceholder()
|
|
233
|
-
node["persona"] = self.arguments[0]
|
|
234
|
-
return [node]
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
class AppsForPersonaPlaceholder(nodes.General, nodes.Element):
|
|
238
|
-
"""Placeholder node for apps-for-persona, replaced at doctree-resolved."""
|
|
239
|
-
|
|
240
|
-
pass
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def build_app_content(
|
|
244
|
-
app_slug: str,
|
|
245
|
-
docname: str,
|
|
246
|
-
story_registry: list,
|
|
247
|
-
journey_registry: dict,
|
|
248
|
-
epic_registry: dict,
|
|
249
|
-
known_personas: set,
|
|
250
|
-
):
|
|
251
|
-
"""Build the content nodes for an app."""
|
|
252
|
-
from sphinx.addnodes import seealso
|
|
253
|
-
|
|
254
|
-
config = get_config()
|
|
255
|
-
|
|
256
|
-
if app_slug not in _app_registry:
|
|
257
|
-
para = nodes.paragraph()
|
|
258
|
-
para += nodes.problematic(text=f"App '{app_slug}' not found in apps/")
|
|
259
|
-
return [para]
|
|
260
|
-
|
|
261
|
-
app_data = _app_registry[app_slug]
|
|
262
|
-
result_nodes = []
|
|
263
|
-
|
|
264
|
-
prefix = path_to_root(docname)
|
|
265
|
-
|
|
266
|
-
# Description first
|
|
267
|
-
if app_data["description"]:
|
|
268
|
-
desc_para = nodes.paragraph()
|
|
269
|
-
desc_para += nodes.Text(app_data["description"])
|
|
270
|
-
result_nodes.append(desc_para)
|
|
271
|
-
|
|
272
|
-
# Stories count and link
|
|
273
|
-
app_stories = [
|
|
274
|
-
s for s in story_registry if s["app_normalized"] == normalize_name(app_slug)
|
|
275
|
-
]
|
|
276
|
-
|
|
277
|
-
if app_stories:
|
|
278
|
-
story_count = len(app_stories)
|
|
279
|
-
stories_para = nodes.paragraph()
|
|
280
|
-
stories_para += nodes.Text(f"The {app_data['name']} has ")
|
|
281
|
-
story_path = f"{prefix}{config.get_doc_path('stories')}/{app_slug}.html"
|
|
282
|
-
ref = nodes.reference("", "", refuri=story_path)
|
|
283
|
-
ref += nodes.Text(f"{story_count} stories")
|
|
284
|
-
stories_para += ref
|
|
285
|
-
stories_para += nodes.Text(".")
|
|
286
|
-
result_nodes.append(stories_para)
|
|
287
|
-
|
|
288
|
-
# Build seealso box with metadata
|
|
289
|
-
seealso_node = seealso()
|
|
290
|
-
|
|
291
|
-
# Type
|
|
292
|
-
type_labels = {
|
|
293
|
-
"staff": "Staff Application",
|
|
294
|
-
"external": "External Application",
|
|
295
|
-
"member-tool": "Member Tool",
|
|
296
|
-
}
|
|
297
|
-
type_para = nodes.paragraph()
|
|
298
|
-
type_para += nodes.strong(text="Type: ")
|
|
299
|
-
type_para += nodes.Text(type_labels.get(app_data["type"], app_data["type"]))
|
|
300
|
-
seealso_node += type_para
|
|
301
|
-
|
|
302
|
-
# Status (if present)
|
|
303
|
-
if app_data["status"]:
|
|
304
|
-
status_para = nodes.paragraph()
|
|
305
|
-
status_para += nodes.strong(text="Status: ")
|
|
306
|
-
status_para += nodes.Text(app_data["status"])
|
|
307
|
-
seealso_node += status_para
|
|
308
|
-
|
|
309
|
-
# Personas (derived from stories)
|
|
310
|
-
personas = get_personas_for_app(app_slug, story_registry)
|
|
311
|
-
if personas:
|
|
312
|
-
persona_para = nodes.paragraph()
|
|
313
|
-
persona_para += nodes.strong(text="Personas: ")
|
|
314
|
-
for i, persona in enumerate(personas):
|
|
315
|
-
persona_slug = slugify(persona)
|
|
316
|
-
persona_path = (
|
|
317
|
-
f"{prefix}{config.get_doc_path('personas')}/{persona_slug}.html"
|
|
318
|
-
)
|
|
319
|
-
persona_normalized = normalize_name(persona)
|
|
320
|
-
|
|
321
|
-
if persona_normalized in known_personas:
|
|
322
|
-
ref = nodes.reference("", "", refuri=persona_path)
|
|
323
|
-
ref += nodes.Text(persona)
|
|
324
|
-
persona_para += ref
|
|
325
|
-
else:
|
|
326
|
-
persona_para += nodes.Text(persona)
|
|
327
|
-
|
|
328
|
-
if i < len(personas) - 1:
|
|
329
|
-
persona_para += nodes.Text(", ")
|
|
330
|
-
|
|
331
|
-
seealso_node += persona_para
|
|
332
|
-
|
|
333
|
-
# Related Journeys
|
|
334
|
-
journeys = get_journeys_for_app(app_slug, story_registry, journey_registry)
|
|
335
|
-
if journeys:
|
|
336
|
-
journey_para = nodes.paragraph()
|
|
337
|
-
journey_para += nodes.strong(text="Journeys: ")
|
|
338
|
-
for i, journey_slug in enumerate(journeys):
|
|
339
|
-
journey_path = (
|
|
340
|
-
f"{prefix}{config.get_doc_path('journeys')}/{journey_slug}.html"
|
|
341
|
-
)
|
|
342
|
-
ref = nodes.reference("", "", refuri=journey_path)
|
|
343
|
-
ref += nodes.Text(journey_slug.replace("-", " ").title())
|
|
344
|
-
journey_para += ref
|
|
345
|
-
if i < len(journeys) - 1:
|
|
346
|
-
journey_para += nodes.Text(", ")
|
|
347
|
-
seealso_node += journey_para
|
|
348
|
-
|
|
349
|
-
# Related Epics
|
|
350
|
-
epics = get_epics_for_app(app_slug, story_registry, epic_registry)
|
|
351
|
-
if epics:
|
|
352
|
-
epic_para = nodes.paragraph()
|
|
353
|
-
epic_para += nodes.strong(text="Epics: ")
|
|
354
|
-
for i, epic_slug in enumerate(epics):
|
|
355
|
-
epic_path = f"{prefix}{config.get_doc_path('epics')}/{epic_slug}.html"
|
|
356
|
-
ref = nodes.reference("", "", refuri=epic_path)
|
|
357
|
-
ref += nodes.Text(epic_slug.replace("-", " ").title())
|
|
358
|
-
epic_para += ref
|
|
359
|
-
if i < len(epics) - 1:
|
|
360
|
-
epic_para += nodes.Text(", ")
|
|
361
|
-
seealso_node += epic_para
|
|
362
|
-
|
|
363
|
-
result_nodes.append(seealso_node)
|
|
364
|
-
|
|
365
|
-
return result_nodes
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
def build_app_index(docname: str, story_registry: list):
|
|
369
|
-
"""Build the app index grouped by type."""
|
|
370
|
-
if not _app_registry:
|
|
371
|
-
para = nodes.paragraph()
|
|
372
|
-
para += nodes.emphasis(text="No apps defined")
|
|
373
|
-
return [para]
|
|
374
|
-
|
|
375
|
-
# Group apps by type
|
|
376
|
-
by_type = {"staff": [], "external": [], "member-tool": []}
|
|
377
|
-
for slug, app_data in _app_registry.items():
|
|
378
|
-
app_type = app_data["type"]
|
|
379
|
-
if app_type in by_type:
|
|
380
|
-
by_type[app_type].append((slug, app_data))
|
|
381
|
-
else:
|
|
382
|
-
by_type.setdefault(app_type, []).append((slug, app_data))
|
|
383
|
-
|
|
384
|
-
result_nodes = []
|
|
385
|
-
|
|
386
|
-
type_sections = [
|
|
387
|
-
("staff", "Staff Applications"),
|
|
388
|
-
("external", "External Applications"),
|
|
389
|
-
("member-tool", "Member Tools"),
|
|
390
|
-
]
|
|
391
|
-
|
|
392
|
-
for type_key, type_label in type_sections:
|
|
393
|
-
apps = by_type.get(type_key, [])
|
|
394
|
-
if not apps:
|
|
395
|
-
continue
|
|
396
|
-
|
|
397
|
-
# Section heading
|
|
398
|
-
heading = nodes.paragraph()
|
|
399
|
-
heading += nodes.strong(text=type_label)
|
|
400
|
-
result_nodes.append(heading)
|
|
401
|
-
|
|
402
|
-
# App list
|
|
403
|
-
app_list = nodes.bullet_list()
|
|
404
|
-
|
|
405
|
-
for slug, app_data in sorted(apps, key=lambda x: x[1]["name"]):
|
|
406
|
-
item = nodes.list_item()
|
|
407
|
-
para = nodes.paragraph()
|
|
408
|
-
|
|
409
|
-
# Link to app
|
|
410
|
-
app_path = f"{slug}.html"
|
|
411
|
-
ref = nodes.reference("", "", refuri=app_path)
|
|
412
|
-
ref += nodes.Text(app_data["name"])
|
|
413
|
-
para += ref
|
|
414
|
-
|
|
415
|
-
# Personas
|
|
416
|
-
personas = get_personas_for_app(slug, story_registry)
|
|
417
|
-
if personas:
|
|
418
|
-
para += nodes.Text(f" ({', '.join(personas)})")
|
|
419
|
-
|
|
420
|
-
item += para
|
|
421
|
-
app_list += item
|
|
422
|
-
|
|
423
|
-
result_nodes.append(app_list)
|
|
424
|
-
|
|
425
|
-
return result_nodes
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
def build_apps_for_persona(docname: str, persona_arg: str, story_registry: list):
|
|
429
|
-
"""Build list of apps for a persona."""
|
|
430
|
-
config = get_config()
|
|
431
|
-
persona_normalized = normalize_name(persona_arg)
|
|
432
|
-
|
|
433
|
-
prefix = path_to_root(docname)
|
|
434
|
-
|
|
435
|
-
# Find apps that have stories for this persona
|
|
436
|
-
matching_apps = []
|
|
437
|
-
for slug in _app_registry:
|
|
438
|
-
personas = get_personas_for_app(slug, story_registry)
|
|
439
|
-
persona_names_normalized = {normalize_name(p) for p in personas}
|
|
440
|
-
if persona_normalized in persona_names_normalized:
|
|
441
|
-
matching_apps.append((slug, _app_registry[slug]))
|
|
442
|
-
|
|
443
|
-
if not matching_apps:
|
|
444
|
-
para = nodes.paragraph()
|
|
445
|
-
para += nodes.emphasis(text=f"No apps found for persona '{persona_arg}'")
|
|
446
|
-
return [para]
|
|
447
|
-
|
|
448
|
-
bullet_list = nodes.bullet_list()
|
|
449
|
-
|
|
450
|
-
for slug, app_data in sorted(matching_apps, key=lambda x: x[1]["name"]):
|
|
451
|
-
item = nodes.list_item()
|
|
452
|
-
para = nodes.paragraph()
|
|
453
|
-
|
|
454
|
-
app_path = f"{prefix}{config.get_doc_path('applications')}/{slug}.html"
|
|
455
|
-
ref = nodes.reference("", "", refuri=app_path)
|
|
456
|
-
ref += nodes.Text(app_data["name"])
|
|
457
|
-
para += ref
|
|
458
|
-
|
|
459
|
-
item += para
|
|
460
|
-
bullet_list += item
|
|
461
|
-
|
|
462
|
-
return [bullet_list]
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
def process_app_placeholders(app, doctree, docname):
|
|
466
|
-
"""Replace app placeholders with rendered content."""
|
|
467
|
-
from . import epics, journeys, stories
|
|
468
|
-
|
|
469
|
-
env = app.env
|
|
470
|
-
|
|
471
|
-
_story_registry = stories.get_story_registry()
|
|
472
|
-
_known_personas = stories.get_known_personas()
|
|
473
|
-
journey_registry = journeys.get_journey_registry(env)
|
|
474
|
-
epic_registry = epics.get_epic_registry(env)
|
|
475
|
-
|
|
476
|
-
# Process define-app placeholders
|
|
477
|
-
for node in doctree.traverse(DefineAppPlaceholder):
|
|
478
|
-
app_slug = node["app_slug"]
|
|
479
|
-
content = build_app_content(
|
|
480
|
-
app_slug,
|
|
481
|
-
docname,
|
|
482
|
-
_story_registry,
|
|
483
|
-
journey_registry,
|
|
484
|
-
epic_registry,
|
|
485
|
-
_known_personas,
|
|
486
|
-
)
|
|
487
|
-
node.replace_self(content)
|
|
488
|
-
|
|
489
|
-
# Process app-index placeholders
|
|
490
|
-
for node in doctree.traverse(AppIndexPlaceholder):
|
|
491
|
-
content = build_app_index(docname, _story_registry)
|
|
492
|
-
node.replace_self(content)
|
|
493
|
-
|
|
494
|
-
# Process apps-for-persona placeholders
|
|
495
|
-
for node in doctree.traverse(AppsForPersonaPlaceholder):
|
|
496
|
-
persona = node["persona"]
|
|
497
|
-
content = build_apps_for_persona(docname, persona, _story_registry)
|
|
498
|
-
node.replace_self(content)
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
def setup(app):
|
|
502
|
-
app.connect("builder-inited", scan_app_manifests)
|
|
503
|
-
app.connect("env-check-consistency", validate_apps)
|
|
504
|
-
app.connect("doctree-resolved", process_app_placeholders)
|
|
505
|
-
|
|
506
|
-
app.add_directive("define-app", DefineAppDirective)
|
|
507
|
-
app.add_directive("app-index", AppIndexDirective)
|
|
508
|
-
app.add_directive("apps-for-persona", AppsForPersonaDirective)
|
|
509
|
-
|
|
510
|
-
app.add_node(DefineAppPlaceholder)
|
|
511
|
-
app.add_node(AppIndexPlaceholder)
|
|
512
|
-
app.add_node(AppsForPersonaPlaceholder)
|
|
513
|
-
|
|
514
|
-
return {
|
|
515
|
-
"version": "1.0",
|
|
516
|
-
"parallel_read_safe": False,
|
|
517
|
-
"parallel_write_safe": True,
|
|
518
|
-
}
|