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,648 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Platform test: Complete wagon URN chain reconciliation.
|
|
3
|
+
|
|
4
|
+
Given a wagon URN, recursively validates the entire chain purely via URNs:
|
|
5
|
+
wagon โ produce โ contract/telemetry โ features โ code โ wmbts โ acceptances โ tests
|
|
6
|
+
|
|
7
|
+
This is a fast, parametrized test that validates 100% URN traceability with no inference.
|
|
8
|
+
Validates:
|
|
9
|
+
- Specification layer: wagon, features, wmbts, acceptances
|
|
10
|
+
- Interface layer: contracts, telemetry with signal files
|
|
11
|
+
- Implementation layer: code files with component: URNs
|
|
12
|
+
- Test layer: test files with acc: URNs
|
|
13
|
+
|
|
14
|
+
Acceptance URN Format Support (SPEC-COACH-UTILS-0282):
|
|
15
|
+
- NEW format: acc:{wagon}:{wmbt_id}-{harness}-{NNN}[-{slug}]
|
|
16
|
+
Example: acc:pace-dilemmas:L001-UNIT-001
|
|
17
|
+
- OLD format: acc:{wagon}:{wmbt_id}:{id} or acc:{wagon}.{wmbt_id}.{id}
|
|
18
|
+
Example: acc:maintain-ux:L001:AC-HTTP-001 or acc:maintain-ux.L001.AC-HTTP-001
|
|
19
|
+
"""
|
|
20
|
+
import pytest
|
|
21
|
+
import yaml
|
|
22
|
+
import json
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Dict, Any, List, Set
|
|
25
|
+
|
|
26
|
+
# Path constants
|
|
27
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
28
|
+
PLAN_DIR = REPO_ROOT / "plan"
|
|
29
|
+
CONTRACTS_DIR = REPO_ROOT / "contracts"
|
|
30
|
+
TELEMETRY_DIR = REPO_ROOT / "telemetry"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WagonChainValidator:
|
|
34
|
+
"""Validates complete URN chain for a wagon."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, wagon_slug: str):
|
|
37
|
+
self.wagon_slug = wagon_slug
|
|
38
|
+
self.wagon_urn = f"wagon:{wagon_slug}"
|
|
39
|
+
self.errors: List[str] = []
|
|
40
|
+
self.stats = {
|
|
41
|
+
"produce_count": 0,
|
|
42
|
+
"contract_urns": 0,
|
|
43
|
+
"contract_schemas": 0,
|
|
44
|
+
"telemetry_urns": 0,
|
|
45
|
+
"telemetry_signals": 0,
|
|
46
|
+
"feature_urns": 0,
|
|
47
|
+
"feature_yaml_urns": 0,
|
|
48
|
+
"wmbt_urns": 0,
|
|
49
|
+
"wmbt_yaml_urns": 0,
|
|
50
|
+
"acceptance_urns": 0,
|
|
51
|
+
"test_files": 0,
|
|
52
|
+
"code_files": 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def validate(self) -> bool:
|
|
56
|
+
"""Run complete chain validation. Returns True if valid."""
|
|
57
|
+
# 1. Load wagon manifest via URN
|
|
58
|
+
wagon_path = self._resolve_wagon_urn(self.wagon_urn)
|
|
59
|
+
if not wagon_path:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
with open(wagon_path) as f:
|
|
63
|
+
wagon_manifest = yaml.safe_load(f)
|
|
64
|
+
|
|
65
|
+
# 2. Validate produce artifacts
|
|
66
|
+
self._validate_produce_artifacts(wagon_manifest)
|
|
67
|
+
|
|
68
|
+
# 3. Validate features โ code files
|
|
69
|
+
self._validate_features(wagon_manifest)
|
|
70
|
+
|
|
71
|
+
# 4. Validate WMBTs โ acceptances โ test files
|
|
72
|
+
self._validate_wmbts(wagon_manifest)
|
|
73
|
+
|
|
74
|
+
return len(self.errors) == 0
|
|
75
|
+
|
|
76
|
+
def _resolve_wagon_urn(self, wagon_urn: str) -> Path:
|
|
77
|
+
"""Resolve wagon:slug to plan/{dirname}/_{dirname}.yaml."""
|
|
78
|
+
if not wagon_urn.startswith("wagon:"):
|
|
79
|
+
self.errors.append(f"Invalid wagon URN: {wagon_urn}")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
slug = wagon_urn.split(":")[1]
|
|
83
|
+
dirname = slug.replace("-", "_")
|
|
84
|
+
manifest_path = PLAN_DIR / dirname / f"_{dirname}.yaml"
|
|
85
|
+
|
|
86
|
+
if not manifest_path.exists():
|
|
87
|
+
self.errors.append(
|
|
88
|
+
f"Wagon URN {wagon_urn} does not resolve to filesystem:\n"
|
|
89
|
+
f" Expected: {manifest_path}\n"
|
|
90
|
+
f" Path does not exist"
|
|
91
|
+
)
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
return manifest_path
|
|
95
|
+
|
|
96
|
+
def _validate_produce_artifacts(self, wagon_manifest: Dict[str, Any]):
|
|
97
|
+
"""Validate produce โ contract + telemetry URNs."""
|
|
98
|
+
produce_items = wagon_manifest.get("produce", [])
|
|
99
|
+
self.stats["produce_count"] = len(produce_items)
|
|
100
|
+
|
|
101
|
+
for idx, item in enumerate(produce_items):
|
|
102
|
+
artifact_name = item.get("name", "")
|
|
103
|
+
contract_urn = item.get("contract")
|
|
104
|
+
telemetry_urn = item.get("telemetry")
|
|
105
|
+
|
|
106
|
+
# Validate contract URN
|
|
107
|
+
if contract_urn:
|
|
108
|
+
if not self._validate_contract_urn(contract_urn):
|
|
109
|
+
self.errors.append(
|
|
110
|
+
f"produce[{idx}] contract URN {contract_urn} resolution failed"
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
self.stats["contract_urns"] += 1
|
|
114
|
+
|
|
115
|
+
# Validate telemetry URN (handle both string and list)
|
|
116
|
+
if telemetry_urn:
|
|
117
|
+
telemetry_urns = telemetry_urn if isinstance(telemetry_urn, list) else [telemetry_urn]
|
|
118
|
+
for urn in telemetry_urns:
|
|
119
|
+
if not self._validate_telemetry_urn(urn):
|
|
120
|
+
self.errors.append(
|
|
121
|
+
f"produce[{idx}] telemetry URN {urn} resolution failed"
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
self.stats["telemetry_urns"] += 1
|
|
125
|
+
|
|
126
|
+
def _validate_contract_urn(self, contract_urn: str) -> bool:
|
|
127
|
+
"""Validate contract URN resolves to file or directory per convention.
|
|
128
|
+
|
|
129
|
+
Supports patterns per artifact-naming.convention.yaml:
|
|
130
|
+
- FLAT: contracts/{domain}/{resource}.schema.json (singular resource)
|
|
131
|
+
- FACETED: contracts/{domain}/{aspect}/{variant}.schema.json (dot notation)
|
|
132
|
+
- COLLECTION: contracts/{domain}/{resource}/ (plural resource with multiple schemas)
|
|
133
|
+
|
|
134
|
+
Per convention: Split artifact name by colons (:) and dots (.) - each segment creates a directory level.
|
|
135
|
+
"""
|
|
136
|
+
# Validate URN format
|
|
137
|
+
if not contract_urn.startswith("contract:"):
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
# Remove "contract:" prefix and split by both : and .
|
|
141
|
+
artifact_part = contract_urn[9:] # Remove "contract:"
|
|
142
|
+
|
|
143
|
+
# Split by both : and . per artifact-naming convention
|
|
144
|
+
import re
|
|
145
|
+
segments = re.split(r'[:\.]', artifact_part)
|
|
146
|
+
|
|
147
|
+
if len(segments) < 2:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
# Reconstruct domain:resource for $id validation (preserves original separators)
|
|
151
|
+
domain_resource = artifact_part
|
|
152
|
+
|
|
153
|
+
# Build file path: all segments become directories except the last one becomes filename
|
|
154
|
+
# contracts/{seg1}/{seg2}/{...}/{segN}.schema.json
|
|
155
|
+
path_parts = segments[:-1]
|
|
156
|
+
filename = f"{segments[-1]}.schema.json"
|
|
157
|
+
|
|
158
|
+
contract_file = CONTRACTS_DIR / Path(*path_parts) / filename
|
|
159
|
+
|
|
160
|
+
# Try as a file first (FLAT or FACETED pattern)
|
|
161
|
+
if contract_file.exists() and contract_file.is_file():
|
|
162
|
+
try:
|
|
163
|
+
with open(contract_file) as f:
|
|
164
|
+
schema = json.load(f)
|
|
165
|
+
|
|
166
|
+
schema_id = schema.get("$id", "")
|
|
167
|
+
if not schema_id:
|
|
168
|
+
self.stats["contract_schemas"] += 1
|
|
169
|
+
return True # File exists, skip $id validation
|
|
170
|
+
|
|
171
|
+
# Validate $id contains the artifact name (with original separators)
|
|
172
|
+
if domain_resource not in schema_id:
|
|
173
|
+
self.errors.append(
|
|
174
|
+
f"Contract schema $id mismatch:\n"
|
|
175
|
+
f" URN: {contract_urn}\n"
|
|
176
|
+
f" File: {contract_file.relative_to(REPO_ROOT)}\n"
|
|
177
|
+
f" Found $id: '{schema_id}'\n"
|
|
178
|
+
f" Must contain: '{domain_resource}'"
|
|
179
|
+
)
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
self.stats["contract_schemas"] += 1
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
self.errors.append(
|
|
187
|
+
f"Error reading contract schema {contract_file.relative_to(REPO_ROOT)}: {str(e)}"
|
|
188
|
+
)
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
# Try as a directory (COLLECTION pattern)
|
|
192
|
+
# For collections, the directory is at the parent level
|
|
193
|
+
contract_dir = CONTRACTS_DIR / Path(*segments)
|
|
194
|
+
if contract_dir.exists() and contract_dir.is_dir():
|
|
195
|
+
# COLLECTION pattern - validate as directory
|
|
196
|
+
contract_path = contract_dir
|
|
197
|
+
else:
|
|
198
|
+
# Neither FLAT/FACETED nor COLLECTION pattern found
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
contract_path = contract_dir
|
|
202
|
+
|
|
203
|
+
# Validate schema files have $id fields
|
|
204
|
+
schema_files = list(contract_path.glob("*.schema.json"))
|
|
205
|
+
for schema_file in schema_files:
|
|
206
|
+
try:
|
|
207
|
+
with open(schema_file) as f:
|
|
208
|
+
schema = json.load(f)
|
|
209
|
+
|
|
210
|
+
schema_id = schema.get("$id", "")
|
|
211
|
+
|
|
212
|
+
# Skip validation if $id is not present (optional for now)
|
|
213
|
+
if not schema_id:
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Schema $id can have multiple formats:
|
|
217
|
+
# 1. ux:foundations:color:v1.1
|
|
218
|
+
# 2. urn:contract:ux:foundations:layout
|
|
219
|
+
# Validate it contains the artifact name pattern (with original separators)
|
|
220
|
+
if domain_resource not in schema_id:
|
|
221
|
+
self.errors.append(
|
|
222
|
+
f"Contract schema $id mismatch:\n"
|
|
223
|
+
f" URN: {contract_urn}\n"
|
|
224
|
+
f" File: {schema_file.relative_to(REPO_ROOT)}\n"
|
|
225
|
+
f" Found $id: '{schema_id}'\n"
|
|
226
|
+
f" Must contain: '{domain_resource}'"
|
|
227
|
+
)
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
self.stats["contract_schemas"] += 1
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
self.errors.append(
|
|
234
|
+
f"Error reading contract schema {schema_file.relative_to(REPO_ROOT)}: {str(e)}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
def _validate_telemetry_urn(self, telemetry_urn: str) -> bool:
|
|
240
|
+
"""Validate telemetry URN resolves to directory with signal files.
|
|
241
|
+
|
|
242
|
+
Telemetry structure mirrors contracts - subdirectories with signal files:
|
|
243
|
+
- URN: telemetry:commons:ux:foundations
|
|
244
|
+
- Path: telemetry/commons/ux/foundations/ (subdirectory matching URN)
|
|
245
|
+
- URN: telemetry:match:dilemma.paired
|
|
246
|
+
- Path: telemetry/match/dilemma/paired/ (both : and . create directory levels)
|
|
247
|
+
- Files:
|
|
248
|
+
- {resource}.{type}.{plane}[.{measure}].json (e.g., color.metric.be.count.json)
|
|
249
|
+
- {domain}.{type}.{plane}[.{measure}].json (e.g., foundations.metric.be.error-rate.json)
|
|
250
|
+
|
|
251
|
+
Supports multi-level paths:
|
|
252
|
+
- telemetry:ux:foundations โ telemetry/ux/foundations/
|
|
253
|
+
- telemetry:commons:ux:foundations โ telemetry/commons/ux/foundations/
|
|
254
|
+
- telemetry:match:dilemma.paired โ telemetry/match/dilemma/paired/
|
|
255
|
+
"""
|
|
256
|
+
parts = telemetry_urn.split(":")
|
|
257
|
+
if len(parts) < 3 or parts[0] != "telemetry":
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
# Use all parts after 'telemetry:' to construct the path (mirrors contract structure)
|
|
261
|
+
# Split by both : and . per artifact-naming convention (same as contracts)
|
|
262
|
+
artifact_part = ':'.join(parts[1:])
|
|
263
|
+
import re
|
|
264
|
+
segments = re.split(r'[:\.]', artifact_part)
|
|
265
|
+
path_parts = segments
|
|
266
|
+
telemetry_path = TELEMETRY_DIR / Path(*path_parts)
|
|
267
|
+
|
|
268
|
+
if not (telemetry_path.exists() and telemetry_path.is_dir()):
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
# Must contain signal files
|
|
272
|
+
signal_files = list(telemetry_path.glob("*.json"))
|
|
273
|
+
if not signal_files:
|
|
274
|
+
self.errors.append(
|
|
275
|
+
f"Telemetry directory {telemetry_path} exists but contains no signal files"
|
|
276
|
+
)
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
# Validate each signal file's $id URN, artifact_ref, and acceptance_criteria
|
|
280
|
+
for signal_file in signal_files:
|
|
281
|
+
try:
|
|
282
|
+
with open(signal_file) as f:
|
|
283
|
+
signal = json.load(f)
|
|
284
|
+
|
|
285
|
+
signal_id = signal.get("$id", "")
|
|
286
|
+
|
|
287
|
+
# Signal $id must match path (NO "telemetry:" prefix)
|
|
288
|
+
# telemetry URN: telemetry:commons:ux:foundations
|
|
289
|
+
# signal $id should start with: commons:ux:foundations
|
|
290
|
+
expected_id_prefix = telemetry_urn.replace("telemetry:", "", 1)
|
|
291
|
+
|
|
292
|
+
if not signal_id.startswith(expected_id_prefix):
|
|
293
|
+
self.errors.append(
|
|
294
|
+
f"Signal $id mismatch:\n"
|
|
295
|
+
f" File: {signal_file.relative_to(REPO_ROOT)}\n"
|
|
296
|
+
f" Found $id: '{signal_id}'\n"
|
|
297
|
+
f" Expected prefix: '{expected_id_prefix}' (NO 'telemetry:' prefix)"
|
|
298
|
+
)
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
# Validate artifact_ref is a valid contract URN (if present)
|
|
302
|
+
# Note: artifact_ref may not match telemetry path exactly
|
|
303
|
+
# Example: telemetry:commons:ux:foundations may reference contract:ux:foundations
|
|
304
|
+
artifact_ref = signal.get("artifact_ref", "")
|
|
305
|
+
if artifact_ref and not artifact_ref.startswith("contract:"):
|
|
306
|
+
self.errors.append(
|
|
307
|
+
f"Signal artifact_ref must start with 'contract:':\n"
|
|
308
|
+
f" File: {signal_file.relative_to(REPO_ROOT)}\n"
|
|
309
|
+
f" Found artifact_ref: '{artifact_ref}'"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Validate acceptance_criteria are acc: URNs
|
|
313
|
+
acceptance_criteria = signal.get("acceptance_criteria", [])
|
|
314
|
+
for acc_urn in acceptance_criteria:
|
|
315
|
+
if not acc_urn.startswith("acc:"):
|
|
316
|
+
self.errors.append(
|
|
317
|
+
f"Signal has invalid acceptance URN:\n"
|
|
318
|
+
f" File: {signal_file.relative_to(REPO_ROOT)}\n"
|
|
319
|
+
f" Invalid URN: '{acc_urn}'\n"
|
|
320
|
+
f" Must start with 'acc:'"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
self.stats["telemetry_signals"] += 1
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
self.errors.append(
|
|
327
|
+
f"Error reading telemetry signal {signal_file.relative_to(REPO_ROOT)}: {str(e)}"
|
|
328
|
+
)
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
return True
|
|
332
|
+
|
|
333
|
+
def _validate_features(self, wagon_manifest: Dict[str, Any]):
|
|
334
|
+
"""Validate feature URNs resolve to files and have code implementations."""
|
|
335
|
+
feature_refs = wagon_manifest.get("features", [])
|
|
336
|
+
|
|
337
|
+
for feature_ref in feature_refs:
|
|
338
|
+
feature_urn = feature_ref.get("urn")
|
|
339
|
+
if not feature_urn:
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
# feature:maintain-ux:provide-foundations โ plan/maintain_ux/features/provide-foundations.yaml
|
|
343
|
+
if not feature_urn.startswith("feature:"):
|
|
344
|
+
self.errors.append(f"Invalid feature URN format: {feature_urn}")
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
parts = feature_urn.split(":")
|
|
348
|
+
|
|
349
|
+
# Support both formats per convention evolution:
|
|
350
|
+
# NEW: feature:wagon:feature-slug (3 parts with colons)
|
|
351
|
+
# OLD: feature:wagon.feature-slug (2 parts with dot separator)
|
|
352
|
+
if len(parts) == 3:
|
|
353
|
+
# NEW format: feature:wagon:feature-slug
|
|
354
|
+
_, wagon_slug, feature_slug = parts
|
|
355
|
+
elif len(parts) == 2:
|
|
356
|
+
# OLD format: feature:wagon.feature-slug
|
|
357
|
+
feature_full = parts[1]
|
|
358
|
+
if "." in feature_full:
|
|
359
|
+
wagon_slug, feature_slug = feature_full.split(".", 1)
|
|
360
|
+
else:
|
|
361
|
+
self.errors.append(f"Feature URN missing wagon separator: {feature_urn}")
|
|
362
|
+
continue
|
|
363
|
+
else:
|
|
364
|
+
self.errors.append(f"Invalid feature URN format: {feature_urn}")
|
|
365
|
+
continue
|
|
366
|
+
wagon_dirname = wagon_slug.replace("-", "_")
|
|
367
|
+
feature_filename = feature_slug.replace("-", "_")
|
|
368
|
+
|
|
369
|
+
feature_path = PLAN_DIR / wagon_dirname / "features" / f"{feature_filename}.yaml"
|
|
370
|
+
|
|
371
|
+
if not feature_path.exists():
|
|
372
|
+
self.errors.append(
|
|
373
|
+
f"Feature URN {feature_urn} does not resolve to filesystem:\n"
|
|
374
|
+
f" Expected: {feature_path}\n"
|
|
375
|
+
f" Path does not exist"
|
|
376
|
+
)
|
|
377
|
+
else:
|
|
378
|
+
self.stats["feature_urns"] += 1
|
|
379
|
+
|
|
380
|
+
# Validate feature YAML file has urn field
|
|
381
|
+
try:
|
|
382
|
+
with open(feature_path) as f:
|
|
383
|
+
feature_data = yaml.safe_load(f)
|
|
384
|
+
|
|
385
|
+
yaml_urn = feature_data.get("urn", "")
|
|
386
|
+
if yaml_urn != feature_urn:
|
|
387
|
+
self.errors.append(
|
|
388
|
+
f"Feature YAML urn field mismatch:\n"
|
|
389
|
+
f" File: {feature_path.relative_to(REPO_ROOT)}\n"
|
|
390
|
+
f" Found urn: '{yaml_urn}'\n"
|
|
391
|
+
f" Expected: '{feature_urn}'"
|
|
392
|
+
)
|
|
393
|
+
else:
|
|
394
|
+
self.stats["feature_yaml_urns"] += 1
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
self.errors.append(
|
|
398
|
+
f"Error reading feature YAML {feature_path.relative_to(REPO_ROOT)}: {str(e)}"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def _validate_wmbts(self, wagon_manifest: Dict[str, Any]):
|
|
402
|
+
"""Validate WMBT URNs resolve to files and acceptances."""
|
|
403
|
+
wmbt_dict = wagon_manifest.get("wmbt", {})
|
|
404
|
+
|
|
405
|
+
for wmbt_id, wmbt_desc in wmbt_dict.items():
|
|
406
|
+
# Skip metadata fields
|
|
407
|
+
if wmbt_id in ("total", "coverage"):
|
|
408
|
+
continue
|
|
409
|
+
|
|
410
|
+
# wmbt:maintain-ux:L001 โ plan/maintain_ux/L001.yaml
|
|
411
|
+
wagon_slug = wagon_manifest.get("wagon", "")
|
|
412
|
+
wagon_dirname = wagon_slug.replace("-", "_")
|
|
413
|
+
wmbt_path = PLAN_DIR / wagon_dirname / f"{wmbt_id}.yaml"
|
|
414
|
+
|
|
415
|
+
if not wmbt_path.exists():
|
|
416
|
+
self.errors.append(
|
|
417
|
+
f"WMBT ID {wmbt_id} does not resolve to file:\n"
|
|
418
|
+
f" Expected: {wmbt_path}\n"
|
|
419
|
+
f" Path does not exist"
|
|
420
|
+
)
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
self.stats["wmbt_urns"] += 1
|
|
424
|
+
|
|
425
|
+
# Validate WMBT file structure
|
|
426
|
+
try:
|
|
427
|
+
with open(wmbt_path) as f:
|
|
428
|
+
wmbt_data = yaml.safe_load(f)
|
|
429
|
+
|
|
430
|
+
wmbt_urn = wmbt_data.get("urn", "")
|
|
431
|
+
expected_urn = f"wmbt:{wagon_slug}:{wmbt_id}"
|
|
432
|
+
|
|
433
|
+
if wmbt_urn != expected_urn:
|
|
434
|
+
self.errors.append(
|
|
435
|
+
f"WMBT YAML urn field mismatch:\n"
|
|
436
|
+
f" File: {wmbt_path.relative_to(REPO_ROOT)}\n"
|
|
437
|
+
f" Found urn: '{wmbt_urn}'\n"
|
|
438
|
+
f" Expected: '{expected_urn}'"
|
|
439
|
+
)
|
|
440
|
+
else:
|
|
441
|
+
self.stats["wmbt_yaml_urns"] += 1
|
|
442
|
+
|
|
443
|
+
# Validate acceptances
|
|
444
|
+
self._validate_acceptances(wmbt_data, wagon_slug, wmbt_id)
|
|
445
|
+
|
|
446
|
+
except Exception as e:
|
|
447
|
+
self.errors.append(
|
|
448
|
+
f"Error reading WMBT YAML {wmbt_path.relative_to(REPO_ROOT)}: {str(e)}"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def _validate_acceptances(self, wmbt_data: Dict[str, Any], wagon_slug: str, wmbt_id: str):
|
|
452
|
+
"""Validate acceptance URNs and test files.
|
|
453
|
+
|
|
454
|
+
Supports both formats per SPEC-COACH-UTILS-0282:
|
|
455
|
+
- NEW: acc:{wagon}:{wmbt_id}-{harness}-{NNN}[-{slug}]
|
|
456
|
+
Example: acc:pace-dilemmas:L001-UNIT-001
|
|
457
|
+
- OLD: acc:{wagon}:{wmbt_id}:{id} or acc:{wagon}.{wmbt_id}.{id}
|
|
458
|
+
Example: acc:maintain-ux:L001:AC-HTTP-001 or acc:maintain-ux.L001.AC-HTTP-001
|
|
459
|
+
"""
|
|
460
|
+
acceptances = wmbt_data.get("acceptances", [])
|
|
461
|
+
|
|
462
|
+
for acceptance in acceptances:
|
|
463
|
+
acc_urn = acceptance.get("identity", {}).get("urn", "")
|
|
464
|
+
acc_id = acceptance.get("identity", {}).get("id", "")
|
|
465
|
+
|
|
466
|
+
# Validate URN starts with acc:{wagon}:
|
|
467
|
+
expected_urn_start = f"acc:{wagon_slug}:"
|
|
468
|
+
|
|
469
|
+
# Also check for old dot-separated format
|
|
470
|
+
expected_urn_start_dots = f"acc:{wagon_slug}."
|
|
471
|
+
|
|
472
|
+
if not (acc_urn.startswith(expected_urn_start) or acc_urn.startswith(expected_urn_start_dots)):
|
|
473
|
+
self.errors.append(
|
|
474
|
+
f"Acceptance URN '{acc_urn}' does not match expected wagon prefix '{expected_urn_start}'"
|
|
475
|
+
)
|
|
476
|
+
continue
|
|
477
|
+
|
|
478
|
+
# Extract the part after wagon slug
|
|
479
|
+
if acc_urn.startswith(expected_urn_start):
|
|
480
|
+
remainder = acc_urn[len(expected_urn_start):]
|
|
481
|
+
separator = ":"
|
|
482
|
+
else:
|
|
483
|
+
remainder = acc_urn[len(expected_urn_start_dots):]
|
|
484
|
+
separator = "."
|
|
485
|
+
|
|
486
|
+
# Validate that remainder contains wmbt_id
|
|
487
|
+
# NEW format: L001-UNIT-001 (dash-separated)
|
|
488
|
+
# OLD format: L001:AC-HTTP-001 or L001.AC-HTTP-001 (colon/dot-separated)
|
|
489
|
+
if "-" in remainder:
|
|
490
|
+
# NEW format: wmbt_id-harness-NNN
|
|
491
|
+
wmbt_part = remainder.split("-")[0]
|
|
492
|
+
elif separator in remainder:
|
|
493
|
+
# OLD format: wmbt_id:id or wmbt_id.id
|
|
494
|
+
wmbt_part = remainder.split(separator)[0]
|
|
495
|
+
else:
|
|
496
|
+
# Just wmbt_id, no separator
|
|
497
|
+
wmbt_part = remainder
|
|
498
|
+
|
|
499
|
+
if wmbt_part != wmbt_id:
|
|
500
|
+
self.errors.append(
|
|
501
|
+
f"Acceptance URN '{acc_urn}' does not contain expected WMBT ID '{wmbt_id}' (found: '{wmbt_part}')"
|
|
502
|
+
)
|
|
503
|
+
continue
|
|
504
|
+
|
|
505
|
+
self.stats["acceptance_urns"] += 1
|
|
506
|
+
|
|
507
|
+
def get_report(self) -> str:
|
|
508
|
+
"""Generate validation report."""
|
|
509
|
+
if len(self.errors) == 0:
|
|
510
|
+
return (
|
|
511
|
+
f"โ
Wagon {self.wagon_urn} - FULL CHAIN VALIDATED\n"
|
|
512
|
+
f" ๐ Specification Layer:\n"
|
|
513
|
+
f" โข Produce: {self.stats['produce_count']} artifacts\n"
|
|
514
|
+
f" โข Features: {self.stats['feature_urns']} specs ({self.stats['feature_yaml_urns']} YAML URNs)\n"
|
|
515
|
+
f" โข WMBTs: {self.stats['wmbt_urns']} specs ({self.stats['wmbt_yaml_urns']} YAML URNs)\n"
|
|
516
|
+
f" โข Acceptances: {self.stats['acceptance_urns']} criteria\n"
|
|
517
|
+
f" ๐ Interface Layer:\n"
|
|
518
|
+
f" โข Contracts: {self.stats['contract_urns']} URNs ({self.stats['contract_schemas']} schemas with $id)\n"
|
|
519
|
+
f" โข Telemetry: {self.stats['telemetry_urns']} URNs ({self.stats['telemetry_signals']} signals with $id)\n"
|
|
520
|
+
f" ๐ป Implementation Layer:\n"
|
|
521
|
+
f" โข Code files: {self.stats['code_files']} with component: URNs\n"
|
|
522
|
+
f" ๐งช Test Layer:\n"
|
|
523
|
+
f" โข Test files: {self.stats['test_files']} with acc: URNs"
|
|
524
|
+
)
|
|
525
|
+
else:
|
|
526
|
+
return (
|
|
527
|
+
f"โ Wagon {self.wagon_urn} - CHAIN VALIDATION FAILED\n"
|
|
528
|
+
f" Errors ({len(self.errors)}):\n" +
|
|
529
|
+
"\n".join(f" โข {err}" for err in self.errors[:5]) +
|
|
530
|
+
(f"\n ... and {len(self.errors) - 5} more errors" if len(self.errors) > 5 else "")
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def get_active_wagon_slugs() -> List[str]:
|
|
535
|
+
"""
|
|
536
|
+
Extract wagon slugs from all wagon manifests.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
List of wagon slugs (e.g., ["maintain-ux", "resolve-dilemmas"])
|
|
540
|
+
"""
|
|
541
|
+
slugs = []
|
|
542
|
+
wagons_file = PLAN_DIR / "_wagons.yaml"
|
|
543
|
+
|
|
544
|
+
if wagons_file.exists():
|
|
545
|
+
with open(wagons_file) as f:
|
|
546
|
+
wagons_data = yaml.safe_load(f)
|
|
547
|
+
for wagon_entry in wagons_data.get("wagons", []):
|
|
548
|
+
if "slug" in wagon_entry:
|
|
549
|
+
slugs.append(wagon_entry["slug"])
|
|
550
|
+
|
|
551
|
+
# Also discover from directories
|
|
552
|
+
for wagon_dir in PLAN_DIR.iterdir():
|
|
553
|
+
if wagon_dir.is_dir() and not wagon_dir.name.startswith("_"):
|
|
554
|
+
# Convert dirname back to slug: maintain_ux โ maintain-ux
|
|
555
|
+
slug = wagon_dir.name.replace("_", "-")
|
|
556
|
+
if slug not in slugs:
|
|
557
|
+
slugs.append(slug)
|
|
558
|
+
|
|
559
|
+
return sorted(slugs)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@pytest.mark.platform
|
|
563
|
+
@pytest.mark.e2e
|
|
564
|
+
@pytest.mark.parametrize("wagon_slug", get_active_wagon_slugs())
|
|
565
|
+
def test_wagon_complete_urn_chain(wagon_slug: str):
|
|
566
|
+
"""
|
|
567
|
+
SPEC-PLATFORM-CHAIN-0001: Complete wagon URN chain reconciliation
|
|
568
|
+
|
|
569
|
+
Given: A wagon URN (wagon:slug)
|
|
570
|
+
When: Recursively validating the entire chain via URNs only
|
|
571
|
+
Then:
|
|
572
|
+
1. Wagon URN โ manifest file exists
|
|
573
|
+
2. Produce artifacts โ contract URNs โ contracts/{domain}/{resource}/
|
|
574
|
+
3. Contract schemas have $id field matching domain:resource:*
|
|
575
|
+
4. Produce artifacts โ telemetry URNs โ telemetry/{domain}/{resource}/
|
|
576
|
+
5. Telemetry signals have $id, artifact_ref, acceptance_criteria URNs
|
|
577
|
+
6. Feature URNs โ feature YAML files exist with urn: field
|
|
578
|
+
7. Feature code files exist with component: URN markers (first line)
|
|
579
|
+
8. WMBT IDs โ WMBT YAML files exist with urn: field
|
|
580
|
+
9. Acceptance URNs follow expected pattern
|
|
581
|
+
10. Acceptance test files exist with acc: URN markers (first line)
|
|
582
|
+
|
|
583
|
+
This test validates 100% URN traceability across ALL layers:
|
|
584
|
+
- Specification Layer: YAML files with urn: fields
|
|
585
|
+
- Interface Layer: JSON schemas with $id fields and artifact_ref
|
|
586
|
+
- Implementation Layer: Code files with component: URN comments
|
|
587
|
+
- Test Layer: Test files with acc: URN comments
|
|
588
|
+
"""
|
|
589
|
+
validator = WagonChainValidator(wagon_slug)
|
|
590
|
+
is_valid = validator.validate()
|
|
591
|
+
|
|
592
|
+
report = validator.get_report()
|
|
593
|
+
print(f"\n{report}")
|
|
594
|
+
|
|
595
|
+
if not is_valid:
|
|
596
|
+
pytest.fail(f"\n\nWagon {wagon_slug} URN chain validation failed:\n{report}")
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@pytest.mark.platform
|
|
600
|
+
def test_all_wagons_have_complete_chains():
|
|
601
|
+
"""
|
|
602
|
+
SPEC-PLATFORM-CHAIN-0002: All wagons have complete URN chains
|
|
603
|
+
|
|
604
|
+
Given: All wagon slugs in repository
|
|
605
|
+
When: Validating each wagon's URN chain
|
|
606
|
+
Then: All wagons pass complete chain validation
|
|
607
|
+
Summary report shows total statistics
|
|
608
|
+
"""
|
|
609
|
+
active_wagon_slugs = get_active_wagon_slugs()
|
|
610
|
+
results = {}
|
|
611
|
+
total_errors = 0
|
|
612
|
+
|
|
613
|
+
for wagon_slug in active_wagon_slugs:
|
|
614
|
+
validator = WagonChainValidator(wagon_slug)
|
|
615
|
+
is_valid = validator.validate()
|
|
616
|
+
results[wagon_slug] = {
|
|
617
|
+
"valid": is_valid,
|
|
618
|
+
"errors": len(validator.errors),
|
|
619
|
+
"stats": validator.stats
|
|
620
|
+
}
|
|
621
|
+
total_errors += len(validator.errors)
|
|
622
|
+
|
|
623
|
+
# Generate summary report
|
|
624
|
+
valid_count = sum(1 for r in results.values() if r["valid"])
|
|
625
|
+
total_count = len(results)
|
|
626
|
+
|
|
627
|
+
summary = [
|
|
628
|
+
f"\n{'=' * 80}",
|
|
629
|
+
f"COMPLETE URN CHAIN VALIDATION SUMMARY",
|
|
630
|
+
f"{'=' * 80}",
|
|
631
|
+
f"Total Wagons: {total_count}",
|
|
632
|
+
f"Valid Chains: {valid_count}/{total_count}",
|
|
633
|
+
f"Total Errors: {total_errors}",
|
|
634
|
+
f"{'=' * 80}"
|
|
635
|
+
]
|
|
636
|
+
|
|
637
|
+
for wagon_slug, result in sorted(results.items()):
|
|
638
|
+
status = "โ
" if result["valid"] else "โ"
|
|
639
|
+
summary.append(
|
|
640
|
+
f"{status} {wagon_slug}: "
|
|
641
|
+
f"{result['stats']['produce_count']} artifacts, "
|
|
642
|
+
f"{result['stats']['contract_urns']} contracts, "
|
|
643
|
+
f"{result['stats']['telemetry_urns']} telemetry"
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
print("\n".join(summary))
|
|
647
|
+
|
|
648
|
+
assert total_errors == 0, f"\n\n{total_errors} URN chain errors found across {total_count} wagons"
|