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.
- atdd/__init__.py +6 -0
- atdd/__main__.py +4 -0
- atdd/cli.py +404 -0
- atdd/coach/__init__.py +0 -0
- atdd/coach/commands/__init__.py +0 -0
- atdd/coach/commands/add_persistence_metadata.py +215 -0
- atdd/coach/commands/analyze_migrations.py +188 -0
- atdd/coach/commands/consumers.py +720 -0
- atdd/coach/commands/infer_governance_status.py +149 -0
- atdd/coach/commands/initializer.py +177 -0
- atdd/coach/commands/interface.py +1078 -0
- atdd/coach/commands/inventory.py +565 -0
- atdd/coach/commands/migration.py +240 -0
- atdd/coach/commands/registry.py +1560 -0
- atdd/coach/commands/session.py +430 -0
- atdd/coach/commands/sync.py +405 -0
- atdd/coach/commands/test_interface.py +399 -0
- atdd/coach/commands/test_runner.py +141 -0
- atdd/coach/commands/tests/__init__.py +1 -0
- atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
- atdd/coach/commands/traceability.py +4264 -0
- atdd/coach/conventions/session.convention.yaml +754 -0
- atdd/coach/overlays/__init__.py +2 -0
- atdd/coach/overlays/claude.md +2 -0
- atdd/coach/schemas/config.schema.json +34 -0
- atdd/coach/schemas/manifest.schema.json +101 -0
- atdd/coach/templates/ATDD.md +282 -0
- atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
- atdd/coach/utils/__init__.py +0 -0
- atdd/coach/utils/graph/__init__.py +0 -0
- atdd/coach/utils/graph/urn.py +875 -0
- atdd/coach/validators/__init__.py +0 -0
- atdd/coach/validators/shared_fixtures.py +365 -0
- atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
- atdd/coach/validators/test_registry.py +575 -0
- atdd/coach/validators/test_session_validation.py +1183 -0
- atdd/coach/validators/test_traceability.py +448 -0
- atdd/coach/validators/test_update_feature_paths.py +108 -0
- atdd/coach/validators/test_validate_contract_consumers.py +297 -0
- atdd/coder/__init__.py +1 -0
- atdd/coder/conventions/adapter.recipe.yaml +88 -0
- atdd/coder/conventions/backend.convention.yaml +460 -0
- atdd/coder/conventions/boundaries.convention.yaml +666 -0
- atdd/coder/conventions/commons.convention.yaml +460 -0
- atdd/coder/conventions/complexity.recipe.yaml +109 -0
- atdd/coder/conventions/component-naming.convention.yaml +178 -0
- atdd/coder/conventions/design.convention.yaml +327 -0
- atdd/coder/conventions/design.recipe.yaml +273 -0
- atdd/coder/conventions/dto.convention.yaml +660 -0
- atdd/coder/conventions/frontend.convention.yaml +542 -0
- atdd/coder/conventions/green.convention.yaml +1012 -0
- atdd/coder/conventions/presentation.convention.yaml +587 -0
- atdd/coder/conventions/refactor.convention.yaml +535 -0
- atdd/coder/conventions/technology.convention.yaml +206 -0
- atdd/coder/conventions/tests/__init__.py +0 -0
- atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
- atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
- atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
- atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
- atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
- atdd/coder/conventions/thinness.recipe.yaml +82 -0
- atdd/coder/conventions/train.convention.yaml +325 -0
- atdd/coder/conventions/verification.protocol.yaml +53 -0
- atdd/coder/schemas/design_system.schema.json +361 -0
- atdd/coder/validators/__init__.py +0 -0
- atdd/coder/validators/test_commons_structure.py +485 -0
- atdd/coder/validators/test_complexity.py +416 -0
- atdd/coder/validators/test_cross_language_consistency.py +431 -0
- atdd/coder/validators/test_design_system_compliance.py +413 -0
- atdd/coder/validators/test_dto_testing_patterns.py +268 -0
- atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
- atdd/coder/validators/test_green_layer_dependencies.py +148 -0
- atdd/coder/validators/test_green_python_layer_structure.py +103 -0
- atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
- atdd/coder/validators/test_import_boundaries.py +396 -0
- atdd/coder/validators/test_init_file_urns.py +593 -0
- atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
- atdd/coder/validators/test_presentation_convention.py +260 -0
- atdd/coder/validators/test_python_architecture.py +674 -0
- atdd/coder/validators/test_quality_metrics.py +420 -0
- atdd/coder/validators/test_station_master_pattern.py +244 -0
- atdd/coder/validators/test_train_infrastructure.py +454 -0
- atdd/coder/validators/test_train_urns.py +293 -0
- atdd/coder/validators/test_typescript_architecture.py +616 -0
- atdd/coder/validators/test_usecase_structure.py +421 -0
- atdd/coder/validators/test_wagon_boundaries.py +586 -0
- atdd/conftest.py +126 -0
- atdd/planner/__init__.py +1 -0
- atdd/planner/conventions/acceptance.convention.yaml +538 -0
- atdd/planner/conventions/appendix.convention.yaml +187 -0
- atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
- atdd/planner/conventions/component.convention.yaml +670 -0
- atdd/planner/conventions/criteria.convention.yaml +141 -0
- atdd/planner/conventions/feature.convention.yaml +371 -0
- atdd/planner/conventions/interface.convention.yaml +382 -0
- atdd/planner/conventions/steps.convention.yaml +141 -0
- atdd/planner/conventions/train.convention.yaml +552 -0
- atdd/planner/conventions/wagon.convention.yaml +275 -0
- atdd/planner/conventions/wmbt.convention.yaml +258 -0
- atdd/planner/schemas/acceptance.schema.json +336 -0
- atdd/planner/schemas/appendix.schema.json +78 -0
- atdd/planner/schemas/component.schema.json +114 -0
- atdd/planner/schemas/feature.schema.json +197 -0
- atdd/planner/schemas/train.schema.json +192 -0
- atdd/planner/schemas/wagon.schema.json +281 -0
- atdd/planner/schemas/wmbt.schema.json +59 -0
- atdd/planner/validators/__init__.py +0 -0
- atdd/planner/validators/conftest.py +5 -0
- atdd/planner/validators/test_draft_wagon_registry.py +374 -0
- atdd/planner/validators/test_plan_cross_refs.py +240 -0
- atdd/planner/validators/test_plan_uniqueness.py +224 -0
- atdd/planner/validators/test_plan_urn_resolution.py +268 -0
- atdd/planner/validators/test_plan_wagons.py +174 -0
- atdd/planner/validators/test_train_validation.py +514 -0
- atdd/planner/validators/test_wagon_urn_chain.py +648 -0
- atdd/planner/validators/test_wmbt_consistency.py +327 -0
- atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
- atdd/tester/__init__.py +1 -0
- atdd/tester/conventions/artifact.convention.yaml +257 -0
- atdd/tester/conventions/contract.convention.yaml +1009 -0
- atdd/tester/conventions/filename.convention.yaml +555 -0
- atdd/tester/conventions/migration.convention.yaml +509 -0
- atdd/tester/conventions/red.convention.yaml +797 -0
- atdd/tester/conventions/routing.convention.yaml +51 -0
- atdd/tester/conventions/telemetry.convention.yaml +458 -0
- atdd/tester/schemas/a11y.tmpl.json +17 -0
- atdd/tester/schemas/artifact.schema.json +189 -0
- atdd/tester/schemas/contract.schema.json +591 -0
- atdd/tester/schemas/contract.tmpl.json +95 -0
- atdd/tester/schemas/db.tmpl.json +20 -0
- atdd/tester/schemas/e2e.tmpl.json +17 -0
- atdd/tester/schemas/edge_function.tmpl.json +17 -0
- atdd/tester/schemas/event.tmpl.json +17 -0
- atdd/tester/schemas/http.tmpl.json +19 -0
- atdd/tester/schemas/job.tmpl.json +18 -0
- atdd/tester/schemas/load.tmpl.json +21 -0
- atdd/tester/schemas/metric.tmpl.json +19 -0
- atdd/tester/schemas/pack.schema.json +139 -0
- atdd/tester/schemas/realtime.tmpl.json +20 -0
- atdd/tester/schemas/rls.tmpl.json +18 -0
- atdd/tester/schemas/script.tmpl.json +16 -0
- atdd/tester/schemas/sec.tmpl.json +18 -0
- atdd/tester/schemas/storage.tmpl.json +18 -0
- atdd/tester/schemas/telemetry.schema.json +128 -0
- atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
- atdd/tester/schemas/test_filename.schema.json +194 -0
- atdd/tester/schemas/test_intent.schema.json +179 -0
- atdd/tester/schemas/unit.tmpl.json +18 -0
- atdd/tester/schemas/visual.tmpl.json +18 -0
- atdd/tester/schemas/ws.tmpl.json +17 -0
- atdd/tester/utils/__init__.py +0 -0
- atdd/tester/utils/filename.py +300 -0
- atdd/tester/validators/__init__.py +0 -0
- atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
- atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
- atdd/tester/validators/conftest.py +5 -0
- atdd/tester/validators/coverage_gap_report.py +321 -0
- atdd/tester/validators/fix_dual_ac_references.py +179 -0
- atdd/tester/validators/remove_duplicate_lines.py +93 -0
- atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
- atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
- atdd/tester/validators/test_artifact_naming_category.py +307 -0
- atdd/tester/validators/test_contract_schema_compliance.py +706 -0
- atdd/tester/validators/test_contracts_structure.py +200 -0
- atdd/tester/validators/test_coverage_adequacy.py +797 -0
- atdd/tester/validators/test_dual_ac_reference.py +225 -0
- atdd/tester/validators/test_fixture_validity.py +372 -0
- atdd/tester/validators/test_isolation.py +487 -0
- atdd/tester/validators/test_migration_coverage.py +204 -0
- atdd/tester/validators/test_migration_criteria.py +276 -0
- atdd/tester/validators/test_migration_generation.py +116 -0
- atdd/tester/validators/test_python_test_naming.py +410 -0
- atdd/tester/validators/test_red_layer_validation.py +95 -0
- atdd/tester/validators/test_red_python_layer_structure.py +87 -0
- atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
- atdd/tester/validators/test_telemetry_structure.py +634 -0
- atdd/tester/validators/test_typescript_test_naming.py +301 -0
- atdd/tester/validators/test_typescript_test_structure.py +84 -0
- atdd-0.2.1.dist-info/METADATA +221 -0
- atdd-0.2.1.dist-info/RECORD +184 -0
- atdd-0.2.1.dist-info/WHEEL +5 -0
- atdd-0.2.1.dist-info/entry_points.txt +2 -0
- atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
- 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()
|