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.
- atdd/__init__.py +0 -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.1.0.dist-info/METADATA +191 -0
- atdd-0.1.0.dist-info/RECORD +183 -0
- atdd-0.1.0.dist-info/WHEEL +5 -0
- atdd-0.1.0.dist-info/entry_points.txt +2 -0
- atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
- atdd-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test code quality metrics meet minimum standards.
|
|
3
|
+
|
|
4
|
+
Validates:
|
|
5
|
+
- Maintainability index > 60
|
|
6
|
+
- Code has appropriate comments
|
|
7
|
+
- No code duplication
|
|
8
|
+
- Consistent naming conventions
|
|
9
|
+
|
|
10
|
+
Inspired by: .claude/utils/coder/quality_metrics.py
|
|
11
|
+
But: Self-contained, no utility dependencies
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List, Tuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Path constants
|
|
21
|
+
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
22
|
+
PYTHON_DIR = REPO_ROOT / "python"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Quality thresholds
|
|
26
|
+
MIN_MAINTAINABILITY_INDEX = 60
|
|
27
|
+
MIN_COMMENT_RATIO = 0.10 # 10% comments
|
|
28
|
+
MAX_DUPLICATE_LINES = 5
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def find_python_files() -> List[Path]:
|
|
32
|
+
"""Find all Python source files (excluding tests)."""
|
|
33
|
+
if not PYTHON_DIR.exists():
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
files = []
|
|
37
|
+
for py_file in PYTHON_DIR.rglob("*.py"):
|
|
38
|
+
if '/test/' in str(py_file) or py_file.name.startswith('test_'):
|
|
39
|
+
continue
|
|
40
|
+
if '__pycache__' in str(py_file):
|
|
41
|
+
continue
|
|
42
|
+
files.append(py_file)
|
|
43
|
+
|
|
44
|
+
return files
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def calculate_maintainability_index(file_path: Path) -> float:
|
|
48
|
+
"""
|
|
49
|
+
Calculate simplified maintainability index for a file.
|
|
50
|
+
|
|
51
|
+
Based on:
|
|
52
|
+
- Lines of code
|
|
53
|
+
- Cyclomatic complexity (simplified)
|
|
54
|
+
- Comment ratio
|
|
55
|
+
|
|
56
|
+
Returns value 0-100 (higher is better)
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
60
|
+
lines = f.readlines()
|
|
61
|
+
except Exception:
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
total_lines = len(lines)
|
|
65
|
+
code_lines = 0
|
|
66
|
+
comment_lines = 0
|
|
67
|
+
blank_lines = 0
|
|
68
|
+
|
|
69
|
+
for line in lines:
|
|
70
|
+
stripped = line.strip()
|
|
71
|
+
if not stripped:
|
|
72
|
+
blank_lines += 1
|
|
73
|
+
elif stripped.startswith('#'):
|
|
74
|
+
comment_lines += 1
|
|
75
|
+
else:
|
|
76
|
+
code_lines += 1
|
|
77
|
+
|
|
78
|
+
# Simple heuristic for maintainability
|
|
79
|
+
# Higher comment ratio = better
|
|
80
|
+
comment_ratio = comment_lines / total_lines if total_lines > 0 else 0
|
|
81
|
+
|
|
82
|
+
# Shorter files = better
|
|
83
|
+
size_factor = max(0, 100 - (code_lines / 10))
|
|
84
|
+
|
|
85
|
+
# Calculate index (simplified)
|
|
86
|
+
index = (size_factor * 0.4) + (comment_ratio * 100 * 0.6)
|
|
87
|
+
|
|
88
|
+
return min(100, max(0, index))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def calculate_comment_ratio(file_path: Path) -> float:
|
|
92
|
+
"""
|
|
93
|
+
Calculate ratio of comments and docstrings to code.
|
|
94
|
+
|
|
95
|
+
Counts both:
|
|
96
|
+
- Inline comments (lines starting with #)
|
|
97
|
+
- Docstrings (triple-quoted strings)
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Ratio (0.0 to 1.0)
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
104
|
+
lines = f.readlines()
|
|
105
|
+
except Exception:
|
|
106
|
+
return 0.0
|
|
107
|
+
|
|
108
|
+
code_lines = 0
|
|
109
|
+
comment_lines = 0
|
|
110
|
+
in_docstring = False
|
|
111
|
+
docstring_delim = None
|
|
112
|
+
|
|
113
|
+
for line in lines:
|
|
114
|
+
stripped = line.strip()
|
|
115
|
+
if not stripped:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# Check for docstring delimiters
|
|
119
|
+
if '"""' in stripped or "'''" in stripped:
|
|
120
|
+
# Determine delimiter type
|
|
121
|
+
delim = '"""' if '"""' in stripped else "'''"
|
|
122
|
+
|
|
123
|
+
if not in_docstring:
|
|
124
|
+
# Starting a docstring
|
|
125
|
+
in_docstring = True
|
|
126
|
+
docstring_delim = delim
|
|
127
|
+
comment_lines += 1
|
|
128
|
+
|
|
129
|
+
# Check if docstring closes on same line
|
|
130
|
+
if stripped.count(delim) >= 2:
|
|
131
|
+
in_docstring = False
|
|
132
|
+
docstring_delim = None
|
|
133
|
+
else:
|
|
134
|
+
# Ending a docstring
|
|
135
|
+
if delim == docstring_delim:
|
|
136
|
+
in_docstring = False
|
|
137
|
+
docstring_delim = None
|
|
138
|
+
comment_lines += 1
|
|
139
|
+
elif in_docstring:
|
|
140
|
+
# Inside a docstring
|
|
141
|
+
comment_lines += 1
|
|
142
|
+
elif stripped.startswith('#'):
|
|
143
|
+
# Inline comment
|
|
144
|
+
comment_lines += 1
|
|
145
|
+
else:
|
|
146
|
+
# Code line
|
|
147
|
+
code_lines += 1
|
|
148
|
+
|
|
149
|
+
total = code_lines + comment_lines
|
|
150
|
+
return comment_lines / total if total > 0 else 0.0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def find_duplicate_code_blocks(files: List[Path]) -> List[Tuple[Path, Path, List[str]]]:
|
|
154
|
+
"""
|
|
155
|
+
Find duplicate code blocks across files.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List of (file1, file2, duplicate_lines) tuples
|
|
159
|
+
"""
|
|
160
|
+
duplicates = []
|
|
161
|
+
|
|
162
|
+
# Simplified duplicate detection
|
|
163
|
+
# In reality, would use more sophisticated algorithm
|
|
164
|
+
|
|
165
|
+
file_contents = {}
|
|
166
|
+
for file in files:
|
|
167
|
+
try:
|
|
168
|
+
with open(file, 'r', encoding='utf-8') as f:
|
|
169
|
+
# Get normalized lines (stripped of whitespace)
|
|
170
|
+
lines = [line.strip() for line in f.readlines() if line.strip() and not line.strip().startswith('#')]
|
|
171
|
+
file_contents[file] = lines
|
|
172
|
+
except Exception:
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Compare files pairwise (simplified)
|
|
176
|
+
files_list = list(file_contents.keys())
|
|
177
|
+
for i, file1 in enumerate(files_list):
|
|
178
|
+
for file2 in files_list[i+1:]:
|
|
179
|
+
lines1 = file_contents[file1]
|
|
180
|
+
lines2 = file_contents[file2]
|
|
181
|
+
|
|
182
|
+
# Find consecutive duplicate lines
|
|
183
|
+
for start1 in range(len(lines1) - MAX_DUPLICATE_LINES):
|
|
184
|
+
block1 = lines1[start1:start1 + MAX_DUPLICATE_LINES]
|
|
185
|
+
|
|
186
|
+
for start2 in range(len(lines2) - MAX_DUPLICATE_LINES):
|
|
187
|
+
block2 = lines2[start2:start2 + MAX_DUPLICATE_LINES]
|
|
188
|
+
|
|
189
|
+
if block1 == block2:
|
|
190
|
+
# Skip standard import blocks (common across port/adapter files)
|
|
191
|
+
block_text = '\n'.join(block1)
|
|
192
|
+
if 'from abc import' in block_text and 'from dataclasses import' in block_text:
|
|
193
|
+
# Standard port/adapter imports - acceptable
|
|
194
|
+
continue
|
|
195
|
+
duplicates.append((file1, file2, block1))
|
|
196
|
+
break
|
|
197
|
+
|
|
198
|
+
return duplicates
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def check_naming_consistency(file_path: Path) -> List[str]:
|
|
202
|
+
"""
|
|
203
|
+
Check naming conventions consistency.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
List of naming violations
|
|
207
|
+
"""
|
|
208
|
+
violations = []
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
212
|
+
content = f.read()
|
|
213
|
+
except Exception:
|
|
214
|
+
return violations
|
|
215
|
+
|
|
216
|
+
# Check class names (should be PascalCase)
|
|
217
|
+
class_pattern = r'class\s+([a-z][a-zA-Z0-9_]*)\s*[:\(]'
|
|
218
|
+
lowercase_classes = re.findall(class_pattern, content)
|
|
219
|
+
for cls in lowercase_classes:
|
|
220
|
+
violations.append(f"Class '{cls}' should use PascalCase")
|
|
221
|
+
|
|
222
|
+
# Check constant names (should be UPPER_CASE)
|
|
223
|
+
# Pattern: variable assignment at module level that looks like it should be constant
|
|
224
|
+
const_pattern = r'^([a-z][a-z0-9_]*)\s*=\s*["\'\d\[]'
|
|
225
|
+
# pytest special variables that must be lowercase
|
|
226
|
+
pytest_special_vars = ['pytest_plugins']
|
|
227
|
+
|
|
228
|
+
for line in content.split('\n'):
|
|
229
|
+
if not line.startswith(' ') and not line.startswith('\t'): # Module level
|
|
230
|
+
match = re.match(const_pattern, line)
|
|
231
|
+
if match and match.group(1).isupper():
|
|
232
|
+
# Good - already uppercase
|
|
233
|
+
pass
|
|
234
|
+
elif match and match.group(1) in pytest_special_vars:
|
|
235
|
+
# pytest special variable - must be lowercase
|
|
236
|
+
pass
|
|
237
|
+
elif match and '_' in match.group(1):
|
|
238
|
+
# Might be a constant with wrong case
|
|
239
|
+
violations.append(f"Constant '{match.group(1)}' should use UPPER_CASE")
|
|
240
|
+
|
|
241
|
+
return violations
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@pytest.mark.coder
|
|
245
|
+
def test_maintainability_index_above_threshold():
|
|
246
|
+
"""
|
|
247
|
+
SPEC-CODER-QUALITY-0001: Code has acceptable maintainability index.
|
|
248
|
+
|
|
249
|
+
Maintainability index measures:
|
|
250
|
+
- Code complexity
|
|
251
|
+
- Code size
|
|
252
|
+
- Documentation level
|
|
253
|
+
|
|
254
|
+
Threshold: > 60 (scale 0-100)
|
|
255
|
+
|
|
256
|
+
Given: All Python files
|
|
257
|
+
When: Calculating maintainability index
|
|
258
|
+
Then: Index > 60 for all files
|
|
259
|
+
"""
|
|
260
|
+
python_files = find_python_files()
|
|
261
|
+
|
|
262
|
+
if not python_files:
|
|
263
|
+
pytest.skip("No Python files found")
|
|
264
|
+
|
|
265
|
+
violations = []
|
|
266
|
+
|
|
267
|
+
for py_file in python_files:
|
|
268
|
+
# Skip very small files
|
|
269
|
+
try:
|
|
270
|
+
with open(py_file, 'r', encoding='utf-8') as f:
|
|
271
|
+
lines = f.readlines()
|
|
272
|
+
if len(lines) < 10:
|
|
273
|
+
continue
|
|
274
|
+
except Exception:
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
index = calculate_maintainability_index(py_file)
|
|
278
|
+
|
|
279
|
+
if index < MIN_MAINTAINABILITY_INDEX:
|
|
280
|
+
rel_path = py_file.relative_to(REPO_ROOT)
|
|
281
|
+
violations.append(
|
|
282
|
+
f"{rel_path}\\n"
|
|
283
|
+
f" Maintainability Index: {index:.1f} (min: {MIN_MAINTAINABILITY_INDEX})\\n"
|
|
284
|
+
f" Suggestion: Add comments, reduce complexity, or split file"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if violations:
|
|
288
|
+
pytest.fail(
|
|
289
|
+
f"\\n\\nFound {len(violations)} maintainability violations:\\n\\n" +
|
|
290
|
+
"\\n\\n".join(violations[:10]) +
|
|
291
|
+
(f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@pytest.mark.coder
|
|
296
|
+
def test_adequate_code_comments():
|
|
297
|
+
"""
|
|
298
|
+
SPEC-CODER-QUALITY-0002: Code has adequate comments.
|
|
299
|
+
|
|
300
|
+
Well-commented code is easier to maintain.
|
|
301
|
+
|
|
302
|
+
Threshold: > 10% comment ratio
|
|
303
|
+
|
|
304
|
+
Given: All Python files
|
|
305
|
+
When: Calculating comment ratio
|
|
306
|
+
Then: At least 10% comments
|
|
307
|
+
"""
|
|
308
|
+
python_files = find_python_files()
|
|
309
|
+
|
|
310
|
+
if not python_files:
|
|
311
|
+
pytest.skip("No Python files found")
|
|
312
|
+
|
|
313
|
+
violations = []
|
|
314
|
+
|
|
315
|
+
for py_file in python_files:
|
|
316
|
+
# Skip very small files
|
|
317
|
+
try:
|
|
318
|
+
with open(py_file, 'r', encoding='utf-8') as f:
|
|
319
|
+
lines = f.readlines()
|
|
320
|
+
if len(lines) < 20:
|
|
321
|
+
continue
|
|
322
|
+
except Exception:
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
ratio = calculate_comment_ratio(py_file)
|
|
326
|
+
|
|
327
|
+
if ratio < MIN_COMMENT_RATIO:
|
|
328
|
+
rel_path = py_file.relative_to(REPO_ROOT)
|
|
329
|
+
violations.append(
|
|
330
|
+
f"{rel_path}\\n"
|
|
331
|
+
f" Comment ratio: {ratio*100:.1f}% (min: {MIN_COMMENT_RATIO*100:.0f}%)\\n"
|
|
332
|
+
f" Suggestion: Add docstrings and inline comments"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if violations:
|
|
336
|
+
pytest.fail(
|
|
337
|
+
f"\\n\\nFound {len(violations)} files with insufficient comments:\\n\\n" +
|
|
338
|
+
"\\n\\n".join(violations[:10]) +
|
|
339
|
+
(f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@pytest.mark.coder
|
|
344
|
+
def test_no_significant_code_duplication():
|
|
345
|
+
"""
|
|
346
|
+
SPEC-CODER-QUALITY-0003: No significant code duplication.
|
|
347
|
+
|
|
348
|
+
Duplicate code should be extracted into functions.
|
|
349
|
+
|
|
350
|
+
Threshold: < 5 consecutive duplicate lines
|
|
351
|
+
|
|
352
|
+
Given: All Python files
|
|
353
|
+
When: Checking for duplicate code blocks
|
|
354
|
+
Then: No significant duplication found
|
|
355
|
+
"""
|
|
356
|
+
python_files = find_python_files()
|
|
357
|
+
|
|
358
|
+
if not python_files:
|
|
359
|
+
pytest.skip("No Python files found")
|
|
360
|
+
|
|
361
|
+
# Limit to avoid long running time
|
|
362
|
+
sample_files = python_files[:50]
|
|
363
|
+
|
|
364
|
+
duplicates = find_duplicate_code_blocks(sample_files)
|
|
365
|
+
|
|
366
|
+
if duplicates:
|
|
367
|
+
violations = []
|
|
368
|
+
for file1, file2, block in duplicates[:10]:
|
|
369
|
+
violations.append(
|
|
370
|
+
f"{file1.relative_to(REPO_ROOT)} ↔ {file2.relative_to(REPO_ROOT)}\\n"
|
|
371
|
+
f" Duplicate block ({len(block)} lines):\\n" +
|
|
372
|
+
"\\n".join(f" {line[:60]}" for line in block[:3])
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
pytest.fail(
|
|
376
|
+
f"\\n\\nFound {len(duplicates)} code duplication instances:\\n\\n" +
|
|
377
|
+
"\\n\\n".join(violations) +
|
|
378
|
+
(f"\\n\\n... and {len(duplicates) - 10} more" if len(duplicates) > 10 else "") +
|
|
379
|
+
"\\n\\nConsider extracting duplicate code into shared functions."
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@pytest.mark.coder
|
|
384
|
+
def test_consistent_naming_conventions():
|
|
385
|
+
"""
|
|
386
|
+
SPEC-CODER-QUALITY-0004: Code follows consistent naming conventions.
|
|
387
|
+
|
|
388
|
+
Naming conventions:
|
|
389
|
+
- Classes: PascalCase
|
|
390
|
+
- Functions: snake_case
|
|
391
|
+
- Constants: UPPER_CASE
|
|
392
|
+
- Variables: snake_case
|
|
393
|
+
|
|
394
|
+
Given: All Python files
|
|
395
|
+
When: Checking naming patterns
|
|
396
|
+
Then: Consistent naming conventions
|
|
397
|
+
"""
|
|
398
|
+
python_files = find_python_files()
|
|
399
|
+
|
|
400
|
+
if not python_files:
|
|
401
|
+
pytest.skip("No Python files found")
|
|
402
|
+
|
|
403
|
+
all_violations = []
|
|
404
|
+
|
|
405
|
+
for py_file in python_files:
|
|
406
|
+
violations = check_naming_consistency(py_file)
|
|
407
|
+
|
|
408
|
+
if violations:
|
|
409
|
+
rel_path = py_file.relative_to(REPO_ROOT)
|
|
410
|
+
all_violations.append(
|
|
411
|
+
f"{rel_path}\\n" +
|
|
412
|
+
"\\n".join(f" - {v}" for v in violations[:5])
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
if all_violations:
|
|
416
|
+
pytest.fail(
|
|
417
|
+
f"\\n\\nFound {len(all_violations)} files with naming violations:\\n\\n" +
|
|
418
|
+
"\\n\\n".join(all_violations[:10]) +
|
|
419
|
+
(f"\\n\\n... and {len(all_violations) - 10} more" if len(all_violations) > 10 else "")
|
|
420
|
+
)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Station Master Pattern Validator
|
|
3
|
+
|
|
4
|
+
Validates that wagons follow the Station Master pattern for monolith composition:
|
|
5
|
+
1. composition.py accepts optional shared dependency parameters
|
|
6
|
+
2. Direct adapters exist for cross-wagon data access
|
|
7
|
+
3. game.py delegates to composition.py instead of duplicating wiring
|
|
8
|
+
|
|
9
|
+
Convention: atdd/coder/conventions/boundaries.convention.yaml::station_master_pattern
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import List, Dict, Any, Tuple
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_python_dir() -> Path:
|
|
19
|
+
"""Get the python directory path."""
|
|
20
|
+
return Path(__file__).parent.parent.parent.parent / "python"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_composition_accepts_shared_dependencies():
|
|
24
|
+
"""
|
|
25
|
+
Validate that wagon composition.py files accept optional shared dependencies.
|
|
26
|
+
|
|
27
|
+
Convention: boundaries.convention.yaml::station_master_pattern.composition_function_signature
|
|
28
|
+
|
|
29
|
+
Expected pattern:
|
|
30
|
+
def wire_api_dependencies(
|
|
31
|
+
state_repository=None,
|
|
32
|
+
player_timebanks=None,
|
|
33
|
+
match_repository=None,
|
|
34
|
+
event_bus=None
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
python_dir = get_python_dir()
|
|
38
|
+
|
|
39
|
+
# Find all composition.py files in wagon directories
|
|
40
|
+
composition_files = list(python_dir.glob("*/*/composition.py"))
|
|
41
|
+
|
|
42
|
+
# Track which compositions have wire_api_dependencies
|
|
43
|
+
wagons_with_wire_function: List[str] = []
|
|
44
|
+
wagons_missing_optional_params: List[Tuple[str, List[str]]] = []
|
|
45
|
+
|
|
46
|
+
for comp_file in composition_files:
|
|
47
|
+
wagon_name = comp_file.parent.parent.name
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
source = comp_file.read_text()
|
|
51
|
+
tree = ast.parse(source)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
continue # Skip files that can't be parsed
|
|
54
|
+
|
|
55
|
+
# Find wire_api_dependencies function
|
|
56
|
+
for node in ast.walk(tree):
|
|
57
|
+
if isinstance(node, ast.FunctionDef) and node.name == "wire_api_dependencies":
|
|
58
|
+
wagons_with_wire_function.append(wagon_name)
|
|
59
|
+
|
|
60
|
+
# Check if it has optional parameters (defaults)
|
|
61
|
+
# Parameters with defaults are in node.args.defaults
|
|
62
|
+
# kw_only args with defaults are in node.args.kw_defaults
|
|
63
|
+
|
|
64
|
+
# Get all argument names
|
|
65
|
+
arg_names = [arg.arg for arg in node.args.args]
|
|
66
|
+
|
|
67
|
+
# Get number of defaults (these apply to the LAST n arguments)
|
|
68
|
+
num_defaults = len(node.args.defaults)
|
|
69
|
+
num_args = len(arg_names)
|
|
70
|
+
|
|
71
|
+
# Arguments without defaults (required)
|
|
72
|
+
required_args = arg_names[:num_args - num_defaults] if num_defaults < num_args else []
|
|
73
|
+
|
|
74
|
+
# Recommended optional params for Station Master pattern
|
|
75
|
+
recommended_optional = ["state_repository", "player_timebanks", "match_repository", "event_bus"]
|
|
76
|
+
|
|
77
|
+
# Check if function has any optional parameters
|
|
78
|
+
optional_count = num_defaults + len([d for d in node.args.kw_defaults if d is not None])
|
|
79
|
+
|
|
80
|
+
if optional_count == 0 and num_args > 0:
|
|
81
|
+
# Function has only required args - doesn't follow pattern
|
|
82
|
+
wagons_missing_optional_params.append((wagon_name, required_args))
|
|
83
|
+
|
|
84
|
+
# Report results
|
|
85
|
+
print("\n" + "=" * 70)
|
|
86
|
+
print(" Station Master Pattern: Composition Dependencies")
|
|
87
|
+
print("=" * 70)
|
|
88
|
+
print(f"\nWagons with wire_api_dependencies(): {len(wagons_with_wire_function)}")
|
|
89
|
+
for wagon in wagons_with_wire_function:
|
|
90
|
+
print(f" ✓ {wagon}")
|
|
91
|
+
|
|
92
|
+
if wagons_missing_optional_params:
|
|
93
|
+
print(f"\n⚠️ Wagons missing optional shared dependency parameters:")
|
|
94
|
+
for wagon, required in wagons_missing_optional_params:
|
|
95
|
+
print(f" ❌ {wagon}: has only required params: {required}")
|
|
96
|
+
print("\n Recommendation: Add optional params like state_repository=None")
|
|
97
|
+
|
|
98
|
+
# This is a soft check - we want to encourage the pattern but not fail builds
|
|
99
|
+
# for wagons that don't need cross-wagon data
|
|
100
|
+
assert True, "Station Master pattern check completed (advisory)"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_direct_adapters_exist_for_cross_wagon_clients():
|
|
104
|
+
"""
|
|
105
|
+
Validate that Direct adapters exist alongside HTTP clients for cross-wagon communication.
|
|
106
|
+
|
|
107
|
+
Convention: backend.convention.yaml::clients.adapter_variants.direct_adapter
|
|
108
|
+
|
|
109
|
+
Expected: If http_*_client.py exists, direct_*_client.py should also exist.
|
|
110
|
+
"""
|
|
111
|
+
python_dir = get_python_dir()
|
|
112
|
+
|
|
113
|
+
# Find all client directories
|
|
114
|
+
client_dirs = list(python_dir.glob("*/*/src/integration/clients"))
|
|
115
|
+
|
|
116
|
+
http_without_direct: List[Tuple[str, str]] = []
|
|
117
|
+
direct_adapters_found: List[str] = []
|
|
118
|
+
|
|
119
|
+
for client_dir in client_dirs:
|
|
120
|
+
if not client_dir.is_dir():
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
wagon_name = client_dir.parent.parent.parent.parent.name
|
|
124
|
+
|
|
125
|
+
# Find HTTP clients
|
|
126
|
+
http_clients = list(client_dir.glob("http_*_client.py"))
|
|
127
|
+
|
|
128
|
+
for http_client in http_clients:
|
|
129
|
+
# Extract the service name (e.g., "commit_state" from "http_commit_state_client.py")
|
|
130
|
+
http_name = http_client.stem # http_commit_state_client
|
|
131
|
+
service_name = http_name.replace("http_", "").replace("_client", "")
|
|
132
|
+
|
|
133
|
+
# Check for corresponding direct adapter
|
|
134
|
+
direct_name = f"direct_{service_name}_client.py"
|
|
135
|
+
direct_path = client_dir / direct_name
|
|
136
|
+
|
|
137
|
+
if direct_path.exists():
|
|
138
|
+
direct_adapters_found.append(f"{wagon_name}/{direct_name}")
|
|
139
|
+
else:
|
|
140
|
+
http_without_direct.append((wagon_name, http_client.name))
|
|
141
|
+
|
|
142
|
+
# Report results
|
|
143
|
+
print("\n" + "=" * 70)
|
|
144
|
+
print(" Station Master Pattern: Direct Adapters")
|
|
145
|
+
print("=" * 70)
|
|
146
|
+
print(f"\nDirect adapters found: {len(direct_adapters_found)}")
|
|
147
|
+
for adapter in direct_adapters_found:
|
|
148
|
+
print(f" ✓ {adapter}")
|
|
149
|
+
|
|
150
|
+
if http_without_direct:
|
|
151
|
+
print(f"\n⚠️ HTTP clients without corresponding Direct adapters:")
|
|
152
|
+
for wagon, http_file in http_without_direct:
|
|
153
|
+
print(f" ⚠️ {wagon}/{http_file} → missing direct_*_client.py")
|
|
154
|
+
print("\n Note: Direct adapters enable monolith mode without HTTP self-calls")
|
|
155
|
+
|
|
156
|
+
# Advisory check - not all HTTP clients need Direct adapters
|
|
157
|
+
assert True, "Direct adapter check completed (advisory)"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_game_py_delegates_to_composition():
|
|
161
|
+
"""
|
|
162
|
+
Validate that game.py delegates wiring to wagon composition.py files
|
|
163
|
+
instead of duplicating wiring logic.
|
|
164
|
+
|
|
165
|
+
Convention: boundaries.convention.yaml::station_master_pattern.station_master_responsibilities
|
|
166
|
+
|
|
167
|
+
Forbidden patterns in game.py:
|
|
168
|
+
- Creating use cases that composition.py should own
|
|
169
|
+
- Directly instantiating wagon clients without delegation
|
|
170
|
+
|
|
171
|
+
Expected patterns:
|
|
172
|
+
- from wagon.composition import wire_api_dependencies
|
|
173
|
+
- wire_api_dependencies(state_repository=..., ...)
|
|
174
|
+
"""
|
|
175
|
+
python_dir = get_python_dir()
|
|
176
|
+
game_py = python_dir / "game.py"
|
|
177
|
+
|
|
178
|
+
if not game_py.exists():
|
|
179
|
+
print("game.py not found - skipping Station Master delegation check")
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
source = game_py.read_text()
|
|
183
|
+
|
|
184
|
+
# Check for composition imports
|
|
185
|
+
imports_composition = "from play_match.orchestrate_match.composition import wire_api_dependencies" in source
|
|
186
|
+
|
|
187
|
+
# Check for delegation calls
|
|
188
|
+
calls_wire_api = "wire_api_dependencies(" in source
|
|
189
|
+
|
|
190
|
+
# Check for forbidden patterns (duplicated wiring)
|
|
191
|
+
# These are patterns that should be in composition.py, not game.py
|
|
192
|
+
forbidden_patterns = [
|
|
193
|
+
("PlayMatchUseCase(", "PlayMatchUseCase should be created in composition.py"),
|
|
194
|
+
("CommitStateClient(mode=", "CommitStateClient mode should be set in composition.py"),
|
|
195
|
+
("set_play_match_use_case(PlayMatchUseCase", "Use case creation should be in composition.py"),
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
violations: List[Tuple[str, str]] = []
|
|
199
|
+
for pattern, message in forbidden_patterns:
|
|
200
|
+
if pattern in source:
|
|
201
|
+
violations.append((pattern, message))
|
|
202
|
+
|
|
203
|
+
# Report results
|
|
204
|
+
print("\n" + "=" * 70)
|
|
205
|
+
print(" Station Master Pattern: game.py Delegation")
|
|
206
|
+
print("=" * 70)
|
|
207
|
+
|
|
208
|
+
print(f"\nDelegation to composition.py:")
|
|
209
|
+
print(f" {'✓' if imports_composition else '❌'} Imports wire_api_dependencies from composition")
|
|
210
|
+
print(f" {'✓' if calls_wire_api else '❌'} Calls wire_api_dependencies()")
|
|
211
|
+
|
|
212
|
+
if violations:
|
|
213
|
+
print(f"\n❌ Violations found in game.py:")
|
|
214
|
+
for pattern, message in violations:
|
|
215
|
+
print(f" ❌ {message}")
|
|
216
|
+
print(f" Found: {pattern}")
|
|
217
|
+
|
|
218
|
+
# This is a real validation
|
|
219
|
+
assert imports_composition or not calls_wire_api, \
|
|
220
|
+
"game.py should import wire_api_dependencies from composition.py"
|
|
221
|
+
|
|
222
|
+
assert len(violations) == 0, \
|
|
223
|
+
f"game.py has {len(violations)} Station Master pattern violations"
|
|
224
|
+
|
|
225
|
+
print("\n✓ game.py follows Station Master pattern")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def main():
|
|
229
|
+
"""Run all Station Master pattern validators."""
|
|
230
|
+
print("\n" + "=" * 70)
|
|
231
|
+
print(" STATION MASTER PATTERN VALIDATION")
|
|
232
|
+
print("=" * 70)
|
|
233
|
+
|
|
234
|
+
test_composition_accepts_shared_dependencies()
|
|
235
|
+
test_direct_adapters_exist_for_cross_wagon_clients()
|
|
236
|
+
test_game_py_delegates_to_composition()
|
|
237
|
+
|
|
238
|
+
print("\n" + "=" * 70)
|
|
239
|
+
print(" ✓ All Station Master pattern checks passed")
|
|
240
|
+
print("=" * 70 + "\n")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
main()
|