atdd 0.2.1__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 (184) hide show
  1. atdd/__init__.py +6 -0
  2. atdd/__main__.py +4 -0
  3. atdd/cli.py +404 -0
  4. atdd/coach/__init__.py +0 -0
  5. atdd/coach/commands/__init__.py +0 -0
  6. atdd/coach/commands/add_persistence_metadata.py +215 -0
  7. atdd/coach/commands/analyze_migrations.py +188 -0
  8. atdd/coach/commands/consumers.py +720 -0
  9. atdd/coach/commands/infer_governance_status.py +149 -0
  10. atdd/coach/commands/initializer.py +177 -0
  11. atdd/coach/commands/interface.py +1078 -0
  12. atdd/coach/commands/inventory.py +565 -0
  13. atdd/coach/commands/migration.py +240 -0
  14. atdd/coach/commands/registry.py +1560 -0
  15. atdd/coach/commands/session.py +430 -0
  16. atdd/coach/commands/sync.py +405 -0
  17. atdd/coach/commands/test_interface.py +399 -0
  18. atdd/coach/commands/test_runner.py +141 -0
  19. atdd/coach/commands/tests/__init__.py +1 -0
  20. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  21. atdd/coach/commands/traceability.py +4264 -0
  22. atdd/coach/conventions/session.convention.yaml +754 -0
  23. atdd/coach/overlays/__init__.py +2 -0
  24. atdd/coach/overlays/claude.md +2 -0
  25. atdd/coach/schemas/config.schema.json +34 -0
  26. atdd/coach/schemas/manifest.schema.json +101 -0
  27. atdd/coach/templates/ATDD.md +282 -0
  28. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  29. atdd/coach/utils/__init__.py +0 -0
  30. atdd/coach/utils/graph/__init__.py +0 -0
  31. atdd/coach/utils/graph/urn.py +875 -0
  32. atdd/coach/validators/__init__.py +0 -0
  33. atdd/coach/validators/shared_fixtures.py +365 -0
  34. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  35. atdd/coach/validators/test_registry.py +575 -0
  36. atdd/coach/validators/test_session_validation.py +1183 -0
  37. atdd/coach/validators/test_traceability.py +448 -0
  38. atdd/coach/validators/test_update_feature_paths.py +108 -0
  39. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  40. atdd/coder/__init__.py +1 -0
  41. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  42. atdd/coder/conventions/backend.convention.yaml +460 -0
  43. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  44. atdd/coder/conventions/commons.convention.yaml +460 -0
  45. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  46. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  47. atdd/coder/conventions/design.convention.yaml +327 -0
  48. atdd/coder/conventions/design.recipe.yaml +273 -0
  49. atdd/coder/conventions/dto.convention.yaml +660 -0
  50. atdd/coder/conventions/frontend.convention.yaml +542 -0
  51. atdd/coder/conventions/green.convention.yaml +1012 -0
  52. atdd/coder/conventions/presentation.convention.yaml +587 -0
  53. atdd/coder/conventions/refactor.convention.yaml +535 -0
  54. atdd/coder/conventions/technology.convention.yaml +206 -0
  55. atdd/coder/conventions/tests/__init__.py +0 -0
  56. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  57. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  58. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  59. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  60. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  61. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  62. atdd/coder/conventions/train.convention.yaml +325 -0
  63. atdd/coder/conventions/verification.protocol.yaml +53 -0
  64. atdd/coder/schemas/design_system.schema.json +361 -0
  65. atdd/coder/validators/__init__.py +0 -0
  66. atdd/coder/validators/test_commons_structure.py +485 -0
  67. atdd/coder/validators/test_complexity.py +416 -0
  68. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  69. atdd/coder/validators/test_design_system_compliance.py +413 -0
  70. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  71. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  72. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  73. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  74. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  75. atdd/coder/validators/test_import_boundaries.py +396 -0
  76. atdd/coder/validators/test_init_file_urns.py +593 -0
  77. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  78. atdd/coder/validators/test_presentation_convention.py +260 -0
  79. atdd/coder/validators/test_python_architecture.py +674 -0
  80. atdd/coder/validators/test_quality_metrics.py +420 -0
  81. atdd/coder/validators/test_station_master_pattern.py +244 -0
  82. atdd/coder/validators/test_train_infrastructure.py +454 -0
  83. atdd/coder/validators/test_train_urns.py +293 -0
  84. atdd/coder/validators/test_typescript_architecture.py +616 -0
  85. atdd/coder/validators/test_usecase_structure.py +421 -0
  86. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  87. atdd/conftest.py +126 -0
  88. atdd/planner/__init__.py +1 -0
  89. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  90. atdd/planner/conventions/appendix.convention.yaml +187 -0
  91. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  92. atdd/planner/conventions/component.convention.yaml +670 -0
  93. atdd/planner/conventions/criteria.convention.yaml +141 -0
  94. atdd/planner/conventions/feature.convention.yaml +371 -0
  95. atdd/planner/conventions/interface.convention.yaml +382 -0
  96. atdd/planner/conventions/steps.convention.yaml +141 -0
  97. atdd/planner/conventions/train.convention.yaml +552 -0
  98. atdd/planner/conventions/wagon.convention.yaml +275 -0
  99. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  100. atdd/planner/schemas/acceptance.schema.json +336 -0
  101. atdd/planner/schemas/appendix.schema.json +78 -0
  102. atdd/planner/schemas/component.schema.json +114 -0
  103. atdd/planner/schemas/feature.schema.json +197 -0
  104. atdd/planner/schemas/train.schema.json +192 -0
  105. atdd/planner/schemas/wagon.schema.json +281 -0
  106. atdd/planner/schemas/wmbt.schema.json +59 -0
  107. atdd/planner/validators/__init__.py +0 -0
  108. atdd/planner/validators/conftest.py +5 -0
  109. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  110. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  111. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  112. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  113. atdd/planner/validators/test_plan_wagons.py +174 -0
  114. atdd/planner/validators/test_train_validation.py +514 -0
  115. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  116. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  117. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  118. atdd/tester/__init__.py +1 -0
  119. atdd/tester/conventions/artifact.convention.yaml +257 -0
  120. atdd/tester/conventions/contract.convention.yaml +1009 -0
  121. atdd/tester/conventions/filename.convention.yaml +555 -0
  122. atdd/tester/conventions/migration.convention.yaml +509 -0
  123. atdd/tester/conventions/red.convention.yaml +797 -0
  124. atdd/tester/conventions/routing.convention.yaml +51 -0
  125. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  126. atdd/tester/schemas/a11y.tmpl.json +17 -0
  127. atdd/tester/schemas/artifact.schema.json +189 -0
  128. atdd/tester/schemas/contract.schema.json +591 -0
  129. atdd/tester/schemas/contract.tmpl.json +95 -0
  130. atdd/tester/schemas/db.tmpl.json +20 -0
  131. atdd/tester/schemas/e2e.tmpl.json +17 -0
  132. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  133. atdd/tester/schemas/event.tmpl.json +17 -0
  134. atdd/tester/schemas/http.tmpl.json +19 -0
  135. atdd/tester/schemas/job.tmpl.json +18 -0
  136. atdd/tester/schemas/load.tmpl.json +21 -0
  137. atdd/tester/schemas/metric.tmpl.json +19 -0
  138. atdd/tester/schemas/pack.schema.json +139 -0
  139. atdd/tester/schemas/realtime.tmpl.json +20 -0
  140. atdd/tester/schemas/rls.tmpl.json +18 -0
  141. atdd/tester/schemas/script.tmpl.json +16 -0
  142. atdd/tester/schemas/sec.tmpl.json +18 -0
  143. atdd/tester/schemas/storage.tmpl.json +18 -0
  144. atdd/tester/schemas/telemetry.schema.json +128 -0
  145. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  146. atdd/tester/schemas/test_filename.schema.json +194 -0
  147. atdd/tester/schemas/test_intent.schema.json +179 -0
  148. atdd/tester/schemas/unit.tmpl.json +18 -0
  149. atdd/tester/schemas/visual.tmpl.json +18 -0
  150. atdd/tester/schemas/ws.tmpl.json +17 -0
  151. atdd/tester/utils/__init__.py +0 -0
  152. atdd/tester/utils/filename.py +300 -0
  153. atdd/tester/validators/__init__.py +0 -0
  154. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  155. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  156. atdd/tester/validators/conftest.py +5 -0
  157. atdd/tester/validators/coverage_gap_report.py +321 -0
  158. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  159. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  160. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  161. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  162. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  163. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  164. atdd/tester/validators/test_contracts_structure.py +200 -0
  165. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  166. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  167. atdd/tester/validators/test_fixture_validity.py +372 -0
  168. atdd/tester/validators/test_isolation.py +487 -0
  169. atdd/tester/validators/test_migration_coverage.py +204 -0
  170. atdd/tester/validators/test_migration_criteria.py +276 -0
  171. atdd/tester/validators/test_migration_generation.py +116 -0
  172. atdd/tester/validators/test_python_test_naming.py +410 -0
  173. atdd/tester/validators/test_red_layer_validation.py +95 -0
  174. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  175. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  176. atdd/tester/validators/test_telemetry_structure.py +634 -0
  177. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  178. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  179. atdd-0.2.1.dist-info/METADATA +221 -0
  180. atdd-0.2.1.dist-info/RECORD +184 -0
  181. atdd-0.2.1.dist-info/WHEEL +5 -0
  182. atdd-0.2.1.dist-info/entry_points.txt +2 -0
  183. atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
  184. atdd-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,221 @@
