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.
- atdd/__init__.py +6 -0
- atdd/__main__.py +4 -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.2.1.dist-info/METADATA +221 -0
- atdd-0.2.1.dist-info/RECORD +184 -0
- atdd-0.2.1.dist-info/WHEEL +5 -0
- atdd-0.2.1.dist-info/entry_points.txt +2 -0
- atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
- 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}}"
|