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,224 @@
1
+ """
2
+ Platform tests: Uniqueness constraints.
3
+
4
+ Validates that entities have unique identifiers across the repository.
5
+ Tests ensure no duplicate wagon slugs, train IDs, or WMBT IDs.
6
+ """
7
+ import pytest
8
+ from collections import Counter
9
+ from typing import Dict, List, Any
10
+
11
+
12
+ @pytest.mark.platform
13
+ def test_wagon_slugs_are_unique(wagon_manifests):
14
+ """
15
+ SPEC-PLATFORM-UNIQUE-0001: Wagon slugs are unique across repository
16
+
17
+ Given: All wagon manifests
18
+ When: Checking wagon slug uniqueness
19
+ Then: Each wagon slug appears exactly once
20
+ No two wagons share the same slug
21
+ """
22
+ wagon_slugs = [manifest.get("wagon", "") for _, manifest in wagon_manifests]
23
+ slug_counts = Counter(wagon_slugs)
24
+
25
+ duplicates = {slug: count for slug, count in slug_counts.items() if count > 1}
26
+
27
+ assert not duplicates, \
28
+ f"Found duplicate wagon slugs:\n" + \
29
+ "\n".join(f" '{slug}': {count} occurrences" for slug, count in duplicates.items())
30
+
31
+
32
+ @pytest.mark.platform
33
+ def test_train_ids_are_unique(trains_registry):
34
+ """
35
+ SPEC-PLATFORM-UNIQUE-0002: Train IDs are unique across repository
36
+
37
+ Given: All train definitions in plan/_trains.yaml
38
+ When: Checking train_id uniqueness
39
+ Then: Each train_id appears exactly once
40
+ """
41
+ train_ids = [train.get("train_id", "") for train in trains_registry.get("trains", [])]
42
+ train_id_counts = Counter(train_ids)
43
+
44
+ duplicates = {tid: count for tid, count in train_id_counts.items() if count > 1}
45
+
46
+ assert not duplicates, \
47
+ f"Found duplicate train IDs:\n" + \
48
+ "\n".join(f" '{tid}': {count} occurrences" for tid, count in duplicates.items())
49
+
50
+
51
+ @pytest.mark.platform
52
+ def test_produce_artifact_names_unique_per_wagon(wagon_manifests):
53
+ """
54
+ SPEC-PLATFORM-UNIQUE-0003: Produce artifact names unique within each wagon
55
+
56
+ Given: Wagon produce items
57
+ When: Checking artifact name uniqueness per wagon
58
+ Then: Each wagon has unique produce artifact names
59
+ No wagon produces the same artifact name twice
60
+ """
61
+ errors = []
62
+
63
+ for path, manifest in wagon_manifests:
64
+ wagon_slug = manifest.get("wagon", "")
65
+ artifact_names = [item.get("name", "") for item in manifest.get("produce", [])]
66
+
67
+ name_counts = Counter(artifact_names)
68
+ duplicates = {name: count for name, count in name_counts.items() if count > 1}
69
+
70
+ if duplicates:
71
+ errors.append(
72
+ f"Wagon '{wagon_slug}' at {path} has duplicate produce names:\n" +
73
+ "\n".join(f" '{name}': {count} occurrences" for name, count in duplicates.items())
74
+ )
75
+
76
+ if errors:
77
+ pytest.fail("\n\n".join(errors))
78
+
79
+
80
+ @pytest.mark.platform
81
+ def test_wmbt_ids_unique_per_wagon(wagon_manifests):
82
+ """
83
+ SPEC-PLATFORM-UNIQUE-0004: WMBT IDs are unique within each wagon
84
+
85
+ Given: Wagon WMBT definitions
86
+ When: Checking WMBT ID uniqueness per wagon
87
+ Then: Each wagon has unique WMBT IDs
88
+ No wagon defines the same WMBT ID twice
89
+ """
90
+ errors = []
91
+
92
+ for path, manifest in wagon_manifests:
93
+ wagon_slug = manifest.get("wagon", "")
94
+ wmbt_section = manifest.get("wmbt", {})
95
+
96
+ # WMBT section can be dict or list - handle both formats
97
+ if isinstance(wmbt_section, dict):
98
+ wmbt_ids = list(wmbt_section.keys())
99
+ elif isinstance(wmbt_section, list):
100
+ wmbt_ids = [item.get("id", "") for item in wmbt_section]
101
+ else:
102
+ continue # Skip if wmbt is not dict or list
103
+
104
+ wmbt_id_counts = Counter(wmbt_ids)
105
+ duplicates = {wid: count for wid, count in wmbt_id_counts.items() if count > 1}
106
+
107
+ if duplicates:
108
+ errors.append(
109
+ f"Wagon '{wagon_slug}' at {path} has duplicate WMBT IDs:\n" +
110
+ "\n".join(f" '{wid}': {count} occurrences" for wid, count in duplicates.items())
111
+ )
112
+
113
+ if errors:
114
+ pytest.fail("\n\n".join(errors))
115
+
116
+
117
+ @pytest.mark.platform
118
+ def test_feature_urns_unique_per_wagon(wagon_manifests):
119
+ """
120
+ SPEC-PLATFORM-UNIQUE-0005: Feature URNs are unique within each wagon
121
+
122
+ Given: Wagon feature definitions (URN array format)
123
+ When: Checking feature URN uniqueness per wagon
124
+ Then: Each wagon has unique feature URNs
125
+ No wagon defines the same feature URN twice
126
+ """
127
+ errors = []
128
+
129
+ for path, manifest in wagon_manifests:
130
+ wagon_slug = manifest.get("wagon", "")
131
+ features = manifest.get("features", [])
132
+
133
+ # Features can be array of URN objects or legacy dict - handle array format
134
+ if isinstance(features, list):
135
+ feature_urns = [item.get("urn", "") for item in features if isinstance(item, dict)]
136
+
137
+ urn_counts = Counter(feature_urns)
138
+ duplicates = {urn: count for urn, count in urn_counts.items() if count > 1}
139
+
140
+ if duplicates:
141
+ errors.append(
142
+ f"Wagon '{wagon_slug}' at {path} has duplicate feature URNs:\n" +
143
+ "\n".join(f" '{urn}': {count} occurrences" for urn, count in duplicates.items())
144
+ )
145
+
146
+ if errors:
147
+ pytest.fail("\n\n".join(errors))
148
+
149
+
150
+ @pytest.mark.platform
151
+ def test_contract_urns_unique_globally(wagon_manifests):
152
+ """
153
+ SPEC-PLATFORM-UNIQUE-0006: Contract URNs are unique globally
154
+
155
+ Given: All contract URNs from wagon produce items
156
+ When: Checking global uniqueness
157
+ Then: Each contract URN is produced by exactly one wagon
158
+ No two wagons produce the same contract URN
159
+ """
160
+ contract_to_wagons: Dict[str, List[str]] = {}
161
+
162
+ for path, manifest in wagon_manifests:
163
+ wagon_slug = manifest.get("wagon", "")
164
+
165
+ for produce_item in manifest.get("produce", []):
166
+ contract = produce_item.get("contract")
167
+ if contract and contract is not None:
168
+ contract_to_wagons.setdefault(contract, []).append(wagon_slug)
169
+
170
+ # Find contracts produced by multiple wagons
171
+ duplicates = {
172
+ contract: wagons
173
+ for contract, wagons in contract_to_wagons.items()
174
+ if len(wagons) > 1
175
+ }
176
+
177
+ if duplicates:
178
+ pytest.fail(
179
+ f"Found contract URNs produced by multiple wagons:\n" +
180
+ "\n".join(
181
+ f" '{contract}': {wagons}"
182
+ for contract, wagons in duplicates.items()
183
+ )
184
+ )
185
+
186
+
187
+ @pytest.mark.platform
188
+ def test_telemetry_urns_unique_globally(wagon_manifests):
189
+ """
190
+ SPEC-PLATFORM-UNIQUE-0007: Telemetry URNs are unique globally
191
+
192
+ Given: All telemetry URNs from wagon produce items
193
+ When: Checking global uniqueness
194
+ Then: Each telemetry URN is produced by exactly one wagon
195
+ No two wagons produce the same telemetry URN
196
+ """
197
+ telemetry_to_wagons: Dict[str, List[str]] = {}
198
+
199
+ for path, manifest in wagon_manifests:
200
+ wagon_slug = manifest.get("wagon", "")
201
+
202
+ for produce_item in manifest.get("produce", []):
203
+ telemetry = produce_item.get("telemetry")
204
+ if telemetry and telemetry is not None:
205
+ # Handle both string and list types
206
+ telemetry_urns = telemetry if isinstance(telemetry, list) else [telemetry]
207
+ for urn in telemetry_urns:
208
+ telemetry_to_wagons.setdefault(urn, []).append(wagon_slug)
209
+
210
+ # Find telemetry URNs produced by multiple wagons
211
+ duplicates = {
212
+ telemetry: wagons
213
+ for telemetry, wagons in telemetry_to_wagons.items()
214
+ if len(wagons) > 1
215
+ }
216
+
217
+ if duplicates:
218
+ pytest.fail(
219
+ f"Found telemetry URNs produced by multiple wagons:\n" +
220
+ "\n".join(
221
+ f" '{telemetry}': {wagons}"
222
+ for telemetry, wagons in duplicates.items()
223
+ )
224
+ )
@@ -0,0 +1,268 @@
1
+ """
2
+ Platform tests: URN resolution to filesystem.
3
+
4
+ Validates that contract and telemetry URNs resolve to actual directories/files.
5
+ Tests ensure URN to filesystem mapping follows conventions.
6
+ """
7
+ import pytest
8
+ from pathlib import Path
9
+ from typing import Tuple
10
+
11
+ # Path constants
12
+ REPO_ROOT = Path(__file__).resolve().parents[4]
13
+ CONTRACTS_DIR = REPO_ROOT / "contracts"
14
+ TELEMETRY_DIR = REPO_ROOT / "telemetry"
15
+
16
+
17
+ def parse_urn(urn: str) -> Tuple:
18
+ """Parse URN into components.
19
+
20
+ Returns tuple of (type, *path_parts) where:
21
+ - type: contract or telemetry
22
+ - path_parts: remaining segments (2+ for multi-level paths)
23
+ - Both : and . create directory levels per artifact-naming convention
24
+
25
+ Examples:
26
+ - contract:ux:foundations → ('contract', 'ux', 'foundations')
27
+ - telemetry:commons:ux:foundations → ('telemetry', 'commons', 'ux', 'foundations')
28
+ - telemetry:match:dilemma.paired → ('telemetry', 'match', 'dilemma', 'paired')
29
+ """
30
+ import re
31
+ parts = urn.split(":", 1) # Split into [type, rest]
32
+ if len(parts) < 2:
33
+ raise ValueError(f"Invalid URN format: {urn} (must have URN type)")
34
+
35
+ urn_type = parts[0]
36
+ rest = parts[1]
37
+
38
+ # Split rest by both : and . per artifact-naming convention
39
+ segments = re.split(r'[:\.]', rest)
40
+
41
+ if len(segments) < 2:
42
+ raise ValueError(f"Invalid URN format: {urn} (must have at least 2 path segments)")
43
+
44
+ return tuple([urn_type] + segments)
45
+
46
+
47
+ @pytest.mark.platform
48
+ @pytest.mark.e2e
49
+ def test_contract_urn_resolves_to_directory(contract_urns):
50
+ """
51
+ SPEC-PLATFORM-URN-0001: Contract URNs resolve to filesystem directories
52
+
53
+ Given: A contract URN from wagon produce (e.g., contract:ux:foundations)
54
+ When: Mapping URN to filesystem path
55
+ Then: contracts/{domain}/{resource}/ directory exists
56
+ OR contracts/{domain}/ exists (for domain-level contracts)
57
+ """
58
+ errors = []
59
+
60
+ for contract_urn in contract_urns:
61
+ parts = parse_urn(contract_urn)
62
+ urn_type = parts[0]
63
+ path_parts = parts[1:] # All parts after 'contract:'
64
+
65
+ # Expected path: contracts/{path_parts joined}/
66
+ # For contract:commons:player:identity -> contracts/commons/player/identity/
67
+ # For contract:ux:foundations -> contracts/ux/foundations/
68
+ expected_path = CONTRACTS_DIR / Path(*path_parts)
69
+
70
+ # Also check for domain-level contracts: contracts/{domain}/
71
+ domain_path = CONTRACTS_DIR / path_parts[0] if len(path_parts) >= 1 else None
72
+
73
+ if not (expected_path.exists() or (domain_path and domain_path.exists())):
74
+ errors.append(
75
+ f"Contract URN {contract_urn} does not resolve to filesystem:\n"
76
+ f" Expected: {expected_path} OR {domain_path}\n"
77
+ f" Neither path exists"
78
+ )
79
+
80
+ if errors:
81
+ pytest.fail("\n\n".join(errors))
82
+
83
+
84
+ @pytest.mark.platform
85
+ @pytest.mark.e2e
86
+ def test_telemetry_urn_resolves_to_directory(telemetry_urns):
87
+ """
88
+ SPEC-PLATFORM-URN-0002: Telemetry URNs resolve to filesystem directories
89
+
90
+ Given: A telemetry URN from wagon produce
91
+ When: Mapping URN to filesystem path
92
+ Then: telemetry/{path}/ directory exists (multi-level paths supported)
93
+
94
+ Examples:
95
+ - telemetry:ux:foundations → telemetry/ux/foundations/
96
+ - telemetry:commons:ux:foundations → telemetry/commons/ux/foundations/
97
+ """
98
+ errors = []
99
+
100
+ for telemetry_urn in telemetry_urns:
101
+ parts = parse_urn(telemetry_urn)
102
+ urn_type = parts[0]
103
+ path_parts = parts[1:] # All parts after 'telemetry:'
104
+
105
+ # Expected path: telemetry/{path}/{to}/{aspect}/
106
+ expected_path = TELEMETRY_DIR / Path(*path_parts)
107
+
108
+ if not expected_path.exists():
109
+ errors.append(
110
+ f"Telemetry URN {telemetry_urn} does not resolve to filesystem:\n"
111
+ f" Expected: {expected_path}\n"
112
+ f" Path does not exist"
113
+ )
114
+
115
+ if errors:
116
+ pytest.fail("\n\n".join(errors))
117
+
118
+
119
+ @pytest.mark.platform
120
+ @pytest.mark.e2e
121
+ def test_telemetry_directory_contains_signal_files(telemetry_urns):
122
+ """
123
+ SPEC-PLATFORM-URN-0003: Telemetry directories contain signal JSON files
124
+
125
+ Given: A telemetry URN that resolves to a directory
126
+ When: Checking directory contents
127
+ Then: Directory contains *.json files (signal definitions)
128
+ Signal files follow pattern: {aspect}.{signal-type}.{plane}[.{measure}].json
129
+ """
130
+ errors = []
131
+
132
+ for telemetry_urn in telemetry_urns:
133
+ parts = parse_urn(telemetry_urn)
134
+ path_parts = parts[1:] # All parts after 'telemetry:'
135
+ telemetry_path = TELEMETRY_DIR / Path(*path_parts)
136
+
137
+ if not telemetry_path.exists():
138
+ continue # Tested in URN-0002
139
+
140
+ # Find all JSON files in directory
141
+ json_files = list(telemetry_path.glob("*.json"))
142
+
143
+ if not json_files:
144
+ errors.append(
145
+ f"Telemetry directory {telemetry_path} exists but contains no *.json signal files"
146
+ )
147
+
148
+ if errors:
149
+ pytest.fail("\n\n".join(errors))
150
+
151
+
152
+ @pytest.mark.platform
153
+ def test_all_contract_urns_are_unique(contract_urns):
154
+ """
155
+ SPEC-PLATFORM-URN-0004: Contract URNs are unique across wagons
156
+
157
+ Given: All contract URNs from wagon produce items
158
+ When: Checking for duplicates
159
+ Then: Each contract URN appears only once
160
+ No two wagons produce the same contract URN
161
+ """
162
+ from collections import Counter
163
+
164
+ # contract_urns fixture already returns unique URNs (set to sorted)
165
+ # This test validates the assumption
166
+ urn_counts = Counter(contract_urns)
167
+ duplicates = {urn: count for urn, count in urn_counts.items() if count > 1}
168
+
169
+ assert not duplicates, \
170
+ f"Found duplicate contract URNs (should be unique):\n" + \
171
+ "\n".join(f" {urn}: {count} occurrences" for urn, count in duplicates.items())
172
+
173
+
174
+ @pytest.mark.platform
175
+ def test_all_telemetry_urns_are_unique(telemetry_urns):
176
+ """
177
+ SPEC-PLATFORM-URN-0005: Telemetry URNs are unique across wagons
178
+
179
+ Given: All telemetry URNs from wagon produce items
180
+ When: Checking for duplicates
181
+ Then: Each telemetry URN appears only once
182
+ No two wagons produce the same telemetry URN
183
+ """
184
+ from collections import Counter
185
+
186
+ # telemetry_urns fixture already returns unique URNs
187
+ urn_counts = Counter(telemetry_urns)
188
+ duplicates = {urn: count for urn, count in urn_counts.items() if count > 1}
189
+
190
+ assert not duplicates, \
191
+ f"Found duplicate telemetry URNs (should be unique):\n" + \
192
+ "\n".join(f" {urn}: {count} occurrences" for urn, count in duplicates.items())
193
+
194
+
195
+ @pytest.mark.platform
196
+ def test_contracts_directory_structure_matches_urns(contract_urns):
197
+ """
198
+ SPEC-PLATFORM-URN-0006: contracts/ structure aligns with URN domain/resource
199
+
200
+ Given: Contract URNs and contracts/ directory
201
+ When: Comparing URN domain/resource to directory structure
202
+ Then: For each contract URN contract:domain:resource,
203
+ contracts/{domain}/{resource}/ exists OR contracts/{domain}/ exists
204
+ """
205
+ if not CONTRACTS_DIR.exists():
206
+ pytest.skip(f"contracts/ directory does not exist at {CONTRACTS_DIR}")
207
+ return
208
+
209
+ missing_paths = []
210
+
211
+ for urn in contract_urns:
212
+ parts = parse_urn(urn)
213
+ urn_type = parts[0]
214
+ path_parts = parts[1:] # All parts after 'contract:'
215
+
216
+ # Expected path: contracts/{path_parts joined}/
217
+ resource_path = CONTRACTS_DIR / Path(*path_parts)
218
+ domain_path = CONTRACTS_DIR / path_parts[0] if len(path_parts) >= 1 else None
219
+
220
+ if not (resource_path.exists() or (domain_path and domain_path.exists())):
221
+ missing_paths.append(
222
+ f" {urn} -> Expected: {resource_path} OR {domain_path}"
223
+ )
224
+
225
+ if missing_paths:
226
+ pytest.fail(
227
+ f"Found {len(missing_paths)} contract URNs without matching filesystem paths:\n" +
228
+ "\n".join(missing_paths[:10]) +
229
+ (f"\n ... and {len(missing_paths) - 10} more" if len(missing_paths) > 10 else "")
230
+ )
231
+
232
+
233
+ @pytest.mark.platform
234
+ def test_telemetry_directory_structure_matches_urns(telemetry_urns):
235
+ """
236
+ SPEC-PLATFORM-URN-0007: telemetry/ structure aligns with URN hierarchy
237
+
238
+ Given: Telemetry URNs and telemetry/ directory
239
+ When: Comparing URN path to directory structure
240
+ Then: For each telemetry URN telemetry:{path}:{aspect},
241
+ telemetry/{path}/{aspect}/ exists (supports multi-level paths)
242
+
243
+ Examples:
244
+ - telemetry:ux:foundations → telemetry/ux/foundations/
245
+ - telemetry:commons:ux:foundations → telemetry/commons/ux/foundations/
246
+ """
247
+ if not TELEMETRY_DIR.exists():
248
+ pytest.skip(f"telemetry/ directory does not exist at {TELEMETRY_DIR}")
249
+ return
250
+
251
+ missing_paths = []
252
+
253
+ for urn in telemetry_urns:
254
+ parts = parse_urn(urn)
255
+ path_parts = parts[1:] # All parts after 'telemetry:'
256
+ expected_path = TELEMETRY_DIR / Path(*path_parts)
257
+
258
+ if not expected_path.exists():
259
+ missing_paths.append(
260
+ f" {urn} -> Expected: {expected_path}"
261
+ )
262
+
263
+ if missing_paths:
264
+ pytest.fail(
265
+ f"Found {len(missing_paths)} telemetry URNs without matching filesystem paths:\n" +
266
+ "\n".join(missing_paths[:10]) +
267
+ (f"\n ... and {len(missing_paths) - 10} more" if len(missing_paths) > 10 else "")
268
+ )
@@ -0,0 +1,174 @@
1
+ """
2
+ Platform tests: Wagon manifest schema validation.
3
+
4
+ Validates that all wagon manifests in plan/ conform to wagon.schema.json.
5
+ Tests are parametrized to run once per wagon manifest for surgical diagnostics.
6
+ """
7
+ import pytest
8
+ import jsonschema
9
+ from pathlib import Path
10
+
11
+
12
+ @pytest.mark.platform
13
+ @pytest.mark.e2e
14
+ def test_wagon_manifest_matches_schema(wagon_schema, wagon_manifests):
15
+ """
16
+ SPEC-PLATFORM-WAGONS-0001: Wagon manifest validates against wagon.schema.json
17
+
18
+ Given: A wagon manifest YAML file in plan/
19
+ When: Validated against .claude/schemas/planner/wagon.schema.json
20
+ Then: Validation passes with no schema errors
21
+ All required fields are present
22
+ URN patterns match expected format
23
+ """
24
+ errors = []
25
+
26
+ for manifest_path, manifest in wagon_manifests:
27
+ try:
28
+ jsonschema.validate(manifest, wagon_schema)
29
+ except jsonschema.ValidationError as e:
30
+ errors.append(
31
+ f"Wagon manifest validation failed for {manifest_path}:\n"
32
+ f" Error: {e.message}\n"
33
+ f" Path: {' -> '.join(str(p) for p in e.path)}\n"
34
+ f" Schema path: {' -> '.join(str(p) for p in e.schema_path)}"
35
+ )
36
+
37
+ if errors:
38
+ pytest.fail("\n\n".join(errors))
39
+
40
+
41
+ @pytest.mark.platform
42
+ def test_all_wagons_have_required_fields(wagon_manifests):
43
+ """
44
+ SPEC-PLATFORM-WAGONS-0002: All wagons have required top-level fields
45
+
46
+ Given: All wagon manifests
47
+ When: Checking required fields
48
+ Then: Each wagon has: wagon, description, subject, context, action, goal, outcome
49
+ """
50
+ required_fields = ["wagon", "description", "subject", "context", "action", "goal", "outcome"]
51
+
52
+ for path, manifest in wagon_manifests:
53
+ missing_fields = [field for field in required_fields if field not in manifest]
54
+ assert not missing_fields, \
55
+ f"Wagon {path} missing required fields: {missing_fields}"
56
+
57
+
58
+ @pytest.mark.platform
59
+ def test_all_produce_items_have_contract_and_telemetry(wagon_manifests):
60
+ """
61
+ SPEC-PLATFORM-WAGONS-0003: All produce items have contract and telemetry fields
62
+
63
+ Given: All wagon manifests with produce items
64
+ When: Checking produce item structure
65
+ Then: Each produce item has 'contract' and 'telemetry' fields
66
+ Fields can be null but must be present
67
+ """
68
+ for path, manifest in wagon_manifests:
69
+ for idx, produce_item in enumerate(manifest.get("produce", [])):
70
+ assert "contract" in produce_item, \
71
+ f"Wagon {path}: produce[{idx}] missing 'contract' field"
72
+ assert "telemetry" in produce_item, \
73
+ f"Wagon {path}: produce[{idx}] missing 'telemetry' field"
74
+
75
+
76
+ @pytest.mark.platform
77
+ def test_wagon_slugs_match_directory_names(wagon_manifests):
78
+ """
79
+ SPEC-PLATFORM-WAGONS-0004: Wagon slugs match their directory names
80
+
81
+ Given: Wagon manifests in plan/{wagon_dirname}/ directories
82
+ When: Comparing wagon field to directory name
83
+ Then: Wagon slug (kebab-case) matches directory name (snake_case) after conversion
84
+ Per SPEC-COACH-UTILS-0281: slug.replace('-','_')→dirname, dirname.replace('_','-')→slug
85
+ """
86
+ for path, manifest in wagon_manifests:
87
+ wagon_slug = manifest.get("wagon", "")
88
+
89
+ # Check if manifest is in a wagon-specific directory (not in plan/ root)
90
+ if path.parent.name != "plan":
91
+ directory_name = path.parent.name
92
+ # Convert directory name (snake_case) to expected slug (kebab-case)
93
+ expected_slug = directory_name.replace('_', '-')
94
+ assert wagon_slug == expected_slug, \
95
+ f"Wagon slug '{wagon_slug}' doesn't match expected slug '{expected_slug}' (from directory '{directory_name}') for {path}"
96
+
97
+
98
+ @pytest.mark.platform
99
+ def test_produce_artifact_names_follow_convention(wagon_manifests):
100
+ """
101
+ SPEC-PLATFORM-WAGONS-0005: Produce artifact names follow naming convention
102
+
103
+ Given: Wagon produce items
104
+ When: Checking artifact name format
105
+ Then: Names follow pattern per artifact-naming.convention.yaml:
106
+ - Hierarchy: domain:resource (colon for hierarchy)
107
+ - Variants: domain:resource.variant (dot for facets)
108
+ - Can have unlimited colons, typically 0-1 dots
109
+ """
110
+ import re
111
+
112
+ # Pattern: domain(:category)*:aspect(.variant)? per artifact-naming.convention.yaml
113
+ # Allows: commons:auth, commons:auth.claims, commons:ux:foundations:color.primary
114
+ name_pattern = re.compile(r"^[a-z][a-z0-9\-]*:[a-z][a-z0-9\-:]*(\.[a-z][a-z0-9\-]+)?$")
115
+
116
+ for path, manifest in wagon_manifests:
117
+ for produce_item in manifest.get("produce", []):
118
+ name = produce_item.get("name", "")
119
+ if name: # Skip empty names
120
+ assert name_pattern.match(name), \
121
+ f"Wagon {path}: produce artifact name '{name}' doesn't match pattern per artifact-naming.convention.yaml"
122
+
123
+
124
+ @pytest.mark.platform
125
+ def test_contract_urns_match_pattern(wagon_manifests):
126
+ """
127
+ SPEC-PLATFORM-WAGONS-0006: Contract URNs follow naming convention
128
+
129
+ Given: Wagon produce items with non-null contract URNs
130
+ When: Validating URN format
131
+ Then: Contract URNs match pattern per artifact-naming.convention.yaml:
132
+ contract:{artifact_name} where artifact_name can have colons and dots
133
+ Pattern: contract:domain(:category)*:aspect(.variant)?
134
+ """
135
+ import re
136
+
137
+ # URN exactly matches artifact name with "contract:" prefix
138
+ # Per artifact-naming.convention.yaml line 627-645
139
+ contract_pattern = re.compile(r"^contract:[a-z][a-z0-9\-]*:[a-z][a-z0-9\-:]*(\.[a-z][a-z0-9\-]+)?$")
140
+
141
+ for path, manifest in wagon_manifests:
142
+ for produce_item in manifest.get("produce", []):
143
+ contract = produce_item.get("contract")
144
+ if contract and contract is not None:
145
+ assert contract_pattern.match(contract), \
146
+ f"Wagon {path}: contract URN '{contract}' doesn't match pattern per artifact-naming.convention.yaml"
147
+
148
+
149
+ @pytest.mark.platform
150
+ def test_telemetry_urns_match_pattern(wagon_manifests):
151
+ """
152
+ SPEC-PLATFORM-WAGONS-0007: Telemetry URNs follow multi-level pattern
153
+
154
+ Given: Wagon produce items with non-null telemetry URNs
155
+ When: Validating URN format
156
+ Then: Telemetry URNs match pattern telemetry:{path}:{aspect}
157
+ Supports multi-level paths (e.g., telemetry:commons:ux:foundations)
158
+ Uses colons (not dots) for hierarchy
159
+ """
160
+ import re
161
+
162
+ # Pattern: telemetry: followed by 2+ colon-separated segments (kebab-case), optional dot for variant
163
+ # Supports: telemetry:commons:ux:foundations, telemetry:match:dilemma.paired
164
+ telemetry_pattern = re.compile(r"^telemetry:([a-z][a-z0-9\-]*:)+[a-z][a-z0-9\-]*(\.[a-z][a-z0-9\-]*)?$")
165
+
166
+ for path, manifest in wagon_manifests:
167
+ for produce_item in manifest.get("produce", []):
168
+ telemetry = produce_item.get("telemetry")
169
+ if telemetry and telemetry is not None:
170
+ # Handle both string and list types
171
+ telemetry_urns = telemetry if isinstance(telemetry, list) else [telemetry]
172
+ for urn in telemetry_urns:
173
+ assert telemetry_pattern.match(urn), \
174
+ f"Wagon {path}: telemetry URN '{urn}' doesn't match pattern telemetry:{{path}}:{{aspect}}"