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,32 @@
1
+ """Domain models for sphinx_hcd.
2
+
3
+ Pydantic models representing HCD entities: stories, journeys, epics,
4
+ apps, accelerators, integrations, and personas.
5
+ """
6
+
7
+ from .accelerator import Accelerator, IntegrationReference
8
+ from .app import App, AppType
9
+ from .code_info import BoundedContextInfo, ClassInfo
10
+ from .epic import Epic
11
+ from .integration import Direction, ExternalDependency, Integration
12
+ from .journey import Journey, JourneyStep, StepType
13
+ from .persona import Persona
14
+ from .story import Story
15
+
16
+ __all__ = [
17
+ "Accelerator",
18
+ "App",
19
+ "AppType",
20
+ "BoundedContextInfo",
21
+ "ClassInfo",
22
+ "Direction",
23
+ "Epic",
24
+ "ExternalDependency",
25
+ "Integration",
26
+ "IntegrationReference",
27
+ "Journey",
28
+ "JourneyStep",
29
+ "Persona",
30
+ "StepType",
31
+ "Story",
32
+ ]
@@ -0,0 +1,152 @@
1
+ """Accelerator domain model.
2
+
3
+ Represents an accelerator (bounded context) in the HCD documentation system.
4
+ Accelerators are defined via RST directives and may have associated code.
5
+ """
6
+
7
+ from pydantic import BaseModel, Field, field_validator
8
+
9
+
10
+ class IntegrationReference(BaseModel):
11
+ """Reference to an integration with optional description.
12
+
13
+ Used for sources_from and publishes_to relationships where
14
+ an accelerator may specify what data it sources or publishes.
15
+
16
+ Attributes:
17
+ slug: Integration slug (e.g., "pilot-data-collection")
18
+ description: What is sourced/published (e.g., "Scheme documentation")
19
+ """
20
+
21
+ slug: str
22
+ description: str = ""
23
+
24
+ @field_validator("slug", mode="before")
25
+ @classmethod
26
+ def validate_slug(cls, v: str) -> str:
27
+ """Validate slug is not empty."""
28
+ if not v or not v.strip():
29
+ raise ValueError("slug cannot be empty")
30
+ return v.strip()
31
+
32
+ @classmethod
33
+ def from_dict(cls, data: dict | str) -> "IntegrationReference":
34
+ """Create from dict or string.
35
+
36
+ Args:
37
+ data: Either a dict with slug/description or a plain string slug
38
+
39
+ Returns:
40
+ IntegrationReference instance
41
+ """
42
+ if isinstance(data, str):
43
+ return cls(slug=data)
44
+ return cls(slug=data.get("slug", ""), description=data.get("description", ""))
45
+
46
+
47
+ class Accelerator(BaseModel):
48
+ """Accelerator entity.
49
+
50
+ An accelerator represents a bounded context that provides business
51
+ capabilities. It may have associated code in src/{slug}/ and is
52
+ exposed through one or more applications.
53
+
54
+ Attributes:
55
+ slug: URL-safe identifier (e.g., "vocabulary")
56
+ status: Development status (e.g., "alpha", "production", "future")
57
+ milestone: Target milestone (e.g., "2 (Nov 2025)")
58
+ acceptance: Acceptance criteria description
59
+ objective: Business objective/description
60
+ sources_from: Integrations this accelerator reads from
61
+ feeds_into: Other accelerators this one feeds data into
62
+ publishes_to: Integrations this accelerator writes to
63
+ depends_on: Other accelerators this one depends on
64
+ docname: RST document name (for incremental builds)
65
+ """
66
+
67
+ slug: str
68
+ status: str = ""
69
+ milestone: str | None = None
70
+ acceptance: str | None = None
71
+ objective: str = ""
72
+ sources_from: list[IntegrationReference] = Field(default_factory=list)
73
+ feeds_into: list[str] = Field(default_factory=list)
74
+ publishes_to: list[IntegrationReference] = Field(default_factory=list)
75
+ depends_on: list[str] = Field(default_factory=list)
76
+ docname: str = ""
77
+
78
+ @field_validator("slug", mode="before")
79
+ @classmethod
80
+ def validate_slug(cls, v: str) -> str:
81
+ """Validate slug is not empty."""
82
+ if not v or not v.strip():
83
+ raise ValueError("slug cannot be empty")
84
+ return v.strip()
85
+
86
+ @property
87
+ def display_title(self) -> str:
88
+ """Get formatted title for display."""
89
+ return self.slug.replace("-", " ").title()
90
+
91
+ @property
92
+ def status_normalized(self) -> str:
93
+ """Get normalized status for grouping."""
94
+ return self.status.lower().strip() if self.status else ""
95
+
96
+ def has_integration_dependency(self, integration_slug: str) -> bool:
97
+ """Check if accelerator depends on an integration.
98
+
99
+ Args:
100
+ integration_slug: Integration slug to check
101
+
102
+ Returns:
103
+ True if sources_from or publishes_to contains this integration
104
+ """
105
+ for ref in self.sources_from:
106
+ if ref.slug == integration_slug:
107
+ return True
108
+ for ref in self.publishes_to:
109
+ if ref.slug == integration_slug:
110
+ return True
111
+ return False
112
+
113
+ def has_accelerator_dependency(self, accelerator_slug: str) -> bool:
114
+ """Check if accelerator depends on another accelerator.
115
+
116
+ Args:
117
+ accelerator_slug: Accelerator slug to check
118
+
119
+ Returns:
120
+ True if depends_on or feeds_into contains this accelerator
121
+ """
122
+ return (
123
+ accelerator_slug in self.depends_on or accelerator_slug in self.feeds_into
124
+ )
125
+
126
+ def get_sources_from_slugs(self) -> list[str]:
127
+ """Get list of integration slugs this accelerator sources from."""
128
+ return [ref.slug for ref in self.sources_from]
129
+
130
+ def get_publishes_to_slugs(self) -> list[str]:
131
+ """Get list of integration slugs this accelerator publishes to."""
132
+ return [ref.slug for ref in self.publishes_to]
133
+
134
+ def get_integration_description(
135
+ self, integration_slug: str, relationship: str
136
+ ) -> str | None:
137
+ """Get description for an integration relationship.
138
+
139
+ Args:
140
+ integration_slug: Integration to look up
141
+ relationship: Either "sources_from" or "publishes_to"
142
+
143
+ Returns:
144
+ Description if found, None otherwise
145
+ """
146
+ refs = (
147
+ self.sources_from if relationship == "sources_from" else self.publishes_to
148
+ )
149
+ for ref in refs:
150
+ if ref.slug == integration_slug:
151
+ return ref.description or None
152
+ return None
@@ -0,0 +1,151 @@
1
+ """App domain model.
2
+
3
+ Represents an application in the HCD documentation system.
4
+ Apps are defined via YAML manifests in apps/*/app.yaml.
5
+ """
6
+
7
+ from enum import Enum
8
+
9
+ from pydantic import BaseModel, Field, field_validator
10
+
11
+ from ...utils import normalize_name
12
+
13
+
14
+ class AppType(str, Enum):
15
+ """Application type classification."""
16
+
17
+ STAFF = "staff"
18
+ EXTERNAL = "external"
19
+ MEMBER_TOOL = "member-tool"
20
+ UNKNOWN = "unknown"
21
+
22
+ @classmethod
23
+ def from_string(cls, value: str) -> "AppType":
24
+ """Convert string to AppType, defaulting to UNKNOWN."""
25
+ try:
26
+ return cls(value.lower())
27
+ except ValueError:
28
+ return cls.UNKNOWN
29
+
30
+
31
+ class App(BaseModel):
32
+ """Application entity.
33
+
34
+ Apps represent distinct applications in the system, defined via YAML
35
+ manifests. They serve as containers for stories and provide organization
36
+ for the documentation.
37
+
38
+ Attributes:
39
+ slug: URL-safe identifier (e.g., "staff-portal")
40
+ name: Display name (e.g., "Staff Portal")
41
+ app_type: Classification (staff, external, member-tool)
42
+ status: Optional status indicator (e.g., "in-development", "live")
43
+ description: Human-readable description
44
+ accelerators: List of accelerator slugs associated with this app
45
+ manifest_path: Path to the app.yaml file
46
+ name_normalized: Lowercase name for matching
47
+ """
48
+
49
+ slug: str
50
+ name: str
51
+ app_type: AppType = AppType.UNKNOWN
52
+ status: str | None = None
53
+ description: str = ""
54
+ accelerators: list[str] = Field(default_factory=list)
55
+ manifest_path: str = ""
56
+ name_normalized: str = ""
57
+
58
+ @field_validator("slug", mode="before")
59
+ @classmethod
60
+ def validate_slug(cls, v: str) -> str:
61
+ """Validate slug is not empty."""
62
+ if not v or not v.strip():
63
+ raise ValueError("slug cannot be empty")
64
+ return v.strip()
65
+
66
+ @field_validator("name", mode="before")
67
+ @classmethod
68
+ def validate_name(cls, v: str) -> str:
69
+ """Validate name is not empty."""
70
+ if not v or not v.strip():
71
+ raise ValueError("name cannot be empty")
72
+ return v.strip()
73
+
74
+ @field_validator("name_normalized", mode="before")
75
+ @classmethod
76
+ def compute_name_normalized(cls, v: str, info) -> str:
77
+ """Compute normalized name from name if not provided."""
78
+ if v:
79
+ return v
80
+ name = info.data.get("name", "")
81
+ return normalize_name(name) if name else ""
82
+
83
+ def model_post_init(self, __context) -> None:
84
+ """Ensure normalized fields are computed after init."""
85
+ if not self.name_normalized and self.name:
86
+ object.__setattr__(self, "name_normalized", normalize_name(self.name))
87
+
88
+ @classmethod
89
+ def from_manifest(
90
+ cls,
91
+ slug: str,
92
+ manifest: dict,
93
+ manifest_path: str,
94
+ ) -> "App":
95
+ """Create an App from a parsed YAML manifest.
96
+
97
+ Args:
98
+ slug: App slug (usually directory name)
99
+ manifest: Parsed YAML content
100
+ manifest_path: Path to the manifest file
101
+
102
+ Returns:
103
+ App instance
104
+ """
105
+ name = manifest.get("name", slug.replace("-", " ").title())
106
+ app_type = AppType.from_string(manifest.get("type", "unknown"))
107
+
108
+ return cls(
109
+ slug=slug,
110
+ name=name,
111
+ app_type=app_type,
112
+ status=manifest.get("status"),
113
+ description=manifest.get("description", "").strip(),
114
+ accelerators=manifest.get("accelerators", []),
115
+ manifest_path=manifest_path,
116
+ )
117
+
118
+ def matches_type(self, app_type: AppType | str) -> bool:
119
+ """Check if this app matches the given type.
120
+
121
+ Args:
122
+ app_type: AppType enum or string to match
123
+
124
+ Returns:
125
+ True if app matches the type
126
+ """
127
+ if isinstance(app_type, str):
128
+ app_type = AppType.from_string(app_type)
129
+ return self.app_type == app_type
130
+
131
+ def matches_name(self, name: str) -> bool:
132
+ """Check if this app matches the given name (case-insensitive).
133
+
134
+ Args:
135
+ name: Name to match against
136
+
137
+ Returns:
138
+ True if normalized names match
139
+ """
140
+ return self.name_normalized == normalize_name(name)
141
+
142
+ @property
143
+ def type_label(self) -> str:
144
+ """Get human-readable type label."""
145
+ labels = {
146
+ AppType.STAFF: "Staff Application",
147
+ AppType.EXTERNAL: "External Application",
148
+ AppType.MEMBER_TOOL: "Member Tool",
149
+ AppType.UNKNOWN: "Unknown",
150
+ }
151
+ return labels.get(self.app_type, str(self.app_type))
@@ -0,0 +1,121 @@
1
+ """Code introspection domain models.
2
+
3
+ Models for representing Python code structure extracted via AST parsing.
4
+ Used to document bounded contexts and their ADR 001-compliant structure.
5
+ """
6
+
7
+ from pydantic import BaseModel, Field, field_validator
8
+
9
+
10
+ class ClassInfo(BaseModel):
11
+ """Information about a Python class extracted via AST.
12
+
13
+ Attributes:
14
+ name: Class name (e.g., "Document", "CreateDocumentUseCase")
15
+ docstring: First line of the class docstring
16
+ file: Source file name (e.g., "document.py")
17
+ """
18
+
19
+ name: str
20
+ docstring: str = ""
21
+ file: str = ""
22
+
23
+ @field_validator("name", mode="before")
24
+ @classmethod
25
+ def validate_name(cls, v: str) -> str:
26
+ """Validate name is not empty."""
27
+ if not v or not v.strip():
28
+ raise ValueError("name cannot be empty")
29
+ return v.strip()
30
+
31
+
32
+ class BoundedContextInfo(BaseModel):
33
+ """Information about a bounded context's code structure.
34
+
35
+ Represents the ADR 001-compliant structure of a bounded context
36
+ with domain models, use cases, and repository/service protocols.
37
+
38
+ Attributes:
39
+ slug: Directory name / identifier (e.g., "vocabulary")
40
+ entities: Domain entity classes from domain/models/
41
+ use_cases: Use case classes from use_cases/
42
+ repository_protocols: Repository protocol classes from domain/repositories/
43
+ service_protocols: Service protocol classes from domain/services/
44
+ has_infrastructure: Whether infrastructure/ directory exists
45
+ code_dir: Actual directory name in src/
46
+ objective: First line of __init__.py docstring
47
+ docstring: Full __init__.py docstring
48
+ """
49
+
50
+ slug: str
51
+ entities: list[ClassInfo] = Field(default_factory=list)
52
+ use_cases: list[ClassInfo] = Field(default_factory=list)
53
+ repository_protocols: list[ClassInfo] = Field(default_factory=list)
54
+ service_protocols: list[ClassInfo] = Field(default_factory=list)
55
+ has_infrastructure: bool = False
56
+ code_dir: str = ""
57
+ objective: str | None = None
58
+ docstring: str | None = None
59
+
60
+ @field_validator("slug", mode="before")
61
+ @classmethod
62
+ def validate_slug(cls, v: str) -> str:
63
+ """Validate slug is not empty."""
64
+ if not v or not v.strip():
65
+ raise ValueError("slug cannot be empty")
66
+ return v.strip()
67
+
68
+ @property
69
+ def entity_count(self) -> int:
70
+ """Get number of domain entities."""
71
+ return len(self.entities)
72
+
73
+ @property
74
+ def use_case_count(self) -> int:
75
+ """Get number of use cases."""
76
+ return len(self.use_cases)
77
+
78
+ @property
79
+ def protocol_count(self) -> int:
80
+ """Get total number of protocols (repository + service)."""
81
+ return len(self.repository_protocols) + len(self.service_protocols)
82
+
83
+ @property
84
+ def has_entities(self) -> bool:
85
+ """Check if bounded context has any entities."""
86
+ return len(self.entities) > 0
87
+
88
+ @property
89
+ def has_use_cases(self) -> bool:
90
+ """Check if bounded context has any use cases."""
91
+ return len(self.use_cases) > 0
92
+
93
+ @property
94
+ def has_protocols(self) -> bool:
95
+ """Check if bounded context has any protocols."""
96
+ return self.protocol_count > 0
97
+
98
+ def get_entity_names(self) -> list[str]:
99
+ """Get list of entity class names."""
100
+ return [e.name for e in self.entities]
101
+
102
+ def get_use_case_names(self) -> list[str]:
103
+ """Get list of use case class names."""
104
+ return [u.name for u in self.use_cases]
105
+
106
+ def summary(self) -> str:
107
+ """Get a brief summary of the bounded context.
108
+
109
+ Returns:
110
+ Summary string like "3 entities, 2 use cases"
111
+ """
112
+ parts = []
113
+ if self.entities:
114
+ parts.append(f"{len(self.entities)} entities")
115
+ if self.use_cases:
116
+ parts.append(f"{len(self.use_cases)} use cases")
117
+ if self.repository_protocols:
118
+ parts.append(f"{len(self.repository_protocols)} repository protocols")
119
+ if self.service_protocols:
120
+ parts.append(f"{len(self.service_protocols)} service protocols")
121
+ return ", ".join(parts) if parts else "empty"
@@ -0,0 +1,79 @@
1
+ """Epic domain model.
2
+
3
+ Represents an epic in the HCD documentation system.
4
+ Epics are defined via RST directives and group related stories together.
5
+ """
6
+
7
+ from pydantic import BaseModel, Field, field_validator
8
+
9
+ from ...utils import normalize_name
10
+
11
+
12
+ class Epic(BaseModel):
13
+ """Epic entity.
14
+
15
+ An epic represents a collection of related stories that together
16
+ deliver a larger piece of functionality or business value.
17
+
18
+ Attributes:
19
+ slug: URL-safe identifier (e.g., "credential-creation")
20
+ description: Human-readable description of the epic
21
+ story_refs: List of story feature titles in this epic
22
+ docname: RST document name (for incremental builds)
23
+ """
24
+
25
+ slug: str
26
+ description: str = ""
27
+ story_refs: list[str] = Field(default_factory=list)
28
+ docname: str = ""
29
+
30
+ @field_validator("slug", mode="before")
31
+ @classmethod
32
+ def validate_slug(cls, v: str) -> str:
33
+ """Validate slug is not empty."""
34
+ if not v or not v.strip():
35
+ raise ValueError("slug cannot be empty")
36
+ return v.strip()
37
+
38
+ def add_story(self, story_title: str) -> None:
39
+ """Add a story reference to this epic.
40
+
41
+ Args:
42
+ story_title: Feature title of the story to add
43
+ """
44
+ self.story_refs.append(story_title)
45
+
46
+ def has_story(self, story_title: str) -> bool:
47
+ """Check if this epic contains a specific story.
48
+
49
+ Args:
50
+ story_title: Feature title to check (case-insensitive)
51
+
52
+ Returns:
53
+ True if the story is in this epic
54
+ """
55
+ story_normalized = normalize_name(story_title)
56
+ return any(normalize_name(ref) == story_normalized for ref in self.story_refs)
57
+
58
+ def get_story_refs_normalized(self) -> list[str]:
59
+ """Get normalized story references.
60
+
61
+ Returns:
62
+ List of normalized story titles
63
+ """
64
+ return [normalize_name(ref) for ref in self.story_refs]
65
+
66
+ @property
67
+ def display_title(self) -> str:
68
+ """Get formatted title for display."""
69
+ return self.slug.replace("-", " ").title()
70
+
71
+ @property
72
+ def story_count(self) -> int:
73
+ """Get number of stories in this epic."""
74
+ return len(self.story_refs)
75
+
76
+ @property
77
+ def has_stories(self) -> bool:
78
+ """Check if epic has any stories."""
79
+ return len(self.story_refs) > 0