atdd 0.6.0__py3-none-any.whl → 0.7.0__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.
@@ -121,6 +121,17 @@
121
121
  }
122
122
  },
123
123
  "additionalProperties": false
124
+ },
125
+ "localization": {
126
+ "type": "object",
127
+ "description": "Localization validation settings (Localization Manifest Spec v1)",
128
+ "properties": {
129
+ "manifest": {
130
+ "type": "string",
131
+ "description": "Path to localization manifest.json relative to repo root (e.g., web/locales/manifest.json)"
132
+ }
133
+ },
134
+ "additionalProperties": false
124
135
  }
125
136
  },
126
137
  "required": ["version", "release"],
@@ -71,7 +71,7 @@ code:
71
71
  # Dev Servers
72
72
  dev_servers:
73
73
  backend:
74
- command: "cd python && python3 game.py"
74
+ command: "cd python && python3 app.py"
75
75
  url: "http://127.0.0.1:8000"
76
76
  swagger: "http://127.0.0.1:8000/docs"
77
77
  frontend:
@@ -0,0 +1,97 @@
1
+ """
2
+ Localization Manifest Spec v1 Rollout Phase Controller.
3
+
4
+ Manages the phased rollout of localization validation rules:
5
+ - Phase 1 (WARNINGS_ONLY): All validators emit warnings only
6
+ - Phase 2 (TESTER_ENFORCEMENT): Tester phase validators (LOCALE-TEST-*) strict
7
+ - Phase 3 (FULL_ENFORCEMENT): All validators including Coder (LOCALE-CODE-*) strict
8
+
9
+ Usage in validators:
10
+ from atdd.coach.utils.locale_phase import LocalePhase, should_enforce_locale
11
+
12
+ if should_enforce_locale(LocalePhase.TESTER_ENFORCEMENT):
13
+ assert condition, "Error message"
14
+ else:
15
+ if not condition:
16
+ emit_locale_warning("LOCALE-TEST-1.1", "Warning message", LocalePhase.TESTER_ENFORCEMENT)
17
+ """
18
+
19
+ from enum import IntEnum
20
+ from typing import Optional
21
+ import warnings
22
+
23
+
24
+ class LocalePhase(IntEnum):
25
+ """
26
+ Rollout phases for Localization Manifest Spec v1.
27
+
28
+ Phases are ordered by strictness level:
29
+ - WARNINGS_ONLY (1): All new validators emit warnings, no assertions
30
+ - TESTER_ENFORCEMENT (2): Tester phase validators (LOCALE-TEST-*) strict
31
+ - FULL_ENFORCEMENT (3): All validators including Coder (LOCALE-CODE-*) strict
32
+ """
33
+ WARNINGS_ONLY = 1
34
+ TESTER_ENFORCEMENT = 2
35
+ FULL_ENFORCEMENT = 3
36
+
37
+
38
+ # Current rollout phase - update this to advance through phases
39
+ CURRENT_LOCALE_PHASE = LocalePhase.WARNINGS_ONLY
40
+
41
+
42
+ def should_enforce_locale(validator_phase: LocalePhase) -> bool:
43
+ """
44
+ Check if a locale validator should enforce strict mode.
45
+
46
+ Args:
47
+ validator_phase: The phase at which this validator becomes strict
48
+
49
+ Returns:
50
+ True if current phase >= validator_phase (should enforce)
51
+ False if current phase < validator_phase (should warn only)
52
+
53
+ Example:
54
+ # This validator becomes strict in Phase 2
55
+ if should_enforce_locale(LocalePhase.TESTER_ENFORCEMENT):
56
+ assert all_keys_match, "Keys must match reference locale"
57
+ else:
58
+ if not all_keys_match:
59
+ emit_locale_warning("LOCALE-TEST-1.4", "Keys don't match", LocalePhase.TESTER_ENFORCEMENT)
60
+ """
61
+ return CURRENT_LOCALE_PHASE >= validator_phase
62
+
63
+
64
+ def get_current_locale_phase() -> LocalePhase:
65
+ """Get the current locale rollout phase."""
66
+ return CURRENT_LOCALE_PHASE
67
+
68
+
69
+ def get_locale_phase_name(phase: Optional[LocalePhase] = None) -> str:
70
+ """Get human-readable name for a locale phase."""
71
+ phase = phase or CURRENT_LOCALE_PHASE
72
+ return {
73
+ LocalePhase.WARNINGS_ONLY: "Phase 1: Warnings Only",
74
+ LocalePhase.TESTER_ENFORCEMENT: "Phase 2: Tester Enforcement",
75
+ LocalePhase.FULL_ENFORCEMENT: "Phase 3: Full Enforcement",
76
+ }.get(phase, "Unknown Phase")
77
+
78
+
79
+ def emit_locale_warning(
80
+ spec_id: str,
81
+ message: str,
82
+ validator_phase: LocalePhase = LocalePhase.TESTER_ENFORCEMENT
83
+ ) -> None:
84
+ """
85
+ Emit a locale validation warning with phase context.
86
+
87
+ Args:
88
+ spec_id: The SPEC ID (e.g., "LOCALE-TEST-1.1")
89
+ message: The warning message
90
+ validator_phase: Phase when this becomes an error
91
+ """
92
+ phase_name = get_locale_phase_name(validator_phase)
93
+ warnings.warn(
94
+ f"[{spec_id}] {message} (will become error in {phase_name})",
95
+ category=UserWarning,
96
+ stacklevel=3
97
+ )
@@ -599,3 +599,52 @@ def wagon_to_train_mapping(train_files: List[Tuple[Path, Dict]]) -> Dict[str, Li
599
599
  mapping[wagon_slug].append(train_id)
600
600
 
601
601
  return mapping
602
+
603
+
604
+ # ============================================================================
605
+ # LOCALIZATION FIXTURES (Localization Manifest Spec v1)
606
+ # ============================================================================
607
+
608
+
609
+ @pytest.fixture(scope="module")
610
+ def locale_manifest_path(atdd_config: Dict[str, Any]) -> Optional[Path]:
611
+ """
612
+ Get path to localization manifest file from config.
613
+
614
+ Returns:
615
+ Path to manifest file, or None if localization not configured
616
+ """
617
+ manifest_rel = atdd_config.get("localization", {}).get("manifest")
618
+ if not manifest_rel:
619
+ return None
620
+ return REPO_ROOT / manifest_rel
621
+
622
+
623
+ @pytest.fixture(scope="module")
624
+ def locale_manifest(locale_manifest_path: Optional[Path]) -> Optional[Dict[str, Any]]:
625
+ """
626
+ Load localization manifest from configured path.
627
+
628
+ Returns:
629
+ Manifest dict with reference, locales, namespaces, or None if not configured
630
+ """
631
+ if locale_manifest_path is None:
632
+ return None
633
+ if not locale_manifest_path.exists():
634
+ return None
635
+
636
+ with open(locale_manifest_path) as f:
637
+ return json.load(f)
638
+
639
+
640
+ @pytest.fixture(scope="module")
641
+ def locales_dir(locale_manifest_path: Optional[Path]) -> Optional[Path]:
642
+ """
643
+ Get locales directory (parent of manifest file).
644
+
645
+ Returns:
646
+ Path to locales directory, or None if not configured
647
+ """
648
+ if locale_manifest_path is None:
649
+ return None
650
+ return locale_manifest_path.parent
@@ -281,7 +281,7 @@ backend:
281
281
 
282
282
  direct_adapter:
283
283
  pattern: "direct_*_client.py"
284
- use_when: "Wagons run in same process (monolith via game.py)"
284
+ use_when: "Wagons run in same process (monolith via app.py)"
285
285
  example: "DirectCommitStateClient reads from shared StateRepository"
286
286
  benefits:
287
287
  - "No network latency"
@@ -243,7 +243,7 @@ interaction:
243
243
 
244
244
  composition_roots:
245
245
  description: |
246
- Composition roots (composition.py, wagon.py, trains/runner.py, game.py) are entrypoints
246
+ Composition roots (composition.py, wagon.py, trains/runner.py, app.py) are entrypoints
247
247
  that wire dependencies. They are executed, never imported by other code.
248
248
 
249
249
  feature_composition:
@@ -332,15 +332,15 @@ interaction:
332
332
  # ========================================================================
333
333
  station_master_pattern:
334
334
  description: |
335
- When multiple wagons run in a single process (game.py), the Station Master
335
+ When multiple wagons run in a single process (app.py), the Station Master
336
336
  pattern enables shared dependency injection without HTTP self-calls.
337
337
 
338
- game.py creates shared singletons (StateRepository, EventBus, etc.) and
338
+ app.py creates shared singletons (StateRepository, EventBus, etc.) and
339
339
  passes them to wagon composition.py functions, which decide internally
340
340
  whether to use Direct adapters (monolith) or HTTP adapters (microservices).
341
341
 
342
342
  architecture: |
343
- game.py (Station Master / Thin Router)
343
+ app.py (Station Master / Thin Router)
344
344
 
345
345
  ├── Creates shared singletons:
346
346
  │ - StateRepository (commit-state data)
@@ -391,7 +391,7 @@ interaction:
391
391
  """Wire dependencies with optional SharedDependencies.
392
392
 
393
393
  Args:
394
- shared: SharedDependencies from game.py (monolith mode).
394
+ shared: SharedDependencies from app.py (monolith mode).
395
395
  When provided, uses Direct adapters for cross-wagon data.
396
396
  When None, uses Fake adapters for standalone testing.
397
397
  """
@@ -424,7 +424,7 @@ interaction:
424
424
  Same interface (implements Port), different implementation.
425
425
 
426
426
  station_master_responsibilities:
427
- game_py:
427
+ app_py:
428
428
  - "Create shared singletons (StateRepository, EventBus)"
429
429
  - "Pass shared deps to wagon composition.py"
430
430
  - "Include wagon routers in FastAPI app"
@@ -440,12 +440,12 @@ interaction:
440
440
  required:
441
441
  - "composition.py accepts optional shared dependency parameters"
442
442
  - "Direct adapters exist for cross-wagon data access"
443
- - "game.py calls composition.py, not duplicates wiring"
443
+ - "app.py calls composition.py, not duplicates wiring"
444
444
 
445
445
  forbidden:
446
- - "game.py creating use cases that composition.py should own"
446
+ - "app.py creating use cases that composition.py should own"
447
447
  - "HTTP clients calling localhost in production monolith"
448
- - "Duplicated wiring logic between game.py and composition.py"
448
+ - "Duplicated wiring logic between app.py and composition.py"
449
449
 
450
450
  forbidden_cross_wagon_imports:
451
451
  rule: "Code in wagon A MUST NOT import directly from wagon B"
@@ -540,25 +540,25 @@ validation:
540
540
  - "Simple error handling has TODO(REFACTOR) marker"
541
541
 
542
542
  unified_game_server:
543
- description: "python/game.py must aggregate all wagon presentation layers"
544
- file: "python/game.py"
543
+ description: "python/app.py must aggregate all wagon presentation layers"
544
+ file: "python/app.py"
545
545
  requirements:
546
- - "When new wagon adds FastAPI controller, game.py MUST be updated"
547
- - "game.py imports controller's app and includes via app.include_router()"
546
+ - "When new wagon adds FastAPI controller, app.py MUST be updated"
547
+ - "app.py imports controller's app and includes via app.include_router()"
548
548
  - "All wagon endpoints accessible via unified game server"
549
- - "Validator checks: all wagons with presentation are registered in game.py"
549
+ - "Validator checks: all wagons with presentation are registered in app.py"
550
550
 
551
551
  rationale: |
552
- game.py is the unified entry point for all backend services.
552
+ app.py is the unified entry point for all backend services.
553
553
  Without registration, wagon endpoints are inaccessible to game server/mobile app.
554
554
 
555
555
  pattern: |
556
- # python/game.py
556
+ # python/app.py
557
557
  from burn_timebank.track_remaining.src.presentation.controllers.remaining_controller import app as timebank_app
558
558
  app.include_router(timebank_app.router, prefix="/timebank")
559
559
 
560
560
  anti_pattern: |
561
- # ❌ WRONG: Wagon has FastAPI controller but not registered in game.py
561
+ # ❌ WRONG: Wagon has FastAPI controller but not registered in app.py
562
562
  # Result: Endpoints exist but unreachable from unified server
563
563
 
564
564
  usage: |
@@ -8,8 +8,8 @@ rationale: |
8
8
  defined in YAML specifications by coordinating wagons through contract-based communication.
9
9
 
10
10
  TRANSFORMATION (SESSION-12):
11
- - OLD: Trains in e2e/ (test-only), custom orchestration in game.py
12
- - NEW: Trains in python/trains/ (production), game.py becomes thin Station Master
11
+ - OLD: Trains in e2e/ (test-only), custom orchestration in app.py
12
+ - NEW: Trains in python/trains/ (production), app.py becomes thin Station Master
13
13
 
14
14
  BENEFITS:
15
15
  - Single source of truth (YAML defines both production AND tests)
@@ -21,7 +21,7 @@ cross_references:
21
21
  composition_hierarchy:
22
22
  - file: "refactor.convention.yaml"
23
23
  section: "composition_root.hierarchy"
24
- note: "Trains add new orchestration layer between game.py and wagon.py"
24
+ note: "Trains add new orchestration layer between app.py and wagon.py"
25
25
 
26
26
  boundaries:
27
27
  - file: "boundaries.convention.yaml"
@@ -37,14 +37,15 @@ cross_references:
37
37
  # ============================================================================
38
38
 
39
39
  composition_hierarchy:
40
- description: "Trains sit between application (game.py) and wagons (wagon.py)"
40
+ description: "Trains sit between application (app.py) and wagons (wagon.py)"
41
41
 
42
42
  levels:
43
43
  application:
44
- file: "python/game.py"
44
+ file: "python/app.py"
45
45
  role: "Station Master - thin router"
46
46
  responsibility: "Map user actions → train_ids, invoke TrainRunner"
47
47
  size: "< 50 lines of routing logic"
48
+ note: "app.py is the Station Master entrypoint"
48
49
 
49
50
  train:
50
51
  file: "python/trains/runner.py"
@@ -65,8 +66,8 @@ composition_hierarchy:
65
66
 
66
67
  dependency_flow:
67
68
  rule: "Dependencies flow downward only"
68
- chain: "game.py → trains.runner → wagon.py → composition.py → src/"
69
- forbidden: "NEVER: wagon imports from trains or game"
69
+ chain: "app.py → trains.runner → wagon.py → composition.py → src/"
70
+ forbidden: "NEVER: wagon imports from trains or app"
70
71
 
71
72
  # ============================================================================
72
73
  # TRAIN INFRASTRUCTURE
@@ -138,7 +139,7 @@ wagon_train_mode:
138
139
  multi_mode_pattern:
139
140
  description: "Wagons support multiple execution modes"
140
141
  cli: "Interactive demo (python3 wagon.py)"
141
- http: "API endpoints (via game.py)"
142
+ http: "API endpoints (via app.py)"
142
143
  train: "Production orchestration (via TrainRunner)"
143
144
 
144
145
  # ============================================================================
@@ -170,14 +171,14 @@ cargo_pattern:
170
171
  method: "JSON Schema Draft-07 validation"
171
172
 
172
173
  # ============================================================================
173
- # STATION MASTER PATTERN (game.py)
174
+ # STATION MASTER PATTERN (app.py)
174
175
  # ============================================================================
175
176
 
176
177
  station_master:
177
- description: "game.py becomes thin router that delegates to TrainRunner"
178
+ description: "app.py becomes thin router that delegates to TrainRunner"
178
179
 
179
180
  pattern: |
180
- # game.py - Station Master
181
+ # app.py - Station Master
181
182
  from trains.runner import TrainRunner
182
183
 
183
184
  JOURNEY_MAP = {
@@ -198,7 +199,7 @@ station_master:
198
199
  - "Translate HTTP ↔ artifacts"
199
200
  - "Handle errors"
200
201
 
201
- anti_pattern: "Business logic in game.py (belongs in trains/wagons)"
202
+ anti_pattern: "Business logic in app.py (belongs in trains/wagons)"
202
203
 
203
204
  # ============================================================================
204
205
  # TESTING
@@ -278,10 +279,10 @@ observability:
278
279
  # ============================================================================
279
280
 
280
281
  migration:
281
- step_1: "Identify user journeys in game.py"
282
+ step_1: "Identify user journeys in app.py"
282
283
  step_2: "Create train YAML (plan/_trains/{train_id}.yaml)"
283
284
  step_3: "Add run_train() to participating wagons"
284
- step_4: "Refactor game.py to Station Master pattern"
285
+ step_4: "Refactor app.py to Station Master pattern"
285
286
  step_5: "Update tests to use TrainRunner"
286
287
 
287
288
  # ============================================================================
@@ -0,0 +1,171 @@
1
+ """
2
+ i18n runtime validation (Localization Manifest Spec v1).
3
+
4
+ Validates that runtime code uses the centralized locale manifest:
5
+ - LOCALE-CODE-2.1: i18nConfig.ts imports from manifest (not hardcoded arrays)
6
+ - LOCALE-CODE-2.2: LanguageSwitcher uses shared SUPPORTED_LOCALES
7
+ """
8
+
9
+ import re
10
+ import pytest
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from atdd.coach.utils.locale_phase import (
15
+ LocalePhase,
16
+ should_enforce_locale,
17
+ emit_locale_warning,
18
+ )
19
+ from atdd.coach.utils.repo import find_repo_root
20
+
21
+ # Path constants
22
+ REPO_ROOT = find_repo_root()
23
+ WEB_DIR = REPO_ROOT / "web"
24
+
25
+
26
+ def _find_file(base_dir: Path, *possible_paths: str) -> Optional[Path]:
27
+ """Find first existing file from list of possible paths."""
28
+ for rel_path in possible_paths:
29
+ full_path = base_dir / rel_path
30
+ if full_path.exists():
31
+ return full_path
32
+ return None
33
+
34
+
35
+ def _read_file_content(path: Path) -> Optional[str]:
36
+ """Read file content, return None on error."""
37
+ try:
38
+ return path.read_text()
39
+ except Exception:
40
+ return None
41
+
42
+
43
+ @pytest.mark.locale
44
+ @pytest.mark.coder
45
+ def test_i18n_config_uses_manifest(locale_manifest, locale_manifest_path):
46
+ """
47
+ LOCALE-CODE-2.1: i18nConfig.ts imports from manifest (not hardcoded arrays)
48
+
49
+ Given: Web application with i18n configuration
50
+ When: Checking i18nConfig.ts or i18n.ts
51
+ Then: Configuration imports locales from manifest or shared constant
52
+ NOT hardcoded locale arrays like ['en', 'es', 'fr']
53
+ """
54
+ if locale_manifest is None:
55
+ pytest.skip("Localization not configured")
56
+
57
+ i18n_config = _find_file(
58
+ WEB_DIR,
59
+ "src/i18nConfig.ts",
60
+ "src/i18n/config.ts",
61
+ "src/i18n.ts",
62
+ "src/lib/i18n.ts",
63
+ "src/config/i18n.ts",
64
+ )
65
+
66
+ if i18n_config is None:
67
+ pytest.skip("No i18n config file found in web/src/")
68
+
69
+ content = _read_file_content(i18n_config)
70
+ if content is None:
71
+ pytest.skip(f"Cannot read {i18n_config}")
72
+
73
+ hardcoded_array_pattern = re.compile(
74
+ r"(?:locales|supportedLocales|SUPPORTED_LOCALES|languages)\s*[=:]\s*\[\s*['\"][a-z]{2}",
75
+ re.IGNORECASE
76
+ )
77
+
78
+ if hardcoded_array_pattern.search(content):
79
+ manifest_import_patterns = [
80
+ r"from\s+['\"].*manifest",
81
+ r"import.*manifest",
82
+ r"require\s*\(\s*['\"].*manifest",
83
+ r"SUPPORTED_LOCALES",
84
+ r"getSupportedLocales",
85
+ ]
86
+
87
+ has_manifest_usage = any(
88
+ re.search(pattern, content, re.IGNORECASE)
89
+ for pattern in manifest_import_patterns
90
+ )
91
+
92
+ if not has_manifest_usage:
93
+ msg = (
94
+ f"i18n config has hardcoded locale array: {i18n_config.relative_to(REPO_ROOT)}\n"
95
+ f" Should import from manifest.json or use shared SUPPORTED_LOCALES constant"
96
+ )
97
+ if should_enforce_locale(LocalePhase.FULL_ENFORCEMENT):
98
+ pytest.fail(msg)
99
+ else:
100
+ emit_locale_warning("LOCALE-CODE-2.1", msg, LocalePhase.FULL_ENFORCEMENT)
101
+ pytest.skip(msg)
102
+
103
+
104
+ @pytest.mark.locale
105
+ @pytest.mark.coder
106
+ def test_language_switcher_uses_shared_locales(locale_manifest, locale_manifest_path):
107
+ """
108
+ LOCALE-CODE-2.2: LanguageSwitcher uses shared SUPPORTED_LOCALES
109
+
110
+ Given: Web application with language switcher component
111
+ When: Checking LanguageSwitcher component
112
+ Then: Component imports locales from shared constant or manifest
113
+ NOT hardcoded locale arrays
114
+ """
115
+ if locale_manifest is None:
116
+ pytest.skip("Localization not configured")
117
+
118
+ switcher_patterns = [
119
+ "src/components/LanguageSwitcher.tsx",
120
+ "src/components/LocaleSwitcher.tsx",
121
+ "src/components/ui/LanguageSwitcher.tsx",
122
+ "src/components/common/LanguageSwitcher.tsx",
123
+ "src/features/i18n/LanguageSwitcher.tsx",
124
+ ]
125
+
126
+ switcher_file = _find_file(WEB_DIR, *switcher_patterns)
127
+
128
+ if switcher_file is None:
129
+ switcher_files = list(WEB_DIR.rglob("*[Ll]anguage*[Ss]witcher*.tsx"))
130
+ if not switcher_files:
131
+ switcher_files = list(WEB_DIR.rglob("*[Ll]ocale*[Ss]witcher*.tsx"))
132
+ if switcher_files:
133
+ switcher_file = switcher_files[0]
134
+
135
+ if switcher_file is None:
136
+ pytest.skip("No LanguageSwitcher component found")
137
+
138
+ content = _read_file_content(switcher_file)
139
+ if content is None:
140
+ pytest.skip(f"Cannot read {switcher_file}")
141
+
142
+ hardcoded_array_pattern = re.compile(
143
+ r"(?:locales|languages|options)\s*[=:]\s*\[\s*(?:\{[^}]*locale[^}]*['\"][a-z]{2}|['\"][a-z]{2})",
144
+ re.IGNORECASE
145
+ )
146
+
147
+ if hardcoded_array_pattern.search(content):
148
+ shared_patterns = [
149
+ r"SUPPORTED_LOCALES",
150
+ r"getSupportedLocales",
151
+ r"from\s+['\"].*manifest",
152
+ r"from\s+['\"].*i18n",
153
+ r"from\s+['\"].*config",
154
+ r"useLocales",
155
+ ]
156
+
157
+ has_shared_usage = any(
158
+ re.search(pattern, content, re.IGNORECASE)
159
+ for pattern in shared_patterns
160
+ )
161
+
162
+ if not has_shared_usage:
163
+ msg = (
164
+ f"LanguageSwitcher has hardcoded locale array: {switcher_file.relative_to(REPO_ROOT)}\n"
165
+ f" Should import from shared SUPPORTED_LOCALES or manifest"
166
+ )
167
+ if should_enforce_locale(LocalePhase.FULL_ENFORCEMENT):
168
+ pytest.fail(msg)
169
+ else:
170
+ emit_locale_warning("LOCALE-CODE-2.2", msg, LocalePhase.FULL_ENFORCEMENT)
171
+ pytest.skip(msg)
@@ -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"