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,59 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "WMBT (What Must Be True) Schema",
4
+ "description": "Schema for What Must Be True statements that define measurable outcomes",
5
+ "type": "object",
6
+ "required": ["urn", "step", "direction", "dimension", "object_of_control", "lens"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "urn": {
10
+ "type": "string",
11
+ "pattern": "^wmbt:[a-z][a-z0-9-]*:[DLPCEMYRK][0-9]{3}$",
12
+ "description": "WMBT URN: wmbt:{wagon}:{step_coded_id} (e.g., wmbt:burn-timebank:E001) - step-coded sequence"
13
+ },
14
+ "step": {
15
+ "type": "string",
16
+ "enum": ["define", "locate", "prepare", "confirm", "execute", "monitor", "modify", "resolve", "conclude"],
17
+ "description": "JTBD decomposition step indicating where in the job flow this outcome matters"
18
+ },
19
+ "direction": {
20
+ "type": "string",
21
+ "enum": ["minimize", "maximize", "increase", "decrease"],
22
+ "description": "Optimization direction indicating how to improve the outcome"
23
+ },
24
+ "dimension": {
25
+ "type": "string",
26
+ "enum": ["time", "effort", "likelihood", "frequency", "quantity", "financial value"],
27
+ "description": "Measurement dimension - exactly one per WMBT to maintain focus"
28
+ },
29
+ "object_of_control": {
30
+ "type": "string",
31
+ "pattern": "^[a-z][a-z0-9-]*$",
32
+ "minLength": 2,
33
+ "description": "Descriptive of what is being controlled or measured"
34
+ },
35
+ "context_clarifier": {
36
+ "type": "string",
37
+ "pattern": "^[a-z].{1,250}$",
38
+ "description": "Detailed situational phrase specifying when/where/how this applies (one line maximum)"
39
+ },
40
+ "lens": {
41
+ "type": "string",
42
+ "pattern": "^(functional|emotional|social)\\.[a-z][a-z0-9-]*$",
43
+ "description": "Specific lens aspect reference (e.g., functional.efficiency, emotional.trust, social.belong)"
44
+ },
45
+ "statement": {
46
+ "type": "string",
47
+ "minLength": 10,
48
+ "description": "Human-readable statement: direction + dimension + object_of_control + context_clarifier"
49
+ },
50
+
51
+ "appendices": {
52
+ "type": "array",
53
+ "description": "Supplementary files specific to this WMBT scenario",
54
+ "items": {
55
+ "$ref": "appendix.schema.json#/definitions/appendix_reference"
56
+ }
57
+ }
58
+ }
59
+ }
File without changes
@@ -0,0 +1,5 @@
1
+ """
2
+ Shared fixtures for planner tests.
3
+ """
4
+ # Import all shared fixtures from coach via absolute import
5
+ from atdd.coach.validators.shared_fixtures import *
@@ -0,0 +1,374 @@
1
+ """
2
+ Platform tests: Draft Wagon Registry Validation.
3
+
4
+ Validates draft wagons in plan/_wagons.yaml registry for coherence with:
5
+ - Existing wagon manifests (prevent duplicates)
6
+ - Contract/telemetry references (check artifact resolution)
7
+ - Traceability (cross-reference validation)
8
+ - Implementation status (manifest vs draft)
9
+
10
+ Tests help agents make better decisions when encountering references to non-existent wagons
11
+ by checking the registry first before assuming external services.
12
+ """
13
+ import pytest
14
+ import yaml
15
+ from pathlib import Path
16
+ from atdd.coach.validators.shared_fixtures import PLAN_DIR
17
+
18
+
19
+ @pytest.fixture(scope="module")
20
+ def wagon_registry():
21
+ """Load plan/_wagons.yaml registry."""
22
+ registry_path = PLAN_DIR / "_wagons.yaml"
23
+ if not registry_path.exists():
24
+ pytest.skip(f"Wagon registry not found: {registry_path}")
25
+
26
+ with open(registry_path) as f:
27
+ data = yaml.safe_load(f)
28
+
29
+ return data.get("wagons", [])
30
+
31
+
32
+ @pytest.fixture(scope="module")
33
+ def implemented_wagon_slugs(wagon_manifests):
34
+ """Extract wagon slugs that have manifests (implemented)."""
35
+ slugs = set()
36
+ for manifest_path, manifest in wagon_manifests:
37
+ slug = manifest.get("wagon", "")
38
+ if slug:
39
+ slugs.add(slug)
40
+ return slugs
41
+
42
+
43
+ @pytest.fixture(scope="module")
44
+ def draft_wagons(wagon_registry, implemented_wagon_slugs):
45
+ """Filter wagons from registry that are drafts (no manifest/path)."""
46
+ drafts = []
47
+ for wagon in wagon_registry:
48
+ slug = wagon.get("wagon", "")
49
+ # Draft wagons don't have 'manifest' or 'path' fields
50
+ has_manifest = wagon.get("manifest") or wagon.get("path")
51
+ is_implemented = slug in implemented_wagon_slugs
52
+
53
+ if not has_manifest and not is_implemented:
54
+ drafts.append(wagon)
55
+
56
+ return drafts
57
+
58
+
59
+ @pytest.fixture(scope="module")
60
+ def all_registry_wagon_slugs(wagon_registry):
61
+ """Get all wagon slugs from registry (both draft and implemented)."""
62
+ return {wagon.get("wagon", "") for wagon in wagon_registry if wagon.get("wagon")}
63
+
64
+
65
+ @pytest.mark.platform
66
+ def test_draft_wagons_are_valid_yaml(draft_wagons):
67
+ """
68
+ SPEC-PLATFORM-REGISTRY-0001: Draft wagons have valid structure
69
+
70
+ Given: Draft wagons in plan/_wagons.yaml
71
+ When: Checking basic structure
72
+ Then: Each draft has required fields: wagon, description, theme, subject
73
+ """
74
+ required_fields = ["wagon", "description", "theme", "subject", "context", "action", "goal", "outcome"]
75
+
76
+ errors = []
77
+ for draft in draft_wagons:
78
+ wagon_slug = draft.get("wagon", "UNKNOWN")
79
+ for field in required_fields:
80
+ if field not in draft:
81
+ errors.append(
82
+ f"Draft wagon '{wagon_slug}' missing required field: {field}"
83
+ )
84
+
85
+ if errors:
86
+ pytest.fail("\n".join(errors))
87
+
88
+
89
+ @pytest.mark.platform
90
+ def test_draft_wagons_not_duplicated_in_manifests(draft_wagons, implemented_wagon_slugs):
91
+ """
92
+ SPEC-PLATFORM-REGISTRY-0002: Draft wagons don't have manifests
93
+
94
+ Given: Draft wagons in registry
95
+ When: Checking against implemented wagon manifests
96
+ Then: Draft wagon slugs should NOT have manifest files
97
+ (If they do, they're implemented, not draft)
98
+ """
99
+ errors = []
100
+ for draft in draft_wagons:
101
+ slug = draft.get("wagon", "")
102
+ if slug in implemented_wagon_slugs:
103
+ errors.append(
104
+ f"Wagon '{slug}' is in registry as draft but has manifest file - "
105
+ f"should add 'manifest' and 'path' fields to registry entry"
106
+ )
107
+
108
+ if errors:
109
+ pytest.fail("\n\n".join(errors))
110
+
111
+
112
+ @pytest.mark.platform
113
+ def test_registry_produce_artifacts_follow_convention(wagon_registry):
114
+ """
115
+ SPEC-PLATFORM-REGISTRY-0003: Registry produce artifacts follow artifact-naming convention v2.1
116
+
117
+ Given: All wagons in registry (draft + implemented)
118
+ When: Checking produce artifact names
119
+ Then: All artifacts follow pattern: {theme}(:{category})*:{aspect}(.{variant})?
120
+ Supports unlimited hierarchical depth with colons (e.g., commons:ux:foundations)
121
+ Supports optional variant with dot (e.g., match:dilemma.paired)
122
+ """
123
+ import re
124
+ # Pattern per artifact-naming.convention.yaml v2.1:
125
+ # {theme}(:{category})*:{aspect}(.{variant})?
126
+ # - theme: required (1 segment)
127
+ # - categories: optional (0+ segments with colons)
128
+ # - aspect: required (1 segment)
129
+ # - variant: optional (1 segment with dot)
130
+ artifact_pattern = re.compile(r"^[a-z][a-z0-9-]+:[a-z][a-z0-9-]+(:[a-z][a-z0-9-]+)*(\.[a-z][a-z0-9-]+)?$")
131
+
132
+ errors = []
133
+ for wagon in wagon_registry:
134
+ wagon_slug = wagon.get("wagon", "UNKNOWN")
135
+ produce_items = wagon.get("produce", [])
136
+
137
+ for idx, item in enumerate(produce_items):
138
+ name = item.get("name", "")
139
+ if not artifact_pattern.match(name):
140
+ errors.append(
141
+ f"Wagon '{wagon_slug}' produce[{idx}] has invalid artifact name: '{name}'\n"
142
+ f" Expected pattern: {{theme}}(:{{category}})*:{{aspect}}(.{{variant}})?\n"
143
+ f" Examples: commons:ux:foundations, match:dilemma.paired"
144
+ )
145
+
146
+ if errors:
147
+ pytest.fail("\n\n".join(errors))
148
+
149
+
150
+ @pytest.mark.platform
151
+ def test_registry_consume_references_valid_wagons(wagon_registry, all_registry_wagon_slugs):
152
+ """
153
+ SPEC-PLATFORM-REGISTRY-0004: Registry consume references are coherent
154
+
155
+ Given: All wagons in registry
156
+ When: Checking consume 'from' references
157
+ Then: wagon:slug references resolve to wagons in registry OR
158
+ References use valid patterns: system:*, appendix:*, internal
159
+ """
160
+ errors = []
161
+
162
+ for wagon in wagon_registry:
163
+ wagon_slug = wagon.get("wagon", "UNKNOWN")
164
+ consume_items = wagon.get("consume", [])
165
+
166
+ for idx, item in enumerate(consume_items):
167
+ from_ref = item.get("from", "")
168
+ if not from_ref:
169
+ continue
170
+
171
+ # Check pattern validity
172
+ if from_ref.startswith("wagon:"):
173
+ referenced_wagon = from_ref.split(":", 1)[1]
174
+ if referenced_wagon not in all_registry_wagon_slugs:
175
+ errors.append(
176
+ f"Wagon '{wagon_slug}' consume[{idx}] references unknown wagon: '{referenced_wagon}'\n"
177
+ f" Reference: {from_ref}\n"
178
+ f" Artifact: {item.get('name', 'UNKNOWN')}\n"
179
+ f" Hint: Check if wagon exists in registry or should be system:* reference"
180
+ )
181
+
182
+ elif from_ref.startswith("system:"):
183
+ # System references are valid
184
+ pass
185
+
186
+ elif from_ref.startswith("appendix:"):
187
+ # Appendix references are valid
188
+ pass
189
+
190
+ elif from_ref == "internal":
191
+ # Internal reference is valid
192
+ pass
193
+
194
+ else:
195
+ errors.append(
196
+ f"Wagon '{wagon_slug}' consume[{idx}] has invalid 'from' pattern: '{from_ref}'\n"
197
+ f" Expected: wagon:slug, system:service, appendix:type, or internal"
198
+ )
199
+
200
+ if errors:
201
+ pytest.fail("\n\n".join(errors))
202
+
203
+
204
+ @pytest.mark.platform
205
+ def test_registry_produce_artifacts_have_consumers(wagon_registry):
206
+ """
207
+ SPEC-PLATFORM-REGISTRY-0005: Registry produce artifacts are consumed
208
+
209
+ Given: All wagons in registry
210
+ When: Checking produce artifacts against all consume references
211
+ Then: Each produce artifact should be consumed by at least one wagon
212
+ (Orphaned artifacts should be flagged as warnings)
213
+
214
+ Note: This is a soft validation - some artifacts may be consumed externally
215
+ """
216
+ # Build produce artifact registry
217
+ produced_artifacts = {} # artifact_name -> wagon_slug
218
+ for wagon in wagon_registry:
219
+ wagon_slug = wagon.get("wagon", "")
220
+ for item in wagon.get("produce", []):
221
+ artifact_name = item.get("name", "")
222
+ if artifact_name:
223
+ if artifact_name not in produced_artifacts:
224
+ produced_artifacts[artifact_name] = []
225
+ produced_artifacts[artifact_name].append(wagon_slug)
226
+
227
+ # Build consume artifact set
228
+ consumed_artifacts = set()
229
+ for wagon in wagon_registry:
230
+ for item in wagon.get("consume", []):
231
+ artifact_name = item.get("name", "")
232
+ if artifact_name:
233
+ consumed_artifacts.add(artifact_name)
234
+
235
+ # Find orphaned artifacts
236
+ warnings = []
237
+ for artifact_name, producers in produced_artifacts.items():
238
+ if artifact_name not in consumed_artifacts:
239
+ producers_str = ", ".join(producers)
240
+ warnings.append(
241
+ f"Artifact '{artifact_name}' produced by [{producers_str}] "
242
+ f"but not consumed by any wagon (may be external/endpoint)"
243
+ )
244
+
245
+ # Report warnings (not failures) - informational only
246
+ if warnings:
247
+ print(f"\n\n⚠️ Orphaned Artifacts (may be intentional for external consumption):")
248
+ for warning in warnings:
249
+ print(f" • {warning}")
250
+
251
+
252
+ @pytest.mark.platform
253
+ def test_draft_wagon_contract_coherence(draft_wagons):
254
+ """
255
+ SPEC-PLATFORM-REGISTRY-0006: Draft wagon contract references are coherent
256
+
257
+ Given: Draft wagons in registry
258
+ When: Checking contract/telemetry URN references
259
+ Then: URNs follow expected pattern: contract:domain:resource
260
+ Telemetry follows: telemetry:domain:resource
261
+
262
+ Note: This doesn't validate filesystem resolution (draft wagons don't have contracts yet)
263
+ Just validates URN format coherence
264
+ """
265
+ import re
266
+ contract_pattern = re.compile(r"^contract:[a-z]+:[a-z][a-z0-9-]+(\.[a-z][a-z0-9-]+)?$")
267
+ telemetry_pattern = re.compile(r"^telemetry:[a-z]+:[a-z][a-z0-9-]+(\.[a-z][a-z0-9-]+)?$")
268
+
269
+ errors = []
270
+
271
+ for draft in draft_wagons:
272
+ wagon_slug = draft.get("wagon", "UNKNOWN")
273
+
274
+ # Check produce items
275
+ for idx, item in enumerate(draft.get("produce", [])):
276
+ # Contract URN
277
+ if "contract" in item and item["contract"]:
278
+ contract_urn = item["contract"]
279
+ if not contract_pattern.match(contract_urn):
280
+ errors.append(
281
+ f"Draft wagon '{wagon_slug}' produce[{idx}] has invalid contract URN: '{contract_urn}'\n"
282
+ f" Expected pattern: contract:domain:resource[.category]"
283
+ )
284
+
285
+ # Telemetry URN
286
+ if "telemetry" in item and item["telemetry"]:
287
+ telemetry_urn = item["telemetry"]
288
+ if not telemetry_pattern.match(telemetry_urn):
289
+ errors.append(
290
+ f"Draft wagon '{wagon_slug}' produce[{idx}] has invalid telemetry URN: '{telemetry_urn}'\n"
291
+ f" Expected pattern: telemetry:domain:resource[.category]"
292
+ )
293
+
294
+ if errors:
295
+ pytest.fail("\n\n".join(errors))
296
+
297
+
298
+ @pytest.mark.platform
299
+ def test_registry_wagon_slugs_are_unique(wagon_registry):
300
+ """
301
+ SPEC-PLATFORM-REGISTRY-0007: Wagon slugs in registry are unique
302
+
303
+ Given: All wagons in registry
304
+ When: Checking wagon slugs
305
+ Then: Each slug appears only once
306
+ """
307
+ slug_counts = {}
308
+ for wagon in wagon_registry:
309
+ slug = wagon.get("wagon", "")
310
+ if slug:
311
+ slug_counts[slug] = slug_counts.get(slug, 0) + 1
312
+
313
+ duplicates = {slug: count for slug, count in slug_counts.items() if count > 1}
314
+
315
+ if duplicates:
316
+ errors = []
317
+ for slug, count in duplicates.items():
318
+ errors.append(f"Wagon slug '{slug}' appears {count} times in registry")
319
+ pytest.fail("\n".join(errors))
320
+
321
+
322
+ @pytest.mark.platform
323
+ def test_registry_has_all_implemented_wagons(wagon_registry, implemented_wagon_slugs):
324
+ """
325
+ SPEC-PLATFORM-REGISTRY-0008: All implemented wagons are in registry
326
+
327
+ Given: Wagon manifests in plan/*/
328
+ When: Checking against registry
329
+ Then: All wagon manifest slugs should be in registry
330
+ (Registry is the source of truth)
331
+ """
332
+ registry_slugs = {wagon.get("wagon", "") for wagon in wagon_registry}
333
+
334
+ missing_from_registry = implemented_wagon_slugs - registry_slugs
335
+
336
+ if missing_from_registry:
337
+ errors = [
338
+ f"Wagon '{slug}' has manifest but is NOT in registry plan/_wagons.yaml"
339
+ for slug in sorted(missing_from_registry)
340
+ ]
341
+ pytest.fail(
342
+ "Implemented wagons missing from registry:\n" +
343
+ "\n".join(f" • {e}" for e in errors)
344
+ )
345
+
346
+
347
+ @pytest.mark.platform
348
+ def test_registry_implemented_wagons_have_path_and_manifest(wagon_registry, implemented_wagon_slugs):
349
+ """
350
+ SPEC-PLATFORM-REGISTRY-0009: Implemented wagons have manifest/path fields
351
+
352
+ Given: Wagons in registry that have manifests
353
+ When: Checking registry entries
354
+ Then: Registry entry should have 'manifest' and 'path' fields
355
+ These fields help distinguish implemented vs draft wagons
356
+ """
357
+ errors = []
358
+
359
+ for wagon in wagon_registry:
360
+ slug = wagon.get("wagon", "")
361
+ if slug in implemented_wagon_slugs:
362
+ if not wagon.get("manifest"):
363
+ errors.append(
364
+ f"Implemented wagon '{slug}' missing 'manifest' field in registry\n"
365
+ f" Expected: manifest: plan/{slug.replace('-', '_')}/_{slug.replace('-', '_')}.yaml"
366
+ )
367
+ if not wagon.get("path"):
368
+ errors.append(
369
+ f"Implemented wagon '{slug}' missing 'path' field in registry\n"
370
+ f" Expected: path: plan/{slug.replace('-', '_')}/"
371
+ )
372
+
373
+ if errors:
374
+ pytest.fail("\n\n".join(errors))
@@ -0,0 +1,240 @@
1
+ """
2
+ Platform tests: Cross-reference validation.
3
+
4
+ Validates that cross-references between wagons, trains, and artifacts are coherent.
5
+ Tests ensure that consume references point to valid produce artifacts.
6
+ """
7
+ import pytest
8
+ from typing import Dict, Set, List, Tuple, Any
9
+
10
+
11
+ @pytest.mark.platform
12
+ @pytest.mark.e2e
13
+ def test_wagon_consume_references_valid_produce_or_external(wagon_manifests):
14
+ """
15
+ SPEC-PLATFORM-REFS-0001: Wagon consume references point to valid sources
16
+
17
+ Given: Wagon consume items with 'from' field
18
+ When: Checking consume references
19
+ Then: Each 'from' reference either:
20
+ - Points to another wagon's produce (wagon:slug format)
21
+ - Points to external system (system:external)
22
+ - Points to appendix (appendix:type)
23
+ - Is omitted (defaults to inferred/external)
24
+ """
25
+ # Build produce registry: {artifact_name: [wagon_slugs]}
26
+ produce_registry: Dict[str, List[str]] = {}
27
+ wagon_slugs: Set[str] = set()
28
+
29
+ for path, manifest in wagon_manifests:
30
+ wagon_slug = manifest.get("wagon", "")
31
+ wagon_slugs.add(wagon_slug)
32
+
33
+ for produce_item in manifest.get("produce", []):
34
+ artifact_name = produce_item.get("name", "")
35
+ if artifact_name:
36
+ produce_registry.setdefault(artifact_name, []).append(wagon_slug)
37
+
38
+ # Validate consume references
39
+ for path, manifest in wagon_manifests:
40
+ wagon_slug = manifest.get("wagon", "")
41
+
42
+ for consume_item in manifest.get("consume", []):
43
+ from_ref = consume_item.get("from", "")
44
+
45
+ # Skip if from is not specified (defaults to external/inferred)
46
+ if not from_ref:
47
+ continue
48
+
49
+ # Valid patterns:
50
+ # - wagon:slug (reference to another wagon)
51
+ # - system:external (external dependency)
52
+ # - appendix:type (appendix artifact)
53
+ if from_ref.startswith("wagon:"):
54
+ referenced_wagon = from_ref.split(":", 1)[1]
55
+ assert referenced_wagon in wagon_slugs, \
56
+ f"Wagon {wagon_slug} at {path} consumes from unknown wagon: {referenced_wagon}"
57
+
58
+ elif from_ref.startswith("system:"):
59
+ # System references are allowed (e.g., system:external)
60
+ pass
61
+
62
+ elif from_ref.startswith("appendix:"):
63
+ # Appendix references are allowed
64
+ pass
65
+
66
+ else:
67
+ # Unknown pattern - should match wagon:, system:, or appendix:
68
+ pytest.fail(
69
+ f"Wagon {wagon_slug} at {path} has invalid 'from' reference: {from_ref}\n"
70
+ f" Expected format: wagon:slug, system:external, or appendix:type"
71
+ )
72
+
73
+
74
+ @pytest.mark.platform
75
+ def test_no_circular_dependencies_simple(wagon_manifests):
76
+ """
77
+ SPEC-PLATFORM-REFS-0002: No direct circular dependencies between wagons
78
+
79
+ Given: Wagon consume to produce graph
80
+ When: Checking for circular dependencies
81
+ Then: No wagon directly consumes its own produce
82
+ (Advanced cycle detection in separate test)
83
+ """
84
+ for path, manifest in wagon_manifests:
85
+ wagon_slug = manifest.get("wagon", "")
86
+
87
+ for consume_item in manifest.get("consume", []):
88
+ from_ref = consume_item.get("from", "")
89
+
90
+ if from_ref.startswith("wagon:"):
91
+ referenced_wagon = from_ref.split(":", 1)[1]
92
+ assert referenced_wagon != wagon_slug, \
93
+ f"Wagon {wagon_slug} at {path} has circular dependency (consumes from itself)"
94
+
95
+
96
+ @pytest.mark.platform
97
+ def test_trains_reference_valid_wagons(trains_registry, wagon_manifests):
98
+ """
99
+ SPEC-PLATFORM-REFS-0003: Train participants reference existing wagons
100
+
101
+ Given: Train definitions in plan/_trains/ (theme-grouped registry)
102
+ When: Checking train participant references
103
+ Then: All referenced wagons exist in wagon registry
104
+ """
105
+ # Build wagon slug set
106
+ wagon_slugs = {manifest.get("wagon", "") for _, manifest in wagon_manifests}
107
+
108
+ # Check each train's participants (theme-grouped structure)
109
+ for theme, trains in trains_registry.items():
110
+ if not trains:
111
+ continue
112
+
113
+ for train in trains:
114
+ train_id = train.get("train_id", "")
115
+ train_path = train.get("path", "")
116
+
117
+ # Load individual train file if path exists
118
+ if train_path:
119
+ import yaml
120
+ from pathlib import Path
121
+ train_file = Path(__file__).resolve().parents[4] / train_path
122
+
123
+ if train_file.exists():
124
+ with open(train_file) as f:
125
+ train_data = yaml.safe_load(f)
126
+
127
+ # Check participants if present
128
+ participants = train_data.get("participants", [])
129
+ for participant in participants:
130
+ # Handle both formats: string ("wagon:slug") or object ({wagon: "slug"})
131
+ if isinstance(participant, str):
132
+ # String format: "wagon:slug" or "system:user"
133
+ if participant.startswith("wagon:"):
134
+ wagon_ref = participant.split(":", 1)[1]
135
+ else:
136
+ # Skip non-wagon participants (system:*, user:*, etc.)
137
+ continue
138
+ else:
139
+ # Object format: {wagon: "slug"}
140
+ wagon_ref = participant.get("wagon", "")
141
+
142
+ if wagon_ref:
143
+ assert wagon_ref in wagon_slugs, \
144
+ f"Train {train_id} (theme: {theme}) references unknown wagon: {wagon_ref}"
145
+
146
+
147
+ @pytest.mark.platform
148
+ def test_produce_and_consume_artifact_names_are_coherent(wagon_manifests):
149
+ """
150
+ SPEC-PLATFORM-REFS-0004: Consumed artifacts exist in produce registry
151
+
152
+ Given: Wagon consume items without explicit 'from' field
153
+ When: Artifact name is used to infer source
154
+ Then: Artifact name should match a produced artifact somewhere
155
+ OR be a known external/appendix pattern
156
+ """
157
+ # Build produce artifact name registry
158
+ produce_names: Set[str] = set()
159
+
160
+ for _, manifest in wagon_manifests:
161
+ for produce_item in manifest.get("produce", []):
162
+ artifact_name = produce_item.get("name", "")
163
+ if artifact_name:
164
+ produce_names.add(artifact_name)
165
+
166
+ # Check consume references
167
+ warnings = []
168
+
169
+ for path, manifest in wagon_manifests:
170
+ wagon_slug = manifest.get("wagon", "")
171
+
172
+ for consume_item in manifest.get("consume", []):
173
+ artifact_name = consume_item.get("name", "")
174
+ from_ref = consume_item.get("from", "")
175
+
176
+ # Skip if from is explicitly set to external/appendix/system
177
+ if from_ref and (
178
+ from_ref.startswith("system:") or
179
+ from_ref.startswith("appendix:")
180
+ ):
181
+ continue
182
+
183
+ # Check if artifact name exists in produce registry
184
+ if artifact_name and artifact_name not in produce_names:
185
+ # Special patterns that are allowed even if not produced
186
+ if any(artifact_name.startswith(prefix) for prefix in [
187
+ "appendix:", "system:", "external:"
188
+ ]):
189
+ continue
190
+
191
+ warnings.append(
192
+ f"Wagon {wagon_slug} at {path} consumes artifact '{artifact_name}' "
193
+ f"which is not produced by any wagon"
194
+ )
195
+
196
+ # Report warnings if any
197
+ if warnings:
198
+ pytest.skip(
199
+ f"Found {len(warnings)} orphaned consume references:\n" +
200
+ "\n".join(f" - {w}" for w in warnings[:5]) +
201
+ (f"\n ... and {len(warnings) - 5} more" if len(warnings) > 5 else "")
202
+ )
203
+
204
+
205
+ @pytest.mark.platform
206
+ def test_wagon_to_field_references_valid_destinations(wagon_manifests):
207
+ """
208
+ SPEC-PLATFORM-REFS-0005: Produce 'to' field references valid destinations
209
+
210
+ Given: Wagon produce items with 'to' field
211
+ When: Checking destination references
212
+ Then: Each 'to' reference is either:
213
+ - 'external' (default)
214
+ - 'internal' (wagon-internal artifact)
215
+ - wagon:slug (specific wagon destination)
216
+ """
217
+ wagon_slugs = {manifest.get("wagon", "") for _, manifest in wagon_manifests}
218
+
219
+ for path, manifest in wagon_manifests:
220
+ wagon_slug = manifest.get("wagon", "")
221
+
222
+ for produce_item in manifest.get("produce", []):
223
+ to_ref = produce_item.get("to", "external") # Default to external
224
+
225
+ # Valid patterns:
226
+ # - 'external' (public artifact)
227
+ # - 'internal' (wagon-internal)
228
+ # - wagon:slug (specific destination)
229
+ if to_ref in ["external", "internal"]:
230
+ continue
231
+
232
+ if to_ref.startswith("wagon:"):
233
+ referenced_wagon = to_ref.split(":", 1)[1]
234
+ assert referenced_wagon in wagon_slugs, \
235
+ f"Wagon {wagon_slug} at {path} produces to unknown wagon: {referenced_wagon}"
236
+ else:
237
+ pytest.fail(
238
+ f"Wagon {wagon_slug} at {path} has invalid 'to' reference: {to_ref}\n"
239
+ f" Expected: 'external', 'internal', or wagon:slug"
240
+ )