atdd 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atdd/__init__.py +6 -0
- atdd/__main__.py +4 -0
- atdd/cli.py +404 -0
- atdd/coach/__init__.py +0 -0
- atdd/coach/commands/__init__.py +0 -0
- atdd/coach/commands/add_persistence_metadata.py +215 -0
- atdd/coach/commands/analyze_migrations.py +188 -0
- atdd/coach/commands/consumers.py +720 -0
- atdd/coach/commands/infer_governance_status.py +149 -0
- atdd/coach/commands/initializer.py +177 -0
- atdd/coach/commands/interface.py +1078 -0
- atdd/coach/commands/inventory.py +565 -0
- atdd/coach/commands/migration.py +240 -0
- atdd/coach/commands/registry.py +1560 -0
- atdd/coach/commands/session.py +430 -0
- atdd/coach/commands/sync.py +405 -0
- atdd/coach/commands/test_interface.py +399 -0
- atdd/coach/commands/test_runner.py +141 -0
- atdd/coach/commands/tests/__init__.py +1 -0
- atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
- atdd/coach/commands/traceability.py +4264 -0
- atdd/coach/conventions/session.convention.yaml +754 -0
- atdd/coach/overlays/__init__.py +2 -0
- atdd/coach/overlays/claude.md +2 -0
- atdd/coach/schemas/config.schema.json +34 -0
- atdd/coach/schemas/manifest.schema.json +101 -0
- atdd/coach/templates/ATDD.md +282 -0
- atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
- atdd/coach/utils/__init__.py +0 -0
- atdd/coach/utils/graph/__init__.py +0 -0
- atdd/coach/utils/graph/urn.py +875 -0
- atdd/coach/validators/__init__.py +0 -0
- atdd/coach/validators/shared_fixtures.py +365 -0
- atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
- atdd/coach/validators/test_registry.py +575 -0
- atdd/coach/validators/test_session_validation.py +1183 -0
- atdd/coach/validators/test_traceability.py +448 -0
- atdd/coach/validators/test_update_feature_paths.py +108 -0
- atdd/coach/validators/test_validate_contract_consumers.py +297 -0
- atdd/coder/__init__.py +1 -0
- atdd/coder/conventions/adapter.recipe.yaml +88 -0
- atdd/coder/conventions/backend.convention.yaml +460 -0
- atdd/coder/conventions/boundaries.convention.yaml +666 -0
- atdd/coder/conventions/commons.convention.yaml +460 -0
- atdd/coder/conventions/complexity.recipe.yaml +109 -0
- atdd/coder/conventions/component-naming.convention.yaml +178 -0
- atdd/coder/conventions/design.convention.yaml +327 -0
- atdd/coder/conventions/design.recipe.yaml +273 -0
- atdd/coder/conventions/dto.convention.yaml +660 -0
- atdd/coder/conventions/frontend.convention.yaml +542 -0
- atdd/coder/conventions/green.convention.yaml +1012 -0
- atdd/coder/conventions/presentation.convention.yaml +587 -0
- atdd/coder/conventions/refactor.convention.yaml +535 -0
- atdd/coder/conventions/technology.convention.yaml +206 -0
- atdd/coder/conventions/tests/__init__.py +0 -0
- atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
- atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
- atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
- atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
- atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
- atdd/coder/conventions/thinness.recipe.yaml +82 -0
- atdd/coder/conventions/train.convention.yaml +325 -0
- atdd/coder/conventions/verification.protocol.yaml +53 -0
- atdd/coder/schemas/design_system.schema.json +361 -0
- atdd/coder/validators/__init__.py +0 -0
- atdd/coder/validators/test_commons_structure.py +485 -0
- atdd/coder/validators/test_complexity.py +416 -0
- atdd/coder/validators/test_cross_language_consistency.py +431 -0
- atdd/coder/validators/test_design_system_compliance.py +413 -0
- atdd/coder/validators/test_dto_testing_patterns.py +268 -0
- atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
- atdd/coder/validators/test_green_layer_dependencies.py +148 -0
- atdd/coder/validators/test_green_python_layer_structure.py +103 -0
- atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
- atdd/coder/validators/test_import_boundaries.py +396 -0
- atdd/coder/validators/test_init_file_urns.py +593 -0
- atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
- atdd/coder/validators/test_presentation_convention.py +260 -0
- atdd/coder/validators/test_python_architecture.py +674 -0
- atdd/coder/validators/test_quality_metrics.py +420 -0
- atdd/coder/validators/test_station_master_pattern.py +244 -0
- atdd/coder/validators/test_train_infrastructure.py +454 -0
- atdd/coder/validators/test_train_urns.py +293 -0
- atdd/coder/validators/test_typescript_architecture.py +616 -0
- atdd/coder/validators/test_usecase_structure.py +421 -0
- atdd/coder/validators/test_wagon_boundaries.py +586 -0
- atdd/conftest.py +126 -0
- atdd/planner/__init__.py +1 -0
- atdd/planner/conventions/acceptance.convention.yaml +538 -0
- atdd/planner/conventions/appendix.convention.yaml +187 -0
- atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
- atdd/planner/conventions/component.convention.yaml +670 -0
- atdd/planner/conventions/criteria.convention.yaml +141 -0
- atdd/planner/conventions/feature.convention.yaml +371 -0
- atdd/planner/conventions/interface.convention.yaml +382 -0
- atdd/planner/conventions/steps.convention.yaml +141 -0
- atdd/planner/conventions/train.convention.yaml +552 -0
- atdd/planner/conventions/wagon.convention.yaml +275 -0
- atdd/planner/conventions/wmbt.convention.yaml +258 -0
- atdd/planner/schemas/acceptance.schema.json +336 -0
- atdd/planner/schemas/appendix.schema.json +78 -0
- atdd/planner/schemas/component.schema.json +114 -0
- atdd/planner/schemas/feature.schema.json +197 -0
- atdd/planner/schemas/train.schema.json +192 -0
- atdd/planner/schemas/wagon.schema.json +281 -0
- atdd/planner/schemas/wmbt.schema.json +59 -0
- atdd/planner/validators/__init__.py +0 -0
- atdd/planner/validators/conftest.py +5 -0
- atdd/planner/validators/test_draft_wagon_registry.py +374 -0
- atdd/planner/validators/test_plan_cross_refs.py +240 -0
- atdd/planner/validators/test_plan_uniqueness.py +224 -0
- atdd/planner/validators/test_plan_urn_resolution.py +268 -0
- atdd/planner/validators/test_plan_wagons.py +174 -0
- atdd/planner/validators/test_train_validation.py +514 -0
- atdd/planner/validators/test_wagon_urn_chain.py +648 -0
- atdd/planner/validators/test_wmbt_consistency.py +327 -0
- atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
- atdd/tester/__init__.py +1 -0
- atdd/tester/conventions/artifact.convention.yaml +257 -0
- atdd/tester/conventions/contract.convention.yaml +1009 -0
- atdd/tester/conventions/filename.convention.yaml +555 -0
- atdd/tester/conventions/migration.convention.yaml +509 -0
- atdd/tester/conventions/red.convention.yaml +797 -0
- atdd/tester/conventions/routing.convention.yaml +51 -0
- atdd/tester/conventions/telemetry.convention.yaml +458 -0
- atdd/tester/schemas/a11y.tmpl.json +17 -0
- atdd/tester/schemas/artifact.schema.json +189 -0
- atdd/tester/schemas/contract.schema.json +591 -0
- atdd/tester/schemas/contract.tmpl.json +95 -0
- atdd/tester/schemas/db.tmpl.json +20 -0
- atdd/tester/schemas/e2e.tmpl.json +17 -0
- atdd/tester/schemas/edge_function.tmpl.json +17 -0
- atdd/tester/schemas/event.tmpl.json +17 -0
- atdd/tester/schemas/http.tmpl.json +19 -0
- atdd/tester/schemas/job.tmpl.json +18 -0
- atdd/tester/schemas/load.tmpl.json +21 -0
- atdd/tester/schemas/metric.tmpl.json +19 -0
- atdd/tester/schemas/pack.schema.json +139 -0
- atdd/tester/schemas/realtime.tmpl.json +20 -0
- atdd/tester/schemas/rls.tmpl.json +18 -0
- atdd/tester/schemas/script.tmpl.json +16 -0
- atdd/tester/schemas/sec.tmpl.json +18 -0
- atdd/tester/schemas/storage.tmpl.json +18 -0
- atdd/tester/schemas/telemetry.schema.json +128 -0
- atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
- atdd/tester/schemas/test_filename.schema.json +194 -0
- atdd/tester/schemas/test_intent.schema.json +179 -0
- atdd/tester/schemas/unit.tmpl.json +18 -0
- atdd/tester/schemas/visual.tmpl.json +18 -0
- atdd/tester/schemas/ws.tmpl.json +17 -0
- atdd/tester/utils/__init__.py +0 -0
- atdd/tester/utils/filename.py +300 -0
- atdd/tester/validators/__init__.py +0 -0
- atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
- atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
- atdd/tester/validators/conftest.py +5 -0
- atdd/tester/validators/coverage_gap_report.py +321 -0
- atdd/tester/validators/fix_dual_ac_references.py +179 -0
- atdd/tester/validators/remove_duplicate_lines.py +93 -0
- atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
- atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
- atdd/tester/validators/test_artifact_naming_category.py +307 -0
- atdd/tester/validators/test_contract_schema_compliance.py +706 -0
- atdd/tester/validators/test_contracts_structure.py +200 -0
- atdd/tester/validators/test_coverage_adequacy.py +797 -0
- atdd/tester/validators/test_dual_ac_reference.py +225 -0
- atdd/tester/validators/test_fixture_validity.py +372 -0
- atdd/tester/validators/test_isolation.py +487 -0
- atdd/tester/validators/test_migration_coverage.py +204 -0
- atdd/tester/validators/test_migration_criteria.py +276 -0
- atdd/tester/validators/test_migration_generation.py +116 -0
- atdd/tester/validators/test_python_test_naming.py +410 -0
- atdd/tester/validators/test_red_layer_validation.py +95 -0
- atdd/tester/validators/test_red_python_layer_structure.py +87 -0
- atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
- atdd/tester/validators/test_telemetry_structure.py +634 -0
- atdd/tester/validators/test_typescript_test_naming.py +301 -0
- atdd/tester/validators/test_typescript_test_structure.py +84 -0
- atdd-0.2.1.dist-info/METADATA +221 -0
- atdd-0.2.1.dist-info/RECORD +184 -0
- atdd-0.2.1.dist-info/WHEEL +5 -0
- atdd-0.2.1.dist-info/entry_points.txt +2 -0
- atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
- atdd-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|
+
)
|
atdd/tester/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tester audits, conventions and schemas."""
|