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,586 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test wagon boundary isolation via qualified imports.
|
|
3
|
+
|
|
4
|
+
Validates conventions from:
|
|
5
|
+
- atdd/coder/conventions/boundaries.convention.yaml
|
|
6
|
+
|
|
7
|
+
Enforces:
|
|
8
|
+
- No bare layer imports (from domain.X import Y)
|
|
9
|
+
- No sys.path manipulation in test files
|
|
10
|
+
- No cross-wagon imports (wagon A importing wagon B)
|
|
11
|
+
- Qualified imports pattern (from {wagon}.{feature}.src.{layer}.{module} import Class)
|
|
12
|
+
- Package hierarchy exists (__init__.py files)
|
|
13
|
+
- pytest pythonpath configured
|
|
14
|
+
|
|
15
|
+
Rationale:
|
|
16
|
+
Multiple wagons use identical layer names (domain, application, integration).
|
|
17
|
+
Without qualified imports, Python cannot distinguish between:
|
|
18
|
+
- commit_state/sign_commit/src/domain/signature_algorithm.py
|
|
19
|
+
- juggle_domains/score_domains/src/domain/choice.py
|
|
20
|
+
|
|
21
|
+
Both would resolve to "domain.X" causing module shadowing when tests run together.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
import re
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import List, Tuple, Set
|
|
28
|
+
import ast
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Path constants
|
|
32
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
33
|
+
PYTHON_DIR = REPO_ROOT / "python"
|
|
34
|
+
PYPROJECT_TOML = PYTHON_DIR / "pyproject.toml"
|
|
35
|
+
BOUNDARIES_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "boundaries.convention.yaml"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_test_files() -> List[Path]:
|
|
39
|
+
"""Find all test files in wagons."""
|
|
40
|
+
if not PYTHON_DIR.exists():
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
test_files = []
|
|
44
|
+
for py_file in PYTHON_DIR.rglob("test_*.py"):
|
|
45
|
+
# Skip __pycache__
|
|
46
|
+
if '__pycache__' in str(py_file):
|
|
47
|
+
continue
|
|
48
|
+
test_files.append(py_file)
|
|
49
|
+
|
|
50
|
+
return test_files
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def find_implementation_files() -> List[Path]:
|
|
54
|
+
"""
|
|
55
|
+
Find all implementation files in wagons (excluding tests and orchestration layers).
|
|
56
|
+
|
|
57
|
+
Excluded from wagon boundary checks:
|
|
58
|
+
- Test files (testing infrastructure)
|
|
59
|
+
- wagon.py, composition.py (wagon-level orchestration)
|
|
60
|
+
- shared/ directory (theme/train-level orchestration)
|
|
61
|
+
- contracts/ directory (neutral DTO boundary layer)
|
|
62
|
+
- scripts/ directory (utility scripts and tools)
|
|
63
|
+
"""
|
|
64
|
+
if not PYTHON_DIR.exists():
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
impl_files = []
|
|
68
|
+
for py_file in PYTHON_DIR.rglob("*.py"):
|
|
69
|
+
# Skip test files
|
|
70
|
+
if '/test/' in str(py_file) or py_file.name.startswith('test_'):
|
|
71
|
+
continue
|
|
72
|
+
# Skip __pycache__
|
|
73
|
+
if '__pycache__' in str(py_file):
|
|
74
|
+
continue
|
|
75
|
+
# Skip wagon.py, composition.py, and game.py (wagon/app-level orchestration)
|
|
76
|
+
if py_file.name in ['wagon.py', 'composition.py', 'game.py']:
|
|
77
|
+
continue
|
|
78
|
+
# Skip shared/ directory (theme/train-level orchestration - can import across wagons)
|
|
79
|
+
if '/shared/' in str(py_file):
|
|
80
|
+
continue
|
|
81
|
+
# Skip contracts/ directory (neutral DTO layer)
|
|
82
|
+
if '/contracts/' in str(py_file):
|
|
83
|
+
continue
|
|
84
|
+
# Skip scripts/ directory (utility scripts - can import across wagons for tooling)
|
|
85
|
+
if '/scripts/' in str(py_file):
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
impl_files.append(py_file)
|
|
89
|
+
|
|
90
|
+
return impl_files
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def extract_imports_ast(file_path: Path) -> List[Tuple[str, int]]:
|
|
94
|
+
"""
|
|
95
|
+
Extract imports using AST parsing.
|
|
96
|
+
|
|
97
|
+
Excludes imports that are:
|
|
98
|
+
- Inside `if TYPE_CHECKING:` blocks (type-only imports, never executed)
|
|
99
|
+
- Inside function/method definitions (lazy imports for architecture compliance)
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
List of (import_path, line_number) tuples
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
106
|
+
content = f.read()
|
|
107
|
+
except Exception:
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
imports = []
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
tree = ast.parse(content, filename=str(file_path))
|
|
114
|
+
|
|
115
|
+
def is_inside_type_checking(node, tree):
|
|
116
|
+
"""Check if node is inside an 'if TYPE_CHECKING:' block."""
|
|
117
|
+
for parent in ast.walk(tree):
|
|
118
|
+
if isinstance(parent, ast.If):
|
|
119
|
+
# Check if condition is TYPE_CHECKING
|
|
120
|
+
if isinstance(parent.test, ast.Name) and parent.test.id == 'TYPE_CHECKING':
|
|
121
|
+
# Check if the import node is in the body of this if
|
|
122
|
+
for child in ast.walk(parent):
|
|
123
|
+
if child is node:
|
|
124
|
+
return True
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
def is_inside_function(node, tree):
|
|
128
|
+
"""Check if node is inside a function/method definition."""
|
|
129
|
+
for parent in ast.walk(tree):
|
|
130
|
+
if isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
131
|
+
for child in ast.walk(parent):
|
|
132
|
+
if child is node:
|
|
133
|
+
return True
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
for node in ast.walk(tree):
|
|
137
|
+
if isinstance(node, ast.ImportFrom):
|
|
138
|
+
if node.module and node.level == 0: # Only absolute imports (not relative imports with ...)
|
|
139
|
+
# Skip TYPE_CHECKING and function-level imports
|
|
140
|
+
if not is_inside_type_checking(node, tree) and not is_inside_function(node, tree):
|
|
141
|
+
imports.append((node.module, node.lineno))
|
|
142
|
+
elif isinstance(node, ast.Import):
|
|
143
|
+
# Skip TYPE_CHECKING and function-level imports
|
|
144
|
+
if not is_inside_type_checking(node, tree) and not is_inside_function(node, tree):
|
|
145
|
+
for alias in node.names:
|
|
146
|
+
imports.append((alias.name, node.lineno))
|
|
147
|
+
except SyntaxError:
|
|
148
|
+
# Fall back to regex if AST parsing fails
|
|
149
|
+
return extract_imports_regex(file_path)
|
|
150
|
+
|
|
151
|
+
return imports
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def extract_imports_regex(file_path: Path) -> List[Tuple[str, int]]:
|
|
155
|
+
"""Extract imports using regex (fallback)."""
|
|
156
|
+
try:
|
|
157
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
158
|
+
lines = f.readlines()
|
|
159
|
+
except Exception:
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
imports = []
|
|
163
|
+
|
|
164
|
+
for line_no, line in enumerate(lines, start=1):
|
|
165
|
+
# from X import Y
|
|
166
|
+
match = re.match(r'from\s+([^\s]+)\s+import', line)
|
|
167
|
+
if match:
|
|
168
|
+
imports.append((match.group(1), line_no))
|
|
169
|
+
|
|
170
|
+
# import X
|
|
171
|
+
match = re.match(r'^\s*import\s+([^\s;#]+)', line)
|
|
172
|
+
if match:
|
|
173
|
+
for imp in match.group(1).split(','):
|
|
174
|
+
imports.append((imp.strip(), line_no))
|
|
175
|
+
|
|
176
|
+
return imports
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def check_for_syspath_manipulation(file_path: Path) -> List[Tuple[str, int]]:
|
|
180
|
+
"""
|
|
181
|
+
Check if file manipulates sys.path.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
List of (line_content, line_number) tuples where sys.path is manipulated
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
188
|
+
lines = f.readlines()
|
|
189
|
+
except Exception:
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
violations = []
|
|
193
|
+
|
|
194
|
+
for line_no, line in enumerate(lines, start=1):
|
|
195
|
+
if 'sys.path.insert' in line or 'sys.path.append' in line:
|
|
196
|
+
violations.append((line.strip(), line_no))
|
|
197
|
+
|
|
198
|
+
return violations
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_wagon_from_path(file_path: Path) -> str:
|
|
202
|
+
"""Extract wagon name from file path."""
|
|
203
|
+
try:
|
|
204
|
+
rel_path = file_path.relative_to(PYTHON_DIR)
|
|
205
|
+
parts = rel_path.parts
|
|
206
|
+
if len(parts) > 0:
|
|
207
|
+
return parts[0]
|
|
208
|
+
except ValueError:
|
|
209
|
+
pass
|
|
210
|
+
return ""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def is_bare_layer_import(import_path: str) -> bool:
|
|
214
|
+
"""
|
|
215
|
+
Check if import is a bare layer import.
|
|
216
|
+
|
|
217
|
+
Bare imports like:
|
|
218
|
+
- from domain.X import Y
|
|
219
|
+
- from application.X import Y
|
|
220
|
+
- from integration.X import Y
|
|
221
|
+
- from presentation.X import Y
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
True if bare layer import
|
|
225
|
+
"""
|
|
226
|
+
# Check if starts with layer name (and not a qualified path)
|
|
227
|
+
if import_path.startswith(('domain.', 'application.', 'integration.', 'presentation.')):
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
# Check exact match (import domain, import application, etc.)
|
|
231
|
+
if import_path in ['domain', 'application', 'integration', 'presentation']:
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def is_cross_wagon_import(file_path: Path, import_path: str) -> Tuple[bool, str, str]:
|
|
238
|
+
"""
|
|
239
|
+
Check if import crosses wagon boundaries.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
(is_cross_wagon, source_wagon, target_wagon)
|
|
243
|
+
"""
|
|
244
|
+
source_wagon = get_wagon_from_path(file_path)
|
|
245
|
+
|
|
246
|
+
# Check if import is from a different wagon
|
|
247
|
+
# Pattern: {wagon}.{feature}.src.{layer}.{module}
|
|
248
|
+
match = re.match(r'([^.]+)\.', import_path)
|
|
249
|
+
if match:
|
|
250
|
+
target_wagon = match.group(1)
|
|
251
|
+
|
|
252
|
+
# Check if it's a different wagon (not shared utilities, commons, or contracts)
|
|
253
|
+
# generate_identifiers is a utility wagon providing cross-cutting concerns
|
|
254
|
+
if target_wagon != source_wagon and target_wagon not in ['shared', 'commons', 'generate_identifiers', '__init__', 'contracts']:
|
|
255
|
+
# Verify it's an actual wagon directory
|
|
256
|
+
wagon_dir = PYTHON_DIR / target_wagon
|
|
257
|
+
if wagon_dir.exists() and wagon_dir.is_dir():
|
|
258
|
+
return (True, source_wagon, target_wagon)
|
|
259
|
+
|
|
260
|
+
return (False, source_wagon, "")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def check_package_hierarchy() -> List[str]:
|
|
264
|
+
"""
|
|
265
|
+
Check if required __init__.py files exist for package hierarchy.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List of missing __init__.py paths
|
|
269
|
+
"""
|
|
270
|
+
missing = []
|
|
271
|
+
|
|
272
|
+
# Check python/__init__.py
|
|
273
|
+
if not (PYTHON_DIR / "__init__.py").exists():
|
|
274
|
+
missing.append("python/__init__.py")
|
|
275
|
+
|
|
276
|
+
# Check each wagon
|
|
277
|
+
for wagon_dir in PYTHON_DIR.iterdir():
|
|
278
|
+
if not wagon_dir.is_dir() or wagon_dir.name.startswith('.') or wagon_dir.name == '__pycache__':
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
# Check wagon/__init__.py
|
|
282
|
+
if not (wagon_dir / "__init__.py").exists():
|
|
283
|
+
missing.append(f"python/{wagon_dir.name}/__init__.py")
|
|
284
|
+
|
|
285
|
+
# Check each feature in wagon
|
|
286
|
+
for feature_dir in wagon_dir.iterdir():
|
|
287
|
+
if not feature_dir.is_dir() or feature_dir.name.startswith('.') or feature_dir.name == '__pycache__':
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
# Skip non-feature directories
|
|
291
|
+
if feature_dir.name in ['__pycache__', 'test', 'tests']:
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
# Check if has src/ (indicates it's a feature)
|
|
295
|
+
src_dir = feature_dir / "src"
|
|
296
|
+
if src_dir.exists():
|
|
297
|
+
# Check feature/__init__.py
|
|
298
|
+
if not (feature_dir / "__init__.py").exists():
|
|
299
|
+
missing.append(f"python/{wagon_dir.name}/{feature_dir.name}/__init__.py")
|
|
300
|
+
|
|
301
|
+
return missing
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def check_pytest_pythonpath() -> Tuple[bool, str]:
|
|
305
|
+
"""
|
|
306
|
+
Check if pytest pythonpath is configured in pyproject.toml.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
(is_configured, message)
|
|
310
|
+
"""
|
|
311
|
+
if not PYPROJECT_TOML.exists():
|
|
312
|
+
return (False, "python/pyproject.toml not found")
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
with open(PYPROJECT_TOML, 'r') as f:
|
|
316
|
+
content = f.read()
|
|
317
|
+
|
|
318
|
+
# Check for [tool.pytest.ini_options] section with pythonpath
|
|
319
|
+
if 'tool.pytest.ini_options' in content and 'pythonpath' in content:
|
|
320
|
+
return (True, "pythonpath configured")
|
|
321
|
+
else:
|
|
322
|
+
return (False, "pythonpath not configured in [tool.pytest.ini_options]")
|
|
323
|
+
except Exception as e:
|
|
324
|
+
return (False, f"Error reading pyproject.toml: {e}")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@pytest.mark.coder
|
|
328
|
+
def test_no_bare_layer_imports_in_tests():
|
|
329
|
+
"""
|
|
330
|
+
SPEC-BOUNDARIES-0001: Test files must use qualified imports.
|
|
331
|
+
|
|
332
|
+
Convention: boundaries.convention.yaml::namespacing.forbidden_patterns.bare_layer_imports
|
|
333
|
+
|
|
334
|
+
Forbidden:
|
|
335
|
+
- from domain.signature_algorithm import SignatureAlgorithm
|
|
336
|
+
- from application.use_cases.X import Y
|
|
337
|
+
|
|
338
|
+
Required:
|
|
339
|
+
- from commit_state.sign_commit.src.domain.signature_algorithm import SignatureAlgorithm
|
|
340
|
+
- from juggle_domains.score_domains.src.application.use_cases.X import Y
|
|
341
|
+
|
|
342
|
+
Given: All test files in python/
|
|
343
|
+
When: Checking imports
|
|
344
|
+
Then: No bare layer imports (from domain.X, from application.X, etc.)
|
|
345
|
+
"""
|
|
346
|
+
test_files = find_test_files()
|
|
347
|
+
|
|
348
|
+
if not test_files:
|
|
349
|
+
pytest.skip("No test files found to validate")
|
|
350
|
+
|
|
351
|
+
violations = []
|
|
352
|
+
|
|
353
|
+
for test_file in test_files:
|
|
354
|
+
imports = extract_imports_ast(test_file)
|
|
355
|
+
|
|
356
|
+
for import_path, line_no in imports:
|
|
357
|
+
if is_bare_layer_import(import_path):
|
|
358
|
+
rel_path = test_file.relative_to(REPO_ROOT)
|
|
359
|
+
violations.append(
|
|
360
|
+
f"{rel_path}:{line_no}\n"
|
|
361
|
+
f" Import: from {import_path} import ...\n"
|
|
362
|
+
f" Issue: Bare layer import causes module shadowing\n"
|
|
363
|
+
f" Fix: Use qualified import from {{wagon}}.{{feature}}.src.{import_path.split('.')[0]}.{{module}}"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
if violations:
|
|
367
|
+
pytest.fail(
|
|
368
|
+
f"\n\nFound {len(violations)} bare layer imports in test files:\n\n" +
|
|
369
|
+
"\n\n".join(violations[:10]) +
|
|
370
|
+
(f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
|
|
371
|
+
"\n\nSee: atdd/coder/conventions/boundaries.convention.yaml::namespacing.forbidden_patterns"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@pytest.mark.coder
|
|
376
|
+
def test_no_syspath_manipulation_in_tests():
|
|
377
|
+
"""
|
|
378
|
+
SPEC-BOUNDARIES-0002: Test files must not manipulate sys.path.
|
|
379
|
+
|
|
380
|
+
Convention: boundaries.convention.yaml::namespacing.syspath_prohibition
|
|
381
|
+
|
|
382
|
+
Forbidden in test files:
|
|
383
|
+
- sys.path.insert(0, str(src_path))
|
|
384
|
+
- sys.path.append(...)
|
|
385
|
+
|
|
386
|
+
Reason: Causes cross-wagon path collisions; use pytest pythonpath instead
|
|
387
|
+
|
|
388
|
+
Given: All test files
|
|
389
|
+
When: Checking for sys.path manipulation
|
|
390
|
+
Then: No sys.path.insert() or sys.path.append() in test files
|
|
391
|
+
"""
|
|
392
|
+
test_files = find_test_files()
|
|
393
|
+
|
|
394
|
+
if not test_files:
|
|
395
|
+
pytest.skip("No test files found to validate")
|
|
396
|
+
|
|
397
|
+
violations = []
|
|
398
|
+
|
|
399
|
+
for test_file in test_files:
|
|
400
|
+
syspath_lines = check_for_syspath_manipulation(test_file)
|
|
401
|
+
|
|
402
|
+
if syspath_lines:
|
|
403
|
+
rel_path = test_file.relative_to(REPO_ROOT)
|
|
404
|
+
for line_content, line_no in syspath_lines:
|
|
405
|
+
violations.append(
|
|
406
|
+
f"{rel_path}:{line_no}\n"
|
|
407
|
+
f" Code: {line_content}\n"
|
|
408
|
+
f" Issue: Test file manipulates sys.path\n"
|
|
409
|
+
f" Fix: Remove sys.path manipulation; pytest pythonpath handles this"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if violations:
|
|
413
|
+
pytest.fail(
|
|
414
|
+
f"\n\nFound {len(violations)} sys.path manipulations in test files:\n\n" +
|
|
415
|
+
"\n\n".join(violations[:10]) +
|
|
416
|
+
(f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
|
|
417
|
+
"\n\nSee: atdd/coder/conventions/boundaries.convention.yaml::namespacing.syspath_prohibition"
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@pytest.mark.coder
|
|
422
|
+
def test_no_cross_wagon_imports():
|
|
423
|
+
"""
|
|
424
|
+
SPEC-BOUNDARIES-0003: Wagons cannot import directly from other wagons.
|
|
425
|
+
|
|
426
|
+
Convention: boundaries.convention.yaml::interaction.forbidden_cross_wagon_imports
|
|
427
|
+
Cross-reference: design.convention.yaml::VC-DS-06
|
|
428
|
+
|
|
429
|
+
Forbidden:
|
|
430
|
+
- from juggle_domains.score_domains.src.domain.choice import Choice # in commit_state wagon
|
|
431
|
+
|
|
432
|
+
Required:
|
|
433
|
+
- Wagons communicate only via contracts (see contract.convention.yaml)
|
|
434
|
+
|
|
435
|
+
Given: All implementation files
|
|
436
|
+
When: Checking imports
|
|
437
|
+
Then: No imports from other wagons (only via contracts)
|
|
438
|
+
"""
|
|
439
|
+
impl_files = find_implementation_files()
|
|
440
|
+
|
|
441
|
+
if not impl_files:
|
|
442
|
+
pytest.skip("No implementation files found to validate")
|
|
443
|
+
|
|
444
|
+
violations = []
|
|
445
|
+
|
|
446
|
+
for impl_file in impl_files:
|
|
447
|
+
imports = extract_imports_ast(impl_file)
|
|
448
|
+
|
|
449
|
+
for import_path, line_no in imports:
|
|
450
|
+
is_cross, source_wagon, target_wagon = is_cross_wagon_import(impl_file, import_path)
|
|
451
|
+
|
|
452
|
+
if is_cross:
|
|
453
|
+
rel_path = impl_file.relative_to(REPO_ROOT)
|
|
454
|
+
violations.append(
|
|
455
|
+
f"{rel_path}:{line_no}\n"
|
|
456
|
+
f" Source wagon: {source_wagon}\n"
|
|
457
|
+
f" Target wagon: {target_wagon}\n"
|
|
458
|
+
f" Import: {import_path}\n"
|
|
459
|
+
f" Issue: Direct cross-wagon import creates tight coupling\n"
|
|
460
|
+
f" Fix: Use contracts for wagon-to-wagon communication"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
if violations:
|
|
464
|
+
pytest.fail(
|
|
465
|
+
f"\n\nFound {len(violations)} cross-wagon imports:\n\n" +
|
|
466
|
+
"\n\n".join(violations[:10]) +
|
|
467
|
+
(f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
|
|
468
|
+
"\n\nSee: atdd/coder/conventions/boundaries.convention.yaml::interaction.forbidden_cross_wagon_imports"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@pytest.mark.coder
|
|
473
|
+
def test_package_hierarchy_exists():
|
|
474
|
+
"""
|
|
475
|
+
SPEC-BOUNDARIES-0004: Package hierarchy must be complete.
|
|
476
|
+
|
|
477
|
+
Convention: boundaries.convention.yaml::namespacing.package_hierarchy
|
|
478
|
+
|
|
479
|
+
Required __init__.py files:
|
|
480
|
+
- python/__init__.py
|
|
481
|
+
- python/{wagon}/__init__.py
|
|
482
|
+
- python/{wagon}/{feature}/__init__.py
|
|
483
|
+
|
|
484
|
+
Given: Python directory structure
|
|
485
|
+
When: Checking for __init__.py files
|
|
486
|
+
Then: All required __init__.py files exist
|
|
487
|
+
"""
|
|
488
|
+
missing = check_package_hierarchy()
|
|
489
|
+
|
|
490
|
+
if missing:
|
|
491
|
+
pytest.fail(
|
|
492
|
+
f"\n\nMissing {len(missing)} required __init__.py files:\n\n" +
|
|
493
|
+
"\n".join(f" - {path}" for path in missing) +
|
|
494
|
+
"\n\nPackage hierarchy is required for qualified imports to work.\n" +
|
|
495
|
+
"See: atdd/coder/conventions/boundaries.convention.yaml::namespacing.package_hierarchy"
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
@pytest.mark.coder
|
|
500
|
+
def test_pytest_pythonpath_configured():
|
|
501
|
+
"""
|
|
502
|
+
SPEC-BOUNDARIES-0005: pytest pythonpath must be configured.
|
|
503
|
+
|
|
504
|
+
Convention: boundaries.convention.yaml::namespacing.test_configuration
|
|
505
|
+
|
|
506
|
+
Required in python/pyproject.toml:
|
|
507
|
+
[tool.pytest.ini_options]
|
|
508
|
+
pythonpath = ["."]
|
|
509
|
+
|
|
510
|
+
Given: python/pyproject.toml
|
|
511
|
+
When: Checking [tool.pytest.ini_options]
|
|
512
|
+
Then: pythonpath = ["."] is configured
|
|
513
|
+
"""
|
|
514
|
+
is_configured, message = check_pytest_pythonpath()
|
|
515
|
+
|
|
516
|
+
if not is_configured:
|
|
517
|
+
pytest.fail(
|
|
518
|
+
f"\n\npytest pythonpath not configured:\n\n"
|
|
519
|
+
f" Issue: {message}\n\n"
|
|
520
|
+
f"Required configuration in python/pyproject.toml:\n"
|
|
521
|
+
f" [tool.pytest.ini_options]\n"
|
|
522
|
+
f" pythonpath = [\".\"]\n\n"
|
|
523
|
+
f"This is required for qualified imports to work across wagons.\n"
|
|
524
|
+
f"See: atdd/coder/conventions/boundaries.convention.yaml::namespacing.test_configuration"
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@pytest.mark.coder
|
|
529
|
+
def test_no_bare_layer_imports_in_implementation():
|
|
530
|
+
"""
|
|
531
|
+
SPEC-BOUNDARIES-0006: Implementation files should use qualified imports.
|
|
532
|
+
|
|
533
|
+
Convention: boundaries.convention.yaml::namespacing.forbidden_patterns.bare_layer_imports
|
|
534
|
+
|
|
535
|
+
Note: composition.py and wagon.py are excluded (they may use bare imports)
|
|
536
|
+
|
|
537
|
+
Forbidden in implementation files:
|
|
538
|
+
- from domain.signature_algorithm import SignatureAlgorithm
|
|
539
|
+
- from src.domain.X import Y
|
|
540
|
+
|
|
541
|
+
Required:
|
|
542
|
+
- from commit_state.sign_commit.src.domain.signature_algorithm import SignatureAlgorithm
|
|
543
|
+
- Relative imports within same layer: from .base_repository import BaseRepository
|
|
544
|
+
|
|
545
|
+
Given: All implementation files (excluding composition.py/wagon.py)
|
|
546
|
+
When: Checking imports
|
|
547
|
+
Then: No bare layer imports or src-relative imports
|
|
548
|
+
"""
|
|
549
|
+
impl_files = find_implementation_files()
|
|
550
|
+
|
|
551
|
+
if not impl_files:
|
|
552
|
+
pytest.skip("No implementation files found to validate")
|
|
553
|
+
|
|
554
|
+
violations = []
|
|
555
|
+
|
|
556
|
+
for impl_file in impl_files:
|
|
557
|
+
imports = extract_imports_ast(impl_file)
|
|
558
|
+
|
|
559
|
+
for import_path, line_no in imports:
|
|
560
|
+
# Check for bare layer imports
|
|
561
|
+
if is_bare_layer_import(import_path):
|
|
562
|
+
rel_path = impl_file.relative_to(REPO_ROOT)
|
|
563
|
+
violations.append(
|
|
564
|
+
f"{rel_path}:{line_no}\n"
|
|
565
|
+
f" Import: from {import_path} import ...\n"
|
|
566
|
+
f" Issue: Bare layer import in implementation file\n"
|
|
567
|
+
f" Fix: Use qualified import or relative import within same layer"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Check for src-relative imports
|
|
571
|
+
elif import_path.startswith('src.'):
|
|
572
|
+
rel_path = impl_file.relative_to(REPO_ROOT)
|
|
573
|
+
violations.append(
|
|
574
|
+
f"{rel_path}:{line_no}\n"
|
|
575
|
+
f" Import: from {import_path} import ...\n"
|
|
576
|
+
f" Issue: src-relative import only works with sys.path manipulation\n"
|
|
577
|
+
f" Fix: Use qualified import from {{wagon}}.{{feature}}.src.{{layer}}.{{module}}"
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
if violations:
|
|
581
|
+
pytest.fail(
|
|
582
|
+
f"\n\nFound {len(violations)} bare/src-relative imports in implementation:\n\n" +
|
|
583
|
+
"\n\n".join(violations[:10]) +
|
|
584
|
+
(f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
|
|
585
|
+
"\n\nSee: atdd/coder/conventions/boundaries.convention.yaml::namespacing.forbidden_patterns"
|
|
586
|
+
)
|