atdd 0.5.0__py3-none-any.whl → 0.6.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/coach/schemas/config.schema.json +63 -0
- atdd/coach/templates/ATDD.md +1 -1
- atdd/coach/utils/coverage_phase.py +97 -0
- atdd/coach/validators/shared_fixtures.py +154 -0
- atdd/coder/conventions/backend.convention.yaml +1 -1
- atdd/coder/conventions/boundaries.convention.yaml +9 -9
- atdd/coder/conventions/coverage.convention.yaml +85 -0
- atdd/coder/conventions/presentation.convention.yaml +8 -8
- atdd/coder/conventions/train.convention.yaml +15 -14
- atdd/coder/validators/conftest.py +5 -0
- atdd/coder/validators/test_hierarchy_coverage.py +361 -0
- atdd/coder/validators/test_presentation_convention.py +11 -11
- atdd/coder/validators/test_station_master_pattern.py +16 -14
- atdd/coder/validators/test_train_infrastructure.py +22 -14
- atdd/coder/validators/test_wagon_boundaries.py +2 -2
- atdd/planner/conventions/coverage.convention.yaml +95 -0
- atdd/planner/validators/test_hierarchy_coverage.py +433 -0
- atdd/tester/conventions/coverage.convention.yaml +114 -0
- atdd/tester/validators/test_hierarchy_coverage.py +604 -0
- {atdd-0.5.0.dist-info → atdd-0.6.1.dist-info}/METADATA +1 -1
- {atdd-0.5.0.dist-info → atdd-0.6.1.dist-info}/RECORD +25 -17
- {atdd-0.5.0.dist-info → atdd-0.6.1.dist-info}/WHEEL +0 -0
- {atdd-0.5.0.dist-info → atdd-0.6.1.dist-info}/entry_points.txt +0 -0
- {atdd-0.5.0.dist-info → atdd-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {atdd-0.5.0.dist-info → atdd-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Coder hierarchy coverage validation.
|
|
3
|
+
|
|
4
|
+
ATDD Hierarchy Coverage Spec v0.1 - Section 4: Coder Coverage Rules
|
|
5
|
+
|
|
6
|
+
Validates:
|
|
7
|
+
- Feature <-> Implementation (COVERAGE-CODE-4.1)
|
|
8
|
+
- Implementation <-> Tests (COVERAGE-CODE-4.2)
|
|
9
|
+
|
|
10
|
+
Architecture:
|
|
11
|
+
- Uses shared fixtures from atdd.coach.validators.shared_fixtures
|
|
12
|
+
- Phased rollout via atdd.coach.utils.coverage_phase
|
|
13
|
+
- Exception handling via .atdd/config.yaml coverage.exceptions
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Dict, List, Set, Tuple, Any
|
|
19
|
+
|
|
20
|
+
from atdd.coach.utils.repo import find_repo_root
|
|
21
|
+
from atdd.coach.utils.coverage_phase import (
|
|
22
|
+
CoveragePhase,
|
|
23
|
+
should_enforce,
|
|
24
|
+
emit_coverage_warning
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Path constants
|
|
29
|
+
REPO_ROOT = find_repo_root()
|
|
30
|
+
PLAN_DIR = REPO_ROOT / "plan"
|
|
31
|
+
PYTHON_DIR = REPO_ROOT / "python"
|
|
32
|
+
SUPABASE_DIR = REPO_ROOT / "supabase"
|
|
33
|
+
WEB_DIR = REPO_ROOT / "web"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ============================================================================
|
|
37
|
+
# HELPER FUNCTIONS
|
|
38
|
+
# ============================================================================
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def find_python_implementations(wagon_slug: str, feature_slug: str) -> List[Path]:
|
|
42
|
+
"""
|
|
43
|
+
Find Python implementation files for a feature.
|
|
44
|
+
|
|
45
|
+
Searches for:
|
|
46
|
+
- python/{wagon}/use_case_{feature}.py
|
|
47
|
+
- python/{wagon}/service_{feature}.py
|
|
48
|
+
- python/{wagon}/{feature}_handler.py
|
|
49
|
+
- python/{wagon}/{feature}.py
|
|
50
|
+
"""
|
|
51
|
+
implementations = []
|
|
52
|
+
|
|
53
|
+
# Convert slugs to filesystem format
|
|
54
|
+
wagon_dir = wagon_slug.replace("-", "_")
|
|
55
|
+
feature_file = feature_slug.replace("-", "_")
|
|
56
|
+
|
|
57
|
+
wagon_path = PYTHON_DIR / wagon_dir
|
|
58
|
+
if not wagon_path.exists():
|
|
59
|
+
return implementations
|
|
60
|
+
|
|
61
|
+
# Check various patterns
|
|
62
|
+
patterns = [
|
|
63
|
+
f"use_case_{feature_file}.py",
|
|
64
|
+
f"service_{feature_file}.py",
|
|
65
|
+
f"{feature_file}_handler.py",
|
|
66
|
+
f"{feature_file}.py",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
for pattern in patterns:
|
|
70
|
+
impl_path = wagon_path / pattern
|
|
71
|
+
if impl_path.exists():
|
|
72
|
+
implementations.append(impl_path)
|
|
73
|
+
|
|
74
|
+
# Also search subdirectories
|
|
75
|
+
for subdir in wagon_path.iterdir():
|
|
76
|
+
if subdir.is_dir() and not subdir.name.startswith("_"):
|
|
77
|
+
for pattern in patterns:
|
|
78
|
+
impl_path = subdir / pattern
|
|
79
|
+
if impl_path.exists():
|
|
80
|
+
implementations.append(impl_path)
|
|
81
|
+
|
|
82
|
+
return implementations
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def find_typescript_implementations(wagon_slug: str, feature_slug: str) -> List[Path]:
|
|
86
|
+
"""
|
|
87
|
+
Find TypeScript implementation files for a feature.
|
|
88
|
+
|
|
89
|
+
Searches for:
|
|
90
|
+
- supabase/functions/{wagon}/{feature}/index.ts
|
|
91
|
+
- supabase/functions/{wagon}/{feature}/handler.ts
|
|
92
|
+
- supabase/functions/{wagon}/{feature}.ts
|
|
93
|
+
"""
|
|
94
|
+
implementations = []
|
|
95
|
+
|
|
96
|
+
functions_dir = SUPABASE_DIR / "functions"
|
|
97
|
+
if not functions_dir.exists():
|
|
98
|
+
return implementations
|
|
99
|
+
|
|
100
|
+
# Check various structures
|
|
101
|
+
# Pattern 1: supabase/functions/{wagon}/{feature}/
|
|
102
|
+
feature_dir = functions_dir / wagon_slug / feature_slug
|
|
103
|
+
if feature_dir.exists():
|
|
104
|
+
for pattern in ["index.ts", "handler.ts"]:
|
|
105
|
+
impl_path = feature_dir / pattern
|
|
106
|
+
if impl_path.exists():
|
|
107
|
+
implementations.append(impl_path)
|
|
108
|
+
|
|
109
|
+
# Pattern 2: supabase/functions/{wagon}/{feature}.ts
|
|
110
|
+
wagon_dir = functions_dir / wagon_slug
|
|
111
|
+
if wagon_dir.exists():
|
|
112
|
+
feature_file = wagon_dir / f"{feature_slug}.ts"
|
|
113
|
+
if feature_file.exists():
|
|
114
|
+
implementations.append(feature_file)
|
|
115
|
+
|
|
116
|
+
return implementations
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def find_web_implementations(wagon_slug: str, feature_slug: str) -> List[Path]:
|
|
120
|
+
"""
|
|
121
|
+
Find web/frontend implementation files for a feature.
|
|
122
|
+
|
|
123
|
+
Searches for:
|
|
124
|
+
- web/src/features/{wagon}/{feature}/
|
|
125
|
+
- web/src/components/{feature}/
|
|
126
|
+
"""
|
|
127
|
+
implementations = []
|
|
128
|
+
|
|
129
|
+
# Pattern 1: web/src/features/{wagon}/{feature}/
|
|
130
|
+
features_dir = WEB_DIR / "src" / "features" / wagon_slug / feature_slug
|
|
131
|
+
if features_dir.exists():
|
|
132
|
+
for pattern in ["index.tsx", "index.ts", f"{feature_slug}.tsx"]:
|
|
133
|
+
impl_path = features_dir / pattern
|
|
134
|
+
if impl_path.exists():
|
|
135
|
+
implementations.append(impl_path)
|
|
136
|
+
|
|
137
|
+
# Pattern 2: web/src/components/{feature}/
|
|
138
|
+
components_dir = WEB_DIR / "src" / "components" / feature_slug
|
|
139
|
+
if components_dir.exists():
|
|
140
|
+
implementations.append(components_dir)
|
|
141
|
+
|
|
142
|
+
return implementations
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def has_implementation(wagon_slug: str, feature_slug: str) -> bool:
|
|
146
|
+
"""
|
|
147
|
+
Check if a feature has any implementation.
|
|
148
|
+
"""
|
|
149
|
+
python_impls = find_python_implementations(wagon_slug, feature_slug)
|
|
150
|
+
ts_impls = find_typescript_implementations(wagon_slug, feature_slug)
|
|
151
|
+
web_impls = find_web_implementations(wagon_slug, feature_slug)
|
|
152
|
+
|
|
153
|
+
return len(python_impls) > 0 or len(ts_impls) > 0 or len(web_impls) > 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def find_tests_for_implementation(impl_path: Path) -> List[Path]:
|
|
157
|
+
"""
|
|
158
|
+
Find test files that might test an implementation.
|
|
159
|
+
"""
|
|
160
|
+
tests = []
|
|
161
|
+
|
|
162
|
+
if not impl_path.exists():
|
|
163
|
+
return tests
|
|
164
|
+
|
|
165
|
+
# For Python implementations
|
|
166
|
+
if impl_path.suffix == ".py":
|
|
167
|
+
impl_dir = impl_path.parent
|
|
168
|
+
impl_name = impl_path.stem
|
|
169
|
+
|
|
170
|
+
# Look for test_*.py in same directory
|
|
171
|
+
for test_file in impl_dir.glob("test_*.py"):
|
|
172
|
+
if impl_name in test_file.stem:
|
|
173
|
+
tests.append(test_file)
|
|
174
|
+
|
|
175
|
+
# Look for test file with matching name
|
|
176
|
+
test_file = impl_dir / f"test_{impl_name}.py"
|
|
177
|
+
if test_file.exists() and test_file not in tests:
|
|
178
|
+
tests.append(test_file)
|
|
179
|
+
|
|
180
|
+
# For TypeScript implementations
|
|
181
|
+
elif impl_path.suffix == ".ts":
|
|
182
|
+
impl_dir = impl_path.parent
|
|
183
|
+
|
|
184
|
+
# Look for *.test.ts in same directory or test/ subdirectory
|
|
185
|
+
for test_file in impl_dir.glob("*.test.ts"):
|
|
186
|
+
tests.append(test_file)
|
|
187
|
+
|
|
188
|
+
test_dir = impl_dir / "test"
|
|
189
|
+
if test_dir.exists():
|
|
190
|
+
for test_file in test_dir.glob("*.test.ts"):
|
|
191
|
+
tests.append(test_file)
|
|
192
|
+
|
|
193
|
+
return tests
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ============================================================================
|
|
197
|
+
# COVERAGE-CODE-4.1: Feature <-> Implementation Coverage
|
|
198
|
+
# ============================================================================
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.mark.coder
|
|
202
|
+
def test_all_features_have_implementations(feature_files, coverage_exceptions):
|
|
203
|
+
"""
|
|
204
|
+
COVERAGE-CODE-4.1: Every feature has implementation code.
|
|
205
|
+
|
|
206
|
+
Given: Feature files in plan/*/features/
|
|
207
|
+
When: Searching for corresponding implementation files
|
|
208
|
+
Then: Every feature has at least one implementation in python/, supabase/, or web/
|
|
209
|
+
"""
|
|
210
|
+
allowed_features = set(coverage_exceptions.get("features_without_implementation", []))
|
|
211
|
+
violations = []
|
|
212
|
+
|
|
213
|
+
for path, feature_data in feature_files:
|
|
214
|
+
# Get wagon slug from path
|
|
215
|
+
wagon_dir = path.parent.parent.name # plan/{wagon}/features/{feature}.yaml
|
|
216
|
+
wagon_slug = wagon_dir.replace("_", "-")
|
|
217
|
+
|
|
218
|
+
# Get feature slug
|
|
219
|
+
feature_slug = path.stem.replace("_", "-")
|
|
220
|
+
feature_urn = feature_data.get("urn", f"feature:{wagon_slug}:{feature_slug}")
|
|
221
|
+
|
|
222
|
+
# Skip draft features
|
|
223
|
+
status = feature_data.get("status", "")
|
|
224
|
+
if status == "draft":
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
# Skip allowed exceptions
|
|
228
|
+
if feature_urn in allowed_features or feature_slug in allowed_features:
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
# Check for implementations
|
|
232
|
+
if not has_implementation(wagon_slug, feature_slug):
|
|
233
|
+
violations.append(
|
|
234
|
+
f"{feature_urn}: no implementation found in python/, supabase/, or web/"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if violations:
|
|
238
|
+
if should_enforce(CoveragePhase.FULL_ENFORCEMENT):
|
|
239
|
+
pytest.fail(
|
|
240
|
+
f"COVERAGE-CODE-4.1: Features without implementations:\n " +
|
|
241
|
+
"\n ".join(violations[:20]) +
|
|
242
|
+
(f"\n ... and {len(violations) - 20} more" if len(violations) > 20 else "") +
|
|
243
|
+
"\n\nImplement feature or add to coverage.exceptions.features_without_implementation"
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
for violation in violations[:10]:
|
|
247
|
+
emit_coverage_warning(
|
|
248
|
+
"COVERAGE-CODE-4.1",
|
|
249
|
+
violation,
|
|
250
|
+
CoveragePhase.FULL_ENFORCEMENT
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# ============================================================================
|
|
255
|
+
# COVERAGE-CODE-4.2: Implementation <-> Tests Coverage
|
|
256
|
+
# ============================================================================
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@pytest.mark.coder
|
|
260
|
+
def test_all_implementations_have_tests(feature_files):
|
|
261
|
+
"""
|
|
262
|
+
COVERAGE-CODE-4.2: Every implementation has at least one test.
|
|
263
|
+
|
|
264
|
+
Given: Feature implementations in python/, supabase/, web/
|
|
265
|
+
When: Searching for corresponding test files
|
|
266
|
+
Then: Every implementation has at least one test file
|
|
267
|
+
"""
|
|
268
|
+
violations = []
|
|
269
|
+
|
|
270
|
+
for path, feature_data in feature_files:
|
|
271
|
+
# Get wagon and feature slugs
|
|
272
|
+
wagon_dir = path.parent.parent.name
|
|
273
|
+
wagon_slug = wagon_dir.replace("_", "-")
|
|
274
|
+
feature_slug = path.stem.replace("_", "-")
|
|
275
|
+
|
|
276
|
+
# Skip draft features
|
|
277
|
+
status = feature_data.get("status", "")
|
|
278
|
+
if status == "draft":
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
# Find all implementations for this feature
|
|
282
|
+
all_impls = (
|
|
283
|
+
find_python_implementations(wagon_slug, feature_slug) +
|
|
284
|
+
find_typescript_implementations(wagon_slug, feature_slug)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
for impl_path in all_impls:
|
|
288
|
+
tests = find_tests_for_implementation(impl_path)
|
|
289
|
+
if not tests:
|
|
290
|
+
violations.append(
|
|
291
|
+
f"{impl_path.relative_to(REPO_ROOT)}: no tests found"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if violations:
|
|
295
|
+
if should_enforce(CoveragePhase.FULL_ENFORCEMENT):
|
|
296
|
+
pytest.fail(
|
|
297
|
+
f"COVERAGE-CODE-4.2: Implementations without tests:\n " +
|
|
298
|
+
"\n ".join(violations[:20]) +
|
|
299
|
+
(f"\n ... and {len(violations) - 20} more" if len(violations) > 20 else "") +
|
|
300
|
+
"\n\nAdd tests for the implementation"
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
for violation in violations[:10]:
|
|
304
|
+
emit_coverage_warning(
|
|
305
|
+
"COVERAGE-CODE-4.2",
|
|
306
|
+
violation,
|
|
307
|
+
CoveragePhase.FULL_ENFORCEMENT
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ============================================================================
|
|
312
|
+
# COVERAGE SUMMARY
|
|
313
|
+
# ============================================================================
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@pytest.mark.coder
|
|
317
|
+
def test_coder_coverage_summary(feature_files):
|
|
318
|
+
"""
|
|
319
|
+
COVERAGE-CODE-SUMMARY: Report coder coverage statistics.
|
|
320
|
+
|
|
321
|
+
This test always passes but reports coverage metrics for visibility.
|
|
322
|
+
"""
|
|
323
|
+
total_features = len(feature_files)
|
|
324
|
+
features_with_impl = 0
|
|
325
|
+
total_implementations = 0
|
|
326
|
+
implementations_with_tests = 0
|
|
327
|
+
|
|
328
|
+
for path, feature_data in feature_files:
|
|
329
|
+
wagon_dir = path.parent.parent.name
|
|
330
|
+
wagon_slug = wagon_dir.replace("_", "-")
|
|
331
|
+
feature_slug = path.stem.replace("_", "-")
|
|
332
|
+
|
|
333
|
+
# Count implementations
|
|
334
|
+
python_impls = find_python_implementations(wagon_slug, feature_slug)
|
|
335
|
+
ts_impls = find_typescript_implementations(wagon_slug, feature_slug)
|
|
336
|
+
web_impls = find_web_implementations(wagon_slug, feature_slug)
|
|
337
|
+
|
|
338
|
+
all_impls = python_impls + ts_impls + web_impls
|
|
339
|
+
if all_impls:
|
|
340
|
+
features_with_impl += 1
|
|
341
|
+
total_implementations += len(all_impls)
|
|
342
|
+
|
|
343
|
+
# Count implementations with tests
|
|
344
|
+
for impl_path in python_impls + ts_impls:
|
|
345
|
+
if find_tests_for_implementation(impl_path):
|
|
346
|
+
implementations_with_tests += 1
|
|
347
|
+
|
|
348
|
+
# Calculate percentages
|
|
349
|
+
feature_impl_pct = (features_with_impl / total_features * 100) if total_features > 0 else 0
|
|
350
|
+
impl_test_pct = (implementations_with_tests / total_implementations * 100) if total_implementations > 0 else 0
|
|
351
|
+
|
|
352
|
+
# Report summary
|
|
353
|
+
summary = (
|
|
354
|
+
f"\n\nCoder Coverage Summary:\n"
|
|
355
|
+
f" Features with implementations: {features_with_impl}/{total_features} ({feature_impl_pct:.1f}%)\n"
|
|
356
|
+
f" Total implementations: {total_implementations}\n"
|
|
357
|
+
f" Implementations with tests: {implementations_with_tests}/{total_implementations} ({impl_test_pct:.1f}%)"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# This test always passes - it's informational
|
|
361
|
+
assert True, summary
|
|
@@ -180,16 +180,16 @@ class PresentationValidator:
|
|
|
180
180
|
|
|
181
181
|
|
|
182
182
|
def validate_game_server_registration(self):
|
|
183
|
-
"""Validate python/
|
|
184
|
-
|
|
183
|
+
"""Validate python/app.py includes all wagons with presentation."""
|
|
184
|
+
server_file = self.python_root / "app.py"
|
|
185
185
|
|
|
186
|
-
if not
|
|
187
|
-
self.violations.append("❌ python/
|
|
186
|
+
if not server_file.exists():
|
|
187
|
+
self.violations.append("❌ python/app.py not found - unified server missing")
|
|
188
188
|
return
|
|
189
189
|
|
|
190
|
-
print(
|
|
190
|
+
print("\nValidating unified server: python/app.py")
|
|
191
191
|
|
|
192
|
-
content =
|
|
192
|
+
content = server_file.read_text()
|
|
193
193
|
|
|
194
194
|
# Find all wagons with FastAPI controllers
|
|
195
195
|
wagons_with_controllers = {}
|
|
@@ -209,7 +209,7 @@ class PresentationValidator:
|
|
|
209
209
|
if "fastapi" in controller_file.read_text().lower():
|
|
210
210
|
wagons_with_controllers[f"{wagon_dir.name}/{feature_dir.name}"] = controller_file
|
|
211
211
|
|
|
212
|
-
# Check if each controller is registered in
|
|
212
|
+
# Check if each controller is registered in the unified server file
|
|
213
213
|
for wagon_feature, controller_file in wagons_with_controllers.items():
|
|
214
214
|
wagon, feature = wagon_feature.split('/')
|
|
215
215
|
|
|
@@ -217,17 +217,17 @@ class PresentationValidator:
|
|
|
217
217
|
import_pattern = f"from {wagon}.{feature}.src.presentation.controllers"
|
|
218
218
|
if import_pattern not in content:
|
|
219
219
|
self.violations.append(
|
|
220
|
-
f"❌
|
|
220
|
+
f"❌ app.py missing import for {wagon}/{feature} controller"
|
|
221
221
|
)
|
|
222
222
|
|
|
223
223
|
# Check for include_router
|
|
224
224
|
if "include_router" in content and wagon not in content.lower():
|
|
225
225
|
self.violations.append(
|
|
226
|
-
f"⚠️
|
|
226
|
+
f"⚠️ app.py may not be registering {wagon}/{feature} routes"
|
|
227
227
|
)
|
|
228
228
|
|
|
229
229
|
if not self.violations:
|
|
230
|
-
print(f" ✓ All {len(wagons_with_controllers)} wagons registered in
|
|
230
|
+
print(f" ✓ All {len(wagons_with_controllers)} wagons registered in app.py")
|
|
231
231
|
|
|
232
232
|
|
|
233
233
|
def main():
|
|
@@ -236,7 +236,7 @@ def main():
|
|
|
236
236
|
|
|
237
237
|
parser = argparse.ArgumentParser(description="Validate presentation layer convention")
|
|
238
238
|
parser.add_argument("--check-game-server", action="store_true",
|
|
239
|
-
help="Validate python/
|
|
239
|
+
help="Validate python/app.py is up to date")
|
|
240
240
|
args = parser.parse_args()
|
|
241
241
|
|
|
242
242
|
python_root = REPO_ROOT / "python"
|
|
@@ -4,7 +4,7 @@ Station Master Pattern Validator
|
|
|
4
4
|
Validates that wagons follow the Station Master pattern for monolith composition:
|
|
5
5
|
1. composition.py accepts optional shared dependency parameters
|
|
6
6
|
2. Direct adapters exist for cross-wagon data access
|
|
7
|
-
3.
|
|
7
|
+
3. app.py delegates to composition.py instead of duplicating wiring
|
|
8
8
|
|
|
9
9
|
Convention: atdd/coder/conventions/boundaries.convention.yaml::station_master_pattern
|
|
10
10
|
"""
|
|
@@ -21,6 +21,9 @@ def get_python_dir() -> Path:
|
|
|
21
21
|
"""Get the python directory path in the consumer repo."""
|
|
22
22
|
return find_repo_root() / "python"
|
|
23
23
|
|
|
24
|
+
def resolve_server_file() -> Path:
|
|
25
|
+
"""Resolve station master entrypoint (app.py)."""
|
|
26
|
+
return get_python_dir() / "app.py"
|
|
24
27
|
|
|
25
28
|
def test_composition_accepts_shared_dependencies():
|
|
26
29
|
"""
|
|
@@ -161,12 +164,12 @@ def test_direct_adapters_exist_for_cross_wagon_clients():
|
|
|
161
164
|
|
|
162
165
|
def test_game_py_delegates_to_composition():
|
|
163
166
|
"""
|
|
164
|
-
Validate that
|
|
167
|
+
Validate that app.py delegates wiring to wagon composition.py files
|
|
165
168
|
instead of duplicating wiring logic.
|
|
166
169
|
|
|
167
170
|
Convention: boundaries.convention.yaml::station_master_pattern.station_master_responsibilities
|
|
168
171
|
|
|
169
|
-
Forbidden patterns in
|
|
172
|
+
Forbidden patterns in app.py:
|
|
170
173
|
- Creating use cases that composition.py should own
|
|
171
174
|
- Directly instantiating wagon clients without delegation
|
|
172
175
|
|
|
@@ -174,14 +177,13 @@ def test_game_py_delegates_to_composition():
|
|
|
174
177
|
- from wagon.composition import wire_api_dependencies
|
|
175
178
|
- wire_api_dependencies(state_repository=..., ...)
|
|
176
179
|
"""
|
|
177
|
-
|
|
178
|
-
game_py = python_dir / "game.py"
|
|
180
|
+
server_file = resolve_server_file()
|
|
179
181
|
|
|
180
|
-
if not
|
|
181
|
-
print("
|
|
182
|
+
if not server_file.exists():
|
|
183
|
+
print("app.py not found - skipping Station Master delegation check")
|
|
182
184
|
return
|
|
183
185
|
|
|
184
|
-
source =
|
|
186
|
+
source = server_file.read_text()
|
|
185
187
|
|
|
186
188
|
# Check for composition imports
|
|
187
189
|
imports_composition = "from play_match.orchestrate_match.composition import wire_api_dependencies" in source
|
|
@@ -190,7 +192,7 @@ def test_game_py_delegates_to_composition():
|
|
|
190
192
|
calls_wire_api = "wire_api_dependencies(" in source
|
|
191
193
|
|
|
192
194
|
# Check for forbidden patterns (duplicated wiring)
|
|
193
|
-
# These are patterns that should be in composition.py, not
|
|
195
|
+
# These are patterns that should be in composition.py, not app.py
|
|
194
196
|
forbidden_patterns = [
|
|
195
197
|
("PlayMatchUseCase(", "PlayMatchUseCase should be created in composition.py"),
|
|
196
198
|
("CommitStateClient(mode=", "CommitStateClient mode should be set in composition.py"),
|
|
@@ -204,7 +206,7 @@ def test_game_py_delegates_to_composition():
|
|
|
204
206
|
|
|
205
207
|
# Report results
|
|
206
208
|
print("\n" + "=" * 70)
|
|
207
|
-
print(" Station Master Pattern:
|
|
209
|
+
print(" Station Master Pattern: app.py Delegation")
|
|
208
210
|
print("=" * 70)
|
|
209
211
|
|
|
210
212
|
print(f"\nDelegation to composition.py:")
|
|
@@ -212,19 +214,19 @@ def test_game_py_delegates_to_composition():
|
|
|
212
214
|
print(f" {'✓' if calls_wire_api else '❌'} Calls wire_api_dependencies()")
|
|
213
215
|
|
|
214
216
|
if violations:
|
|
215
|
-
print(
|
|
217
|
+
print("\n❌ Violations found in app.py:")
|
|
216
218
|
for pattern, message in violations:
|
|
217
219
|
print(f" ❌ {message}")
|
|
218
220
|
print(f" Found: {pattern}")
|
|
219
221
|
|
|
220
222
|
# This is a real validation
|
|
221
223
|
assert imports_composition or not calls_wire_api, \
|
|
222
|
-
"
|
|
224
|
+
"app.py should import wire_api_dependencies from composition.py"
|
|
223
225
|
|
|
224
226
|
assert len(violations) == 0, \
|
|
225
|
-
f"
|
|
227
|
+
f"app.py has {len(violations)} Station Master pattern violations"
|
|
226
228
|
|
|
227
|
-
print("\n✓
|
|
229
|
+
print("\n✓ app.py follows Station Master pattern")
|
|
228
230
|
|
|
229
231
|
|
|
230
232
|
def main():
|
|
@@ -11,7 +11,7 @@ Enforces:
|
|
|
11
11
|
- Wagons implement run_train() for train mode
|
|
12
12
|
- Contract validator is real (not mock)
|
|
13
13
|
- E2E tests use production TrainRunner
|
|
14
|
-
- Station Master pattern in
|
|
14
|
+
- Station Master pattern in app.py
|
|
15
15
|
|
|
16
16
|
Train First-Class Spec v0.6 additions:
|
|
17
17
|
- SPEC-TRAIN-VAL-0031: Backend runner paths
|
|
@@ -45,7 +45,7 @@ from atdd.coach.utils.config import get_train_config
|
|
|
45
45
|
REPO_ROOT = find_repo_root()
|
|
46
46
|
TRAINS_DIR = REPO_ROOT / "python" / "trains"
|
|
47
47
|
WAGONS_DIR = REPO_ROOT / "python"
|
|
48
|
-
|
|
48
|
+
APP_PY = REPO_ROOT / "python" / "app.py"
|
|
49
49
|
E2E_CONFTEST = REPO_ROOT / "e2e" / "conftest.py"
|
|
50
50
|
CONTRACT_VALIDATOR = REPO_ROOT / "e2e" / "shared" / "fixtures" / "contract_validator.py"
|
|
51
51
|
|
|
@@ -110,6 +110,11 @@ def extract_imports_from_file(file_path: Path) -> Set[str]:
|
|
|
110
110
|
return imports
|
|
111
111
|
|
|
112
112
|
|
|
113
|
+
def resolve_server_file() -> Path:
|
|
114
|
+
"""Resolve station master entrypoint (app.py)."""
|
|
115
|
+
return APP_PY
|
|
116
|
+
|
|
117
|
+
|
|
113
118
|
# ============================================================================
|
|
114
119
|
# TRAIN INFRASTRUCTURE TESTS
|
|
115
120
|
# ============================================================================
|
|
@@ -247,45 +252,48 @@ def test_wagons_implement_run_train():
|
|
|
247
252
|
|
|
248
253
|
|
|
249
254
|
# ============================================================================
|
|
250
|
-
# STATION MASTER TESTS (
|
|
255
|
+
# STATION MASTER TESTS (app.py)
|
|
251
256
|
# ============================================================================
|
|
252
257
|
|
|
253
258
|
def test_game_py_imports_train_runner():
|
|
254
|
-
"""
|
|
255
|
-
|
|
259
|
+
"""app.py must import TrainRunner (Station Master pattern)."""
|
|
260
|
+
server_file = resolve_server_file()
|
|
261
|
+
assert server_file.exists(), f"app.py not found: {server_file}"
|
|
256
262
|
|
|
257
|
-
imports = extract_imports_from_file(
|
|
263
|
+
imports = extract_imports_from_file(server_file)
|
|
258
264
|
|
|
259
265
|
has_train_import = any("trains.runner import TrainRunner" in imp for imp in imports)
|
|
260
266
|
|
|
261
267
|
assert has_train_import, (
|
|
262
|
-
"
|
|
268
|
+
"app.py must import TrainRunner\n"
|
|
263
269
|
"Expected: from trains.runner import TrainRunner\n"
|
|
264
270
|
"See: atdd/coder/conventions/train.convention.yaml::station_master"
|
|
265
271
|
)
|
|
266
272
|
|
|
267
273
|
|
|
268
274
|
def test_game_py_has_journey_map():
|
|
269
|
-
"""
|
|
270
|
-
|
|
275
|
+
"""app.py must have JOURNEY_MAP routing actions to trains."""
|
|
276
|
+
server_file = resolve_server_file()
|
|
277
|
+
with open(server_file, 'r', encoding='utf-8') as f:
|
|
271
278
|
content = f.read()
|
|
272
279
|
|
|
273
280
|
assert "JOURNEY_MAP" in content, (
|
|
274
|
-
"
|
|
281
|
+
"app.py must define JOURNEY_MAP dictionary\n"
|
|
275
282
|
"Expected: JOURNEY_MAP = {'action': 'train_id', ...}\n"
|
|
276
283
|
"See: atdd/coder/conventions/train.convention.yaml::station_master"
|
|
277
284
|
)
|
|
278
285
|
|
|
279
286
|
|
|
280
287
|
def test_game_py_has_train_execution_endpoint():
|
|
281
|
-
"""
|
|
282
|
-
|
|
288
|
+
"""app.py must have /trains/execute endpoint."""
|
|
289
|
+
server_file = resolve_server_file()
|
|
290
|
+
with open(server_file, 'r', encoding='utf-8') as f:
|
|
283
291
|
content = f.read()
|
|
284
292
|
|
|
285
293
|
has_endpoint = '"/trains/execute"' in content or "'/trains/execute'" in content
|
|
286
294
|
|
|
287
295
|
assert has_endpoint, (
|
|
288
|
-
"
|
|
296
|
+
"app.py must have /trains/execute endpoint\n"
|
|
289
297
|
"Expected: @app.post('/trains/execute')\n"
|
|
290
298
|
"See: atdd/coder/conventions/train.convention.yaml::station_master"
|
|
291
299
|
)
|
|
@@ -407,7 +415,7 @@ def test_train_convention_documents_key_patterns():
|
|
|
407
415
|
- composition_hierarchy (with train level)
|
|
408
416
|
- wagon_train_mode (run_train signature)
|
|
409
417
|
- cargo_pattern (artifact flow)
|
|
410
|
-
- station_master (
|
|
418
|
+
- station_master (app.py pattern)
|
|
411
419
|
- testing_pattern (E2E tests)
|
|
412
420
|
"""
|
|
413
421
|
with open(TRAIN_CONVENTION, 'r', encoding='utf-8') as f:
|
|
@@ -78,8 +78,8 @@ def find_implementation_files() -> List[Path]:
|
|
|
78
78
|
# Skip __pycache__
|
|
79
79
|
if '__pycache__' in str(py_file):
|
|
80
80
|
continue
|
|
81
|
-
# Skip wagon.py, composition.py, and
|
|
82
|
-
if py_file.name in ['wagon.py', 'composition.py', '
|
|
81
|
+
# Skip wagon.py, composition.py, and app entrypoint (wagon/app-level orchestration)
|
|
82
|
+
if py_file.name in ['wagon.py', 'composition.py', 'app.py']:
|
|
83
83
|
continue
|
|
84
84
|
# Skip shared/ directory (theme/train-level orchestration - can import across wagons)
|
|
85
85
|
if '/shared/' in str(py_file):
|