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,634 @@
1
+ """
2
+ Platform tests: Telemetry directory structure validation.
3
+
4
+ Validates that telemetry/ follows the signal naming convention.
5
+ Tests ensure telemetry signal files match pattern: {signal-type}.{plane}[.{measure}].json
6
+ """
7
+ import pytest
8
+ from pathlib import Path
9
+ import re
10
+ import json
11
+ from jsonschema import validate, ValidationError
12
+
13
+ # Path constants
14
+ REPO_ROOT = Path(__file__).resolve().parents[4]
15
+ TELEMETRY_DIR = REPO_ROOT / "telemetry"
16
+ CONTRACTS_DIR = REPO_ROOT / "contracts"
17
+ PLAN_DIR = REPO_ROOT / "plan"
18
+ META_SCHEMA_PATH = REPO_ROOT / "atdd" / "tester" / "schemas" / "telemetry.schema.json"
19
+
20
+
21
+ @pytest.fixture
22
+ def telemetry_meta_schema():
23
+ """Load telemetry meta-schema."""
24
+ if not META_SCHEMA_PATH.exists():
25
+ pytest.skip(f"Meta-schema not found: {META_SCHEMA_PATH}")
26
+
27
+ with open(META_SCHEMA_PATH) as f:
28
+ return json.load(f)
29
+
30
+
31
+ def find_all_telemetry_signals():
32
+ """Find telemetry signal files excluding tests/ directories."""
33
+ if not TELEMETRY_DIR.exists():
34
+ return []
35
+ return [
36
+ path for path in TELEMETRY_DIR.rglob("*.json")
37
+ if "tests" not in path.parts
38
+ ]
39
+
40
+
41
+ def load_plan_acceptance_urns():
42
+ """Collect acceptance URNs from plan/ YAML files."""
43
+ if not PLAN_DIR.exists():
44
+ return set()
45
+
46
+ urns = set()
47
+ urn_pattern = re.compile(r"\\burn:\\s*(acc:[^\\s]+)")
48
+
49
+ for plan_path in PLAN_DIR.rglob("*.yaml"):
50
+ try:
51
+ content = plan_path.read_text()
52
+ except OSError:
53
+ continue
54
+ for match in urn_pattern.findall(content):
55
+ urns.add(match.strip())
56
+
57
+ return urns
58
+
59
+
60
+ def collect_contract_urns():
61
+ """Collect contract URNs from contract schemas."""
62
+ if not CONTRACTS_DIR.exists():
63
+ return set()
64
+
65
+ urns = set()
66
+ for contract_path in CONTRACTS_DIR.rglob("*.schema.json"):
67
+ try:
68
+ with open(contract_path) as f:
69
+ contract = json.load(f)
70
+ except json.JSONDecodeError:
71
+ continue
72
+
73
+ contract_id = contract.get("$id")
74
+ if contract_id:
75
+ urns.add(f"contract:{contract_id}")
76
+
77
+ return urns
78
+
79
+
80
+ def is_placeholder_signal(signal_path, signal):
81
+ """Return True if signal is a placeholder signal."""
82
+ if "placeholder" in signal_path.parts:
83
+ return True
84
+
85
+ for key in ("description", "note"):
86
+ value = signal.get(key)
87
+ if isinstance(value, str) and "placeholder" in value.lower():
88
+ return True
89
+
90
+ return False
91
+
92
+
93
+ @pytest.mark.platform
94
+ def test_telemetry_directory_exists():
95
+ """
96
+ SPEC-PLATFORM-TELEMETRY-0001: telemetry/ directory exists
97
+
98
+ Given: Repository root
99
+ When: Checking for telemetry/ directory
100
+ Then: telemetry/ directory exists
101
+ """
102
+ assert TELEMETRY_DIR.exists(), \
103
+ f"telemetry/ directory does not exist at {TELEMETRY_DIR}"
104
+
105
+
106
+ @pytest.mark.platform
107
+ def test_telemetry_follows_theme_domain_pattern():
108
+ """
109
+ SPEC-PLATFORM-TELEMETRY-0002: telemetry/ follows theme/domain pattern with aspect as filename prefix
110
+
111
+ Given: telemetry/ directory structure
112
+ When: Checking directory hierarchy and file naming
113
+ Then: Structure follows telemetry/{theme}/{domain}/ pattern
114
+ Aspect is filename prefix: {aspect}.{type}.{plane}[.{measure}].json
115
+ Mirrors contracts/{theme}/{domain}/{aspect}.schema.json structure
116
+ """
117
+ if not TELEMETRY_DIR.exists():
118
+ pytest.skip(f"telemetry/ directory does not exist")
119
+ return
120
+
121
+ name_pattern = re.compile(r"^[a-z][a-z0-9\-]*$")
122
+
123
+ for theme_dir in TELEMETRY_DIR.iterdir():
124
+ if not theme_dir.is_dir():
125
+ continue
126
+
127
+ # Skip hidden directories and special files
128
+ if theme_dir.name.startswith(".") or theme_dir.name == "__pycache__" or theme_dir.name.startswith("_"):
129
+ continue
130
+
131
+ # Verify theme name follows pattern
132
+ assert name_pattern.match(theme_dir.name), \
133
+ f"Telemetry theme '{theme_dir.name}' doesn't match pattern (lowercase, hyphens only)"
134
+
135
+ # Check domain directories
136
+ for domain_dir in theme_dir.iterdir():
137
+ if not domain_dir.is_dir():
138
+ continue
139
+
140
+ if domain_dir.name.startswith(".") or domain_dir.name == "__pycache__" or domain_dir.name == "tests":
141
+ continue
142
+
143
+ # Verify domain name follows pattern
144
+ assert name_pattern.match(domain_dir.name), \
145
+ f"Telemetry domain '{domain_dir.name}' in theme '{theme_dir.name}' " \
146
+ f"doesn't match pattern (lowercase, hyphens only)"
147
+
148
+ # Verify signal files have aspect prefix (no aspect subdirectories)
149
+ signal_files = list(domain_dir.glob("*.json"))
150
+ if signal_files:
151
+ # Check that files follow aspect.type.plane[.measure].json pattern
152
+ for signal_file in signal_files:
153
+ parts = signal_file.name.split('.')
154
+ assert len(parts) >= 4, \
155
+ f"Signal file '{signal_file.name}' doesn't follow aspect.type.plane[.measure].json pattern"
156
+
157
+ aspect, type_part = parts[0], parts[1]
158
+ assert name_pattern.match(aspect), \
159
+ f"Aspect '{aspect}' in '{signal_file.name}' doesn't match pattern"
160
+
161
+
162
+ @pytest.mark.platform
163
+ def test_telemetry_signal_files_follow_naming_convention():
164
+ """
165
+ SPEC-PLATFORM-TELEMETRY-0003: Signal files follow naming convention
166
+
167
+ Given: Telemetry signal files in telemetry/{domain}/{resource}/
168
+ When: Checking file naming pattern
169
+ Then: Files match pattern: {signal-type}.{plane}[.{measure}].json
170
+ signal-type: metric (with measure) or event (no measure)
171
+ plane: ui, ux, be, nw, db, st, tm, sc, au, fn, if
172
+ measure: optional for metrics (e.g., count, duration, bytes)
173
+ """
174
+ if not TELEMETRY_DIR.exists():
175
+ pytest.skip(f"telemetry/ directory does not exist")
176
+ return
177
+
178
+ # Pattern: metric.{plane}.{measure}.json OR event.{plane}.json
179
+ # Planes: ui, ux, be, nw, db, st, tm, sc, au, fn, if
180
+ metric_pattern = re.compile(
181
+ r"^metric\.(ui|ux|be|nw|db|st|tm|sc|au|fn|if)\.[a-z][a-z0-9\-]*\.json$"
182
+ )
183
+ event_pattern = re.compile(
184
+ r"^event\.(ui|ux|be|nw|db|st|tm|sc|au|fn|if)\.json$"
185
+ )
186
+
187
+ invalid_files = []
188
+
189
+ for theme_dir in TELEMETRY_DIR.iterdir():
190
+ if not theme_dir.is_dir() or theme_dir.name.startswith((".", "_")):
191
+ continue
192
+
193
+ for domain_dir in theme_dir.iterdir():
194
+ if not domain_dir.is_dir() or domain_dir.name.startswith((".", "_")) or domain_dir.name == "tests":
195
+ continue
196
+
197
+ # Check all JSON files in domain directory (not subdirectories)
198
+ for signal_file in domain_dir.glob("*.json"):
199
+ filename = signal_file.name
200
+
201
+ # Skip files in tests directory
202
+ if "tests" in signal_file.parts:
203
+ continue
204
+
205
+ # Files should follow: {aspect}.{type}.{plane}[.{measure}].json
206
+ # So we need to check parts after the aspect prefix
207
+ parts = filename.split('.')
208
+ if len(parts) < 4: # aspect.type.plane.json minimum
209
+ invalid_files.append(str(signal_file.relative_to(REPO_ROOT)))
210
+ continue
211
+
212
+ # Check type.plane[.measure] portion (after aspect prefix)
213
+ type_plane_portion = '.'.join(parts[1:]) # Skip aspect
214
+
215
+ # Check if matches metric or event pattern
216
+ if not (metric_pattern.match(type_plane_portion) or event_pattern.match(type_plane_portion)):
217
+ invalid_files.append(
218
+ str(signal_file.relative_to(REPO_ROOT))
219
+ )
220
+
221
+ if invalid_files:
222
+ pytest.fail(
223
+ f"Found {len(invalid_files)} telemetry files not following naming convention:\n" +
224
+ "\n".join(f" {f}" for f in invalid_files[:10]) +
225
+ (f"\n ... and {len(invalid_files) - 10} more" if len(invalid_files) > 10 else "") +
226
+ "\n\nExpected patterns:\n" +
227
+ " - metric.{plane}.{measure}.json (e.g., metric.ui.duration.json)\n" +
228
+ " - event.{plane}.json (e.g., event.ux.json)\n" +
229
+ " Planes: ui, ux, be, nw, db, st, tm, sc, au, fn, if"
230
+ )
231
+
232
+
233
+ @pytest.mark.platform
234
+ def test_telemetry_directories_contain_signal_files():
235
+ """
236
+ SPEC-PLATFORM-TELEMETRY-0004: Telemetry directories contain signal files
237
+
238
+ Given: telemetry/{theme}/{domain}/{aspect}/ subdirectories (mirroring contracts)
239
+ When: Checking directory contents
240
+ Then: Each aspect subdirectory contains *.json signal files
241
+ Directories are not empty
242
+ """
243
+ if not TELEMETRY_DIR.exists():
244
+ pytest.skip(f"telemetry/ directory does not exist")
245
+ return
246
+
247
+ empty_dirs = []
248
+
249
+ for theme_dir in TELEMETRY_DIR.iterdir():
250
+ if not theme_dir.is_dir() or theme_dir.name.startswith((".", "_")):
251
+ continue
252
+
253
+ for domain_dir in theme_dir.iterdir():
254
+ if not domain_dir.is_dir() or domain_dir.name.startswith((".", "_")) or domain_dir.name == "tests":
255
+ continue
256
+
257
+ # Check subdirectories (aspect directories) for signal files
258
+ # telemetry/commons/ux/ should contain subdirectories like foundations/, primitives/, etc.
259
+ for aspect_dir in domain_dir.iterdir():
260
+ if not aspect_dir.is_dir() or aspect_dir.name.startswith((".", "_")) or aspect_dir.name in ("tests", "atdd"):
261
+ continue
262
+
263
+ # Check if aspect directory has JSON signal files (excluding tests/)
264
+ json_files = [f for f in aspect_dir.glob("*.json") if "tests" not in f.parts]
265
+
266
+ if not json_files:
267
+ empty_dirs.append(str(aspect_dir.relative_to(REPO_ROOT)))
268
+
269
+ if empty_dirs:
270
+ pytest.fail(
271
+ f"Found {len(empty_dirs)} telemetry directories without signal files:\n" +
272
+ "\n".join(f" {d}" for d in empty_dirs[:10]) +
273
+ (f"\n ... and {len(empty_dirs) - 10} more" if len(empty_dirs) > 10 else "")
274
+ )
275
+
276
+
277
+ @pytest.mark.platform
278
+ def test_metric_signals_have_measure_suffix():
279
+ """
280
+ SPEC-PLATFORM-TELEMETRY-0005: Metric signals include measure suffix
281
+
282
+ Given: Telemetry signal files with type 'metric'
283
+ When: Checking file naming
284
+ Then: Metric files match pattern metric.{plane}.{measure}.json
285
+ Measure describes what is measured (e.g., count, duration, bytes)
286
+ """
287
+ if not TELEMETRY_DIR.exists():
288
+ pytest.skip(f"telemetry/ directory does not exist")
289
+ return
290
+
291
+ # Find all metric.* files
292
+ metric_files = list(TELEMETRY_DIR.rglob("metric.*.json"))
293
+
294
+ # Pattern with measure: metric.{plane}.{measure}.json
295
+ metric_with_measure = re.compile(
296
+ r"^metric\.(ui|ux|be|nw|db|st|tm|sc|au|fn|if)\.[a-z][a-z0-9\-]+\.json$"
297
+ )
298
+
299
+ invalid_metrics = []
300
+
301
+ for metric_file in metric_files:
302
+ filename = metric_file.name
303
+
304
+ # Check if it has the measure component
305
+ if not metric_with_measure.match(filename):
306
+ invalid_metrics.append(str(metric_file.relative_to(REPO_ROOT)))
307
+
308
+ if invalid_metrics:
309
+ pytest.fail(
310
+ f"Found {len(invalid_metrics)} metric files without measure suffix:\n" +
311
+ "\n".join(f" {f}" for f in invalid_metrics[:10]) +
312
+ (f"\n ... and {len(invalid_metrics) - 10} more" if len(invalid_metrics) > 10 else "") +
313
+ "\n\nMetric files must include measure: metric.{plane}.{measure}.json"
314
+ )
315
+
316
+
317
+ @pytest.mark.platform
318
+ def test_event_signals_have_no_measure_suffix():
319
+ """
320
+ SPEC-PLATFORM-TELEMETRY-0006: Event signals have no measure suffix
321
+
322
+ Given: Telemetry signal files with type 'event'
323
+ When: Checking file naming
324
+ Then: Event files match pattern event.{plane}.json (no measure)
325
+ """
326
+ if not TELEMETRY_DIR.exists():
327
+ pytest.skip(f"telemetry/ directory does not exist")
328
+ return
329
+
330
+ # Find all event.* files
331
+ event_files = list(TELEMETRY_DIR.rglob("event.*.json"))
332
+
333
+ # Pattern without measure: event.{plane}.json
334
+ event_pattern = re.compile(
335
+ r"^event\.(ui|ux|be|nw|db|st|tm|sc|au|fn|if)\.json$"
336
+ )
337
+
338
+ invalid_events = []
339
+
340
+ for event_file in event_files:
341
+ filename = event_file.name
342
+
343
+ # Check if it matches simple event pattern
344
+ if not event_pattern.match(filename):
345
+ invalid_events.append(str(event_file.relative_to(REPO_ROOT)))
346
+
347
+ if invalid_events:
348
+ pytest.fail(
349
+ f"Found {len(invalid_events)} event files with unexpected format:\n" +
350
+ "\n".join(f" {f}" for f in invalid_events[:10]) +
351
+ (f"\n ... and {len(invalid_events) - 10} more" if len(invalid_events) > 10 else "") +
352
+ "\n\nEvent files should match: event.{plane}.json (no measure suffix)"
353
+ )
354
+
355
+
356
+ @pytest.mark.platform
357
+ def test_telemetry_signals_validate_against_meta_schema(telemetry_meta_schema):
358
+ """
359
+ SPEC-PLATFORM-TELEMETRY-0009: Telemetry signals validate against meta-schema
360
+
361
+ Given: Telemetry signal files
362
+ When: Validating against atdd/tester/schemas/telemetry.schema.json
363
+ Then: All telemetry signals pass meta-schema validation
364
+ """
365
+ signal_files = find_all_telemetry_signals()
366
+
367
+ if not signal_files:
368
+ pytest.skip("No telemetry signal files found")
369
+
370
+ validation_errors = []
371
+
372
+ for signal_path in signal_files:
373
+ try:
374
+ with open(signal_path) as f:
375
+ signal = json.load(f)
376
+ validate(instance=signal, schema=telemetry_meta_schema)
377
+ except ValidationError as exc:
378
+ validation_errors.append(
379
+ f"{signal_path.relative_to(REPO_ROOT)}: {exc.message}"
380
+ )
381
+ except json.JSONDecodeError as exc:
382
+ validation_errors.append(
383
+ f"{signal_path.relative_to(REPO_ROOT)}: Invalid JSON - {exc}"
384
+ )
385
+
386
+ if validation_errors:
387
+ pytest.fail(
388
+ f"Found {len(validation_errors)} telemetry validation errors:\n" +
389
+ "\n".join(f" {err}" for err in validation_errors[:10]) +
390
+ (f"\n ... and {len(validation_errors) - 10} more" if len(validation_errors) > 10 else "")
391
+ )
392
+
393
+
394
+ @pytest.mark.platform
395
+ def test_telemetry_versions_follow_semver():
396
+ """
397
+ SPEC-PLATFORM-TELEMETRY-0010: Telemetry versions follow semantic versioning
398
+
399
+ Given: Telemetry signal version fields
400
+ When: Checking version format
401
+ Then: Versions match pattern: MAJOR.MINOR.PATCH
402
+ """
403
+ signal_files = find_all_telemetry_signals()
404
+
405
+ if not signal_files:
406
+ pytest.skip("No telemetry signal files found")
407
+
408
+ version_pattern = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
409
+ invalid_versions = []
410
+
411
+ for signal_path in signal_files:
412
+ try:
413
+ with open(signal_path) as f:
414
+ signal = json.load(f)
415
+ except json.JSONDecodeError:
416
+ continue
417
+
418
+ version = signal.get("version")
419
+ if not version or not version_pattern.match(version):
420
+ invalid_versions.append(
421
+ f"{signal_path.relative_to(REPO_ROOT)}: version '{version}'"
422
+ )
423
+
424
+ if invalid_versions:
425
+ pytest.fail(
426
+ f"Found {len(invalid_versions)} telemetry signals with invalid versions:\n" +
427
+ "\n".join(f" {err}" for err in invalid_versions[:10]) +
428
+ (f"\n ... and {len(invalid_versions) - 10} more" if len(invalid_versions) > 10 else "")
429
+ )
430
+
431
+
432
+ @pytest.mark.platform
433
+ def test_telemetry_contract_references_exist():
434
+ """
435
+ SPEC-PLATFORM-TELEMETRY-0011: Telemetry contract references point to existing contracts
436
+
437
+ Given: Telemetry signal files with artifact_ref fields
438
+ When: Resolving artifact_ref references
439
+ Then: All referenced contracts exist
440
+ """
441
+ signal_files = find_all_telemetry_signals()
442
+
443
+ if not signal_files:
444
+ pytest.skip("No telemetry signal files found")
445
+
446
+ contract_urns = collect_contract_urns()
447
+ missing_refs = []
448
+
449
+ for signal_path in signal_files:
450
+ try:
451
+ with open(signal_path) as f:
452
+ signal = json.load(f)
453
+ except json.JSONDecodeError:
454
+ continue
455
+
456
+ artifact_ref = signal.get("artifact_ref")
457
+ if artifact_ref and artifact_ref not in contract_urns:
458
+ missing_refs.append(
459
+ f"{signal_path.relative_to(REPO_ROOT)}: artifact_ref '{artifact_ref}' not found"
460
+ )
461
+
462
+ if missing_refs:
463
+ pytest.fail(
464
+ f"Found {len(missing_refs)} telemetry signals with broken contract references:\n" +
465
+ "\n".join(f" {err}" for err in missing_refs[:10]) +
466
+ (f"\n ... and {len(missing_refs) - 10} more" if len(missing_refs) > 10 else "")
467
+ )
468
+
469
+
470
+ @pytest.mark.platform
471
+ def test_telemetry_acceptance_references_exist():
472
+ """
473
+ SPEC-PLATFORM-TELEMETRY-0012: Telemetry acceptance_criteria reference existing criteria
474
+
475
+ Given: Telemetry signal files with acceptance_criteria arrays
476
+ When: Checking acceptance criteria files
477
+ Then: All referenced acceptance URNs exist in plan/ directories
478
+ """
479
+ signal_files = find_all_telemetry_signals()
480
+
481
+ if not signal_files:
482
+ pytest.skip("No telemetry signal files found")
483
+
484
+ acceptance_urns = load_plan_acceptance_urns()
485
+ if not acceptance_urns:
486
+ pytest.skip("No acceptance URNs found in plan/")
487
+
488
+ urn_pattern = re.compile(
489
+ 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})$"
490
+ )
491
+
492
+ missing = []
493
+ empty_criteria = []
494
+
495
+ for signal_path in signal_files:
496
+ try:
497
+ with open(signal_path) as f:
498
+ signal = json.load(f)
499
+ except json.JSONDecodeError:
500
+ continue
501
+
502
+ acceptance_criteria = signal.get("acceptance_criteria", [])
503
+ if not acceptance_criteria:
504
+ empty_criteria.append(signal_path)
505
+ continue
506
+
507
+ for ref in acceptance_criteria:
508
+ if not urn_pattern.match(ref):
509
+ missing.append(
510
+ f"{signal_path.relative_to(REPO_ROOT)}: acceptance_criteria '{ref}' has invalid format"
511
+ )
512
+ continue
513
+ if ref not in acceptance_urns:
514
+ missing.append(
515
+ f"{signal_path.relative_to(REPO_ROOT)}: acceptance_criteria '{ref}' not found in plan/"
516
+ )
517
+
518
+ if empty_criteria:
519
+ print(
520
+ "Telemetry signals missing acceptance_criteria:\n" +
521
+ "\n".join(f" {p.relative_to(REPO_ROOT)}" for p in empty_criteria[:10]) +
522
+ (f"\n ... and {len(empty_criteria) - 10} more" if len(empty_criteria) > 10 else "")
523
+ )
524
+
525
+ if missing:
526
+ pytest.fail(
527
+ f"Found {len(missing)} invalid acceptance references:\n" +
528
+ "\n".join(f" {err}" for err in missing[:10]) +
529
+ (f"\n ... and {len(missing) - 10} more" if len(missing) > 10 else "")
530
+ )
531
+
532
+
533
+ @pytest.mark.platform
534
+ def test_no_duplicate_telemetry_ids():
535
+ """
536
+ SPEC-PLATFORM-TELEMETRY-0013: Telemetry $id fields are unique
537
+
538
+ Given: All telemetry signal files in telemetry/
539
+ When: Collecting $id values
540
+ Then: No two signals have the same $id
541
+ """
542
+ signal_files = find_all_telemetry_signals()
543
+
544
+ if not signal_files:
545
+ pytest.skip("No telemetry signal files found")
546
+
547
+ seen = {}
548
+ duplicates = {}
549
+
550
+ for signal_path in signal_files:
551
+ try:
552
+ with open(signal_path) as f:
553
+ signal = json.load(f)
554
+ except json.JSONDecodeError:
555
+ continue
556
+
557
+ signal_id = signal.get("$id")
558
+ if not signal_id:
559
+ continue
560
+ if signal_id in seen:
561
+ duplicates.setdefault(signal_id, [seen[signal_id]]).append(signal_path)
562
+ else:
563
+ seen[signal_id] = signal_path
564
+
565
+ if duplicates:
566
+ lines = []
567
+ for signal_id, paths in duplicates.items():
568
+ lines.append(f"$id: \"{signal_id}\"")
569
+ for path in paths:
570
+ lines.append(f" - {path.relative_to(REPO_ROOT)}")
571
+
572
+ pytest.fail(
573
+ "Found duplicate telemetry IDs:\n" +
574
+ "\n".join(lines)
575
+ )
576
+
577
+
578
+ @pytest.mark.platform
579
+ def test_no_orphaned_telemetry_directories(telemetry_urns):
580
+ """
581
+ SPEC-PLATFORM-TELEMETRY-0007: No orphaned telemetry directories
582
+
583
+ Given: Telemetry directories in telemetry/
584
+ When: Comparing to telemetry URNs from wagons
585
+ Then: Each telemetry/{theme}/{domain}/{aspect}/ has a corresponding URN
586
+ No orphaned directories that aren't referenced
587
+ """
588
+ if not TELEMETRY_DIR.exists():
589
+ pytest.skip(f"telemetry/ directory does not exist")
590
+ return
591
+
592
+ # Build set of expected paths from URNs
593
+ expected_paths = set()
594
+
595
+ for urn in telemetry_urns:
596
+ parts = urn.split(":")
597
+ if len(parts) == 4:
598
+ _, theme, domain, aspect = parts
599
+ expected_paths.add(f"{theme}/{domain}/{aspect}")
600
+
601
+ # Check actual directory structure
602
+ orphaned = []
603
+
604
+ for theme_dir in TELEMETRY_DIR.iterdir():
605
+ if not theme_dir.is_dir() or theme_dir.name.startswith((".", "_")):
606
+ continue
607
+
608
+ theme_name = theme_dir.name
609
+
610
+ for domain_dir in theme_dir.iterdir():
611
+ if not domain_dir.is_dir() or domain_dir.name.startswith((".", "_")) or domain_dir.name == "tests":
612
+ continue
613
+
614
+ domain_name = domain_dir.name
615
+
616
+ # Check signal files for aspects (aspects are filename prefixes now)
617
+ signal_files = [f for f in domain_dir.glob("*.json") if "tests" not in f.parts]
618
+
619
+ for signal_file in signal_files:
620
+ # Extract aspect from filename (first part before .)
621
+ aspect_name = signal_file.name.split('.')[0]
622
+ path = f"{theme_name}/{domain_name}/{aspect_name}"
623
+
624
+ # Check if this path is referenced by a URN
625
+ if path not in expected_paths:
626
+ orphaned.append(path)
627
+
628
+ if orphaned:
629
+ pytest.skip(
630
+ f"Found {len(orphaned)} telemetry directories without corresponding URNs:\n" +
631
+ "\n".join(f" telemetry/{d}" for d in orphaned[:10]) +
632
+ (f"\n ... and {len(orphaned) - 10} more" if len(orphaned) > 10 else "") +
633
+ "\n (This may be expected for legacy or future telemetry)"
634
+ )