atdd 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. atdd/__init__.py +0 -0
  2. atdd/cli.py +404 -0
  3. atdd/coach/__init__.py +0 -0
  4. atdd/coach/commands/__init__.py +0 -0
  5. atdd/coach/commands/add_persistence_metadata.py +215 -0
  6. atdd/coach/commands/analyze_migrations.py +188 -0
  7. atdd/coach/commands/consumers.py +720 -0
  8. atdd/coach/commands/infer_governance_status.py +149 -0
  9. atdd/coach/commands/initializer.py +177 -0
  10. atdd/coach/commands/interface.py +1078 -0
  11. atdd/coach/commands/inventory.py +565 -0
  12. atdd/coach/commands/migration.py +240 -0
  13. atdd/coach/commands/registry.py +1560 -0
  14. atdd/coach/commands/session.py +430 -0
  15. atdd/coach/commands/sync.py +405 -0
  16. atdd/coach/commands/test_interface.py +399 -0
  17. atdd/coach/commands/test_runner.py +141 -0
  18. atdd/coach/commands/tests/__init__.py +1 -0
  19. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  20. atdd/coach/commands/traceability.py +4264 -0
  21. atdd/coach/conventions/session.convention.yaml +754 -0
  22. atdd/coach/overlays/__init__.py +2 -0
  23. atdd/coach/overlays/claude.md +2 -0
  24. atdd/coach/schemas/config.schema.json +34 -0
  25. atdd/coach/schemas/manifest.schema.json +101 -0
  26. atdd/coach/templates/ATDD.md +282 -0
  27. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  28. atdd/coach/utils/__init__.py +0 -0
  29. atdd/coach/utils/graph/__init__.py +0 -0
  30. atdd/coach/utils/graph/urn.py +875 -0
  31. atdd/coach/validators/__init__.py +0 -0
  32. atdd/coach/validators/shared_fixtures.py +365 -0
  33. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  34. atdd/coach/validators/test_registry.py +575 -0
  35. atdd/coach/validators/test_session_validation.py +1183 -0
  36. atdd/coach/validators/test_traceability.py +448 -0
  37. atdd/coach/validators/test_update_feature_paths.py +108 -0
  38. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  39. atdd/coder/__init__.py +1 -0
  40. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  41. atdd/coder/conventions/backend.convention.yaml +460 -0
  42. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  43. atdd/coder/conventions/commons.convention.yaml +460 -0
  44. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  45. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  46. atdd/coder/conventions/design.convention.yaml +327 -0
  47. atdd/coder/conventions/design.recipe.yaml +273 -0
  48. atdd/coder/conventions/dto.convention.yaml +660 -0
  49. atdd/coder/conventions/frontend.convention.yaml +542 -0
  50. atdd/coder/conventions/green.convention.yaml +1012 -0
  51. atdd/coder/conventions/presentation.convention.yaml +587 -0
  52. atdd/coder/conventions/refactor.convention.yaml +535 -0
  53. atdd/coder/conventions/technology.convention.yaml +206 -0
  54. atdd/coder/conventions/tests/__init__.py +0 -0
  55. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  56. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  57. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  58. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  59. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  60. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  61. atdd/coder/conventions/train.convention.yaml +325 -0
  62. atdd/coder/conventions/verification.protocol.yaml +53 -0
  63. atdd/coder/schemas/design_system.schema.json +361 -0
  64. atdd/coder/validators/__init__.py +0 -0
  65. atdd/coder/validators/test_commons_structure.py +485 -0
  66. atdd/coder/validators/test_complexity.py +416 -0
  67. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  68. atdd/coder/validators/test_design_system_compliance.py +413 -0
  69. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  70. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  71. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  72. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  73. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  74. atdd/coder/validators/test_import_boundaries.py +396 -0
  75. atdd/coder/validators/test_init_file_urns.py +593 -0
  76. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  77. atdd/coder/validators/test_presentation_convention.py +260 -0
  78. atdd/coder/validators/test_python_architecture.py +674 -0
  79. atdd/coder/validators/test_quality_metrics.py +420 -0
  80. atdd/coder/validators/test_station_master_pattern.py +244 -0
  81. atdd/coder/validators/test_train_infrastructure.py +454 -0
  82. atdd/coder/validators/test_train_urns.py +293 -0
  83. atdd/coder/validators/test_typescript_architecture.py +616 -0
  84. atdd/coder/validators/test_usecase_structure.py +421 -0
  85. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  86. atdd/conftest.py +126 -0
  87. atdd/planner/__init__.py +1 -0
  88. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  89. atdd/planner/conventions/appendix.convention.yaml +187 -0
  90. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  91. atdd/planner/conventions/component.convention.yaml +670 -0
  92. atdd/planner/conventions/criteria.convention.yaml +141 -0
  93. atdd/planner/conventions/feature.convention.yaml +371 -0
  94. atdd/planner/conventions/interface.convention.yaml +382 -0
  95. atdd/planner/conventions/steps.convention.yaml +141 -0
  96. atdd/planner/conventions/train.convention.yaml +552 -0
  97. atdd/planner/conventions/wagon.convention.yaml +275 -0
  98. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  99. atdd/planner/schemas/acceptance.schema.json +336 -0
  100. atdd/planner/schemas/appendix.schema.json +78 -0
  101. atdd/planner/schemas/component.schema.json +114 -0
  102. atdd/planner/schemas/feature.schema.json +197 -0
  103. atdd/planner/schemas/train.schema.json +192 -0
  104. atdd/planner/schemas/wagon.schema.json +281 -0
  105. atdd/planner/schemas/wmbt.schema.json +59 -0
  106. atdd/planner/validators/__init__.py +0 -0
  107. atdd/planner/validators/conftest.py +5 -0
  108. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  109. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  110. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  111. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  112. atdd/planner/validators/test_plan_wagons.py +174 -0
  113. atdd/planner/validators/test_train_validation.py +514 -0
  114. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  115. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  116. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  117. atdd/tester/__init__.py +1 -0
  118. atdd/tester/conventions/artifact.convention.yaml +257 -0
  119. atdd/tester/conventions/contract.convention.yaml +1009 -0
  120. atdd/tester/conventions/filename.convention.yaml +555 -0
  121. atdd/tester/conventions/migration.convention.yaml +509 -0
  122. atdd/tester/conventions/red.convention.yaml +797 -0
  123. atdd/tester/conventions/routing.convention.yaml +51 -0
  124. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  125. atdd/tester/schemas/a11y.tmpl.json +17 -0
  126. atdd/tester/schemas/artifact.schema.json +189 -0
  127. atdd/tester/schemas/contract.schema.json +591 -0
  128. atdd/tester/schemas/contract.tmpl.json +95 -0
  129. atdd/tester/schemas/db.tmpl.json +20 -0
  130. atdd/tester/schemas/e2e.tmpl.json +17 -0
  131. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  132. atdd/tester/schemas/event.tmpl.json +17 -0
  133. atdd/tester/schemas/http.tmpl.json +19 -0
  134. atdd/tester/schemas/job.tmpl.json +18 -0
  135. atdd/tester/schemas/load.tmpl.json +21 -0
  136. atdd/tester/schemas/metric.tmpl.json +19 -0
  137. atdd/tester/schemas/pack.schema.json +139 -0
  138. atdd/tester/schemas/realtime.tmpl.json +20 -0
  139. atdd/tester/schemas/rls.tmpl.json +18 -0
  140. atdd/tester/schemas/script.tmpl.json +16 -0
  141. atdd/tester/schemas/sec.tmpl.json +18 -0
  142. atdd/tester/schemas/storage.tmpl.json +18 -0
  143. atdd/tester/schemas/telemetry.schema.json +128 -0
  144. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  145. atdd/tester/schemas/test_filename.schema.json +194 -0
  146. atdd/tester/schemas/test_intent.schema.json +179 -0
  147. atdd/tester/schemas/unit.tmpl.json +18 -0
  148. atdd/tester/schemas/visual.tmpl.json +18 -0
  149. atdd/tester/schemas/ws.tmpl.json +17 -0
  150. atdd/tester/utils/__init__.py +0 -0
  151. atdd/tester/utils/filename.py +300 -0
  152. atdd/tester/validators/__init__.py +0 -0
  153. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  154. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  155. atdd/tester/validators/conftest.py +5 -0
  156. atdd/tester/validators/coverage_gap_report.py +321 -0
  157. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  158. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  159. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  160. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  161. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  162. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  163. atdd/tester/validators/test_contracts_structure.py +200 -0
  164. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  165. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  166. atdd/tester/validators/test_fixture_validity.py +372 -0
  167. atdd/tester/validators/test_isolation.py +487 -0
  168. atdd/tester/validators/test_migration_coverage.py +204 -0
  169. atdd/tester/validators/test_migration_criteria.py +276 -0
  170. atdd/tester/validators/test_migration_generation.py +116 -0
  171. atdd/tester/validators/test_python_test_naming.py +410 -0
  172. atdd/tester/validators/test_red_layer_validation.py +95 -0
  173. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  174. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  175. atdd/tester/validators/test_telemetry_structure.py +634 -0
  176. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  177. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  178. atdd-0.1.0.dist-info/METADATA +191 -0
  179. atdd-0.1.0.dist-info/RECORD +183 -0
  180. atdd-0.1.0.dist-info/WHEEL +5 -0
  181. atdd-0.1.0.dist-info/entry_points.txt +2 -0
  182. atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
  183. atdd-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,632 @@
