atdd 0.1.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.
Files changed (183) hide show
  1. atdd/__init__.py +0 -0
  2. atdd/cli.py +404 -0
  3. atdd/coach/__init__.py +0 -0
  4. atdd/coach/commands/__init__.py +0 -0
  5. atdd/coach/commands/add_persistence_metadata.py +215 -0
  6. atdd/coach/commands/analyze_migrations.py +188 -0
  7. atdd/coach/commands/consumers.py +720 -0
  8. atdd/coach/commands/infer_governance_status.py +149 -0
  9. atdd/coach/commands/initializer.py +177 -0
  10. atdd/coach/commands/interface.py +1078 -0
  11. atdd/coach/commands/inventory.py +565 -0
  12. atdd/coach/commands/migration.py +240 -0
  13. atdd/coach/commands/registry.py +1560 -0
  14. atdd/coach/commands/session.py +430 -0
  15. atdd/coach/commands/sync.py +405 -0
  16. atdd/coach/commands/test_interface.py +399 -0
  17. atdd/coach/commands/test_runner.py +141 -0
  18. atdd/coach/commands/tests/__init__.py +1 -0
  19. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  20. atdd/coach/commands/traceability.py +4264 -0
  21. atdd/coach/conventions/session.convention.yaml +754 -0
  22. atdd/coach/overlays/__init__.py +2 -0
  23. atdd/coach/overlays/claude.md +2 -0
  24. atdd/coach/schemas/config.schema.json +34 -0
  25. atdd/coach/schemas/manifest.schema.json +101 -0
  26. atdd/coach/templates/ATDD.md +282 -0
  27. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  28. atdd/coach/utils/__init__.py +0 -0
  29. atdd/coach/utils/graph/__init__.py +0 -0
  30. atdd/coach/utils/graph/urn.py +875 -0
  31. atdd/coach/validators/__init__.py +0 -0
  32. atdd/coach/validators/shared_fixtures.py +365 -0
  33. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  34. atdd/coach/validators/test_registry.py +575 -0
  35. atdd/coach/validators/test_session_validation.py +1183 -0
  36. atdd/coach/validators/test_traceability.py +448 -0
  37. atdd/coach/validators/test_update_feature_paths.py +108 -0
  38. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  39. atdd/coder/__init__.py +1 -0
  40. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  41. atdd/coder/conventions/backend.convention.yaml +460 -0
  42. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  43. atdd/coder/conventions/commons.convention.yaml +460 -0
  44. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  45. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  46. atdd/coder/conventions/design.convention.yaml +327 -0
  47. atdd/coder/conventions/design.recipe.yaml +273 -0
  48. atdd/coder/conventions/dto.convention.yaml +660 -0
  49. atdd/coder/conventions/frontend.convention.yaml +542 -0
  50. atdd/coder/conventions/green.convention.yaml +1012 -0
  51. atdd/coder/conventions/presentation.convention.yaml +587 -0
  52. atdd/coder/conventions/refactor.convention.yaml +535 -0
  53. atdd/coder/conventions/technology.convention.yaml +206 -0
  54. atdd/coder/conventions/tests/__init__.py +0 -0
  55. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  56. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  57. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  58. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  59. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  60. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  61. atdd/coder/conventions/train.convention.yaml +325 -0
  62. atdd/coder/conventions/verification.protocol.yaml +53 -0
  63. atdd/coder/schemas/design_system.schema.json +361 -0
  64. atdd/coder/validators/__init__.py +0 -0
  65. atdd/coder/validators/test_commons_structure.py +485 -0
  66. atdd/coder/validators/test_complexity.py +416 -0
  67. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  68. atdd/coder/validators/test_design_system_compliance.py +413 -0
  69. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  70. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  71. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  72. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  73. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  74. atdd/coder/validators/test_import_boundaries.py +396 -0
  75. atdd/coder/validators/test_init_file_urns.py +593 -0
  76. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  77. atdd/coder/validators/test_presentation_convention.py +260 -0
  78. atdd/coder/validators/test_python_architecture.py +674 -0
  79. atdd/coder/validators/test_quality_metrics.py +420 -0
  80. atdd/coder/validators/test_station_master_pattern.py +244 -0
  81. atdd/coder/validators/test_train_infrastructure.py +454 -0
  82. atdd/coder/validators/test_train_urns.py +293 -0
  83. atdd/coder/validators/test_typescript_architecture.py +616 -0
  84. atdd/coder/validators/test_usecase_structure.py +421 -0
  85. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  86. atdd/conftest.py +126 -0
  87. atdd/planner/__init__.py +1 -0
  88. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  89. atdd/planner/conventions/appendix.convention.yaml +187 -0
  90. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  91. atdd/planner/conventions/component.convention.yaml +670 -0
  92. atdd/planner/conventions/criteria.convention.yaml +141 -0
  93. atdd/planner/conventions/feature.convention.yaml +371 -0
  94. atdd/planner/conventions/interface.convention.yaml +382 -0
  95. atdd/planner/conventions/steps.convention.yaml +141 -0
  96. atdd/planner/conventions/train.convention.yaml +552 -0
  97. atdd/planner/conventions/wagon.convention.yaml +275 -0
  98. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  99. atdd/planner/schemas/acceptance.schema.json +336 -0
  100. atdd/planner/schemas/appendix.schema.json +78 -0
  101. atdd/planner/schemas/component.schema.json +114 -0
  102. atdd/planner/schemas/feature.schema.json +197 -0
  103. atdd/planner/schemas/train.schema.json +192 -0
  104. atdd/planner/schemas/wagon.schema.json +281 -0
  105. atdd/planner/schemas/wmbt.schema.json +59 -0
  106. atdd/planner/validators/__init__.py +0 -0
  107. atdd/planner/validators/conftest.py +5 -0
  108. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  109. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  110. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  111. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  112. atdd/planner/validators/test_plan_wagons.py +174 -0
  113. atdd/planner/validators/test_train_validation.py +514 -0
  114. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  115. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  116. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  117. atdd/tester/__init__.py +1 -0
  118. atdd/tester/conventions/artifact.convention.yaml +257 -0
  119. atdd/tester/conventions/contract.convention.yaml +1009 -0
  120. atdd/tester/conventions/filename.convention.yaml +555 -0
  121. atdd/tester/conventions/migration.convention.yaml +509 -0
  122. atdd/tester/conventions/red.convention.yaml +797 -0
  123. atdd/tester/conventions/routing.convention.yaml +51 -0
  124. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  125. atdd/tester/schemas/a11y.tmpl.json +17 -0
  126. atdd/tester/schemas/artifact.schema.json +189 -0
  127. atdd/tester/schemas/contract.schema.json +591 -0
  128. atdd/tester/schemas/contract.tmpl.json +95 -0
  129. atdd/tester/schemas/db.tmpl.json +20 -0
  130. atdd/tester/schemas/e2e.tmpl.json +17 -0
  131. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  132. atdd/tester/schemas/event.tmpl.json +17 -0
  133. atdd/tester/schemas/http.tmpl.json +19 -0
  134. atdd/tester/schemas/job.tmpl.json +18 -0
  135. atdd/tester/schemas/load.tmpl.json +21 -0
  136. atdd/tester/schemas/metric.tmpl.json +19 -0
  137. atdd/tester/schemas/pack.schema.json +139 -0
  138. atdd/tester/schemas/realtime.tmpl.json +20 -0
  139. atdd/tester/schemas/rls.tmpl.json +18 -0
  140. atdd/tester/schemas/script.tmpl.json +16 -0
  141. atdd/tester/schemas/sec.tmpl.json +18 -0
  142. atdd/tester/schemas/storage.tmpl.json +18 -0
  143. atdd/tester/schemas/telemetry.schema.json +128 -0
  144. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  145. atdd/tester/schemas/test_filename.schema.json +194 -0
  146. atdd/tester/schemas/test_intent.schema.json +179 -0
  147. atdd/tester/schemas/unit.tmpl.json +18 -0
  148. atdd/tester/schemas/visual.tmpl.json +18 -0
  149. atdd/tester/schemas/ws.tmpl.json +17 -0
  150. atdd/tester/utils/__init__.py +0 -0
  151. atdd/tester/utils/filename.py +300 -0
  152. atdd/tester/validators/__init__.py +0 -0
  153. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  154. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  155. atdd/tester/validators/conftest.py +5 -0
  156. atdd/tester/validators/coverage_gap_report.py +321 -0
  157. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  158. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  159. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  160. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  161. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  162. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  163. atdd/tester/validators/test_contracts_structure.py +200 -0
  164. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  165. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  166. atdd/tester/validators/test_fixture_validity.py +372 -0
  167. atdd/tester/validators/test_isolation.py +487 -0
  168. atdd/tester/validators/test_migration_coverage.py +204 -0
  169. atdd/tester/validators/test_migration_criteria.py +276 -0
  170. atdd/tester/validators/test_migration_generation.py +116 -0
  171. atdd/tester/validators/test_python_test_naming.py +410 -0
  172. atdd/tester/validators/test_red_layer_validation.py +95 -0
  173. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  174. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  175. atdd/tester/validators/test_telemetry_structure.py +634 -0
  176. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  177. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  178. atdd-0.1.0.dist-info/METADATA +191 -0
  179. atdd-0.1.0.dist-info/RECORD +183 -0
  180. atdd-0.1.0.dist-info/WHEEL +5 -0
  181. atdd-0.1.0.dist-info/entry_points.txt +2 -0
  182. atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
  183. atdd-0.1.0.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,365 @@
