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