julee 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. julee/__init__.py +1 -1
  2. julee/api/tests/routers/test_assembly_specifications.py +2 -0
  3. julee/api/tests/routers/test_documents.py +2 -0
  4. julee/api/tests/routers/test_knowledge_service_configs.py +2 -0
  5. julee/api/tests/routers/test_knowledge_service_queries.py +2 -0
  6. julee/api/tests/routers/test_system.py +2 -0
  7. julee/api/tests/routers/test_workflows.py +2 -0
  8. julee/api/tests/test_app.py +2 -0
  9. julee/api/tests/test_dependencies.py +2 -0
  10. julee/api/tests/test_requests.py +2 -0
  11. julee/contrib/polling/__init__.py +22 -19
  12. julee/contrib/polling/apps/__init__.py +17 -0
  13. julee/contrib/polling/apps/worker/__init__.py +17 -0
  14. julee/contrib/polling/apps/worker/pipelines.py +288 -0
  15. julee/contrib/polling/domain/__init__.py +7 -9
  16. julee/contrib/polling/domain/models/__init__.py +6 -7
  17. julee/contrib/polling/domain/models/polling_config.py +18 -1
  18. julee/contrib/polling/domain/services/__init__.py +6 -5
  19. julee/contrib/polling/domain/services/poller.py +1 -1
  20. julee/contrib/polling/infrastructure/__init__.py +9 -8
  21. julee/contrib/polling/infrastructure/services/__init__.py +6 -5
  22. julee/contrib/polling/infrastructure/services/polling/__init__.py +6 -5
  23. julee/contrib/polling/infrastructure/services/polling/http/__init__.py +6 -5
  24. julee/contrib/polling/infrastructure/services/polling/http/http_poller_service.py +5 -2
  25. julee/contrib/polling/infrastructure/temporal/__init__.py +12 -12
  26. julee/contrib/polling/infrastructure/temporal/activities.py +1 -1
  27. julee/contrib/polling/infrastructure/temporal/manager.py +291 -0
  28. julee/contrib/polling/infrastructure/temporal/proxies.py +1 -1
  29. julee/contrib/polling/tests/unit/apps/worker/test_pipelines.py +580 -0
  30. julee/contrib/polling/tests/unit/infrastructure/services/polling/http/test_http_poller_service.py +40 -2
  31. julee/contrib/polling/tests/unit/infrastructure/temporal/__init__.py +7 -0
  32. julee/contrib/polling/tests/unit/infrastructure/temporal/test_manager.py +475 -0
  33. julee/docs/sphinx_hcd/__init__.py +146 -13
  34. julee/docs/sphinx_hcd/domain/__init__.py +5 -0
  35. julee/docs/sphinx_hcd/domain/models/__init__.py +32 -0
  36. julee/docs/sphinx_hcd/domain/models/accelerator.py +152 -0
  37. julee/docs/sphinx_hcd/domain/models/app.py +151 -0
  38. julee/docs/sphinx_hcd/domain/models/code_info.py +121 -0
  39. julee/docs/sphinx_hcd/domain/models/epic.py +79 -0
  40. julee/docs/sphinx_hcd/domain/models/integration.py +230 -0
  41. julee/docs/sphinx_hcd/domain/models/journey.py +222 -0
  42. julee/docs/sphinx_hcd/domain/models/persona.py +106 -0
  43. julee/docs/sphinx_hcd/domain/models/story.py +128 -0
  44. julee/docs/sphinx_hcd/domain/repositories/__init__.py +25 -0
  45. julee/docs/sphinx_hcd/domain/repositories/accelerator.py +98 -0
  46. julee/docs/sphinx_hcd/domain/repositories/app.py +57 -0
  47. julee/docs/sphinx_hcd/domain/repositories/base.py +89 -0
  48. julee/docs/sphinx_hcd/domain/repositories/code_info.py +69 -0
  49. julee/docs/sphinx_hcd/domain/repositories/epic.py +62 -0
  50. julee/docs/sphinx_hcd/domain/repositories/integration.py +79 -0
  51. julee/docs/sphinx_hcd/domain/repositories/journey.py +106 -0
  52. julee/docs/sphinx_hcd/domain/repositories/story.py +68 -0
  53. julee/docs/sphinx_hcd/domain/use_cases/__init__.py +64 -0
  54. julee/docs/sphinx_hcd/domain/use_cases/derive_personas.py +166 -0
  55. julee/docs/sphinx_hcd/domain/use_cases/resolve_accelerator_references.py +236 -0
  56. julee/docs/sphinx_hcd/domain/use_cases/resolve_app_references.py +144 -0
  57. julee/docs/sphinx_hcd/domain/use_cases/resolve_story_references.py +121 -0
  58. julee/docs/sphinx_hcd/parsers/__init__.py +48 -0
  59. julee/docs/sphinx_hcd/parsers/ast.py +150 -0
  60. julee/docs/sphinx_hcd/parsers/gherkin.py +155 -0
  61. julee/docs/sphinx_hcd/parsers/yaml.py +184 -0
  62. julee/docs/sphinx_hcd/repositories/__init__.py +4 -0
  63. julee/docs/sphinx_hcd/repositories/memory/__init__.py +25 -0
  64. julee/docs/sphinx_hcd/repositories/memory/accelerator.py +86 -0
  65. julee/docs/sphinx_hcd/repositories/memory/app.py +45 -0
  66. julee/docs/sphinx_hcd/repositories/memory/base.py +106 -0
  67. julee/docs/sphinx_hcd/repositories/memory/code_info.py +59 -0
  68. julee/docs/sphinx_hcd/repositories/memory/epic.py +54 -0
  69. julee/docs/sphinx_hcd/repositories/memory/integration.py +70 -0
  70. julee/docs/sphinx_hcd/repositories/memory/journey.py +96 -0
  71. julee/docs/sphinx_hcd/repositories/memory/story.py +63 -0
  72. julee/docs/sphinx_hcd/sphinx/__init__.py +28 -0
  73. julee/docs/sphinx_hcd/sphinx/adapters.py +116 -0
  74. julee/docs/sphinx_hcd/sphinx/context.py +163 -0
  75. julee/docs/sphinx_hcd/sphinx/directives/__init__.py +160 -0
  76. julee/docs/sphinx_hcd/sphinx/directives/accelerator.py +576 -0
  77. julee/docs/sphinx_hcd/sphinx/directives/app.py +349 -0
  78. julee/docs/sphinx_hcd/sphinx/directives/base.py +211 -0
  79. julee/docs/sphinx_hcd/sphinx/directives/epic.py +434 -0
  80. julee/docs/sphinx_hcd/sphinx/directives/integration.py +220 -0
  81. julee/docs/sphinx_hcd/sphinx/directives/journey.py +642 -0
  82. julee/docs/sphinx_hcd/sphinx/directives/persona.py +345 -0
  83. julee/docs/sphinx_hcd/sphinx/directives/story.py +575 -0
  84. julee/docs/sphinx_hcd/sphinx/event_handlers/__init__.py +16 -0
  85. julee/docs/sphinx_hcd/sphinx/event_handlers/builder_inited.py +31 -0
  86. julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_read.py +27 -0
  87. julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_resolved.py +43 -0
  88. julee/docs/sphinx_hcd/sphinx/event_handlers/env_purge_doc.py +42 -0
  89. julee/docs/sphinx_hcd/sphinx/initialization.py +139 -0
  90. julee/docs/sphinx_hcd/tests/__init__.py +9 -0
  91. julee/docs/sphinx_hcd/tests/conftest.py +6 -0
  92. julee/docs/sphinx_hcd/tests/domain/__init__.py +1 -0
  93. julee/docs/sphinx_hcd/tests/domain/models/__init__.py +1 -0
  94. julee/docs/sphinx_hcd/tests/domain/models/test_accelerator.py +266 -0
  95. julee/docs/sphinx_hcd/tests/domain/models/test_app.py +258 -0
  96. julee/docs/sphinx_hcd/tests/domain/models/test_code_info.py +231 -0
  97. julee/docs/sphinx_hcd/tests/domain/models/test_epic.py +163 -0
  98. julee/docs/sphinx_hcd/tests/domain/models/test_integration.py +327 -0
  99. julee/docs/sphinx_hcd/tests/domain/models/test_journey.py +249 -0
  100. julee/docs/sphinx_hcd/tests/domain/models/test_persona.py +172 -0
  101. julee/docs/sphinx_hcd/tests/domain/models/test_story.py +216 -0
  102. julee/docs/sphinx_hcd/tests/domain/use_cases/__init__.py +1 -0
  103. julee/docs/sphinx_hcd/tests/domain/use_cases/test_derive_personas.py +314 -0
  104. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_accelerator_references.py +476 -0
  105. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_app_references.py +265 -0
  106. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_story_references.py +229 -0
  107. julee/docs/sphinx_hcd/tests/integration/__init__.py +1 -0
  108. julee/docs/sphinx_hcd/tests/parsers/__init__.py +1 -0
  109. julee/docs/sphinx_hcd/tests/parsers/test_ast.py +298 -0
  110. julee/docs/sphinx_hcd/tests/parsers/test_gherkin.py +282 -0
  111. julee/docs/sphinx_hcd/tests/parsers/test_yaml.py +496 -0
  112. julee/docs/sphinx_hcd/tests/repositories/__init__.py +1 -0
  113. julee/docs/sphinx_hcd/tests/repositories/test_accelerator.py +298 -0
  114. julee/docs/sphinx_hcd/tests/repositories/test_app.py +218 -0
  115. julee/docs/sphinx_hcd/tests/repositories/test_base.py +151 -0
  116. julee/docs/sphinx_hcd/tests/repositories/test_code_info.py +253 -0
  117. julee/docs/sphinx_hcd/tests/repositories/test_epic.py +237 -0
  118. julee/docs/sphinx_hcd/tests/repositories/test_integration.py +268 -0
  119. julee/docs/sphinx_hcd/tests/repositories/test_journey.py +294 -0
  120. julee/docs/sphinx_hcd/tests/repositories/test_story.py +236 -0
  121. julee/docs/sphinx_hcd/tests/sphinx/__init__.py +1 -0
  122. julee/docs/sphinx_hcd/tests/sphinx/directives/__init__.py +1 -0
  123. julee/docs/sphinx_hcd/tests/sphinx/directives/test_base.py +160 -0
  124. julee/docs/sphinx_hcd/tests/sphinx/test_adapters.py +176 -0
  125. julee/docs/sphinx_hcd/tests/sphinx/test_context.py +257 -0
  126. julee/domain/models/assembly/tests/test_assembly.py +2 -0
  127. julee/domain/models/assembly_specification/tests/test_assembly_specification.py +2 -0
  128. julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +2 -0
  129. julee/domain/models/custom_fields/tests/test_custom_fields.py +2 -0
  130. julee/domain/models/document/tests/test_document.py +2 -0
  131. julee/domain/models/policy/tests/test_document_policy_validation.py +2 -0
  132. julee/domain/models/policy/tests/test_policy.py +2 -0
  133. julee/domain/use_cases/tests/test_extract_assemble_data.py +2 -0
  134. julee/domain/use_cases/tests/test_initialize_system_data.py +2 -0
  135. julee/domain/use_cases/tests/test_validate_document.py +2 -0
  136. julee/maintenance/release.py +10 -5
  137. julee/repositories/memory/tests/test_document.py +2 -0
  138. julee/repositories/memory/tests/test_document_policy_validation.py +2 -0
  139. julee/repositories/memory/tests/test_policy.py +2 -0
  140. julee/repositories/minio/tests/test_assembly.py +2 -0
  141. julee/repositories/minio/tests/test_assembly_specification.py +2 -0
  142. julee/repositories/minio/tests/test_client_protocol.py +3 -0
  143. julee/repositories/minio/tests/test_document.py +2 -0
  144. julee/repositories/minio/tests/test_document_policy_validation.py +2 -0
  145. julee/repositories/minio/tests/test_knowledge_service_config.py +2 -0
  146. julee/repositories/minio/tests/test_knowledge_service_query.py +2 -0
  147. julee/repositories/minio/tests/test_policy.py +2 -0
  148. julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +2 -0
  149. julee/services/knowledge_service/memory/test_knowledge_service.py +2 -0
  150. julee/services/knowledge_service/test_factory.py +2 -0
  151. julee/util/tests/test_decorators.py +2 -0
  152. julee-0.1.6.dist-info/METADATA +104 -0
  153. julee-0.1.6.dist-info/RECORD +288 -0
  154. julee/docs/sphinx_hcd/accelerators.py +0 -1175
  155. julee/docs/sphinx_hcd/apps.py +0 -518
  156. julee/docs/sphinx_hcd/epics.py +0 -453
  157. julee/docs/sphinx_hcd/integrations.py +0 -310
  158. julee/docs/sphinx_hcd/journeys.py +0 -797
  159. julee/docs/sphinx_hcd/personas.py +0 -457
  160. julee/docs/sphinx_hcd/stories.py +0 -960
  161. julee-0.1.4.dist-info/METADATA +0 -197
  162. julee-0.1.4.dist-info/RECORD +0 -196
  163. {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/WHEEL +0 -0
  164. {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/licenses/LICENSE +0 -0
  165. {julee-0.1.4.dist-info → julee-0.1.6.dist-info}/top_level.txt +0 -0
@@ -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
- }