1
+ """
2
+ Shared fixtures for platform tests.
3
+
4
+ Provides schemas, file discovery, and validation utilities for E2E platform tests.
5
+ """
6
+ import json
7
+ import yaml
8
+ from pathlib import Path
9
+ from typing import Dict, Any, List, Tuple
10
+ import pytest
11
+
12
+
13
+ # Path constants
14
+ # File is at atdd/coach/audits/shared_fixtures.py, so go up 3 levels to reach repo root
15
+ REPO_ROOT = Path(__file__).resolve().parents[4]
16
+ PLAN_DIR = REPO_ROOT / "plan"
17
+ ATDD_DIR = REPO_ROOT / "atdd"
18
+ CONTRACTS_DIR = REPO_ROOT / "contracts"
19
+ TELEMETRY_DIR = REPO_ROOT / "telemetry"
20
+ WEB_DIR = REPO_ROOT / "web"
21
+
22
+
23
+ # Schema fixtures - Planner schemas
24
+ @pytest.fixture(scope="module")
25
+ def wagon_schema() -> Dict[str, Any]:
26
+ """Load wagon.schema.json for validation."""
27
+ with open(ATDD_DIR / "planner/schemas/wagon.schema.json") as f:
28
+ return json.load(f)
29
+
30
+
31
+ @pytest.fixture(scope="module")
32
+ def wmbt_schema() -> Dict[str, Any]:
33
+ """Load wmbt.schema.json for validation."""
34
+ with open(ATDD_DIR / "planner/schemas/wmbt.schema.json") as f:
35
+ return json.load(f)
36
+
37
+
38
+ @pytest.fixture(scope="module")
39
+ def feature_schema() -> Dict[str, Any]:
40
+ """Load feature.schema.json for validation."""
41
+ with open(ATDD_DIR / "planner/schemas/feature.schema.json") as f:
42
+ return json.load(f)
43
+
44
+
45
+ @pytest.fixture(scope="module")
46
+ def acceptance_schema() -> Dict[str, Any]:
47
+ """Load acceptance.schema.json for validation."""
48
+ with open(ATDD_DIR / "planner/schemas/acceptance.schema.json") as f:
49
+ return json.load(f)
50
+
51
+
52
+ # Schema fixtures - Tester schemas
53
+ @pytest.fixture(scope="module")
54
+ def telemetry_signal_schema() -> Dict[str, Any]:
55
+ """Load telemetry_signal.schema.json for validation."""
56
+ schema_path = ATDD_DIR / "tester/schemas/telemetry_signal.schema.json"
57
+ if schema_path.exists():
58
+ with open(schema_path) as f:
59
+ return json.load(f)
60
+ return {}
61
+
62
+
63
+ @pytest.fixture(scope="module")
64
+ def telemetry_tracking_manifest_schema() -> Dict[str, Any]:
65
+ """Load telemetry_tracking_manifest.schema.json for validation."""
66
+ schema_path = ATDD_DIR / "tester/schemas/telemetry_tracking_manifest.schema.json"
67
+ if schema_path.exists():
68
+ with open(schema_path) as f:
69
+ return json.load(f)
70
+ return {}
71
+
72
+
73
+ # Generic schema loader
74
+ @pytest.fixture(scope="module")
75
+ def load_schema():
76
+ """Factory fixture to load any schema by path."""
77
+ def _loader(agent: str, schema_name: str) -> Dict[str, Any]:
78
+ """
79
+ Load a schema from atdd/{agent}/schemas/{schema_name}.
80
+
81
+ Args:
82
+ agent: Agent name (planner, tester, coach, coder)
83
+ schema_name: Schema filename (e.g., "wagon.schema.json")
84
+
85
+ Returns:
86
+ Parsed JSON schema
87
+ """
88
+ schema_path = ATDD_DIR / agent / "schemas" / schema_name
89
+ if not schema_path.exists():
90
+ raise FileNotFoundError(f"Schema not found: {schema_path}")
91
+ with open(schema_path) as f:
92
+ return json.load(f)
93
+ return _loader
94
+
95
+
96
+ # File discovery fixtures
97
+ @pytest.fixture(scope="module")
98
+ def wagon_manifests() -> List[Tuple[Path, Dict[str, Any]]]:
99
+ """
100
+ Discover all wagon manifests in plan/.
101
+
102
+ Returns:
103
+ List of (path, manifest_data) tuples
104
+ """
105
+ manifests = []
106
+
107
+ # Load from _wagons.yaml registry
108
+ wagons_file = PLAN_DIR / "_wagons.yaml"
109
+ if wagons_file.exists():
110
+ with open(wagons_file) as f:
111
+ wagons_data = yaml.safe_load(f)
112
+ for wagon_entry in wagons_data.get("wagons", []):
113
+ if "manifest" in wagon_entry:
114
+ manifest_path = REPO_ROOT / wagon_entry["manifest"]
115
+ if manifest_path.exists():
116
+ with open(manifest_path) as mf:
117
+ manifest_data = yaml.safe_load(mf)
118
+ manifests.append((manifest_path, manifest_data))
119
+
120
+ # Also discover individual wagon manifests (pattern: plan/*/_{wagon}.yaml)
121
+ for wagon_dir in PLAN_DIR.iterdir():
122
+ if wagon_dir.is_dir() and not wagon_dir.name.startswith("_"):
123
+ for manifest_file in wagon_dir.glob("_*.yaml"):
124
+ manifest_path = manifest_file
125
+ if manifest_path not in [m[0] for m in manifests]:
126
+ with open(manifest_path) as f:
127
+ manifest_data = yaml.safe_load(f)
128
+ manifests.append((manifest_path, manifest_data))
129
+
130
+ return manifests
131
+
132
+
133
+ @pytest.fixture(scope="module")
134
+ def trains_registry() -> Dict[str, Any]:
135
+ """
136
+ Load trains registry from plan/_trains.yaml.
137
+
138
+ Returns:
139
+ Trains data organized by theme (e.g., {"commons": [...], "scenario": [...]})
140
+ or empty dict with all themes if file doesn't exist
141
+ """
142
+ trains_file = PLAN_DIR / "_trains.yaml"
143
+ if trains_file.exists():
144
+ with open(trains_file) as f:
145
+ data = yaml.safe_load(f)
146
+ trains_data = data.get("trains", {})
147
+
148
+ # Flatten the nested structure
149
+ # Input: {"0-commons": {"00-commons-nominal": [train1, train2], ...}, ...}
150
+ # Output: {"commons": [train1, train2, ...], ...}
151
+ flattened = {}
152
+ for theme_key, categories in trains_data.items():
153
+ # Extract theme name (e.g., "0-commons" -> "commons")
154
+ theme = theme_key.split("-", 1)[1] if "-" in theme_key else theme_key
155
+ flattened[theme] = []
156
+
157
+ # Flatten all category lists into single theme list
158
+ if isinstance(categories, dict):
159
+ for category_key, trains_list in categories.items():
160
+ if isinstance(trains_list, list):
161
+ flattened[theme].extend(trains_list)
162
+
163
+ return flattened
164
+
165
+ # Return empty theme-grouped structure
166
+ return {
167
+ "commons": [],
168
+ "mechanic": [],
169
+ "scenario": [],
170
+ "match": [],
171
+ "sensory": [],
172
+ "player": [],
173
+ "league": [],
174
+ "audience": [],
175
+ "monetization": [],
176
+ "partnership": []
177
+ }
178
+
179
+
180
+ @pytest.fixture(scope="module")
181
+ def wagons_registry() -> Dict[str, Any]:
182
+ """
183
+ Load wagons registry from plan/_wagons.yaml.
184
+
185
+ Returns:
186
+ Wagons data or empty dict if file doesn't exist
187
+ """
188
+ wagons_file = PLAN_DIR / "_wagons.yaml"
189
+ if wagons_file.exists():
190
+ with open(wagons_file) as f:
191
+ return yaml.safe_load(f)
192
+ return {"wagons": []}
193
+
194
+
195
+ # URN resolution fixtures
196
+ @pytest.fixture(scope="module")
197
+ def contract_urns(wagon_manifests: List[Tuple[Path, Dict[str, Any]]]) -> List[str]:
198
+ """
199
+ Extract all contract URNs from wagon produce items.
200
+
201
+ Returns:
202
+ List of unique contract URNs (e.g., "contract:ux:foundations")
203
+ """
204
+ urns = set()
205
+ for _, manifest in wagon_manifests:
206
+ for produce_item in manifest.get("produce", []):
207
+ contract = produce_item.get("contract")
208
+ if contract and contract is not None:
209
+ urns.add(contract)
210
+ return sorted(urns)
211
+
212
+
213
+ @pytest.fixture(scope="module")
214
+ def telemetry_urns(wagon_manifests: List[Tuple[Path, Dict[str, Any]]]) -> List[str]:
215
+ """
216
+ Extract all telemetry URNs from wagon produce items.
217
+
218
+ Returns:
219
+ List of unique telemetry URNs (e.g., "telemetry:ux:foundations")
220
+ """
221
+ urns = set()
222
+ for _, manifest in wagon_manifests:
223
+ for produce_item in manifest.get("produce", []):
224
+ telemetry = produce_item.get("telemetry")
225
+ if telemetry and telemetry is not None:
226
+ # Handle both string and list types
227
+ if isinstance(telemetry, list):
228
+ urns.update(telemetry)
229
+ else:
230
+ urns.add(telemetry)
231
+ return sorted(urns)
232
+
233
+
234
+ @pytest.fixture(scope="module")
235
+ def typescript_test_files() -> List[Path]:
236
+ """
237
+ Discover all TypeScript test files in supabase/ and e2e/ directories.
238
+
239
+ Returns:
240
+ List of Path objects pointing to *.test.ts files
241
+ """
242
+ ts_tests = []
243
+
244
+ # Search in supabase/functions/*/test/
245
+ supabase_dir = REPO_ROOT / "supabase"
246
+ if supabase_dir.exists():
247
+ ts_tests.extend(supabase_dir.rglob("*.test.ts"))
248
+
249
+ # Search in e2e/
250
+ e2e_dir = REPO_ROOT / "e2e"
251
+ if e2e_dir.exists():
252
+ ts_tests.extend(e2e_dir.rglob("*.test.ts"))
253
+
254
+ return sorted(ts_tests)
255
+
256
+
257
+ @pytest.fixture(scope="module")
258
+ def web_typescript_test_files() -> List[Path]:
259
+ """
260
+ Discover all Preact TypeScript test files in web/tests/.
261
+
262
+ Returns:
263
+ List of Path objects pointing to *.test.ts and *.test.tsx files
264
+ """
265
+ web_tests_dir = REPO_ROOT / "web" / "tests"
266
+ if not web_tests_dir.exists():
267
+ return []
268
+
269
+ ts_tests = []
270
+ ts_tests.extend(web_tests_dir.rglob("*.test.ts"))
271
+ ts_tests.extend(web_tests_dir.rglob("*.test.tsx"))
272
+ return sorted(ts_tests)
273
+
274
+
275
+ # Helper functions
276
+ def parse_urn(urn: str) -> Tuple[str, str, str]:
277
+ """
278
+ Parse URN into components.
279
+
280
+ Args:
281
+ urn: URN string like "contract:ux:foundations"
282
+
283
+ Returns:
284
+ Tuple of (type, domain, resource)
285
+
286
+ Example:
287
+ >>> parse_urn("contract:ux:foundations")
288
+ ("contract", "ux", "foundations")
289
+ """
290
+ parts = urn.split(":")
291
+ if len(parts) != 3:
292
+ raise ValueError(f"Invalid URN format: {urn} (expected type:domain:resource)")
293
+ return tuple(parts)
294
+
295
+
296
+ def get_wagon_slug(manifest: Dict[str, Any]) -> str:
297
+ """Extract wagon slug from manifest."""
298
+ return manifest.get("wagon", "")
299
+
300
+
301
+ def get_produce_names(manifest: Dict[str, Any]) -> List[str]:
302
+ """Extract produce artifact names from manifest."""
303
+ return [item.get("name", "") for item in manifest.get("produce", [])]
304
+
305
+
306
+ def get_consume_names(manifest: Dict[str, Any]) -> List[str]:
307
+ """Extract consume artifact names from manifest."""
308
+ return [item.get("name", "") for item in manifest.get("consume", [])]
309
+
310
+
311
+ # HTML Report Customization
312
+ def pytest_html_report_title(report):
313
+ """Customize HTML report title."""
314
+ report.title = "Platform Validation Test Report"
315
+
316
+
317
+ def pytest_configure(config):
318
+ """Add custom metadata to HTML report."""
319
+ config._metadata = {
320
+ "Project": "Wagons Platform",
321
+ "Test Suite": "Platform Validation",
322
+ "Environment": "Development",
323
+ "Python": "3.11",
324
+ "Pytest": "8.4.2",
325
+ }
326
+
327
+
328
+ def pytest_html_results_table_header(cells):
329
+ """Customize HTML report table headers."""
330
+ cells.insert(2, '<th>Category</th>')
331
+ cells.insert(1, '<th class="sortable time" data-column-type="time">Duration</th>')
332
+
333
+
334
+ def pytest_html_results_table_row(report, cells):
335
+ """Customize HTML report table rows."""
336
+ # Add category based on test module
337
+ category = "Unknown"
338
+ if hasattr(report, 'nodeid'):
339
+ if 'wagons' in report.nodeid:
340
+ category = '📋 Schema'
341
+ elif 'cross_refs' in report.nodeid:
342
+ category = '🔗 References'
343
+ elif 'urn_resolution' in report.nodeid:
344
+ category = '🗺️ URN Resolution'
345
+ elif 'uniqueness' in report.nodeid:
346
+ category = '🎯 Uniqueness'
347
+ elif 'contracts_structure' in report.nodeid:
348
+ category = '📄 Contracts'
349
+ elif 'telemetry_structure' in report.nodeid:
350
+ category = '📊 Telemetry'
351
+
352
+ cells.insert(2, f'<td>{category}</td>')
353
+ cells.insert(1, f'<td class="col-duration">{getattr(report, "duration", 0):.2f}s</td>')
354
+
355
+
356
+ def pytest_html_results_summary(prefix, summary, postfix):
357
+ """Add custom summary to HTML report."""
358
+ prefix.extend([
359
+ '<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); '
360
+ 'padding: 20px; border-radius: 8px; color: white; margin: 20px 0;">'
361
+ '<h2 style="margin: 0 0 10px 0;">🚀 Platform Validation Suite</h2>'
362
+ '<p style="margin: 0; opacity: 0.9;">E2E validation of repository data '
363
+ 'against platform schemas and conventions.</p>'
364
+ '</div>'
365
+ ])
@@ -0,0 +1,167 @@
1
+ """
2
+ SPEC-COACH-UTILS-0290: Add features section and simplify WMBT counts in wagon registry
3
+
4
+ Tests enrichment of _wagons.yaml with features list and simplified WMBT totals.
5
+ """
6
+ import pytest
7
+ import yaml
8
+ from pathlib import Path
9
+ from tempfile import TemporaryDirectory
10
+
11
+
12
+ def test_add_features_and_simplify_wmbt(tmp_path):
13
+ """
14
+ SPEC-COACH-UTILS-0290: Add features section and simplify WMBT counts in wagon registry
15
+
16
+ Given: _wagons.yaml exists with wagon entries
17
+ Each wagon entry may have full wmbt details or empty wmbt: {}
18
+ Individual wagon manifests contain features: array with URN objects
19
+ Individual wagon manifests contain wmbt.total count
20
+ When: Enriching _wagons.yaml with features and WMBT counts
21
+ Then: Each wagon entry in _wagons.yaml has a features: section
22
+ features: section contains array of feature URNs from wagon manifest
23
+ Wagons without features in manifest get empty features: []
24
+ wmbt: {} or wmbt detailed entries are replaced with total: N
25
+ total value comes from wagon manifest wmbt.total field
26
+ All other wagon fields remain unchanged
27
+ YAML structure and formatting preserved
28
+ """
29
+ # Setup test directories
30
+ plan_dir = tmp_path / "plan"
31
+ plan_dir.mkdir()
32
+
33
+ # Create sample wagon manifests with features and wmbt.total
34
+ wagon_a_dir = plan_dir / "wagon_a"
35
+ wagon_a_dir.mkdir()
36
+ wagon_a_manifest = wagon_a_dir / "_wagon_a.yaml"
37
+ wagon_a_data = {
38
+ "wagon": "wagon-a",
39
+ "description": "Test wagon A",
40
+ "theme": "commons",
41
+ "subject": "system:test",
42
+ "context": "test",
43
+ "action": "test action",
44
+ "goal": "test goal",
45
+ "outcome": "test outcome",
46
+ "produce": [{"name": "test:artifact", "contract": None, "telemetry": None}],
47
+ "consume": [],
48
+ "features": [
49
+ {"urn": "feature:wagon-a.feature-one"},
50
+ {"urn": "feature:wagon-a.feature-two"}
51
+ ],
52
+ "wmbt": {
53
+ "L001": "Test WMBT 1",
54
+ "P001": "Test WMBT 2",
55
+ "total": 2
56
+ }
57
+ }
58
+ with open(wagon_a_manifest, 'w') as f:
59
+ yaml.dump(wagon_a_data, f, default_flow_style=False, sort_keys=False)
60
+
61
+ # Create wagon B with no features
62
+ wagon_b_dir = plan_dir / "wagon_b"
63
+ wagon_b_dir.mkdir()
64
+ wagon_b_manifest = wagon_b_dir / "_wagon_b.yaml"
65
+ wagon_b_data = {
66
+ "wagon": "wagon-b",
67
+ "description": "Test wagon B",
68
+ "theme": "commons",
69
+ "subject": "system:test",
70
+ "context": "test",
71
+ "action": "test action",
72
+ "goal": "test goal",
73
+ "outcome": "test outcome",
74
+ "produce": [{"name": "test:artifact", "contract": None, "telemetry": None}],
75
+ "consume": [],
76
+ "wmbt": {
77
+ "total": 0
78
+ }
79
+ }
80
+ with open(wagon_b_manifest, 'w') as f:
81
+ yaml.dump(wagon_b_data, f, default_flow_style=False, sort_keys=False)
82
+
83
+ # Create _wagons.yaml with wagons that need enrichment
84
+ wagons_file = plan_dir / "_wagons.yaml"
85
+ wagons_data = {
86
+ "wagons": [
87
+ {
88
+ "wagon": "wagon-a",
89
+ "description": "Test wagon A",
90
+ "theme": "commons",
91
+ "subject": "system:test",
92
+ "context": "test",
93
+ "action": "test action",
94
+ "goal": "test goal",
95
+ "outcome": "test outcome",
96
+ "produce": [{"name": "test:artifact", "to": "external"}],
97
+ "consume": [],
98
+ "wmbt": {
99
+ "L001": "Test WMBT 1",
100
+ "P001": "Test WMBT 2"
101
+ },
102
+ "total": 2,
103
+ "manifest": "plan/wagon_a/_wagon_a.yaml",
104
+ "path": "plan/wagon_a/"
105
+ },
106
+ {
107
+ "wagon": "wagon-b",
108
+ "description": "Test wagon B",
109
+ "theme": "commons",
110
+ "subject": "system:test",
111
+ "context": "test",
112
+ "action": "test action",
113
+ "goal": "test goal",
114
+ "outcome": "test outcome",
115
+ "produce": [{"name": "test:artifact", "to": "external"}],
116
+ "consume": [],
117
+ "wmbt": {},
118
+ "total": 0,
119
+ "manifest": "plan/wagon_b/_wagon_b.yaml",
120
+ "path": "plan/wagon_b/"
121
+ }
122
+ ]
123
+ }
124
+ with open(wagons_file, 'w') as f:
125
+ yaml.dump(wagons_data, f, default_flow_style=False, sort_keys=False)
126
+
127
+ # Import and call the enrichment via RegistryBuilder
128
+ from atdd.coach.commands.registry import RegistryBuilder
129
+
130
+ # Create builder and enrich registry
131
+ builder = RegistryBuilder(tmp_path)
132
+ builder.enrich_wagon_registry()
133
+
134
+ # Load the enriched _wagons.yaml
135
+ with open(wagons_file, 'r') as f:
136
+ enriched_data = yaml.safe_load(f)
137
+
138
+ # Assertions
139
+ wagons = enriched_data["wagons"]
140
+
141
+ # Check wagon-a
142
+ wagon_a = next(w for w in wagons if w["wagon"] == "wagon-a")
143
+ assert "features" in wagon_a, "wagon-a should have features section"
144
+ assert wagon_a["features"] == [
145
+ {"urn": "feature:wagon-a.feature-one"},
146
+ {"urn": "feature:wagon-a.feature-two"}
147
+ ], "wagon-a features should match manifest"
148
+ assert "wmbt" in wagon_a, "wagon-a should have wmbt object"
149
+ assert wagon_a["wmbt"]["total"] == 2, "wagon-a wmbt.total should be 2"
150
+ assert wagon_a["wmbt"]["coverage"] == 0, "wagon-a wmbt.coverage should be 0"
151
+ assert "L001" not in wagon_a["wmbt"], "wagon-a wmbt should not have detailed entries"
152
+ assert "total" not in wagon_a or wagon_a.get("total") is None, "wagon-a should not have root-level total field"
153
+
154
+ # Check wagon-b
155
+ wagon_b = next(w for w in wagons if w["wagon"] == "wagon-b")
156
+ assert "features" in wagon_b, "wagon-b should have features section"
157
+ assert wagon_b["features"] == [], "wagon-b should have empty features list"
158
+ assert "wmbt" in wagon_b, "wagon-b should have wmbt object"
159
+ assert wagon_b["wmbt"]["total"] == 0, "wagon-b wmbt.total should be 0"
160
+ assert wagon_b["wmbt"]["coverage"] == 0, "wagon-b wmbt.coverage should be 0"
161
+ assert "total" not in wagon_b or wagon_b.get("total") is None, "wagon-b should not have root-level total field"
162
+
163
+ # Check other fields remain unchanged
164
+ assert wagon_a["description"] == "Test wagon A"
165
+ assert wagon_a["manifest"] == "plan/wagon_a/_wagon_a.yaml"
166
+ assert wagon_b["description"] == "Test wagon B"
167
+ assert wagon_b["manifest"] == "plan/wagon_b/_wagon_b.yaml"