1
+ """
2
+ Test Preact/web frontend follows 4-layer clean architecture boundaries.
3
+
4
+ Validates Preact-specific architectural rules:
5
+ - Domain layer is framework-agnostic (no Preact/React imports)
6
+ - Application layer has no JSX (no .tsx files)
7
+ - Presentation layer doesn't bypass application layer
8
+ - Component tests use correct file extensions
9
+
10
+ Location: web/src/
11
+ Convention: atdd/coder/conventions/frontend.convention.yaml (preact section)
12
+
13
+ This complements test_typescript_architecture.py which handles Supabase backend TypeScript.
14
+ """
15
+
16
+ import pytest
17
+ import re
18
+ from pathlib import Path
19
+ from typing import List
20
+
21
+
22
+ # Path constants
23
+ REPO_ROOT = Path(__file__).resolve().parents[4] # Go up 3 levels: file -> audits -> coder -> atdd -> repo
24
+ WEB_SRC = REPO_ROOT / "web" / "src"
25
+ WEB_TESTS = REPO_ROOT / "web" / "tests"
26
+
27
+
28
+ def get_typescript_files() -> List[Path]:
29
+ """Find all TypeScript files in web/src/"""
30
+ if not WEB_SRC.exists():
31
+ return []
32
+ return list(WEB_SRC.rglob("*.ts")) + list(WEB_SRC.rglob("*.tsx"))
33
+
34
+
35
+ def get_layer(file_path: Path) -> str:
36
+ """Determine layer from file path"""
37
+ parts = file_path.parts
38
+ if "presentation" in parts:
39
+ return "presentation"
40
+ elif "application" in parts:
41
+ return "application"
42
+ elif "domain" in parts:
43
+ return "domain"
44
+ elif "integration" in parts:
45
+ return "integration"
46
+ return "unknown"
47
+
48
+
49
+ # Pre-filtered layer getters (SESSION-44: reduce skips by filtering at collection time)
50
+ def get_domain_files() -> List[Path]:
51
+ """Get only domain layer TypeScript files from web/src"""
52
+ return [f for f in get_typescript_files() if get_layer(f) == "domain"]
53
+
54
+
55
+ def get_presentation_files() -> List[Path]:
56
+ """Get only presentation layer TypeScript files from web/src"""
57
+ return [f for f in get_typescript_files() if get_layer(f) == "presentation"]
58
+
59
+
60
+ def get_application_files() -> List[Path]:
61
+ """Get only application layer TypeScript files from web/src"""
62
+ return [f for f in get_typescript_files() if get_layer(f) == "application"]
63
+
64
+
65
+ def get_test_files() -> List[Path]:
66
+ """Find all test files in web/tests/"""
67
+ if not WEB_TESTS.exists():
68
+ return []
69
+ return list(WEB_TESTS.rglob("*.test.ts")) + list(WEB_TESTS.rglob("*.test.tsx"))
70
+
71
+
72
+ def extract_imports(file_path: Path) -> List[str]:
73
+ """Extract import statements from TypeScript file"""
74
+ try:
75
+ content = file_path.read_text(encoding='utf-8')
76
+ except Exception:
77
+ return []
78
+
79
+ import_pattern = r"import\s+.+\s+from\s+['\"](.+)['\"]"
80
+ return re.findall(import_pattern, content)
81
+
82
+
83
+ @pytest.mark.parametrize("ts_file", get_domain_files())
84
+ def test_domain_layer_has_no_framework_imports(ts_file):
85
+ """
86
+ SPEC-CODER-ARCH-PREACT-001: Domain layer must not import UI frameworks
87
+
88
+ GIVEN: TypeScript file in domain layer (web/src/.../domain/)
89
+ WHEN: Analyzing imports
90
+ THEN: No imports from 'preact', 'react', or design system aliases
91
+
92
+ Rationale: Domain layer must be framework-agnostic for testability and portability
93
+ """
94
+ imports = extract_imports(ts_file)
95
+
96
+ forbidden = ['preact', 'react', '@maintain-ux']
97
+ violations = [imp for imp in imports if any(f in imp for f in forbidden)]
98
+
99
+ assert not violations, (
100
+ f"Domain layer file {ts_file.relative_to(REPO_ROOT)} has forbidden imports: {violations}\n"
101
+ f"Domain must be framework-agnostic (no UI imports)"
102
+ )
103
+
104
+
105
+ @pytest.mark.parametrize("ts_file", get_presentation_files())
106
+ def test_presentation_cannot_import_integration(ts_file):
107
+ """
108
+ SPEC-CODER-ARCH-PREACT-002: Presentation must not bypass application layer
109
+
110
+ GIVEN: TypeScript file in presentation layer (web/src/.../presentation/)
111
+ WHEN: Analyzing imports
112
+ THEN: No direct imports from integration layer
113
+
114
+ Rationale: Presentation must use application layer (hooks/use cases), not call APIs directly
115
+ """
116
+ imports = extract_imports(ts_file)
117
+
118
+ violations = [imp for imp in imports if '/integration/' in imp or '../integration' in imp]
119
+
120
+ assert not violations, (
121
+ f"Presentation file {ts_file.relative_to(REPO_ROOT)} imports integration layer: {violations}\n"
122
+ f"Presentation must use application layer (use cases/hooks), not APIs directly"
123
+ )
124
+
125
+
126
+ @pytest.mark.parametrize("ts_file", get_application_files())
127
+ def test_application_layer_has_no_jsx(ts_file):
128
+ """
129
+ SPEC-CODER-ARCH-PREACT-003: Application layer must not contain JSX
130
+
131
+ GIVEN: TypeScript file in application layer (web/src/.../application/)
132
+ WHEN: Checking file extension and content
133
+ THEN: No .tsx files, no JSX syntax
134
+
135
+ Rationale: Application layer orchestrates business logic, doesn't render UI
136
+ """
137
+
138
+ # Application should never use .tsx extension
139
+ assert ts_file.suffix != ".tsx", (
140
+ f"Application file {ts_file.relative_to(REPO_ROOT)} uses .tsx extension\n"
141
+ f"Application layer orchestrates, doesn't render UI (use .ts)"
142
+ )
143
+
144
+ # Check for JSX syntax
145
+ try:
146
+ content = ts_file.read_text(encoding='utf-8')
147
+ except Exception:
148
+ return
149
+
150
+ # JSX patterns (excluding TypeScript generics)
151
+ jsx_patterns = [
152
+ r'<[A-Z]\w+', # Component tags: <Component
153
+ r'</\w+>', # Closing tags: </div>
154
+ r'<\w+\s+\w+=', # Tags with attributes: <div className=
155
+ r'<\w+\s*/>', # Self-closing tags: <div />
156
+ r'<>\s*', # Fragment: <>
157
+ r'</>\s*', # Fragment close: </>
158
+ ]
159
+
160
+ # TypeScript generic patterns to exclude
161
+ ts_generic_patterns = [
162
+ r'\bPromise<',
163
+ r'\bArray<',
164
+ r'\bRecord<',
165
+ r'\bSet<',
166
+ r'\bMap<',
167
+ r'\bPartial<',
168
+ r'\bReadonly<',
169
+ r'\bOmit<',
170
+ r'\bPick<',
171
+ r'\bExtract<',
172
+ r'\bExclude<',
173
+ r'\bReturnType<',
174
+ r'\bParameters<',
175
+ r'\bcreateContext<', # React/Preact createContext generic
176
+ r'\buseState<', # React/Preact useState generic
177
+ r'\buseRef<', # React/Preact useRef generic
178
+ r'\buseMemo<', # React/Preact useMemo generic
179
+ r'\buseCallback<', # React/Preact useCallback generic
180
+ ]
181
+
182
+ # Remove TypeScript generic patterns from content
183
+ cleaned_content = content
184
+ for pattern in ts_generic_patterns:
185
+ cleaned_content = re.sub(pattern, '', cleaned_content)
186
+
187
+ # Check for JSX in cleaned content
188
+ has_jsx = any(re.search(pattern, cleaned_content) for pattern in jsx_patterns)
189
+
190
+ assert not has_jsx, (
191
+ f"Application file {ts_file.relative_to(REPO_ROOT)} contains JSX\n"
192
+ f"Application layer should not render components"
193
+ )
194
+
195
+
196
+ @pytest.mark.parametrize("test_file", get_test_files())
197
+ def test_component_tests_use_tsx_extension(test_file):
198
+ """
199
+ SPEC-CODER-ARCH-PREACT-004: Component tests must use .test.tsx
200
+
201
+ GIVEN: Test file with 'render' from @testing-library
202
+ WHEN: Checking file extension
203
+ THEN: File has .tsx extension
204
+
205
+ Rationale: Component tests with JSX should use .test.tsx for proper TypeScript handling
206
+ """
207
+ try:
208
+ content = test_file.read_text(encoding='utf-8')
209
+ except Exception:
210
+ pytest.skip("Cannot read file")
211
+
212
+ has_render = "from '@testing-library/preact'" in content
213
+ # Match JSX tags (not TypeScript generics) - look for tags that start with uppercase or lowercase
214
+ # JSX: <div>, <Component> | Not JSX: <string>, <Record<string, unknown>>
215
+ has_jsx = bool(re.search(r'<[A-Z][a-zA-Z]*[\s/>]', content)) or bool(re.search(r'<[a-z]+[\s/>]', content))
216
+
217
+ if has_render or has_jsx:
218
+ assert test_file.suffix == ".tsx", (
219
+ f"Test file {test_file.relative_to(REPO_ROOT)} uses component testing but has .ts extension\n"
220
+ f"Component tests with JSX should use .test.tsx"
221
+ )
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Presentation Convention Validator
4
+
5
+ Validates presentation layer compliance per presentation.convention.yaml:
6
+ - FastAPI controller structure
7
+ - Pydantic models aligned with contracts
8
+ - Composition integration (CLI/HTTP modes)
9
+ - GREEN simplifications with TODO markers
10
+
11
+ Usage:
12
+ python3 atdd/coder/test_presentation_convention.py
13
+ """
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import List, Tuple, Optional
17
+ import re
18
+
19
+ # Project root constant (pytest pythonpath handles imports)
20
+ REPO_ROOT = Path(__file__).resolve().parents[4]
21
+
22
+
23
+ class PresentationValidator:
24
+ """Validates presentation layer against convention."""
25
+
26
+ def __init__(self, python_root: Path):
27
+ self.python_root = python_root
28
+ self.violations = []
29
+
30
+ def validate_all_wagons(self) -> List[str]:
31
+ """Validate all wagons in python/ directory."""
32
+ print("=" * 70)
33
+ print("PRESENTATION CONVENTION VALIDATION")
34
+ print("=" * 70)
35
+ print()
36
+
37
+ wagons = [d for d in self.python_root.iterdir() if d.is_dir() and not d.name.startswith('_')]
38
+
39
+ for wagon_dir in sorted(wagons):
40
+ if wagon_dir.name in ['shared', 'contracts', 'tools']:
41
+ continue
42
+
43
+ self._validate_wagon(wagon_dir)
44
+
45
+ return self.violations
46
+
47
+ def _validate_wagon(self, wagon_dir: Path):
48
+ """Validate a single wagon's presentation layer."""
49
+ features = [d for d in wagon_dir.iterdir() if d.is_dir() and not d.name.startswith('_')]
50
+
51
+ for feature_dir in features:
52
+ self._validate_feature(wagon_dir.name, feature_dir)
53
+
54
+ def _validate_feature(self, wagon_name: str, feature_dir: Path):
55
+ """Validate a single feature's presentation layer."""
56
+ presentation_dir = feature_dir / "src" / "presentation"
57
+
58
+ if not presentation_dir.exists():
59
+ # No presentation layer is valid (domain-only features)
60
+ return
61
+
62
+ # Find controller files
63
+ controllers_dir = presentation_dir / "controllers"
64
+ if not controllers_dir.exists():
65
+ return
66
+
67
+ for controller_file in controllers_dir.glob("*.py"):
68
+ if controller_file.name == "__init__.py":
69
+ continue
70
+
71
+ self._validate_controller(wagon_name, feature_dir.name, controller_file)
72
+
73
+ # Validate composition.py integration
74
+ composition_file = feature_dir / "composition.py"
75
+ if composition_file.exists():
76
+ self._validate_composition_integration(wagon_name, feature_dir.name, composition_file)
77
+
78
+ def _validate_controller(self, wagon: str, feature: str, controller_file: Path):
79
+ """Validate a FastAPI controller file."""
80
+ content = controller_file.read_text()
81
+
82
+ # Check for FastAPI usage
83
+ if "from fastapi import" not in content and "import fastapi" not in content:
84
+ # Not a FastAPI controller
85
+ return
86
+
87
+ print(f"Validating FastAPI controller: {wagon}/{feature}/{controller_file.name}")
88
+
89
+ # Check URN marker
90
+ urn_pattern = r"# urn: component:[a-z-]+:[a-z-]+\.[A-Za-z]+\.backend\.presentation"
91
+ if not re.search(urn_pattern, content):
92
+ self.violations.append(
93
+ f"❌ {wagon}/{feature}: Missing URN marker in {controller_file.name}"
94
+ )
95
+
96
+ # Check Pydantic response model
97
+ if "from pydantic import BaseModel" not in content and "from pydantic import" not in content:
98
+ self.violations.append(
99
+ f"❌ {wagon}/{feature}: FastAPI controller missing Pydantic imports in {controller_file.name}"
100
+ )
101
+
102
+ # Check for response model with artifact_name field
103
+ if "artifact_name" not in content:
104
+ self.violations.append(
105
+ f"⚠️ {wagon}/{feature}: Response model should have artifact_name field in {controller_file.name}"
106
+ )
107
+
108
+ # Check for endpoint decorators
109
+ if "@app.get" not in content and "@app.post" not in content and "@app.put" not in content:
110
+ self.violations.append(
111
+ f"❌ {wagon}/{feature}: No FastAPI endpoint decorators found in {controller_file.name}"
112
+ )
113
+
114
+ # Check for summary and tags in decorators
115
+ has_endpoints = bool(re.search(r"@app\.(get|post|put|delete)", content))
116
+ if has_endpoints:
117
+ if "summary=" not in content:
118
+ self.violations.append(
119
+ f"⚠️ {wagon}/{feature}: Endpoints should have summary parameter in {controller_file.name}"
120
+ )
121
+ if "tags=" not in content:
122
+ self.violations.append(
123
+ f"⚠️ {wagon}/{feature}: Endpoints should have tags parameter in {controller_file.name}"
124
+ )
125
+
126
+ # Check for GREEN simplifications with TODO markers
127
+ if re.search(r"^[^#]*global\s+\w+", content, re.MULTILINE):
128
+ # Has global state
129
+ if "TODO(REFACTOR)" not in content:
130
+ self.violations.append(
131
+ f"⚠️ {wagon}/{feature}: Global state should have TODO(REFACTOR) marker in {controller_file.name}"
132
+ )
133
+
134
+ # Check Field usage for schema documentation
135
+ if "BaseModel" in content and "Field(" not in content:
136
+ self.violations.append(
137
+ f"⚠️ {wagon}/{feature}: Pydantic models should use Field() for descriptions in {controller_file.name}"
138
+ )
139
+
140
+ print(f" ✓ Controller structure validated")
141
+
142
+ def _validate_composition_integration(self, wagon: str, feature: str, composition_file: Path):
143
+ """Validate composition.py supports CLI and HTTP modes."""
144
+ content = composition_file.read_text()
145
+
146
+ # Check for mode parameter
147
+ if 'mode' in content and ('cli' in content or 'http' in content):
148
+ print(f" ✓ Composition supports dual CLI/HTTP mode")
149
+
150
+ # Check for uvicorn
151
+ if 'http' in content and 'uvicorn' not in content:
152
+ self.violations.append(
153
+ f"⚠️ {wagon}/{feature}: HTTP mode should use uvicorn.run() in composition.py"
154
+ )
155
+
156
+ # Check for controller import in HTTP mode
157
+ if 'http' in content and 'from src.presentation.controllers' not in content:
158
+ self.violations.append(
159
+ f"⚠️ {wagon}/{feature}: HTTP mode should import controller in composition.py"
160
+ )
161
+
162
+ def print_summary(self):
163
+ """Print validation summary."""
164
+ print()
165
+ print("=" * 70)
166
+ print("VALIDATION SUMMARY")
167
+ print("=" * 70)
168
+
169
+ if not self.violations:
170
+ print("✅ All presentation layers comply with convention!")
171
+ return 0
172
+ else:
173
+ print(f"Found {len(self.violations)} issue(s):\n")
174
+ for violation in self.violations:
175
+ print(f" {violation}")
176
+ print()
177
+ return 1
178
+
179
+
180
+ def validate_game_server_registration(self):
181
+ """Validate python/game.py includes all wagons with presentation."""
182
+ game_file = self.python_root / "game.py"
183
+
184
+ if not game_file.exists():
185
+ self.violations.append("❌ python/game.py not found - unified game server missing")
186
+ return
187
+
188
+ print(f"\nValidating unified game server: python/game.py")
189
+
190
+ content = game_file.read_text()
191
+
192
+ # Find all wagons with FastAPI controllers
193
+ wagons_with_controllers = {}
194
+ for wagon_dir in self.python_root.iterdir():
195
+ if not wagon_dir.is_dir() or wagon_dir.name.startswith('_'):
196
+ continue
197
+ if wagon_dir.name in ['shared', 'contracts', 'tools', 'data']:
198
+ continue
199
+
200
+ for feature_dir in wagon_dir.iterdir():
201
+ if not feature_dir.is_dir():
202
+ continue
203
+
204
+ controller_dir = feature_dir / "src" / "presentation" / "controllers"
205
+ if controller_dir.exists():
206
+ for controller_file in controller_dir.glob("*_controller.py"):
207
+ if "fastapi" in controller_file.read_text().lower():
208
+ wagons_with_controllers[f"{wagon_dir.name}/{feature_dir.name}"] = controller_file
209
+
210
+ # Check if each controller is registered in game.py
211
+ for wagon_feature, controller_file in wagons_with_controllers.items():
212
+ wagon, feature = wagon_feature.split('/')
213
+
214
+ # Check for import statement
215
+ import_pattern = f"from {wagon}.{feature}.src.presentation.controllers"
216
+ if import_pattern not in content:
217
+ self.violations.append(
218
+ f"❌ game.py missing import for {wagon}/{feature} controller"
219
+ )
220
+
221
+ # Check for include_router
222
+ if "include_router" in content and wagon not in content.lower():
223
+ self.violations.append(
224
+ f"⚠️ game.py may not be registering {wagon}/{feature} routes"
225
+ )
226
+
227
+ if not self.violations:
228
+ print(f" ✓ All {len(wagons_with_controllers)} wagons registered in game.py")
229
+
230
+
231
+ def main():
232
+ """Run presentation convention validation."""
233
+ import argparse
234
+
235
+ parser = argparse.ArgumentParser(description="Validate presentation layer convention")
236
+ parser.add_argument("--check-game-server", action="store_true",
237
+ help="Validate python/game.py is up to date")
238
+ args = parser.parse_args()
239
+
240
+ python_root = REPO_ROOT / "python"
241
+
242
+ if not python_root.exists():
243
+ print(f"❌ Python directory not found: {python_root}")
244
+ sys.exit(1)
245
+
246
+ validator = PresentationValidator(python_root)
247
+
248
+ if args.check_game_server:
249
+ validator.validate_game_server_registration()
250
+ else:
251
+ validator.validate_all_wagons()
252
+ validator.validate_game_server_registration()
253
+
254
+ exit_code = validator.print_summary()
255
+
256
+ sys.exit(exit_code)
257
+
258
+
259
+ if __name__ == "__main__":
260
+ main()