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.
@@ -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/game.py includes all wagons with presentation."""
184
- game_file = self.python_root / "game.py"
183
+ """Validate python/app.py includes all wagons with presentation."""
184
+ server_file = self.python_root / "app.py"
185
185
 
186
- if not game_file.exists():
187
- self.violations.append("❌ python/game.py not found - unified game server missing")
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(f"\nValidating unified game server: python/game.py")
190
+ print("\nValidating unified server: python/app.py")
191
191
 
192
- content = game_file.read_text()
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 game.py
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"❌ game.py missing import for {wagon}/{feature} controller"
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"⚠️ game.py may not be registering {wagon}/{feature} routes"
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 game.py")
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/game.py is up to date")
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. game.py delegates to composition.py instead of duplicating wiring
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 game.py delegates wiring to wagon composition.py files
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 game.py:
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
- python_dir = get_python_dir()
178
- game_py = python_dir / "game.py"
180
+ server_file = resolve_server_file()
179
181
 
180
- if not game_py.exists():
181
- print("game.py not found - skipping Station Master delegation check")
182
+ if not server_file.exists():
183
+ print("app.py not found - skipping Station Master delegation check")
182
184
  return
183
185
 
184
- source = game_py.read_text()
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 game.py
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: game.py Delegation")
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(f"\n❌ Violations found in game.py:")
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
- "game.py should import wire_api_dependencies from composition.py"
224
+ "app.py should import wire_api_dependencies from composition.py"
223
225
 
224
226
  assert len(violations) == 0, \
225
- f"game.py has {len(violations)} Station Master pattern violations"
227
+ f"app.py has {len(violations)} Station Master pattern violations"
226
228
 
227
- print("\n✓ game.py follows Station Master pattern")
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 game.py
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
- GAME_PY = REPO_ROOT / "python" / "game.py"
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 (game.py)
255
+ # STATION MASTER TESTS (app.py)
251
256
  # ============================================================================
252
257
 
253
258
  def test_game_py_imports_train_runner():
254
- """game.py must import TrainRunner (Station Master pattern)."""
255
- assert GAME_PY.exists(), f"game.py not found: {GAME_PY}"
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(GAME_PY)
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
- "game.py must import TrainRunner\n"
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
- """game.py must have JOURNEY_MAP routing actions to trains."""
270
- with open(GAME_PY, 'r', encoding='utf-8') as f:
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
- "game.py must define JOURNEY_MAP dictionary\n"
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
- """game.py must have /trains/execute endpoint."""
282
- with open(GAME_PY, 'r', encoding='utf-8') as f:
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
- "game.py must have /trains/execute endpoint\n"
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 (game.py pattern)
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 game.py (wagon/app-level orchestration)
82
- if py_file.name in ['wagon.py', 'composition.py', 'game.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):