atdd 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. atdd/__init__.py +6 -0
  2. atdd/__main__.py +4 -0
  3. atdd/cli.py +404 -0
  4. atdd/coach/__init__.py +0 -0
  5. atdd/coach/commands/__init__.py +0 -0
  6. atdd/coach/commands/add_persistence_metadata.py +215 -0
  7. atdd/coach/commands/analyze_migrations.py +188 -0
  8. atdd/coach/commands/consumers.py +720 -0
  9. atdd/coach/commands/infer_governance_status.py +149 -0
  10. atdd/coach/commands/initializer.py +177 -0
  11. atdd/coach/commands/interface.py +1078 -0
  12. atdd/coach/commands/inventory.py +565 -0
  13. atdd/coach/commands/migration.py +240 -0
  14. atdd/coach/commands/registry.py +1560 -0
  15. atdd/coach/commands/session.py +430 -0
  16. atdd/coach/commands/sync.py +405 -0
  17. atdd/coach/commands/test_interface.py +399 -0
  18. atdd/coach/commands/test_runner.py +141 -0
  19. atdd/coach/commands/tests/__init__.py +1 -0
  20. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  21. atdd/coach/commands/traceability.py +4264 -0
  22. atdd/coach/conventions/session.convention.yaml +754 -0
  23. atdd/coach/overlays/__init__.py +2 -0
  24. atdd/coach/overlays/claude.md +2 -0
  25. atdd/coach/schemas/config.schema.json +34 -0
  26. atdd/coach/schemas/manifest.schema.json +101 -0
  27. atdd/coach/templates/ATDD.md +282 -0
  28. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  29. atdd/coach/utils/__init__.py +0 -0
  30. atdd/coach/utils/graph/__init__.py +0 -0
  31. atdd/coach/utils/graph/urn.py +875 -0
  32. atdd/coach/validators/__init__.py +0 -0
  33. atdd/coach/validators/shared_fixtures.py +365 -0
  34. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  35. atdd/coach/validators/test_registry.py +575 -0
  36. atdd/coach/validators/test_session_validation.py +1183 -0
  37. atdd/coach/validators/test_traceability.py +448 -0
  38. atdd/coach/validators/test_update_feature_paths.py +108 -0
  39. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  40. atdd/coder/__init__.py +1 -0
  41. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  42. atdd/coder/conventions/backend.convention.yaml +460 -0
  43. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  44. atdd/coder/conventions/commons.convention.yaml +460 -0
  45. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  46. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  47. atdd/coder/conventions/design.convention.yaml +327 -0
  48. atdd/coder/conventions/design.recipe.yaml +273 -0
  49. atdd/coder/conventions/dto.convention.yaml +660 -0
  50. atdd/coder/conventions/frontend.convention.yaml +542 -0
  51. atdd/coder/conventions/green.convention.yaml +1012 -0
  52. atdd/coder/conventions/presentation.convention.yaml +587 -0
  53. atdd/coder/conventions/refactor.convention.yaml +535 -0
  54. atdd/coder/conventions/technology.convention.yaml +206 -0
  55. atdd/coder/conventions/tests/__init__.py +0 -0
  56. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  57. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  58. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  59. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  60. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  61. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  62. atdd/coder/conventions/train.convention.yaml +325 -0
  63. atdd/coder/conventions/verification.protocol.yaml +53 -0
  64. atdd/coder/schemas/design_system.schema.json +361 -0
  65. atdd/coder/validators/__init__.py +0 -0
  66. atdd/coder/validators/test_commons_structure.py +485 -0
  67. atdd/coder/validators/test_complexity.py +416 -0
  68. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  69. atdd/coder/validators/test_design_system_compliance.py +413 -0
  70. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  71. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  72. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  73. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  74. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  75. atdd/coder/validators/test_import_boundaries.py +396 -0
  76. atdd/coder/validators/test_init_file_urns.py +593 -0
  77. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  78. atdd/coder/validators/test_presentation_convention.py +260 -0
  79. atdd/coder/validators/test_python_architecture.py +674 -0
  80. atdd/coder/validators/test_quality_metrics.py +420 -0
  81. atdd/coder/validators/test_station_master_pattern.py +244 -0
  82. atdd/coder/validators/test_train_infrastructure.py +454 -0
  83. atdd/coder/validators/test_train_urns.py +293 -0
  84. atdd/coder/validators/test_typescript_architecture.py +616 -0
  85. atdd/coder/validators/test_usecase_structure.py +421 -0
  86. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  87. atdd/conftest.py +126 -0
  88. atdd/planner/__init__.py +1 -0
  89. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  90. atdd/planner/conventions/appendix.convention.yaml +187 -0
  91. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  92. atdd/planner/conventions/component.convention.yaml +670 -0
  93. atdd/planner/conventions/criteria.convention.yaml +141 -0
  94. atdd/planner/conventions/feature.convention.yaml +371 -0
  95. atdd/planner/conventions/interface.convention.yaml +382 -0
  96. atdd/planner/conventions/steps.convention.yaml +141 -0
  97. atdd/planner/conventions/train.convention.yaml +552 -0
  98. atdd/planner/conventions/wagon.convention.yaml +275 -0
  99. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  100. atdd/planner/schemas/acceptance.schema.json +336 -0
  101. atdd/planner/schemas/appendix.schema.json +78 -0
  102. atdd/planner/schemas/component.schema.json +114 -0
  103. atdd/planner/schemas/feature.schema.json +197 -0
  104. atdd/planner/schemas/train.schema.json +192 -0
  105. atdd/planner/schemas/wagon.schema.json +281 -0
  106. atdd/planner/schemas/wmbt.schema.json +59 -0
  107. atdd/planner/validators/__init__.py +0 -0
  108. atdd/planner/validators/conftest.py +5 -0
  109. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  110. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  111. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  112. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  113. atdd/planner/validators/test_plan_wagons.py +174 -0
  114. atdd/planner/validators/test_train_validation.py +514 -0
  115. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  116. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  117. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  118. atdd/tester/__init__.py +1 -0
  119. atdd/tester/conventions/artifact.convention.yaml +257 -0
  120. atdd/tester/conventions/contract.convention.yaml +1009 -0
  121. atdd/tester/conventions/filename.convention.yaml +555 -0
  122. atdd/tester/conventions/migration.convention.yaml +509 -0
  123. atdd/tester/conventions/red.convention.yaml +797 -0
  124. atdd/tester/conventions/routing.convention.yaml +51 -0
  125. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  126. atdd/tester/schemas/a11y.tmpl.json +17 -0
  127. atdd/tester/schemas/artifact.schema.json +189 -0
  128. atdd/tester/schemas/contract.schema.json +591 -0
  129. atdd/tester/schemas/contract.tmpl.json +95 -0
  130. atdd/tester/schemas/db.tmpl.json +20 -0
  131. atdd/tester/schemas/e2e.tmpl.json +17 -0
  132. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  133. atdd/tester/schemas/event.tmpl.json +17 -0
  134. atdd/tester/schemas/http.tmpl.json +19 -0
  135. atdd/tester/schemas/job.tmpl.json +18 -0
  136. atdd/tester/schemas/load.tmpl.json +21 -0
  137. atdd/tester/schemas/metric.tmpl.json +19 -0
  138. atdd/tester/schemas/pack.schema.json +139 -0
  139. atdd/tester/schemas/realtime.tmpl.json +20 -0
  140. atdd/tester/schemas/rls.tmpl.json +18 -0
  141. atdd/tester/schemas/script.tmpl.json +16 -0
  142. atdd/tester/schemas/sec.tmpl.json +18 -0
  143. atdd/tester/schemas/storage.tmpl.json +18 -0
  144. atdd/tester/schemas/telemetry.schema.json +128 -0
  145. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  146. atdd/tester/schemas/test_filename.schema.json +194 -0
  147. atdd/tester/schemas/test_intent.schema.json +179 -0
  148. atdd/tester/schemas/unit.tmpl.json +18 -0
  149. atdd/tester/schemas/visual.tmpl.json +18 -0
  150. atdd/tester/schemas/ws.tmpl.json +17 -0
  151. atdd/tester/utils/__init__.py +0 -0
  152. atdd/tester/utils/filename.py +300 -0
  153. atdd/tester/validators/__init__.py +0 -0
  154. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  155. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  156. atdd/tester/validators/conftest.py +5 -0
  157. atdd/tester/validators/coverage_gap_report.py +321 -0
  158. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  159. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  160. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  161. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  162. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  163. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  164. atdd/tester/validators/test_contracts_structure.py +200 -0
  165. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  166. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  167. atdd/tester/validators/test_fixture_validity.py +372 -0
  168. atdd/tester/validators/test_isolation.py +487 -0
  169. atdd/tester/validators/test_migration_coverage.py +204 -0
  170. atdd/tester/validators/test_migration_criteria.py +276 -0
  171. atdd/tester/validators/test_migration_generation.py +116 -0
  172. atdd/tester/validators/test_python_test_naming.py +410 -0
  173. atdd/tester/validators/test_red_layer_validation.py +95 -0
  174. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  175. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  176. atdd/tester/validators/test_telemetry_structure.py +634 -0
  177. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  178. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  179. atdd-0.2.1.dist-info/METADATA +221 -0
  180. atdd-0.2.1.dist-info/RECORD +184 -0
  181. atdd-0.2.1.dist-info/WHEEL +5 -0
  182. atdd-0.2.1.dist-info/entry_points.txt +2 -0
  183. atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
  184. atdd-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
+ )