atdd 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. atdd/__init__.py +6 -0
  2. atdd/__main__.py +4 -0
  3. atdd/cli.py +404 -0
  4. atdd/coach/__init__.py +0 -0
  5. atdd/coach/commands/__init__.py +0 -0
  6. atdd/coach/commands/add_persistence_metadata.py +215 -0
  7. atdd/coach/commands/analyze_migrations.py +188 -0
  8. atdd/coach/commands/consumers.py +720 -0
  9. atdd/coach/commands/infer_governance_status.py +149 -0
  10. atdd/coach/commands/initializer.py +177 -0
  11. atdd/coach/commands/interface.py +1078 -0
  12. atdd/coach/commands/inventory.py +565 -0
  13. atdd/coach/commands/migration.py +240 -0
  14. atdd/coach/commands/registry.py +1560 -0
  15. atdd/coach/commands/session.py +430 -0
  16. atdd/coach/commands/sync.py +405 -0
  17. atdd/coach/commands/test_interface.py +399 -0
  18. atdd/coach/commands/test_runner.py +141 -0
  19. atdd/coach/commands/tests/__init__.py +1 -0
  20. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  21. atdd/coach/commands/traceability.py +4264 -0
  22. atdd/coach/conventions/session.convention.yaml +754 -0
  23. atdd/coach/overlays/__init__.py +2 -0
  24. atdd/coach/overlays/claude.md +2 -0
  25. atdd/coach/schemas/config.schema.json +34 -0
  26. atdd/coach/schemas/manifest.schema.json +101 -0
  27. atdd/coach/templates/ATDD.md +282 -0
  28. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  29. atdd/coach/utils/__init__.py +0 -0
  30. atdd/coach/utils/graph/__init__.py +0 -0
  31. atdd/coach/utils/graph/urn.py +875 -0
  32. atdd/coach/validators/__init__.py +0 -0
  33. atdd/coach/validators/shared_fixtures.py +365 -0
  34. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  35. atdd/coach/validators/test_registry.py +575 -0
  36. atdd/coach/validators/test_session_validation.py +1183 -0
  37. atdd/coach/validators/test_traceability.py +448 -0
  38. atdd/coach/validators/test_update_feature_paths.py +108 -0
  39. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  40. atdd/coder/__init__.py +1 -0
  41. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  42. atdd/coder/conventions/backend.convention.yaml +460 -0
  43. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  44. atdd/coder/conventions/commons.convention.yaml +460 -0
  45. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  46. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  47. atdd/coder/conventions/design.convention.yaml +327 -0
  48. atdd/coder/conventions/design.recipe.yaml +273 -0
  49. atdd/coder/conventions/dto.convention.yaml +660 -0
  50. atdd/coder/conventions/frontend.convention.yaml +542 -0
  51. atdd/coder/conventions/green.convention.yaml +1012 -0
  52. atdd/coder/conventions/presentation.convention.yaml +587 -0
  53. atdd/coder/conventions/refactor.convention.yaml +535 -0
  54. atdd/coder/conventions/technology.convention.yaml +206 -0
  55. atdd/coder/conventions/tests/__init__.py +0 -0
  56. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  57. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  58. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  59. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  60. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  61. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  62. atdd/coder/conventions/train.convention.yaml +325 -0
  63. atdd/coder/conventions/verification.protocol.yaml +53 -0
  64. atdd/coder/schemas/design_system.schema.json +361 -0
  65. atdd/coder/validators/__init__.py +0 -0
  66. atdd/coder/validators/test_commons_structure.py +485 -0
  67. atdd/coder/validators/test_complexity.py +416 -0
  68. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  69. atdd/coder/validators/test_design_system_compliance.py +413 -0
  70. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  71. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  72. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  73. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  74. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  75. atdd/coder/validators/test_import_boundaries.py +396 -0
  76. atdd/coder/validators/test_init_file_urns.py +593 -0
  77. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  78. atdd/coder/validators/test_presentation_convention.py +260 -0
  79. atdd/coder/validators/test_python_architecture.py +674 -0
  80. atdd/coder/validators/test_quality_metrics.py +420 -0
  81. atdd/coder/validators/test_station_master_pattern.py +244 -0
  82. atdd/coder/validators/test_train_infrastructure.py +454 -0
  83. atdd/coder/validators/test_train_urns.py +293 -0
  84. atdd/coder/validators/test_typescript_architecture.py +616 -0
  85. atdd/coder/validators/test_usecase_structure.py +421 -0
  86. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  87. atdd/conftest.py +126 -0
  88. atdd/planner/__init__.py +1 -0
  89. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  90. atdd/planner/conventions/appendix.convention.yaml +187 -0
  91. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  92. atdd/planner/conventions/component.convention.yaml +670 -0
  93. atdd/planner/conventions/criteria.convention.yaml +141 -0
  94. atdd/planner/conventions/feature.convention.yaml +371 -0
  95. atdd/planner/conventions/interface.convention.yaml +382 -0
  96. atdd/planner/conventions/steps.convention.yaml +141 -0
  97. atdd/planner/conventions/train.convention.yaml +552 -0
  98. atdd/planner/conventions/wagon.convention.yaml +275 -0
  99. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  100. atdd/planner/schemas/acceptance.schema.json +336 -0
  101. atdd/planner/schemas/appendix.schema.json +78 -0
  102. atdd/planner/schemas/component.schema.json +114 -0
  103. atdd/planner/schemas/feature.schema.json +197 -0
  104. atdd/planner/schemas/train.schema.json +192 -0
  105. atdd/planner/schemas/wagon.schema.json +281 -0
  106. atdd/planner/schemas/wmbt.schema.json +59 -0
  107. atdd/planner/validators/__init__.py +0 -0
  108. atdd/planner/validators/conftest.py +5 -0
  109. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  110. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  111. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  112. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  113. atdd/planner/validators/test_plan_wagons.py +174 -0
  114. atdd/planner/validators/test_train_validation.py +514 -0
  115. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  116. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  117. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  118. atdd/tester/__init__.py +1 -0
  119. atdd/tester/conventions/artifact.convention.yaml +257 -0
  120. atdd/tester/conventions/contract.convention.yaml +1009 -0
  121. atdd/tester/conventions/filename.convention.yaml +555 -0
  122. atdd/tester/conventions/migration.convention.yaml +509 -0
  123. atdd/tester/conventions/red.convention.yaml +797 -0
  124. atdd/tester/conventions/routing.convention.yaml +51 -0
  125. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  126. atdd/tester/schemas/a11y.tmpl.json +17 -0
  127. atdd/tester/schemas/artifact.schema.json +189 -0
  128. atdd/tester/schemas/contract.schema.json +591 -0
  129. atdd/tester/schemas/contract.tmpl.json +95 -0
  130. atdd/tester/schemas/db.tmpl.json +20 -0
  131. atdd/tester/schemas/e2e.tmpl.json +17 -0
  132. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  133. atdd/tester/schemas/event.tmpl.json +17 -0
  134. atdd/tester/schemas/http.tmpl.json +19 -0
  135. atdd/tester/schemas/job.tmpl.json +18 -0
  136. atdd/tester/schemas/load.tmpl.json +21 -0
  137. atdd/tester/schemas/metric.tmpl.json +19 -0
  138. atdd/tester/schemas/pack.schema.json +139 -0
  139. atdd/tester/schemas/realtime.tmpl.json +20 -0
  140. atdd/tester/schemas/rls.tmpl.json +18 -0
  141. atdd/tester/schemas/script.tmpl.json +16 -0
  142. atdd/tester/schemas/sec.tmpl.json +18 -0
  143. atdd/tester/schemas/storage.tmpl.json +18 -0
  144. atdd/tester/schemas/telemetry.schema.json +128 -0
  145. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  146. atdd/tester/schemas/test_filename.schema.json +194 -0
  147. atdd/tester/schemas/test_intent.schema.json +179 -0
  148. atdd/tester/schemas/unit.tmpl.json +18 -0
  149. atdd/tester/schemas/visual.tmpl.json +18 -0
  150. atdd/tester/schemas/ws.tmpl.json +17 -0
  151. atdd/tester/utils/__init__.py +0 -0
  152. atdd/tester/utils/filename.py +300 -0
  153. atdd/tester/validators/__init__.py +0 -0
  154. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  155. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  156. atdd/tester/validators/conftest.py +5 -0
  157. atdd/tester/validators/coverage_gap_report.py +321 -0
  158. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  159. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  160. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  161. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  162. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  163. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  164. atdd/tester/validators/test_contracts_structure.py +200 -0
  165. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  166. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  167. atdd/tester/validators/test_fixture_validity.py +372 -0
  168. atdd/tester/validators/test_isolation.py +487 -0
  169. atdd/tester/validators/test_migration_coverage.py +204 -0
  170. atdd/tester/validators/test_migration_criteria.py +276 -0
  171. atdd/tester/validators/test_migration_generation.py +116 -0
  172. atdd/tester/validators/test_python_test_naming.py +410 -0
  173. atdd/tester/validators/test_red_layer_validation.py +95 -0
  174. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  175. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  176. atdd/tester/validators/test_telemetry_structure.py +634 -0
  177. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  178. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  179. atdd-0.2.1.dist-info/METADATA +221 -0
  180. atdd-0.2.1.dist-info/RECORD +184 -0
  181. atdd-0.2.1.dist-info/WHEEL +5 -0
  182. atdd-0.2.1.dist-info/entry_points.txt +2 -0
  183. atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
  184. atdd-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,514 @@