1
+ """
2
+ Test WMBT files use authorized vocabulary from convention.
3
+
4
+ Validates that WMBT YAML files use only authorized terms defined in:
5
+ - .claude/conventions/planner/wmbt.convention.yaml
6
+ - .claude/schemas/planner/wmbt.schema.json
7
+
8
+ Enforces:
9
+ - Authorized step codes (D, L, P, C, E, M, Y, R, K)
10
+ - Authorized directions (minimize, maximize, increase, decrease)
11
+ - Authorized dimensions (time, effort, likelihood, frequency, quantity, financial value)
12
+ - Authorized lens patterns (functional.*, emotional.*, social.*)
13
+ - Statement construction follows pattern: {direction} {dimension} of {object_of_control} {context_clarifier}
14
+
15
+ Rationale:
16
+ Controlled vocabulary ensures consistency, traceability, and proper interpretation
17
+ of WMBT statements across the entire platform.
18
+ """
19
+
20
+ import pytest
21
+ import yaml
22
+ import re
23
+ from pathlib import Path
24
+ from typing import Dict, List, Tuple, Set, Optional
25
+
26
+
27
+ # Path constants
28
+ REPO_ROOT = Path(__file__).resolve().parents[4]
29
+ PLAN_DIR = REPO_ROOT / "plan"
30
+ WMBT_CONVENTION = REPO_ROOT / "atdd" / "planner" / "conventions" / "wmbt.convention.yaml"
31
+
32
+
33
+ # Authorized vocabulary from convention
34
+ AUTHORIZED_STEPS = {
35
+ "define": "D",
36
+ "locate": "L",
37
+ "prepare": "P",
38
+ "confirm": "C",
39
+ "execute": "E",
40
+ "monitor": "M",
41
+ "modify": "Y",
42
+ "resolve": "R",
43
+ "conclude": "K",
44
+ }
45
+
46
+ STEP_CODES = set(AUTHORIZED_STEPS.values())
47
+
48
+ AUTHORIZED_DIRECTIONS = {
49
+ "minimize",
50
+ "maximize",
51
+ "increase",
52
+ "decrease",
53
+ }
54
+
55
+ AUTHORIZED_DIMENSIONS = {
56
+ "time",
57
+ "effort",
58
+ "likelihood",
59
+ "frequency",
60
+ "quantity",
61
+ "financial value",
62
+ }
63
+
64
+ AUTHORIZED_LENS_CATEGORIES = {
65
+ "functional",
66
+ "emotional",
67
+ "social",
68
+ }
69
+
70
+ # Specific authorized lens attributes from convention
71
+ AUTHORIZED_FUNCTIONAL_LENSES = {
72
+ "efficiency",
73
+ "effectiveness",
74
+ "availability",
75
+ "adaptability",
76
+ "sustainability",
77
+ }
78
+
79
+ AUTHORIZED_EMOTIONAL_LENSES = {
80
+ "joy",
81
+ "trust",
82
+ "fear",
83
+ "surprise",
84
+ "sadness",
85
+ "disgust",
86
+ "anger",
87
+ "anticipation",
88
+ }
89
+
90
+ AUTHORIZED_SOCIAL_LENSES = {
91
+ "belong",
92
+ "stand_out",
93
+ "affirm",
94
+ "aspire",
95
+ }
96
+
97
+
98
+ def find_wmbt_files() -> List[Tuple[str, Path]]:
99
+ """
100
+ Find all WMBT YAML files in plan/{wagon}/ directories.
101
+
102
+ Returns: List[(wagon_slug, wmbt_file_path)]
103
+ """
104
+ wmbt_files = []
105
+
106
+ if not PLAN_DIR.exists():
107
+ return wmbt_files
108
+
109
+ for wagon_dir in PLAN_DIR.iterdir():
110
+ if not wagon_dir.is_dir():
111
+ continue
112
+
113
+ # Directory name uses underscores, convert to kebab-case slug
114
+ dir_name = wagon_dir.name
115
+ wagon_slug = dir_name.replace("_", "-")
116
+
117
+ # Find all WMBT files matching pattern: {STEP_CODE}{NNN}.yaml
118
+ for yaml_file in wagon_dir.glob("*.yaml"):
119
+ filename = yaml_file.stem
120
+
121
+ # Check if it matches WMBT pattern
122
+ if len(filename) == 4 and filename[0] in STEP_CODES and filename[1:].isdigit():
123
+ wmbt_files.append((wagon_slug, yaml_file))
124
+
125
+ return wmbt_files
126
+
127
+
128
+ def load_wmbt_file(file_path: Path) -> Optional[Dict]:
129
+ """Load and parse WMBT YAML file."""
130
+ try:
131
+ with open(file_path, 'r', encoding='utf-8') as f:
132
+ return yaml.safe_load(f)
133
+ except Exception:
134
+ return None
135
+
136
+
137
+ def validate_step(wmbt_data: Dict, file_path: Path) -> Optional[str]:
138
+ """
139
+ Validate step field uses authorized vocabulary.
140
+
141
+ Returns: Error message if invalid, None if valid
142
+ """
143
+ step = wmbt_data.get("step")
144
+
145
+ if not step:
146
+ return f"Missing required field 'step'"
147
+
148
+ if step not in AUTHORIZED_STEPS:
149
+ return (
150
+ f"Step '{step}' is not authorized. "
151
+ f"Must be one of: {', '.join(sorted(AUTHORIZED_STEPS.keys()))}"
152
+ )
153
+
154
+ return None
155
+
156
+
157
+ def validate_direction(wmbt_data: Dict, file_path: Path) -> Optional[str]:
158
+ """
159
+ Validate direction field uses authorized vocabulary.
160
+
161
+ Returns: Error message if invalid, None if valid
162
+ """
163
+ direction = wmbt_data.get("direction")
164
+
165
+ if not direction:
166
+ return f"Missing required field 'direction'"
167
+
168
+ if direction not in AUTHORIZED_DIRECTIONS:
169
+ return (
170
+ f"Direction '{direction}' is not authorized. "
171
+ f"Must be one of: {', '.join(sorted(AUTHORIZED_DIRECTIONS))}"
172
+ )
173
+
174
+ return None
175
+
176
+
177
+ def validate_dimension(wmbt_data: Dict, file_path: Path) -> Optional[str]:
178
+ """
179
+ Validate dimension field uses authorized vocabulary.
180
+
181
+ Returns: Error message if invalid, None if valid
182
+ """
183
+ dimension = wmbt_data.get("dimension")
184
+
185
+ if not dimension:
186
+ return f"Missing required field 'dimension'"
187
+
188
+ if dimension not in AUTHORIZED_DIMENSIONS:
189
+ return (
190
+ f"Dimension '{dimension}' is not authorized. "
191
+ f"Must be one of: {', '.join(sorted(AUTHORIZED_DIMENSIONS))}"
192
+ )
193
+
194
+ return None
195
+
196
+
197
+ def validate_lens(wmbt_data: Dict, file_path: Path) -> Optional[str]:
198
+ """
199
+ Validate lens field uses authorized vocabulary.
200
+
201
+ Lens pattern: {category}.{attribute}
202
+ - category: functional | emotional | social
203
+ - attribute: specific lens from convention catalog
204
+
205
+ Returns: Error message if invalid, None if valid
206
+ """
207
+ lens = wmbt_data.get("lens")
208
+
209
+ if not lens:
210
+ return f"Missing required field 'lens'"
211
+
212
+ # Check pattern: category.attribute
213
+ if "." not in lens:
214
+ return (
215
+ f"Lens '{lens}' must follow pattern: {{category}}.{{attribute}} "
216
+ f"(e.g., 'functional.efficiency', 'emotional.trust')"
217
+ )
218
+
219
+ parts = lens.split(".", 1)
220
+ category = parts[0]
221
+ attribute = parts[1] if len(parts) > 1 else ""
222
+
223
+ # Validate category
224
+ if category not in AUTHORIZED_LENS_CATEGORIES:
225
+ return (
226
+ f"Lens category '{category}' is not authorized. "
227
+ f"Must be one of: {', '.join(sorted(AUTHORIZED_LENS_CATEGORIES))}"
228
+ )
229
+
230
+ # Validate attribute based on category
231
+ if category == "functional":
232
+ if attribute not in AUTHORIZED_FUNCTIONAL_LENSES:
233
+ return (
234
+ f"Functional lens attribute '{attribute}' is not authorized. "
235
+ f"Must be one of: {', '.join(sorted(AUTHORIZED_FUNCTIONAL_LENSES))}"
236
+ )
237
+ elif category == "emotional":
238
+ if attribute not in AUTHORIZED_EMOTIONAL_LENSES:
239
+ return (
240
+ f"Emotional lens attribute '{attribute}' is not authorized. "
241
+ f"Must be one of: {', '.join(sorted(AUTHORIZED_EMOTIONAL_LENSES))}"
242
+ )
243
+ elif category == "social":
244
+ if attribute not in AUTHORIZED_SOCIAL_LENSES:
245
+ return (
246
+ f"Social lens attribute '{attribute}' is not authorized. "
247
+ f"Must be one of: {', '.join(sorted(AUTHORIZED_SOCIAL_LENSES))}"
248
+ )
249
+
250
+ return None
251
+
252
+
253
+ def validate_object_of_control(wmbt_data: Dict, file_path: Path) -> Optional[str]:
254
+ """
255
+ Validate object_of_control follows naming convention.
256
+
257
+ Pattern: kebab-case noun phrase (lowercase, hyphens, no spaces)
258
+
259
+ Returns: Error message if invalid, None if valid
260
+ """
261
+ object_of_control = wmbt_data.get("object_of_control")
262
+
263
+ if not object_of_control:
264
+ return f"Missing required field 'object_of_control'"
265
+
266
+ # Must be kebab-case
267
+ if not re.match(r'^[a-z][a-z0-9-]*$', object_of_control):
268
+ return (
269
+ f"Object of control '{object_of_control}' must be kebab-case "
270
+ f"(lowercase letters, numbers, hyphens only, starting with letter)"
271
+ )
272
+
273
+ if len(object_of_control) < 2:
274
+ return f"Object of control '{object_of_control}' is too short (min 2 chars)"
275
+
276
+ return None
277
+
278
+
279
+ def validate_statement_construction(wmbt_data: Dict, file_path: Path) -> Optional[str]:
280
+ """
281
+ Validate statement follows construction pattern.
282
+
283
+ Pattern: {direction} {dimension} of {object_of_control} {context_clarifier}
284
+
285
+ Returns: Error message if invalid, None if valid
286
+ """
287
+ statement = wmbt_data.get("statement")
288
+
289
+ if not statement:
290
+ return f"Missing required field 'statement'"
291
+
292
+ direction = wmbt_data.get("direction", "")
293
+ dimension = wmbt_data.get("dimension", "")
294
+ object_of_control = wmbt_data.get("object_of_control", "")
295
+ context_clarifier = wmbt_data.get("context_clarifier", "")
296
+
297
+ # Build expected statement pattern
298
+ expected_parts = [direction, dimension, "of", object_of_control]
299
+ if context_clarifier:
300
+ expected_parts.append(context_clarifier)
301
+
302
+ # Check if statement contains expected components
303
+ statement_lower = statement.lower()
304
+
305
+ # Must start with direction
306
+ if not statement_lower.startswith(direction.lower()):
307
+ return (
308
+ f"Statement must start with direction '{direction}'. "
309
+ f"Current: '{statement}'"
310
+ )
311
+
312
+ # Must contain dimension
313
+ if dimension.lower() not in statement_lower:
314
+ return (
315
+ f"Statement must contain dimension '{dimension}'. "
316
+ f"Current: '{statement}'"
317
+ )
318
+
319
+ # Must contain "of" separator
320
+ if " of " not in statement_lower:
321
+ return (
322
+ f"Statement must contain ' of ' separator. "
323
+ f"Current: '{statement}'"
324
+ )
325
+
326
+ # Must contain object_of_control (with hyphens converted to spaces for natural language)
327
+ object_natural = object_of_control.replace("-", " ")
328
+ if object_natural.lower() not in statement_lower and object_of_control.lower() not in statement_lower:
329
+ return (
330
+ f"Statement must contain object of control '{object_of_control}'. "
331
+ f"Current: '{statement}'"
332
+ )
333
+
334
+ return None
335
+
336
+
337
+ def validate_urn_step_code_consistency(wmbt_data: Dict, file_path: Path) -> Optional[str]:
338
+ """
339
+ Validate URN step code matches step field.
340
+
341
+ URN format: wmbt:{wagon}:{STEP_CODE}{NNN}
342
+ Step code in URN must match the step field.
343
+
344
+ Returns: Error message if invalid, None if valid
345
+ """
346
+ urn = wmbt_data.get("urn", "")
347
+ step = wmbt_data.get("step", "")
348
+
349
+ if not urn or not step:
350
+ return None # Other validators will catch missing fields
351
+
352
+ # Extract step code from URN
353
+ # Pattern: wmbt:{wagon}:{STEP_CODE}{NNN}
354
+ parts = urn.split(":")
355
+ if len(parts) < 3:
356
+ return None # URN pattern validation is handled elsewhere
357
+
358
+ code_part = parts[2] # e.g., "E001"
359
+ if not code_part:
360
+ return None
361
+
362
+ urn_step_code = code_part[0] # e.g., "E"
363
+
364
+ # Get expected step code from step field
365
+ expected_step_code = AUTHORIZED_STEPS.get(step)
366
+
367
+ if not expected_step_code:
368
+ return None # Step validation is handled elsewhere
369
+
370
+ if urn_step_code != expected_step_code:
371
+ return (
372
+ f"URN step code '{urn_step_code}' does not match step field '{step}'. "
373
+ f"Expected step code: '{expected_step_code}' (from step '{step}'). "
374
+ f"URN: {urn}"
375
+ )
376
+
377
+ return None
378
+
379
+
380
+ @pytest.mark.planner
381
+ def test_wmbt_files_use_authorized_steps():
382
+ """
383
+ SPEC-PLANNER-WMBT-VOCAB-001: WMBT files use authorized step vocabulary.
384
+
385
+ Given: WMBT YAML files in plan/{wagon}/ directories
386
+ When: Validating step field
387
+ Then: Step must be one of the authorized values from convention
388
+
389
+ Authorized steps: define, locate, prepare, confirm, execute, monitor, modify, resolve, conclude
390
+ """
391
+ wmbt_files = find_wmbt_files()
392
+
393
+ if not wmbt_files:
394
+ pytest.skip("No WMBT files found")
395
+
396
+ violations = []
397
+
398
+ for wagon_slug, wmbt_file in wmbt_files:
399
+ wmbt_data = load_wmbt_file(wmbt_file)
400
+
401
+ if not wmbt_data:
402
+ violations.append(f"{wmbt_file.relative_to(REPO_ROOT)}: Failed to load file")
403
+ continue
404
+
405
+ error = validate_step(wmbt_data, wmbt_file)
406
+ if error:
407
+ violations.append(f"{wmbt_file.relative_to(REPO_ROOT)}: {error}")
408
+
409
+ if violations:
410
+ pytest.fail(
411
+ f"\n\nFound {len(violations)} step vocabulary violations:\n\n" +
412
+ "\n".join(violations[:20]) +
413
+ (f"\n\n... and {len(violations) - 20} more" if len(violations) > 20 else "")
414
+ )
415
+
416
+
417
+ @pytest.mark.planner
418
+ def test_wmbt_files_use_authorized_directions():
419
+ """
420
+ SPEC-PLANNER-WMBT-VOCAB-002: WMBT files use authorized direction vocabulary.
421
+
422
+ Given: WMBT YAML files in plan/{wagon}/ directories
423
+ When: Validating direction field
424
+ Then: Direction must be one of the authorized values from convention
425
+
426
+ Authorized directions: minimize, maximize, increase, decrease
427
+ """
428
+ wmbt_files = find_wmbt_files()
429
+
430
+ if not wmbt_files:
431
+ pytest.skip("No WMBT files found")
432
+
433
+ violations = []
434
+
435
+ for wagon_slug, wmbt_file in wmbt_files:
436
+ wmbt_data = load_wmbt_file(wmbt_file)
437
+
438
+ if not wmbt_data:
439
+ continue
440
+
441
+ error = validate_direction(wmbt_data, wmbt_file)
442
+ if error:
443
+ violations.append(f"{wmbt_file.relative_to(REPO_ROOT)}: {error}")
444
+
445
+ if violations:
446
+ pytest.fail(
447
+ f"\n\nFound {len(violations)} direction vocabulary violations:\n\n" +
448
+ "\n".join(violations[:20]) +
449
+ (f"\n\n... and {len(violations) - 20} more" if len(violations) > 20 else "")
450
+ )
451
+
452
+
453
+ @pytest.mark.planner
454
+ def test_wmbt_files_use_authorized_dimensions():
455
+ """
456
+ SPEC-PLANNER-WMBT-VOCAB-003: WMBT files use authorized dimension vocabulary.
457
+
458
+ Given: WMBT YAML files in plan/{wagon}/ directories
459
+ When: Validating dimension field
460
+ Then: Dimension must be one of the authorized values from convention
461
+
462
+ Authorized dimensions: time, effort, likelihood, frequency, quantity, financial value
463
+ """
464
+ wmbt_files = find_wmbt_files()
465
+
466
+ if not wmbt_files:
467
+ pytest.skip("No WMBT files found")
468
+
469
+ violations = []
470
+
471
+ for wagon_slug, wmbt_file in wmbt_files:
472
+ wmbt_data = load_wmbt_file(wmbt_file)
473
+
474
+ if not wmbt_data:
475
+ continue
476
+
477
+ error = validate_dimension(wmbt_data, wmbt_file)
478
+ if error:
479
+ violations.append(f"{wmbt_file.relative_to(REPO_ROOT)}: {error}")
480
+
481
+ if violations:
482
+ pytest.fail(
483
+ f"\n\nFound {len(violations)} dimension vocabulary violations:\n\n" +
484
+ "\n".join(violations[:20]) +
485
+ (f"\n\n... and {len(violations) - 20} more" if len(violations) > 20 else "")
486
+ )
487
+
488
+
489
+ @pytest.mark.planner
490
+ def test_wmbt_files_use_authorized_lenses():
491
+ """
492
+ SPEC-PLANNER-WMBT-VOCAB-004: WMBT files use authorized lens vocabulary.
493
+
494
+ Given: WMBT YAML files in plan/{wagon}/ directories
495
+ When: Validating lens field
496
+ Then: Lens must follow pattern {category}.{attribute} with authorized values
497
+
498
+ Authorized categories: functional, emotional, social
499
+ Authorized attributes defined per category in convention
500
+ """
501
+ wmbt_files = find_wmbt_files()
502
+
503
+ if not wmbt_files:
504
+ pytest.skip("No WMBT files found")
505
+
506
+ violations = []
507
+
508
+ for wagon_slug, wmbt_file in wmbt_files:
509
+ wmbt_data = load_wmbt_file(wmbt_file)
510
+
511
+ if not wmbt_data:
512
+ continue
513
+
514
+ error = validate_lens(wmbt_data, wmbt_file)
515
+ if error:
516
+ violations.append(f"{wmbt_file.relative_to(REPO_ROOT)}: {error}")
517
+
518
+ if violations:
519
+ pytest.fail(
520
+ f"\n\nFound {len(violations)} lens vocabulary violations:\n\n" +
521
+ "\n".join(violations[:20]) +
522
+ (f"\n\n... and {len(violations) - 20} more" if len(violations) > 20 else "")
523
+ )
524
+
525
+
526
+ @pytest.mark.planner
527
+ def test_wmbt_files_have_valid_object_of_control():
528
+ """
529
+ SPEC-PLANNER-WMBT-VOCAB-005: WMBT files have valid object_of_control format.
530
+
531
+ Given: WMBT YAML files in plan/{wagon}/ directories
532
+ When: Validating object_of_control field
533
+ Then: Must be kebab-case noun phrase (lowercase, hyphens, min 2 chars)
534
+ """
535
+ wmbt_files = find_wmbt_files()
536
+
537
+ if not wmbt_files:
538
+ pytest.skip("No WMBT files found")
539
+
540
+ violations = []
541
+
542
+ for wagon_slug, wmbt_file in wmbt_files:
543
+ wmbt_data = load_wmbt_file(wmbt_file)
544
+
545
+ if not wmbt_data:
546
+ continue
547
+
548
+ error = validate_object_of_control(wmbt_data, wmbt_file)
549
+ if error:
550
+ violations.append(f"{wmbt_file.relative_to(REPO_ROOT)}: {error}")
551
+
552
+ if violations:
553
+ pytest.fail(
554
+ f"\n\nFound {len(violations)} object_of_control format violations:\n\n" +
555
+ "\n".join(violations[:20]) +
556
+ (f"\n\n... and {len(violations) - 20} more" if len(violations) > 20 else "")
557
+ )
558
+
559
+
560
+ @pytest.mark.planner
561
+ def test_wmbt_statements_follow_construction_pattern():
562
+ """
563
+ SPEC-PLANNER-WMBT-VOCAB-006: WMBT statements follow construction pattern.
564
+
565
+ Given: WMBT YAML files in plan/{wagon}/ directories
566
+ When: Validating statement field
567
+ Then: Statement must follow pattern: {direction} {dimension} of {object_of_control} {context_clarifier}
568
+
569
+ Pattern ensures consistency and readability across all WMBT statements.
570
+ """
571
+ wmbt_files = find_wmbt_files()
572
+
573
+ if not wmbt_files:
574
+ pytest.skip("No WMBT files found")
575
+
576
+ violations = []
577
+
578
+ for wagon_slug, wmbt_file in wmbt_files:
579
+ wmbt_data = load_wmbt_file(wmbt_file)
580
+
581
+ if not wmbt_data:
582
+ continue
583
+
584
+ error = validate_statement_construction(wmbt_data, wmbt_file)
585
+ if error:
586
+ violations.append(f"{wmbt_file.relative_to(REPO_ROOT)}: {error}")
587
+
588
+ if violations:
589
+ pytest.fail(
590
+ f"\n\nFound {len(violations)} statement construction violations:\n\n" +
591
+ "\n".join(violations[:20]) +
592
+ (f"\n\n... and {len(violations) - 20} more" if len(violations) > 20 else "")
593
+ )
594
+
595
+
596
+ @pytest.mark.planner
597
+ def test_wmbt_urn_step_code_matches_step_field():
598
+ """
599
+ SPEC-PLANNER-WMBT-VOCAB-007: WMBT URN step code matches step field.
600
+
601
+ Given: WMBT YAML files with URN and step fields
602
+ When: Validating URN step code against step field
603
+ Then: Step code in URN must match step field
604
+
605
+ Example:
606
+ - URN: wmbt:resolve-dilemmas:E001 (step code: E)
607
+ - Step: execute (maps to: E)
608
+ - Result: Valid (E matches E)
609
+ """
610
+ wmbt_files = find_wmbt_files()
611
+
612
+ if not wmbt_files:
613
+ pytest.skip("No WMBT files found")
614
+
615
+ violations = []
616
+
617
+ for wagon_slug, wmbt_file in wmbt_files:
618
+ wmbt_data = load_wmbt_file(wmbt_file)
619
+
620
+ if not wmbt_data:
621
+ continue
622
+
623
+ error = validate_urn_step_code_consistency(wmbt_data, wmbt_file)
624
+ if error:
625
+ violations.append(f"{wmbt_file.relative_to(REPO_ROOT)}: {error}")
626
+
627
+ if violations:
628
+ pytest.fail(
629
+ f"\n\nFound {len(violations)} URN-step consistency violations:\n\n" +
630
+ "\n".join(violations[:20]) +
631
+ (f"\n\n... and {len(violations) - 20} more" if len(violations) > 20 else "")
632
+ )
@@ -0,0 +1 @@
1
+ """Tester audits, conventions and schemas."""