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
@@ -0,0 +1,64 @@
1
+ """Use cases for sphinx_hcd.
2
+
3
+ Business logic for cross-referencing and deriving entities.
4
+ """
5
+
6
+ from .derive_personas import (
7
+ derive_personas,
8
+ derive_personas_by_app_type,
9
+ get_apps_for_persona,
10
+ get_epics_for_persona,
11
+ )
12
+ from .resolve_accelerator_references import (
13
+ get_accelerator_cross_references,
14
+ get_apps_for_accelerator,
15
+ get_code_info_for_accelerator,
16
+ get_dependent_accelerators,
17
+ get_fed_by_accelerators,
18
+ get_journeys_for_accelerator,
19
+ get_publish_integrations,
20
+ get_source_integrations,
21
+ get_stories_for_accelerator,
22
+ )
23
+ from .resolve_app_references import (
24
+ get_app_cross_references,
25
+ get_epics_for_app,
26
+ get_journeys_for_app,
27
+ get_personas_for_app,
28
+ get_stories_for_app,
29
+ )
30
+ from .resolve_story_references import (
31
+ get_epics_for_story,
32
+ get_journeys_for_story,
33
+ get_related_stories,
34
+ get_story_cross_references,
35
+ )
36
+
37
+ __all__ = [
38
+ # Persona derivation
39
+ "derive_personas",
40
+ "derive_personas_by_app_type",
41
+ "get_apps_for_persona",
42
+ "get_epics_for_persona",
43
+ # Story references
44
+ "get_epics_for_story",
45
+ "get_journeys_for_story",
46
+ "get_related_stories",
47
+ "get_story_cross_references",
48
+ # App references
49
+ "get_app_cross_references",
50
+ "get_epics_for_app",
51
+ "get_journeys_for_app",
52
+ "get_personas_for_app",
53
+ "get_stories_for_app",
54
+ # Accelerator references
55
+ "get_accelerator_cross_references",
56
+ "get_apps_for_accelerator",
57
+ "get_code_info_for_accelerator",
58
+ "get_dependent_accelerators",
59
+ "get_fed_by_accelerators",
60
+ "get_journeys_for_accelerator",
61
+ "get_publish_integrations",
62
+ "get_source_integrations",
63
+ "get_stories_for_accelerator",
64
+ ]
@@ -0,0 +1,166 @@
1
+ """Use case for deriving personas from stories and epics.
2
+
3
+ Personas are not defined directly but are extracted from user stories.
4
+ This use case collects persona information from stories and enriches
5
+ it with epic participation data.
6
+ """
7
+
8
+ from collections import defaultdict
9
+
10
+ from ...utils import normalize_name
11
+ from ..models.app import App
12
+ from ..models.epic import Epic
13
+ from ..models.persona import Persona
14
+ from ..models.story import Story
15
+
16
+
17
+ def derive_personas(
18
+ stories: list[Story],
19
+ epics: list[Epic],
20
+ ) -> list[Persona]:
21
+ """Derive personas from stories and epics.
22
+
23
+ Extracts unique personas from stories, then enriches with:
24
+ - List of apps each persona uses (from stories)
25
+ - List of epics each persona participates in (stories in epics)
26
+
27
+ Args:
28
+ stories: List of Story entities
29
+ epics: List of Epic entities
30
+
31
+ Returns:
32
+ List of Persona entities sorted by name
33
+ """
34
+ # Collect persona data from stories
35
+ persona_data: dict[str, dict] = {} # normalized_name -> {name, apps}
36
+
37
+ for story in stories:
38
+ normalized = story.persona_normalized
39
+ if normalized == "unknown":
40
+ continue
41
+
42
+ if normalized not in persona_data:
43
+ persona_data[normalized] = {
44
+ "name": story.persona,
45
+ "apps": set(),
46
+ "epics": set(),
47
+ }
48
+
49
+ persona_data[normalized]["apps"].add(story.app_slug)
50
+
51
+ # Build lookup of normalized story title -> normalized persona
52
+ story_to_persona: dict[str, str] = {}
53
+ for story in stories:
54
+ story_to_persona[normalize_name(story.feature_title)] = story.persona_normalized
55
+
56
+ # Find epics for each persona
57
+ for epic in epics:
58
+ for story_ref in epic.story_refs:
59
+ story_normalized = normalize_name(story_ref)
60
+ persona_normalized = story_to_persona.get(story_normalized)
61
+ if persona_normalized and persona_normalized in persona_data:
62
+ persona_data[persona_normalized]["epics"].add(epic.slug)
63
+
64
+ # Build Persona entities
65
+ personas = []
66
+ for data in persona_data.values():
67
+ persona = Persona(
68
+ name=data["name"],
69
+ app_slugs=sorted(data["apps"]),
70
+ epic_slugs=sorted(data["epics"]),
71
+ )
72
+ personas.append(persona)
73
+
74
+ return sorted(personas, key=lambda p: p.name)
75
+
76
+
77
+ def derive_personas_by_app_type(
78
+ stories: list[Story],
79
+ epics: list[Epic],
80
+ apps: list[App],
81
+ ) -> dict[str, list[Persona]]:
82
+ """Derive personas grouped by the type of apps they use.
83
+
84
+ Args:
85
+ stories: List of Story entities
86
+ epics: List of Epic entities
87
+ apps: List of App entities
88
+
89
+ Returns:
90
+ Dict mapping app type strings to lists of Persona entities
91
+ """
92
+ # First derive all personas
93
+ all_personas = derive_personas(stories, epics)
94
+
95
+ # Build app slug -> app type lookup
96
+ app_types: dict[str, str] = {}
97
+ for app in apps:
98
+ app_types[app.slug] = app.app_type.value if app.app_type else "unknown"
99
+
100
+ # Group personas by app type
101
+ personas_by_type: dict[str, list[Persona]] = defaultdict(list)
102
+
103
+ for persona in all_personas:
104
+ # Find all app types this persona uses
105
+ persona_types: set[str] = set()
106
+ for app_slug in persona.app_slugs:
107
+ app_type = app_types.get(app_slug, "unknown")
108
+ persona_types.add(app_type)
109
+
110
+ # Add persona to each type group
111
+ for app_type in persona_types:
112
+ personas_by_type[app_type].append(persona)
113
+
114
+ # Sort personas within each group
115
+ return {
116
+ app_type: sorted(personas, key=lambda p: p.name)
117
+ for app_type, personas in personas_by_type.items()
118
+ }
119
+
120
+
121
+ def get_epics_for_persona(
122
+ persona: Persona,
123
+ epics: list[Epic],
124
+ stories: list[Story],
125
+ ) -> list[Epic]:
126
+ """Get Epic entities for a persona.
127
+
128
+ Args:
129
+ persona: Persona to get epics for
130
+ epics: All Epic entities
131
+ stories: All Story entities
132
+
133
+ Returns:
134
+ List of Epic entities containing stories for this persona
135
+ """
136
+ # Build lookup of normalized story title -> normalized persona
137
+ story_to_persona: dict[str, str] = {}
138
+ for story in stories:
139
+ story_to_persona[normalize_name(story.feature_title)] = story.persona_normalized
140
+
141
+ matching_epics = []
142
+ for epic in epics:
143
+ for story_ref in epic.story_refs:
144
+ story_normalized = normalize_name(story_ref)
145
+ if story_to_persona.get(story_normalized) == persona.normalized_name:
146
+ matching_epics.append(epic)
147
+ break
148
+
149
+ return sorted(matching_epics, key=lambda e: e.slug)
150
+
151
+
152
+ def get_apps_for_persona(
153
+ persona: Persona,
154
+ apps: list[App],
155
+ ) -> list[App]:
156
+ """Get App entities for a persona.
157
+
158
+ Args:
159
+ persona: Persona to get apps for
160
+ apps: All App entities
161
+
162
+ Returns:
163
+ List of App entities this persona uses
164
+ """
165
+ app_lookup = {app.slug: app for app in apps}
166
+ return [app_lookup[slug] for slug in persona.app_slugs if slug in app_lookup]
@@ -0,0 +1,236 @@
1
+ """Use case for resolving accelerator references.
2
+
3
+ Finds apps, stories, journeys, and integrations related to an accelerator.
4
+ """
5
+
6
+ from ...utils import normalize_name
7
+ from ..models.accelerator import Accelerator
8
+ from ..models.app import App
9
+ from ..models.code_info import BoundedContextInfo
10
+ from ..models.integration import Integration
11
+ from ..models.journey import Journey
12
+ from ..models.story import Story
13
+
14
+
15
+ def get_apps_for_accelerator(
16
+ accelerator: Accelerator,
17
+ apps: list[App],
18
+ ) -> list[App]:
19
+ """Get apps that expose an accelerator.
20
+
21
+ Apps expose accelerators via the 'accelerators' field in their manifest.
22
+
23
+ Args:
24
+ accelerator: Accelerator to find apps for
25
+ apps: All App entities
26
+
27
+ Returns:
28
+ List of App entities that expose this accelerator, sorted by slug
29
+ """
30
+ matching = [app for app in apps if accelerator.slug in (app.accelerators or [])]
31
+ return sorted(matching, key=lambda a: a.slug)
32
+
33
+
34
+ def get_stories_for_accelerator(
35
+ accelerator: Accelerator,
36
+ apps: list[App],
37
+ stories: list[Story],
38
+ ) -> list[Story]:
39
+ """Get stories for apps that expose an accelerator.
40
+
41
+ Args:
42
+ accelerator: Accelerator to find stories for
43
+ apps: All App entities
44
+ stories: All Story entities
45
+
46
+ Returns:
47
+ List of Story entities from apps that expose this accelerator
48
+ """
49
+ # Get app slugs that expose this accelerator
50
+ app_slugs = {
51
+ app.slug for app in apps if accelerator.slug in (app.accelerators or [])
52
+ }
53
+
54
+ if not app_slugs:
55
+ return []
56
+
57
+ # Find stories for those apps
58
+ matching = [s for s in stories if s.app_slug in app_slugs]
59
+ return sorted(matching, key=lambda s: s.feature_title)
60
+
61
+
62
+ def get_journeys_for_accelerator(
63
+ accelerator: Accelerator,
64
+ apps: list[App],
65
+ stories: list[Story],
66
+ journeys: list[Journey],
67
+ ) -> list[Journey]:
68
+ """Get journeys that include stories from an accelerator's apps.
69
+
70
+ Args:
71
+ accelerator: Accelerator to find journeys for
72
+ apps: All App entities
73
+ stories: All Story entities
74
+ journeys: All Journey entities
75
+
76
+ Returns:
77
+ List of Journey entities containing stories from this accelerator's apps
78
+ """
79
+ # Get stories for this accelerator
80
+ accel_stories = get_stories_for_accelerator(accelerator, apps, stories)
81
+ story_titles = {normalize_name(s.feature_title) for s in accel_stories}
82
+
83
+ if not story_titles:
84
+ return []
85
+
86
+ # Find journeys containing these stories
87
+ matching = []
88
+ for journey in journeys:
89
+ story_refs = journey.get_story_refs()
90
+ if any(normalize_name(ref) in story_titles for ref in story_refs):
91
+ matching.append(journey)
92
+
93
+ return sorted(matching, key=lambda j: j.slug)
94
+
95
+
96
+ def get_source_integrations(
97
+ accelerator: Accelerator,
98
+ integrations: list[Integration],
99
+ ) -> list[Integration]:
100
+ """Get integrations that an accelerator sources from.
101
+
102
+ Args:
103
+ accelerator: Accelerator to find sources for
104
+ integrations: All Integration entities
105
+
106
+ Returns:
107
+ List of Integration entities this accelerator sources from
108
+ """
109
+ source_slugs = accelerator.get_sources_from_slugs()
110
+ integration_lookup = {i.slug: i for i in integrations}
111
+
112
+ return [
113
+ integration_lookup[slug] for slug in source_slugs if slug in integration_lookup
114
+ ]
115
+
116
+
117
+ def get_publish_integrations(
118
+ accelerator: Accelerator,
119
+ integrations: list[Integration],
120
+ ) -> list[Integration]:
121
+ """Get integrations that an accelerator publishes to.
122
+
123
+ Args:
124
+ accelerator: Accelerator to find publish targets for
125
+ integrations: All Integration entities
126
+
127
+ Returns:
128
+ List of Integration entities this accelerator publishes to
129
+ """
130
+ publish_slugs = accelerator.get_publishes_to_slugs()
131
+ integration_lookup = {i.slug: i for i in integrations}
132
+
133
+ return [
134
+ integration_lookup[slug] for slug in publish_slugs if slug in integration_lookup
135
+ ]
136
+
137
+
138
+ def get_dependent_accelerators(
139
+ accelerator: Accelerator,
140
+ accelerators: list[Accelerator],
141
+ ) -> list[Accelerator]:
142
+ """Get accelerators that depend on a specific accelerator.
143
+
144
+ Args:
145
+ accelerator: Accelerator to find dependents of
146
+ accelerators: All Accelerator entities
147
+
148
+ Returns:
149
+ List of Accelerator entities that depend on this one
150
+ """
151
+ matching = [a for a in accelerators if accelerator.slug in a.depends_on]
152
+ return sorted(matching, key=lambda a: a.slug)
153
+
154
+
155
+ def get_fed_by_accelerators(
156
+ accelerator: Accelerator,
157
+ accelerators: list[Accelerator],
158
+ ) -> list[Accelerator]:
159
+ """Get accelerators that feed into a specific accelerator.
160
+
161
+ Args:
162
+ accelerator: Accelerator to find feeders for
163
+ accelerators: All Accelerator entities
164
+
165
+ Returns:
166
+ List of Accelerator entities that feed into this one
167
+ """
168
+ matching = [a for a in accelerators if accelerator.slug in a.feeds_into]
169
+ return sorted(matching, key=lambda a: a.slug)
170
+
171
+
172
+ def get_code_info_for_accelerator(
173
+ accelerator: Accelerator,
174
+ code_infos: list[BoundedContextInfo],
175
+ ) -> BoundedContextInfo | None:
176
+ """Get code info for an accelerator's bounded context.
177
+
178
+ Tries to match by slug or snake_case version of slug.
179
+
180
+ Args:
181
+ accelerator: Accelerator to find code for
182
+ code_infos: All BoundedContextInfo entities
183
+
184
+ Returns:
185
+ BoundedContextInfo if found, None otherwise
186
+ """
187
+ # Try exact match
188
+ for info in code_infos:
189
+ if info.slug == accelerator.slug:
190
+ return info
191
+
192
+ # Try snake_case match
193
+ snake_slug = accelerator.slug.replace("-", "_")
194
+ for info in code_infos:
195
+ if info.slug == snake_slug or info.code_dir == snake_slug:
196
+ return info
197
+
198
+ return None
199
+
200
+
201
+ def get_accelerator_cross_references(
202
+ accelerator: Accelerator,
203
+ accelerators: list[Accelerator],
204
+ apps: list[App],
205
+ stories: list[Story],
206
+ journeys: list[Journey],
207
+ integrations: list[Integration],
208
+ code_infos: list[BoundedContextInfo],
209
+ ) -> dict:
210
+ """Get all cross-references for an accelerator.
211
+
212
+ Convenience function to get all related entities at once.
213
+
214
+ Args:
215
+ accelerator: Accelerator to find references for
216
+ accelerators: All Accelerator entities
217
+ apps: All App entities
218
+ stories: All Story entities
219
+ journeys: All Journey entities
220
+ integrations: All Integration entities
221
+ code_infos: All BoundedContextInfo entities
222
+
223
+ Returns:
224
+ Dict with keys: apps, stories, journeys, source_integrations,
225
+ publish_integrations, dependents, fed_by, code_info
226
+ """
227
+ return {
228
+ "apps": get_apps_for_accelerator(accelerator, apps),
229
+ "stories": get_stories_for_accelerator(accelerator, apps, stories),
230
+ "journeys": get_journeys_for_accelerator(accelerator, apps, stories, journeys),
231
+ "source_integrations": get_source_integrations(accelerator, integrations),
232
+ "publish_integrations": get_publish_integrations(accelerator, integrations),
233
+ "dependents": get_dependent_accelerators(accelerator, accelerators),
234
+ "fed_by": get_fed_by_accelerators(accelerator, accelerators),
235
+ "code_info": get_code_info_for_accelerator(accelerator, code_infos),
236
+ }
@@ -0,0 +1,144 @@
1
+ """Use case for resolving app references.
2
+
3
+ Finds stories, personas, journeys, and epics related to an app.
4
+ """
5
+
6
+ from ...utils import normalize_name
7
+ from ..models.app import App
8
+ from ..models.epic import Epic
9
+ from ..models.journey import Journey
10
+ from ..models.persona import Persona
11
+ from ..models.story import Story
12
+ from .derive_personas import derive_personas
13
+
14
+
15
+ def get_stories_for_app(
16
+ app: App,
17
+ stories: list[Story],
18
+ ) -> list[Story]:
19
+ """Get stories that belong to an app.
20
+
21
+ Args:
22
+ app: App to find stories for
23
+ stories: All Story entities
24
+
25
+ Returns:
26
+ List of Story entities for this app, sorted by feature_title
27
+ """
28
+ matching = [s for s in stories if s.app_slug == app.slug]
29
+ return sorted(matching, key=lambda s: s.feature_title)
30
+
31
+
32
+ def get_personas_for_app(
33
+ app: App,
34
+ stories: list[Story],
35
+ epics: list[Epic],
36
+ ) -> list[Persona]:
37
+ """Get personas that use an app.
38
+
39
+ Args:
40
+ app: App to find personas for
41
+ stories: All Story entities
42
+ epics: All Epic entities (for persona derivation)
43
+
44
+ Returns:
45
+ List of Persona entities that use this app, sorted by name
46
+ """
47
+ # Derive all personas
48
+ all_personas = derive_personas(stories, epics)
49
+
50
+ # Filter to those using this app
51
+ matching = [p for p in all_personas if app.slug in p.app_slugs]
52
+ return sorted(matching, key=lambda p: p.name)
53
+
54
+
55
+ def get_journeys_for_app(
56
+ app: App,
57
+ stories: list[Story],
58
+ journeys: list[Journey],
59
+ ) -> list[Journey]:
60
+ """Get journeys that include stories from an app.
61
+
62
+ Args:
63
+ app: App to find journeys for
64
+ stories: All Story entities
65
+ journeys: All Journey entities
66
+
67
+ Returns:
68
+ List of Journey entities containing stories from this app, sorted by slug
69
+ """
70
+ # Get story titles for this app
71
+ app_story_titles = {
72
+ normalize_name(s.feature_title) for s in stories if s.app_slug == app.slug
73
+ }
74
+
75
+ if not app_story_titles:
76
+ return []
77
+
78
+ # Find journeys containing these stories
79
+ matching = []
80
+ for journey in journeys:
81
+ story_refs = journey.get_story_refs()
82
+ if any(normalize_name(ref) in app_story_titles for ref in story_refs):
83
+ matching.append(journey)
84
+
85
+ return sorted(matching, key=lambda j: j.slug)
86
+
87
+
88
+ def get_epics_for_app(
89
+ app: App,
90
+ stories: list[Story],
91
+ epics: list[Epic],
92
+ ) -> list[Epic]:
93
+ """Get epics that contain stories from an app.
94
+
95
+ Args:
96
+ app: App to find epics for
97
+ stories: All Story entities
98
+ epics: All Epic entities
99
+
100
+ Returns:
101
+ List of Epic entities containing stories from this app, sorted by slug
102
+ """
103
+ # Get story titles for this app
104
+ app_story_titles = {
105
+ normalize_name(s.feature_title) for s in stories if s.app_slug == app.slug
106
+ }
107
+
108
+ if not app_story_titles:
109
+ return []
110
+
111
+ # Find epics containing these stories
112
+ matching = []
113
+ for epic in epics:
114
+ if any(normalize_name(ref) in app_story_titles for ref in epic.story_refs):
115
+ matching.append(epic)
116
+
117
+ return sorted(matching, key=lambda e: e.slug)
118
+
119
+
120
+ def get_app_cross_references(
121
+ app: App,
122
+ stories: list[Story],
123
+ epics: list[Epic],
124
+ journeys: list[Journey],
125
+ ) -> dict:
126
+ """Get all cross-references for an app.
127
+
128
+ Convenience function to get all related entities at once.
129
+
130
+ Args:
131
+ app: App to find references for
132
+ stories: All Story entities
133
+ epics: All Epic entities
134
+ journeys: All Journey entities
135
+
136
+ Returns:
137
+ Dict with keys: stories, personas, journeys, epics
138
+ """
139
+ return {
140
+ "stories": get_stories_for_app(app, stories),
141
+ "personas": get_personas_for_app(app, stories, epics),
142
+ "journeys": get_journeys_for_app(app, stories, journeys),
143
+ "epics": get_epics_for_app(app, stories, epics),
144
+ }