1
+ """
2
+ Platform tests: Train validation with theme-based numbering.
3
+
4
+ Validates that trains follow conventions:
5
+ - Theme-based numbering (00-09, 10-19, 20-29, etc.)
6
+ - Wagon references exist
7
+ - Artifact consistency
8
+ - Dependencies are valid
9
+ - Registry grouping matches numbering
10
+ """
11
+ import pytest
12
+ import yaml
13
+ from pathlib import Path
14
+ from typing import Dict, List, Set, Tuple
15
+
16
+
17
+ @pytest.mark.platform
18
+ def test_train_ids_follow_numbering_convention(trains_registry):
19
+ """
20
+ SPEC-TRAIN-VAL-0001: Train IDs follow theme-based numbering
21
+
22
+ Given: Train registry with train_ids
23
+ When: Checking train_id format
24
+ Then: Each train_id matches pattern: {digit}{digit}{digit}{digit}-{kebab-case-name}
25
+ (4-digit hierarchical: [Theme][Category][Variation])
26
+ """
27
+ import re
28
+
29
+ pattern = re.compile(r"^[0-9]{4}-[a-z][a-z0-9-]*$")
30
+
31
+ for theme, trains in trains_registry.items():
32
+ if not trains:
33
+ continue
34
+
35
+ for train in trains:
36
+ train_id = train.get("train_id", "")
37
+ assert pattern.match(train_id), \
38
+ f"Train ID '{train_id}' doesn't match pattern NNNN-kebab-case (theme: {theme})"
39
+
40
+
41
+ @pytest.mark.platform
42
+ def test_train_theme_matches_first_digit(trains_registry):
43
+ """
44
+ SPEC-TRAIN-VAL-0002: Train theme matches first digit of ID
45
+
46
+ Given: Train registry organized by theme
47
+ When: Checking train_id first digit
48
+ Then: First digit maps to correct theme category
49
+ """
50
+ theme_map = {
51
+ "0": "commons",
52
+ "1": "mechanic",
53
+ "2": "scenario",
54
+ "3": "match",
55
+ "4": "sensory",
56
+ "5": "player",
57
+ "6": "league",
58
+ "7": "audience",
59
+ "8": "monetization",
60
+ "9": "partnership",
61
+ }
62
+
63
+ mismatches = []
64
+ for theme, trains in trains_registry.items():
65
+ if not trains:
66
+ continue
67
+
68
+ for train in trains:
69
+ train_id = train.get("train_id", "")
70
+ if not train_id or len(train_id) < 2:
71
+ continue
72
+
73
+ first_digit = train_id[0]
74
+ expected_theme = theme_map.get(first_digit)
75
+
76
+ if expected_theme != theme:
77
+ mismatches.append(
78
+ f"{train_id}: in '{theme}' but numbering suggests '{expected_theme}'"
79
+ )
80
+
81
+ assert not mismatches, \
82
+ f"Train theme/numbering mismatches:\n " + "\n ".join(mismatches)
83
+
84
+
85
+ @pytest.mark.platform
86
+ def test_train_files_exist_for_registry_entries(trains_registry):
87
+ """
88
+ SPEC-TRAIN-VAL-0003: All trains in registry have corresponding files
89
+
90
+ Given: Trains listed in plan/_trains.yaml
91
+ When: Checking for train files
92
+ Then: Each train has a file at plan/_trains/{train_id}.yaml
93
+ """
94
+ repo_root = Path(__file__).resolve().parents[4]
95
+ trains_dir = repo_root / "plan" / "_trains"
96
+
97
+ missing_files = []
98
+ for theme, trains in trains_registry.items():
99
+ if not trains:
100
+ continue
101
+
102
+ for train in trains:
103
+ train_id = train.get("train_id", "")
104
+ if not train_id:
105
+ continue
106
+
107
+ train_path = trains_dir / f"{train_id}.yaml"
108
+ if not train_path.exists():
109
+ missing_files.append(f"{train_id} (theme: {theme})")
110
+
111
+ assert not missing_files, \
112
+ f"Trains in registry missing files:\n " + "\n ".join(missing_files)
113
+
114
+
115
+ @pytest.mark.platform
116
+ def test_all_train_files_registered(trains_registry):
117
+ """
118
+ SPEC-TRAIN-VAL-0004: All train files are registered in _trains.yaml
119
+
120
+ Given: Train YAML files in plan/_trains/
121
+ When: Checking registry
122
+ Then: Each file is registered in plan/_trains.yaml
123
+ """
124
+ repo_root = Path(__file__).resolve().parents[4]
125
+ trains_dir = repo_root / "plan" / "_trains"
126
+
127
+ # Get all registered train IDs
128
+ registered_ids = set()
129
+ for theme, trains in trains_registry.items():
130
+ if trains:
131
+ for train in trains:
132
+ if "train_id" in train:
133
+ registered_ids.add(train["train_id"])
134
+
135
+ # Check all train files
136
+ unregistered = []
137
+ if trains_dir.exists():
138
+ for train_file in trains_dir.glob("*.yaml"):
139
+ train_id = train_file.stem
140
+ if train_id not in registered_ids:
141
+ unregistered.append(train_id)
142
+
143
+ assert not unregistered, \
144
+ f"Train files not in registry:\n " + "\n ".join(unregistered)
145
+
146
+
147
+ @pytest.mark.platform
148
+ def test_train_id_matches_filename(trains_registry):
149
+ """
150
+ SPEC-TRAIN-VAL-0005: Train file train_id matches filename
151
+
152
+ Given: Train YAML files in plan/_trains/
153
+ When: Loading train data
154
+ Then: train_id field matches filename (without .yaml)
155
+ """
156
+ repo_root = Path(__file__).resolve().parents[4]
157
+ trains_dir = repo_root / "plan" / "_trains"
158
+
159
+ mismatches = []
160
+ if trains_dir.exists():
161
+ for train_file in trains_dir.glob("*.yaml"):
162
+ filename_id = train_file.stem
163
+
164
+ with train_file.open() as f:
165
+ train_data = yaml.safe_load(f)
166
+
167
+ train_id = train_data.get("train_id")
168
+ if train_id != filename_id:
169
+ mismatches.append(
170
+ f"{train_file.name}: train_id '{train_id}' != filename '{filename_id}'"
171
+ )
172
+
173
+ assert not mismatches, \
174
+ f"Train ID/filename mismatches:\n " + "\n ".join(mismatches)
175
+
176
+
177
+ @pytest.mark.platform
178
+ def test_train_wagons_exist(trains_registry, wagon_manifests):
179
+ """
180
+ SPEC-TRAIN-VAL-0006: All wagons in trains exist in registry or plan/*
181
+
182
+ Given: Trains with wagon participants
183
+ When: Checking wagon references
184
+ Then: Each wagon exists in registry or has a manifest in plan/*
185
+ """
186
+ repo_root = Path(__file__).resolve().parents[4]
187
+ trains_dir = repo_root / "plan" / "_trains"
188
+
189
+ # Build wagon name set from manifests
190
+ wagon_names = {manifest.get("wagon", "") for _, manifest in wagon_manifests}
191
+
192
+ missing_wagons = {}
193
+ for theme, trains in trains_registry.items():
194
+ if not trains:
195
+ continue
196
+
197
+ for train in trains:
198
+ train_id = train.get("train_id", "")
199
+ if not train_id:
200
+ continue
201
+
202
+ # Load train file
203
+ train_path = trains_dir / f"{train_id}.yaml"
204
+ if not train_path.exists():
205
+ continue
206
+
207
+ with train_path.open() as f:
208
+ train_data = yaml.safe_load(f)
209
+
210
+ # Extract wagon participants
211
+ participants = train_data.get("participants", [])
212
+ for participant in participants:
213
+ if isinstance(participant, str) and participant.startswith("wagon:"):
214
+ wagon_name = participant.replace("wagon:", "")
215
+ if wagon_name not in wagon_names:
216
+ if train_id not in missing_wagons:
217
+ missing_wagons[train_id] = []
218
+ missing_wagons[train_id].append(wagon_name)
219
+
220
+ assert not missing_wagons, \
221
+ f"Trains reference non-existent wagons:\n" + \
222
+ "\n".join(f" {tid}: {', '.join(wagons)}" for tid, wagons in missing_wagons.items())
223
+
224
+
225
+ @pytest.mark.platform
226
+ def test_train_dependencies_are_valid(trains_registry):
227
+ """
228
+ SPEC-TRAIN-VAL-0007: Train dependencies reference valid trains
229
+
230
+ Given: Trains with dependencies
231
+ When: Checking dependency references
232
+ Then: Each dependency points to a valid train_id
233
+ """
234
+ repo_root = Path(__file__).resolve().parents[4]
235
+ trains_dir = repo_root / "plan" / "_trains"
236
+
237
+ # Get all valid train IDs
238
+ valid_train_ids = set()
239
+ for theme, trains in trains_registry.items():
240
+ if trains:
241
+ for train in trains:
242
+ if "train_id" in train:
243
+ valid_train_ids.add(train["train_id"])
244
+
245
+ # Check dependencies
246
+ invalid_deps = {}
247
+ for theme, trains in trains_registry.items():
248
+ if not trains:
249
+ continue
250
+
251
+ for train in trains:
252
+ train_id = train.get("train_id", "")
253
+ if not train_id:
254
+ continue
255
+
256
+ # Load train file
257
+ train_path = trains_dir / f"{train_id}.yaml"
258
+ if not train_path.exists():
259
+ continue
260
+
261
+ with train_path.open() as f:
262
+ train_data = yaml.safe_load(f)
263
+
264
+ dependencies = train_data.get("dependencies", [])
265
+ for dep in dependencies:
266
+ # Format: train:XX-name
267
+ if dep.startswith("train:"):
268
+ dep_id = dep.replace("train:", "")
269
+ if dep_id not in valid_train_ids:
270
+ if train_id not in invalid_deps:
271
+ invalid_deps[train_id] = []
272
+ invalid_deps[train_id].append(dep)
273
+
274
+ assert not invalid_deps, \
275
+ f"Trains have invalid dependencies:\n" + \
276
+ "\n".join(f" {tid}: {', '.join(deps)}" for tid, deps in invalid_deps.items())
277
+
278
+
279
+ @pytest.mark.platform
280
+ def test_train_artifacts_follow_naming_convention(trains_registry):
281
+ """
282
+ SPEC-TRAIN-VAL-0008: Artifacts in trains follow domain:resource pattern
283
+
284
+ Given: Train sequences with artifacts
285
+ When: Checking artifact names
286
+ Then: Each artifact follows pattern {domain}:{resource}
287
+ """
288
+ import re
289
+
290
+ repo_root = Path(__file__).resolve().parents[4]
291
+ trains_dir = repo_root / "plan" / "_trains"
292
+
293
+ pattern = re.compile(r"^[a-z][a-z0-9-]*(?::[a-z][a-z0-9-]*)+(?:\.[a-z][a-z0-9-]*)*$")
294
+
295
+ invalid_artifacts = {}
296
+
297
+ def extract_artifacts(steps: List[Dict]) -> Set[str]:
298
+ """Recursively extract artifacts from steps, loops, and routes."""
299
+ artifacts = set()
300
+ for item in steps:
301
+ if "step" in item and "artifact" in item:
302
+ artifacts.add(item["artifact"])
303
+ elif "loop" in item:
304
+ loop_data = item["loop"]
305
+ if "steps" in loop_data:
306
+ artifacts.update(extract_artifacts(loop_data["steps"]))
307
+ elif "route" in item:
308
+ route_data = item["route"]
309
+ for branch in route_data.get("branches", []):
310
+ if "steps" in branch:
311
+ artifacts.update(extract_artifacts(branch["steps"]))
312
+ return artifacts
313
+
314
+ for theme, trains in trains_registry.items():
315
+ if not trains:
316
+ continue
317
+
318
+ for train in trains:
319
+ train_id = train.get("train_id", "")
320
+ if not train_id:
321
+ continue
322
+
323
+ # Load train file
324
+ train_path = trains_dir / f"{train_id}.yaml"
325
+ if not train_path.exists():
326
+ continue
327
+
328
+ with train_path.open() as f:
329
+ train_data = yaml.safe_load(f)
330
+
331
+ # Extract all artifacts
332
+ sequence = train_data.get("sequence", [])
333
+ artifacts = extract_artifacts(sequence)
334
+
335
+ # Check each artifact
336
+ for artifact in artifacts:
337
+ if not pattern.match(artifact):
338
+ if train_id not in invalid_artifacts:
339
+ invalid_artifacts[train_id] = []
340
+ invalid_artifacts[train_id].append(artifact)
341
+
342
+ assert not invalid_artifacts, \
343
+ f"Trains have invalid artifact names:\n" + \
344
+ "\n".join(f" {tid}: {', '.join(arts)}" for tid, arts in invalid_artifacts.items())
345
+
346
+
347
+ @pytest.mark.platform
348
+ @pytest.mark.skip(reason="Soft validation - artifacts may come from external sources")
349
+ def test_train_artifacts_exist_in_wagons(trains_registry, wagon_manifests):
350
+ """
351
+ SPEC-TRAIN-VAL-0009: Artifacts in trains are produced/consumed by wagons
352
+
353
+ Given: Train sequences with artifacts
354
+ When: Checking artifact definitions
355
+ Then: Each artifact should be in wagon produce/consume lists
356
+ Note: Soft check - external/system artifacts are allowed
357
+ """
358
+ repo_root = Path(__file__).resolve().parents[4]
359
+ trains_dir = repo_root / "plan" / "_trains"
360
+
361
+ # Build artifact index from wagons
362
+ wagon_artifacts = {}
363
+ for _, manifest in wagon_manifests:
364
+ wagon_name = manifest.get("wagon", "")
365
+ artifacts = set()
366
+
367
+ for produce_item in manifest.get("produce", []):
368
+ if "name" in produce_item:
369
+ artifacts.add(produce_item["name"])
370
+
371
+ for consume_item in manifest.get("consume", []):
372
+ if "name" in consume_item:
373
+ artifacts.add(consume_item["name"])
374
+
375
+ wagon_artifacts[wagon_name] = artifacts
376
+
377
+ def extract_artifacts(steps: List[Dict]) -> Set[str]:
378
+ """Recursively extract artifacts from steps."""
379
+ artifacts = set()
380
+ for item in steps:
381
+ if "step" in item and "artifact" in item:
382
+ artifacts.add(item["artifact"])
383
+ elif "loop" in item:
384
+ if "steps" in item["loop"]:
385
+ artifacts.update(extract_artifacts(item["loop"]["steps"]))
386
+ elif "route" in item:
387
+ for branch in item["route"].get("branches", []):
388
+ if "steps" in branch:
389
+ artifacts.update(extract_artifacts(branch["steps"]))
390
+ return artifacts
391
+
392
+ warnings = []
393
+ for theme, trains in trains_registry.items():
394
+ if not trains:
395
+ continue
396
+
397
+ for train in trains:
398
+ train_id = train.get("train_id", "")
399
+ if not train_id:
400
+ continue
401
+
402
+ train_path = trains_dir / f"{train_id}.yaml"
403
+ if not train_path.exists():
404
+ continue
405
+
406
+ with train_path.open() as f:
407
+ train_data = yaml.safe_load(f)
408
+
409
+ # Get wagons and artifacts
410
+ participants = train_data.get("participants", [])
411
+ wagon_names = [
412
+ p.replace("wagon:", "")
413
+ for p in participants
414
+ if isinstance(p, str) and p.startswith("wagon:")
415
+ ]
416
+
417
+ # Collect all artifacts from participating wagons
418
+ available_artifacts = set()
419
+ for wagon_name in wagon_names:
420
+ if wagon_name in wagon_artifacts:
421
+ available_artifacts.update(wagon_artifacts[wagon_name])
422
+
423
+ # Check train artifacts
424
+ train_artifacts = extract_artifacts(train_data.get("sequence", []))
425
+
426
+ for artifact in train_artifacts:
427
+ # Skip known external patterns
428
+ if any(
429
+ artifact.startswith(prefix)
430
+ for prefix in ["gesture:", "onboarding:", "account:", "auth:", "material:"]
431
+ ):
432
+ continue
433
+
434
+ if artifact not in available_artifacts:
435
+ warnings.append(
436
+ f"{train_id}: artifact '{artifact}' not in wagons {wagon_names}"
437
+ )
438
+
439
+ if warnings:
440
+ pytest.skip(
441
+ f"⚠️ Artifact warnings ({len(warnings)}):\n " +
442
+ "\n ".join(warnings[:10]) +
443
+ (f"\n ... and {len(warnings) - 10} more" if len(warnings) > 10 else "")
444
+ )
445
+
446
+
447
+ @pytest.mark.platform
448
+ def test_registry_themes_are_valid(trains_registry):
449
+ """
450
+ SPEC-TRAIN-VAL-0010: Registry theme keys match schema enum
451
+
452
+ Given: Train registry organized by themes
453
+ When: Checking theme keys
454
+ Then: All theme keys are valid according to train.schema.json
455
+ """
456
+ valid_themes = {
457
+ "commons",
458
+ "mechanic",
459
+ "scenario",
460
+ "match",
461
+ "sensory",
462
+ "player",
463
+ "league",
464
+ "audience",
465
+ "monetization",
466
+ "partnership",
467
+ }
468
+
469
+ invalid_themes = []
470
+ for theme in trains_registry.keys():
471
+ if theme not in valid_themes:
472
+ invalid_themes.append(theme)
473
+
474
+ assert not invalid_themes, \
475
+ f"Invalid themes in registry: {', '.join(invalid_themes)}\n" \
476
+ f"Valid themes: {', '.join(sorted(valid_themes))}"
477
+
478
+
479
+ @pytest.mark.platform
480
+ def test_trains_match_schema(trains_registry):
481
+ """
482
+ SPEC-TRAIN-VAL-0011: All train files validate against train.schema.json
483
+
484
+ Given: Train files in plan/_trains/
485
+ When: Validating against schema
486
+ Then: All trains pass schema validation
487
+ """
488
+ from jsonschema import Draft7Validator
489
+ import json
490
+
491
+ repo_root = Path(__file__).resolve().parents[4]
492
+ schema_path = repo_root / ".claude" / "schemas" / "planner" / "train.schema.json"
493
+ trains_dir = repo_root / "plan" / "_trains"
494
+
495
+ if not schema_path.exists():
496
+ pytest.skip("train.schema.json not found")
497
+
498
+ with schema_path.open() as f:
499
+ schema = json.load(f)
500
+
501
+ validator = Draft7Validator(schema)
502
+
503
+ failures = []
504
+ if trains_dir.exists():
505
+ for train_file in trains_dir.glob("*.yaml"):
506
+ with train_file.open() as f:
507
+ train_data = yaml.safe_load(f)
508
+
509
+ errors = list(validator.iter_errors(train_data))
510
+ if errors:
511
+ failures.append(f"{train_file.name}: {errors[0].message}")
512
+
513
+ assert not failures, \
514
+ f"Schema validation failures:\n " + "\n ".join(failures)