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,706 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Platform tests: Contract schema compliance validation.
|
|
3
|
+
|
|
4
|
+
Validates that all contract schemas follow the meta-schema and conventions:
|
|
5
|
+
- atdd/tester/conventions/contract.convention.yaml
|
|
6
|
+
- atdd/planner/conventions/interface.convention.yaml
|
|
7
|
+
- atdd/tester/schemas/contract.schema.json (meta-schema)
|
|
8
|
+
"""
|
|
9
|
+
import pytest
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from jsonschema import validate, ValidationError, Draft7Validator
|
|
14
|
+
|
|
15
|
+
# Path constants
|
|
16
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
17
|
+
CONTRACTS_DIR = REPO_ROOT / "contracts"
|
|
18
|
+
PLAN_DIR = REPO_ROOT / "plan"
|
|
19
|
+
META_SCHEMA_PATH = REPO_ROOT / "atdd" / "tester" / "schemas" / "contract.schema.json"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def meta_schema():
|
|
24
|
+
"""Load contract meta-schema"""
|
|
25
|
+
if not META_SCHEMA_PATH.exists():
|
|
26
|
+
pytest.skip(f"Meta-schema not found: {META_SCHEMA_PATH}")
|
|
27
|
+
|
|
28
|
+
with open(META_SCHEMA_PATH) as f:
|
|
29
|
+
return json.load(f)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_all_contract_schemas():
|
|
33
|
+
"""Find all contract schema files"""
|
|
34
|
+
if not CONTRACTS_DIR.exists():
|
|
35
|
+
return []
|
|
36
|
+
return list(CONTRACTS_DIR.glob("**/*.schema.json"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_plan_acceptance_urns():
|
|
40
|
+
"""Collect acceptance URNs from plan/ YAML files."""
|
|
41
|
+
if not PLAN_DIR.exists():
|
|
42
|
+
return set()
|
|
43
|
+
|
|
44
|
+
urns = set()
|
|
45
|
+
urn_pattern = re.compile(r"\\burn:\\s*(acc:[^\\s]+)")
|
|
46
|
+
|
|
47
|
+
for plan_path in PLAN_DIR.rglob("*.yaml"):
|
|
48
|
+
try:
|
|
49
|
+
content = plan_path.read_text()
|
|
50
|
+
except OSError:
|
|
51
|
+
continue
|
|
52
|
+
for match in urn_pattern.findall(content):
|
|
53
|
+
urns.add(match.strip())
|
|
54
|
+
|
|
55
|
+
return urns
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def collect_contract_ids():
|
|
59
|
+
"""Return a mapping of contract $id to file path."""
|
|
60
|
+
ids = {}
|
|
61
|
+
for contract_path in find_all_contract_schemas():
|
|
62
|
+
try:
|
|
63
|
+
with open(contract_path) as f:
|
|
64
|
+
contract = json.load(f)
|
|
65
|
+
except json.JSONDecodeError:
|
|
66
|
+
continue
|
|
67
|
+
contract_id = contract.get("$id")
|
|
68
|
+
if contract_id:
|
|
69
|
+
ids[contract_id] = contract_path
|
|
70
|
+
return ids
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def iter_external_refs(schema):
|
|
74
|
+
"""Yield non-local $ref values from a JSON schema object."""
|
|
75
|
+
if isinstance(schema, dict):
|
|
76
|
+
for key, value in schema.items():
|
|
77
|
+
if key == "$ref" and isinstance(value, str) and not value.startswith("#"):
|
|
78
|
+
yield value
|
|
79
|
+
else:
|
|
80
|
+
yield from iter_external_refs(value)
|
|
81
|
+
elif isinstance(schema, list):
|
|
82
|
+
for item in schema:
|
|
83
|
+
yield from iter_external_refs(item)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.platform
|
|
87
|
+
def test_contract_schemas_validate_against_meta_schema(meta_schema):
|
|
88
|
+
"""
|
|
89
|
+
SPEC-PLATFORM-CONTRACTS-0010: All contract schemas validate against meta-schema
|
|
90
|
+
|
|
91
|
+
Given: Contract schemas in contracts/
|
|
92
|
+
When: Validating against .claude/schemas/tester/contract.schema.json
|
|
93
|
+
Then: All contracts pass meta-schema validation
|
|
94
|
+
"""
|
|
95
|
+
contract_files = find_all_contract_schemas()
|
|
96
|
+
|
|
97
|
+
if not contract_files:
|
|
98
|
+
pytest.skip("No contract schema files found")
|
|
99
|
+
|
|
100
|
+
validation_errors = []
|
|
101
|
+
missing_metadata = []
|
|
102
|
+
|
|
103
|
+
for contract_path in contract_files:
|
|
104
|
+
try:
|
|
105
|
+
with open(contract_path) as f:
|
|
106
|
+
contract = json.load(f)
|
|
107
|
+
|
|
108
|
+
if "x-artifact-metadata" not in contract:
|
|
109
|
+
missing_metadata.append(contract_path)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# Validate against meta-schema
|
|
113
|
+
validate(instance=contract, schema=meta_schema)
|
|
114
|
+
|
|
115
|
+
except ValidationError as e:
|
|
116
|
+
validation_errors.append(
|
|
117
|
+
f"{contract_path.relative_to(REPO_ROOT)}: {e.message}"
|
|
118
|
+
)
|
|
119
|
+
except json.JSONDecodeError as e:
|
|
120
|
+
validation_errors.append(
|
|
121
|
+
f"{contract_path.relative_to(REPO_ROOT)}: Invalid JSON - {e}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if missing_metadata:
|
|
125
|
+
print(
|
|
126
|
+
"Skipping meta-schema validation for contracts missing x-artifact-metadata:\n" +
|
|
127
|
+
"\n".join(f" {p.relative_to(REPO_ROOT)}" for p in missing_metadata[:10]) +
|
|
128
|
+
(f"\n ... and {len(missing_metadata) - 10} more" if len(missing_metadata) > 10 else "")
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if validation_errors:
|
|
132
|
+
pytest.fail(
|
|
133
|
+
f"Found {len(validation_errors)} contract validation errors:\n" +
|
|
134
|
+
"\n".join(f" {err}" for err in validation_errors[:10]) +
|
|
135
|
+
(f"\n ... and {len(validation_errors) - 10} more" if len(validation_errors) > 10 else "")
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.mark.platform
|
|
140
|
+
def test_contract_versions_follow_semver():
|
|
141
|
+
"""
|
|
142
|
+
SPEC-PLATFORM-CONTRACTS-0018: Contract versions follow semantic versioning
|
|
143
|
+
|
|
144
|
+
Given: Contract schema version fields
|
|
145
|
+
When: Checking version format
|
|
146
|
+
Then: Versions match pattern: MAJOR.MINOR.PATCH
|
|
147
|
+
"""
|
|
148
|
+
contract_files = find_all_contract_schemas()
|
|
149
|
+
|
|
150
|
+
if not contract_files:
|
|
151
|
+
pytest.skip("No contract schema files found")
|
|
152
|
+
|
|
153
|
+
version_pattern = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
|
154
|
+
invalid_versions = []
|
|
155
|
+
|
|
156
|
+
for contract_path in contract_files:
|
|
157
|
+
try:
|
|
158
|
+
with open(contract_path) as f:
|
|
159
|
+
contract = json.load(f)
|
|
160
|
+
except json.JSONDecodeError:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
version = contract.get("version")
|
|
164
|
+
if not version or not version_pattern.match(version):
|
|
165
|
+
invalid_versions.append(
|
|
166
|
+
f"{contract_path.relative_to(REPO_ROOT)}: version '{version}'"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if invalid_versions:
|
|
170
|
+
pytest.fail(
|
|
171
|
+
f"Found {len(invalid_versions)} contracts with invalid versions:\n" +
|
|
172
|
+
"\n".join(f" {err}" for err in invalid_versions[:10]) +
|
|
173
|
+
(f"\n ... and {len(invalid_versions) - 10} more" if len(invalid_versions) > 10 else "")
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@pytest.mark.platform
|
|
178
|
+
def test_contract_references_are_valid():
|
|
179
|
+
"""
|
|
180
|
+
SPEC-PLATFORM-CONTRACTS-0019: Contract references point to existing contracts
|
|
181
|
+
|
|
182
|
+
Given: Contract schemas with $ref or dependencies fields
|
|
183
|
+
When: Resolving references
|
|
184
|
+
Then: All referenced contracts exist
|
|
185
|
+
"""
|
|
186
|
+
contract_files = find_all_contract_schemas()
|
|
187
|
+
|
|
188
|
+
if not contract_files:
|
|
189
|
+
pytest.skip("No contract schema files found")
|
|
190
|
+
|
|
191
|
+
contract_ids = collect_contract_ids()
|
|
192
|
+
contract_urns = {f"contract:{cid}" for cid in contract_ids.keys()}
|
|
193
|
+
|
|
194
|
+
broken_refs = []
|
|
195
|
+
|
|
196
|
+
for contract_path in contract_files:
|
|
197
|
+
try:
|
|
198
|
+
with open(contract_path) as f:
|
|
199
|
+
contract = json.load(f)
|
|
200
|
+
except json.JSONDecodeError:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
metadata = contract.get("x-artifact-metadata", {})
|
|
204
|
+
dependencies = metadata.get("dependencies", []) if isinstance(metadata, dict) else []
|
|
205
|
+
|
|
206
|
+
for dep in dependencies:
|
|
207
|
+
if dep not in contract_urns:
|
|
208
|
+
broken_refs.append(
|
|
209
|
+
f"{contract_path.relative_to(REPO_ROOT)}: dependency '{dep}' not found"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
for ref in iter_external_refs(contract):
|
|
213
|
+
if ref.startswith(("http://", "https://")):
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
ref_path = ref.split("#", 1)[0]
|
|
217
|
+
|
|
218
|
+
if ref_path.endswith(".schema.json"):
|
|
219
|
+
resolved = (contract_path.parent / ref_path).resolve()
|
|
220
|
+
if not resolved.exists():
|
|
221
|
+
broken_refs.append(
|
|
222
|
+
f"{contract_path.relative_to(REPO_ROOT)}: $ref '{ref}' not found"
|
|
223
|
+
)
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
if ref.startswith("contract:"):
|
|
227
|
+
if ref not in contract_urns:
|
|
228
|
+
broken_refs.append(
|
|
229
|
+
f"{contract_path.relative_to(REPO_ROOT)}: $ref '{ref}' not found"
|
|
230
|
+
)
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
if ref_path not in contract_ids:
|
|
234
|
+
broken_refs.append(
|
|
235
|
+
f"{contract_path.relative_to(REPO_ROOT)}: $ref '{ref}' not found"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if broken_refs:
|
|
239
|
+
pytest.fail(
|
|
240
|
+
f"Found {len(broken_refs)} contract references that cannot be resolved:\n" +
|
|
241
|
+
"\n".join(f" {err}" for err in broken_refs[:10]) +
|
|
242
|
+
(f"\n ... and {len(broken_refs) - 10} more" if len(broken_refs) > 10 else "")
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@pytest.mark.platform
|
|
247
|
+
def test_contract_acceptance_references_exist():
|
|
248
|
+
"""
|
|
249
|
+
SPEC-PLATFORM-CONTRACTS-0020: Contract acceptance_refs point to existing criteria
|
|
250
|
+
|
|
251
|
+
Given: Contract schemas with acceptance_refs array
|
|
252
|
+
When: Checking acceptance criteria files
|
|
253
|
+
Then: All referenced acceptance URNs exist in plan/ directories
|
|
254
|
+
"""
|
|
255
|
+
contract_files = find_all_contract_schemas()
|
|
256
|
+
|
|
257
|
+
if not contract_files:
|
|
258
|
+
pytest.skip("No contract schema files found")
|
|
259
|
+
|
|
260
|
+
acceptance_urns = load_plan_acceptance_urns()
|
|
261
|
+
if not acceptance_urns:
|
|
262
|
+
pytest.skip("No acceptance URNs found in plan/")
|
|
263
|
+
|
|
264
|
+
urn_pattern = re.compile(
|
|
265
|
+
r"^acc:[a-z][a-z0-9_-]*:([DLPCEMYRK][0-9]{3}-(UNIT|HTTP|EVENT|WS|E2E|A11Y|VIS|METRIC|JOB|DB|SEC|LOAD|SCRIPT|WIDGET|GOLDEN|BLOC|INTEGRATION|RLS|EDGE|REALTIME|STORAGE)-[0-9]{3}(?:-[a-z0-9-]+)?|[A-Z][0-9]{3})$"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
missing = []
|
|
269
|
+
|
|
270
|
+
for contract_path in contract_files:
|
|
271
|
+
try:
|
|
272
|
+
with open(contract_path) as f:
|
|
273
|
+
contract = json.load(f)
|
|
274
|
+
except json.JSONDecodeError:
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
metadata = contract.get("x-artifact-metadata", {})
|
|
278
|
+
traceability = metadata.get("traceability", {}) if isinstance(metadata, dict) else {}
|
|
279
|
+
acceptance_refs = traceability.get("acceptance_refs", []) if isinstance(traceability, dict) else []
|
|
280
|
+
|
|
281
|
+
for ref in acceptance_refs:
|
|
282
|
+
if not urn_pattern.match(ref):
|
|
283
|
+
missing.append(
|
|
284
|
+
f"{contract_path.relative_to(REPO_ROOT)}: acceptance_ref '{ref}' has invalid format"
|
|
285
|
+
)
|
|
286
|
+
continue
|
|
287
|
+
if ref not in acceptance_urns:
|
|
288
|
+
missing.append(
|
|
289
|
+
f"{contract_path.relative_to(REPO_ROOT)}: acceptance_ref '{ref}' not found in plan/"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if missing:
|
|
293
|
+
pytest.fail(
|
|
294
|
+
f"Found {len(missing)} invalid acceptance references:\n" +
|
|
295
|
+
"\n".join(f" {err}" for err in missing[:10]) +
|
|
296
|
+
(f"\n ... and {len(missing) - 10} more" if len(missing) > 10 else "")
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@pytest.mark.platform
|
|
301
|
+
def test_no_duplicate_contract_ids():
|
|
302
|
+
"""
|
|
303
|
+
SPEC-PLATFORM-CONTRACTS-0021: Contract $id fields are unique
|
|
304
|
+
|
|
305
|
+
Given: All contract schemas in contracts/
|
|
306
|
+
When: Collecting $id values
|
|
307
|
+
Then: No two schemas have the same $id
|
|
308
|
+
"""
|
|
309
|
+
contract_files = find_all_contract_schemas()
|
|
310
|
+
|
|
311
|
+
if not contract_files:
|
|
312
|
+
pytest.skip("No contract schema files found")
|
|
313
|
+
|
|
314
|
+
seen = {}
|
|
315
|
+
duplicates = {}
|
|
316
|
+
|
|
317
|
+
for contract_path in contract_files:
|
|
318
|
+
try:
|
|
319
|
+
with open(contract_path) as f:
|
|
320
|
+
contract = json.load(f)
|
|
321
|
+
except json.JSONDecodeError:
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
contract_id = contract.get("$id")
|
|
325
|
+
if not contract_id:
|
|
326
|
+
continue
|
|
327
|
+
if contract_id in seen:
|
|
328
|
+
duplicates.setdefault(contract_id, [seen[contract_id]]).append(contract_path)
|
|
329
|
+
else:
|
|
330
|
+
seen[contract_id] = contract_path
|
|
331
|
+
|
|
332
|
+
if duplicates:
|
|
333
|
+
lines = []
|
|
334
|
+
for contract_id, paths in duplicates.items():
|
|
335
|
+
lines.append(f"$id: \"{contract_id}\"")
|
|
336
|
+
for path in paths:
|
|
337
|
+
lines.append(f" - {path.relative_to(REPO_ROOT)}")
|
|
338
|
+
|
|
339
|
+
pytest.fail(
|
|
340
|
+
"Found duplicate contract IDs:\n" +
|
|
341
|
+
"\n".join(lines)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@pytest.mark.platform
|
|
346
|
+
def test_contract_id_format_follows_convention():
|
|
347
|
+
"""
|
|
348
|
+
SPEC-PLATFORM-CONTRACTS-0011: Contract $id follows hierarchical pattern
|
|
349
|
+
|
|
350
|
+
Given: Contract schemas
|
|
351
|
+
When: Checking $id field format
|
|
352
|
+
Then: $id matches pattern: {domain}:{resource}[.{category}]
|
|
353
|
+
Uses colons for domain:resource hierarchy
|
|
354
|
+
Uses dots for resource.category facets
|
|
355
|
+
NO "contract:" prefix in $id (prefix only in wagon URNs)
|
|
356
|
+
Version must be in separate 'version' field (NOT in $id)
|
|
357
|
+
|
|
358
|
+
Examples:
|
|
359
|
+
✓ "$id": "match:result" with "version": "1.0.0"
|
|
360
|
+
✓ "$id": "match:episode.started" with "version": "1.0.0"
|
|
361
|
+
✓ "$id": "mechanic:decision.choice" with "version": "1.0.0"
|
|
362
|
+
✓ "$id": "commons:auth.claims" with "version": "1.0.0"
|
|
363
|
+
✗ "$id": "contract:match:result" (wrong - has "contract:" prefix)
|
|
364
|
+
✗ "$id": "match:result:v1" (wrong - version in $id)
|
|
365
|
+
"""
|
|
366
|
+
contract_files = find_all_contract_schemas()
|
|
367
|
+
|
|
368
|
+
if not contract_files:
|
|
369
|
+
pytest.skip("No contract schema files found")
|
|
370
|
+
|
|
371
|
+
# Pattern: {theme}(:{path})*:{resource}[.{category}][.{subcategory}]...
|
|
372
|
+
# Allows multiple colons for hierarchical path (theme:domain:subdomain:resource)
|
|
373
|
+
# Allows dots for category facets
|
|
374
|
+
# NO "contract:" prefix, NO version in $id
|
|
375
|
+
id_pattern = re.compile(r"^[a-z][a-z0-9\-]+(:[a-z][a-z0-9\-]+)+(\.[a-z][a-z0-9\-]+)*$")
|
|
376
|
+
|
|
377
|
+
invalid_ids = []
|
|
378
|
+
missing_version_field = []
|
|
379
|
+
|
|
380
|
+
for contract_path in contract_files:
|
|
381
|
+
try:
|
|
382
|
+
with open(contract_path) as f:
|
|
383
|
+
contract = json.load(f)
|
|
384
|
+
|
|
385
|
+
contract_id = contract.get("$id")
|
|
386
|
+
version_field = contract.get("version")
|
|
387
|
+
|
|
388
|
+
if not contract_id:
|
|
389
|
+
invalid_ids.append(
|
|
390
|
+
f"{contract_path.relative_to(REPO_ROOT)}: Missing $id field"
|
|
391
|
+
)
|
|
392
|
+
elif not id_pattern.match(contract_id):
|
|
393
|
+
# Check if version is incorrectly included in $id
|
|
394
|
+
if ":v" in contract_id or re.search(r":v?\d+(\.\d+)*$", contract_id):
|
|
395
|
+
invalid_ids.append(
|
|
396
|
+
f"{contract_path.relative_to(REPO_ROOT)}: "
|
|
397
|
+
f"$id '{contract_id}' includes version. Move version to separate 'version' field"
|
|
398
|
+
)
|
|
399
|
+
else:
|
|
400
|
+
invalid_ids.append(
|
|
401
|
+
f"{contract_path.relative_to(REPO_ROOT)}: "
|
|
402
|
+
f"$id '{contract_id}' doesn't match pattern '{id_pattern.pattern}'"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Check for separate version field (recommended)
|
|
406
|
+
if not version_field:
|
|
407
|
+
missing_version_field.append(
|
|
408
|
+
f"{contract_path.relative_to(REPO_ROOT)}: Missing 'version' field (recommended)"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
except json.JSONDecodeError:
|
|
412
|
+
# Skip invalid JSON files (caught by other test)
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
errors = []
|
|
416
|
+
|
|
417
|
+
if invalid_ids:
|
|
418
|
+
errors.append(
|
|
419
|
+
f"Found {len(invalid_ids)} contracts with invalid $id format:\n" +
|
|
420
|
+
"\n".join(f" {err}" for err in invalid_ids[:10]) +
|
|
421
|
+
(f"\n ... and {len(invalid_ids) - 10} more" if len(invalid_ids) > 10 else "")
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
if missing_version_field:
|
|
425
|
+
errors.append(
|
|
426
|
+
f"\nFound {len(missing_version_field)} contracts missing 'version' field:\n" +
|
|
427
|
+
"\n".join(f" {err}" for err in missing_version_field[:10]) +
|
|
428
|
+
(f"\n ... and {len(missing_version_field) - 10} more" if len(missing_version_field) > 10 else "")
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
if errors:
|
|
432
|
+
pytest.fail(
|
|
433
|
+
"\n".join(errors) +
|
|
434
|
+
f"\n\nExpected format:\n" +
|
|
435
|
+
f" $id: {{domain}}:{{resource}}[.{{category}}] (NO 'contract:' prefix, NO version)\n" +
|
|
436
|
+
f" version: \"1.0.0\" (separate field)\n" +
|
|
437
|
+
f"\nExamples:\n" +
|
|
438
|
+
f" $id: match:result\n" +
|
|
439
|
+
f" $id: match:episode.started (dot for category facet)\n" +
|
|
440
|
+
f" $id: mechanic:decision.choice (dot for category facet)\n" +
|
|
441
|
+
f"\nDo NOT use 'contract:' prefix or version in $id field"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@pytest.mark.platform
|
|
446
|
+
def test_contract_directory_structure_matches_artifact():
|
|
447
|
+
"""
|
|
448
|
+
SPEC-PLATFORM-CONTRACTS-0012: Directory structure mirrors $id hierarchy
|
|
449
|
+
|
|
450
|
+
Given: Contract schemas
|
|
451
|
+
When: Checking physical path vs $id field
|
|
452
|
+
Then: File path mirrors $id with colons replaced by slashes
|
|
453
|
+
Pattern: contracts/{$id with : → /}.schema.json
|
|
454
|
+
Dots in $id represent facets (stay as dots in filename)
|
|
455
|
+
|
|
456
|
+
Examples:
|
|
457
|
+
- $id "match:dilemma:current" → contracts/match/dilemma/current.schema.json
|
|
458
|
+
- $id "mechanic:timebank:exhausted" → contracts/mechanic/timebank/exhausted.schema.json
|
|
459
|
+
- $id "commons:ux:foundations:color" → contracts/commons/ux/foundations/color.schema.json
|
|
460
|
+
- $id "match:dilemma.paired" → contracts/match/dilemma.paired.schema.json (dot preserved)
|
|
461
|
+
"""
|
|
462
|
+
contract_files = find_all_contract_schemas()
|
|
463
|
+
|
|
464
|
+
if not contract_files:
|
|
465
|
+
pytest.skip("No contract schema files found")
|
|
466
|
+
|
|
467
|
+
structure_violations = []
|
|
468
|
+
|
|
469
|
+
for contract_path in contract_files:
|
|
470
|
+
try:
|
|
471
|
+
with open(contract_path) as f:
|
|
472
|
+
contract = json.load(f)
|
|
473
|
+
|
|
474
|
+
contract_id = contract.get("$id")
|
|
475
|
+
if not contract_id:
|
|
476
|
+
continue
|
|
477
|
+
|
|
478
|
+
# Convert $id to expected path: replace colons with slashes
|
|
479
|
+
# Example: "mechanic:timebank:exhausted" → "mechanic/timebank/exhausted"
|
|
480
|
+
# Dots stay as dots (facets): "match:dilemma.paired" → "match/dilemma.paired"
|
|
481
|
+
id_parts = contract_id.split(":")
|
|
482
|
+
expected_path_str = "/".join(id_parts) + ".schema.json"
|
|
483
|
+
expected_path = Path(expected_path_str)
|
|
484
|
+
|
|
485
|
+
# Get actual relative path
|
|
486
|
+
actual_path = contract_path.relative_to(CONTRACTS_DIR)
|
|
487
|
+
|
|
488
|
+
# Compare paths
|
|
489
|
+
if actual_path != expected_path:
|
|
490
|
+
structure_violations.append(
|
|
491
|
+
f"{contract_path.relative_to(REPO_ROOT)}: "
|
|
492
|
+
f"$id '{contract_id}' expects path contracts/{expected_path}, "
|
|
493
|
+
f"but found at contracts/{actual_path}"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
except (json.JSONDecodeError, ValueError):
|
|
497
|
+
continue
|
|
498
|
+
|
|
499
|
+
if structure_violations:
|
|
500
|
+
pytest.fail(
|
|
501
|
+
f"Found {len(structure_violations)} directory structure violations:\n" +
|
|
502
|
+
"\n".join(f" {err}" for err in structure_violations[:10]) +
|
|
503
|
+
(f"\n ... and {len(structure_violations) - 10} more" if len(structure_violations) > 10 else "") +
|
|
504
|
+
"\n\nRule: File path must mirror $id structure\n" +
|
|
505
|
+
" Pattern: contracts/{{$id with : replaced by /}}.schema.json\n" +
|
|
506
|
+
" Example: $id 'mechanic:timebank:exhausted' → contracts/mechanic/timebank/exhausted.schema.json"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@pytest.mark.platform
|
|
511
|
+
def test_contract_api_method_inference():
|
|
512
|
+
"""
|
|
513
|
+
SPEC-PLATFORM-CONTRACTS-0014: API method correctly inferred from resource
|
|
514
|
+
|
|
515
|
+
Given: Contract schemas
|
|
516
|
+
When: Checking API method in x-artifact-metadata
|
|
517
|
+
Then: Method follows interface.convention.yaml api_mapping rules
|
|
518
|
+
POST: choice, new, created, started, exhausted
|
|
519
|
+
GET: result, active, config, foundations, identity, current, pool, paired
|
|
520
|
+
PUT: updated, closed, completed
|
|
521
|
+
DELETE: terminated, deleted
|
|
522
|
+
"""
|
|
523
|
+
contract_files = find_all_contract_schemas()
|
|
524
|
+
|
|
525
|
+
if not contract_files:
|
|
526
|
+
pytest.skip("No contract schema files found")
|
|
527
|
+
|
|
528
|
+
# Inference rules from interface.convention.yaml
|
|
529
|
+
method_hints = {
|
|
530
|
+
"POST": ["choice", "new", "created", "registered", "started", "exhausted"],
|
|
531
|
+
"GET": ["result", "active", "config", "foundations", "identity", "current", "pool", "paired"],
|
|
532
|
+
"PUT": ["updated", "closed", "completed"],
|
|
533
|
+
"DELETE": ["terminated", "deleted"],
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
inference_errors = []
|
|
537
|
+
|
|
538
|
+
for contract_path in contract_files:
|
|
539
|
+
try:
|
|
540
|
+
with open(contract_path) as f:
|
|
541
|
+
contract = json.load(f)
|
|
542
|
+
|
|
543
|
+
metadata = contract.get("x-artifact-metadata", {})
|
|
544
|
+
resource = metadata.get("resource", "")
|
|
545
|
+
api = metadata.get("api", {})
|
|
546
|
+
method = api.get("method", "")
|
|
547
|
+
|
|
548
|
+
if not resource or not method:
|
|
549
|
+
continue
|
|
550
|
+
|
|
551
|
+
# Extract base resource (before dot or colon)
|
|
552
|
+
base_resource = resource.split(".")[0].split(":")[0]
|
|
553
|
+
|
|
554
|
+
# Check if method matches inference rules
|
|
555
|
+
expected_method = None
|
|
556
|
+
for http_method, keywords in method_hints.items():
|
|
557
|
+
if any(keyword in base_resource for keyword in keywords):
|
|
558
|
+
expected_method = http_method
|
|
559
|
+
break
|
|
560
|
+
|
|
561
|
+
if expected_method and method != expected_method:
|
|
562
|
+
inference_errors.append(
|
|
563
|
+
f"{contract_path.relative_to(REPO_ROOT)}: "
|
|
564
|
+
f"Resource '{resource}' suggests {expected_method}, "
|
|
565
|
+
f"but API method is {method}"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
except (json.JSONDecodeError, KeyError):
|
|
569
|
+
continue
|
|
570
|
+
|
|
571
|
+
if inference_errors:
|
|
572
|
+
pytest.fail(
|
|
573
|
+
f"Found {len(inference_errors)} API method inference issues:\n" +
|
|
574
|
+
"\n".join(f" {err}" for err in inference_errors[:10]) +
|
|
575
|
+
(f"\n ... and {len(inference_errors) - 10} more" if len(inference_errors) > 10 else "")
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
@pytest.mark.platform
|
|
580
|
+
def test_contract_traceability_richness():
|
|
581
|
+
"""
|
|
582
|
+
SPEC-PLATFORM-CONTRACTS-0015: Contract metadata includes traceability fields
|
|
583
|
+
|
|
584
|
+
Given: Contract schemas
|
|
585
|
+
When: Checking x-artifact-metadata for traceability
|
|
586
|
+
Then: Contracts include recommended fields for rich traceability:
|
|
587
|
+
- testing.directory (path to atdd/)
|
|
588
|
+
- testing.schema_tests (list of test files)
|
|
589
|
+
- dependencies (array of contract URNs this depends on)
|
|
590
|
+
- traceability.wagon_ref (path to wagon YAML)
|
|
591
|
+
- traceability.feature_refs (array of feature URNs)
|
|
592
|
+
|
|
593
|
+
This test generates a traceability report showing completion metrics.
|
|
594
|
+
"""
|
|
595
|
+
contract_files = find_all_contract_schemas()
|
|
596
|
+
|
|
597
|
+
if not contract_files:
|
|
598
|
+
pytest.skip("No contract schema files found")
|
|
599
|
+
|
|
600
|
+
traceability_report = {
|
|
601
|
+
"testing_directory": 0,
|
|
602
|
+
"testing_schema_tests": 0,
|
|
603
|
+
"dependencies": 0,
|
|
604
|
+
"traceability_wagon_ref": 0,
|
|
605
|
+
"traceability_feature_refs": 0,
|
|
606
|
+
"total_contracts": len(contract_files),
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
missing_traceability = []
|
|
610
|
+
|
|
611
|
+
for contract_path in contract_files:
|
|
612
|
+
try:
|
|
613
|
+
with open(contract_path) as f:
|
|
614
|
+
contract = json.load(f)
|
|
615
|
+
|
|
616
|
+
metadata = contract.get("x-artifact-metadata", {})
|
|
617
|
+
testing = metadata.get("testing", {})
|
|
618
|
+
traceability = metadata.get("traceability", {})
|
|
619
|
+
|
|
620
|
+
contract_name = f"{metadata.get('domain')}:{metadata.get('resource')}"
|
|
621
|
+
missing_fields = []
|
|
622
|
+
|
|
623
|
+
# Check testing fields
|
|
624
|
+
if testing.get("directory"):
|
|
625
|
+
traceability_report["testing_directory"] += 1
|
|
626
|
+
else:
|
|
627
|
+
missing_fields.append("testing.directory")
|
|
628
|
+
|
|
629
|
+
if testing.get("schema_tests"):
|
|
630
|
+
traceability_report["testing_schema_tests"] += 1
|
|
631
|
+
else:
|
|
632
|
+
missing_fields.append("testing.schema_tests")
|
|
633
|
+
|
|
634
|
+
# Check dependencies
|
|
635
|
+
if metadata.get("dependencies"):
|
|
636
|
+
traceability_report["dependencies"] += 1
|
|
637
|
+
else:
|
|
638
|
+
missing_fields.append("dependencies")
|
|
639
|
+
|
|
640
|
+
# Check traceability fields
|
|
641
|
+
if traceability.get("wagon_ref"):
|
|
642
|
+
traceability_report["traceability_wagon_ref"] += 1
|
|
643
|
+
else:
|
|
644
|
+
missing_fields.append("traceability.wagon_ref")
|
|
645
|
+
|
|
646
|
+
if traceability.get("feature_refs"):
|
|
647
|
+
traceability_report["traceability_feature_refs"] += 1
|
|
648
|
+
else:
|
|
649
|
+
missing_fields.append("traceability.feature_refs")
|
|
650
|
+
|
|
651
|
+
if missing_fields:
|
|
652
|
+
missing_traceability.append(
|
|
653
|
+
f"{contract_path.relative_to(REPO_ROOT)} ({contract_name}): "
|
|
654
|
+
f"missing {', '.join(missing_fields)}"
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
except (json.JSONDecodeError, KeyError):
|
|
658
|
+
continue
|
|
659
|
+
|
|
660
|
+
# Calculate percentages
|
|
661
|
+
total = traceability_report["total_contracts"]
|
|
662
|
+
report_lines = [
|
|
663
|
+
"\n=== Contract Traceability Report ===",
|
|
664
|
+
f"Total contracts analyzed: {total}",
|
|
665
|
+
"",
|
|
666
|
+
"Field coverage:",
|
|
667
|
+
f" testing.directory: {traceability_report['testing_directory']}/{total} ({traceability_report['testing_directory']*100//total if total else 0}%)",
|
|
668
|
+
f" testing.schema_tests: {traceability_report['testing_schema_tests']}/{total} ({traceability_report['testing_schema_tests']*100//total if total else 0}%)",
|
|
669
|
+
f" dependencies: {traceability_report['dependencies']}/{total} ({traceability_report['dependencies']*100//total if total else 0}%)",
|
|
670
|
+
f" traceability.wagon_ref: {traceability_report['traceability_wagon_ref']}/{total} ({traceability_report['traceability_wagon_ref']*100//total if total else 0}%)",
|
|
671
|
+
f" traceability.feature_refs: {traceability_report['traceability_feature_refs']}/{total} ({traceability_report['traceability_feature_refs']*100//total if total else 0}%)",
|
|
672
|
+
"",
|
|
673
|
+
]
|
|
674
|
+
|
|
675
|
+
# Calculate overall traceability score
|
|
676
|
+
fields_checked = 5
|
|
677
|
+
total_possible = total * fields_checked
|
|
678
|
+
total_present = sum([
|
|
679
|
+
traceability_report['testing_directory'],
|
|
680
|
+
traceability_report['testing_schema_tests'],
|
|
681
|
+
traceability_report['dependencies'],
|
|
682
|
+
traceability_report['traceability_wagon_ref'],
|
|
683
|
+
traceability_report['traceability_feature_refs'],
|
|
684
|
+
])
|
|
685
|
+
overall_score = (total_present * 100 // total_possible) if total_possible else 0
|
|
686
|
+
|
|
687
|
+
report_lines.append(f"Overall traceability score: {overall_score}% ({total_present}/{total_possible} fields)")
|
|
688
|
+
|
|
689
|
+
if missing_traceability:
|
|
690
|
+
report_lines.extend([
|
|
691
|
+
"",
|
|
692
|
+
f"Contracts missing traceability fields ({len(missing_traceability)}):",
|
|
693
|
+
])
|
|
694
|
+
report_lines.extend(f" {item}" for item in missing_traceability[:15])
|
|
695
|
+
if len(missing_traceability) > 15:
|
|
696
|
+
report_lines.append(f" ... and {len(missing_traceability) - 15} more")
|
|
697
|
+
|
|
698
|
+
# Print report (always, even if passing)
|
|
699
|
+
print("\n".join(report_lines))
|
|
700
|
+
|
|
701
|
+
# Test passes with warning if score < 80%
|
|
702
|
+
if overall_score < 80:
|
|
703
|
+
pytest.skip(
|
|
704
|
+
f"Traceability score {overall_score}% is below 80% threshold. "
|
|
705
|
+
"Consider enriching contract metadata for better governance."
|
|
706
|
+
)
|