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,396 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test import boundaries are enforced across layers.
|
|
3
|
+
|
|
4
|
+
Validates:
|
|
5
|
+
- No circular dependencies between modules
|
|
6
|
+
- Imports follow dependency flow (inward)
|
|
7
|
+
- No imports from test code into production code
|
|
8
|
+
|
|
9
|
+
Inspired by: .claude/utils/coder/import_scan.py
|
|
10
|
+
But: Self-contained, no utility dependencies
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, List, Set
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Path constants
|
|
20
|
+
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
21
|
+
PYTHON_DIR = REPO_ROOT / "python"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def extract_all_imports(file_path: Path) -> List[str]:
|
|
25
|
+
"""
|
|
26
|
+
Extract all import statements from Python file.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
file_path: Path to Python file
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
List of imported module names
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
36
|
+
content = f.read()
|
|
37
|
+
except Exception:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
imports = []
|
|
41
|
+
|
|
42
|
+
# Match: from X import Y
|
|
43
|
+
from_imports = re.findall(r'from\s+([^\s]+)\s+import', content)
|
|
44
|
+
imports.extend(from_imports)
|
|
45
|
+
|
|
46
|
+
# Match: import X, Y, Z
|
|
47
|
+
direct_imports = re.findall(r'^\s*import\s+([^\s;#]+)', content, re.MULTILINE)
|
|
48
|
+
for imp in direct_imports:
|
|
49
|
+
# Split comma-separated imports
|
|
50
|
+
imports.extend([i.strip() for i in imp.split(',')])
|
|
51
|
+
|
|
52
|
+
return imports
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_internal_import(import_path: str, base_module: str) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
Check if import is internal to the project.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
import_path: Import statement
|
|
61
|
+
base_module: Base module name to check against
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if import is internal to project
|
|
65
|
+
"""
|
|
66
|
+
# Relative imports
|
|
67
|
+
if import_path.startswith('.'):
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
# Absolute imports from same base module
|
|
71
|
+
if import_path.startswith(base_module):
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
# Check if it's from python/ directory structure
|
|
75
|
+
if 'pace_dilemmas' in import_path or 'juggle_domains' in import_path:
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_module_name(file_path: Path) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Get module name from file path.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
file_path: Path to Python file
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Module name (e.g., "pace_dilemmas.pair_fragments.domain.entities")
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
rel_path = file_path.relative_to(PYTHON_DIR)
|
|
93
|
+
# Remove .py extension
|
|
94
|
+
module_path = str(rel_path).replace('.py', '')
|
|
95
|
+
# Convert path to module name
|
|
96
|
+
module_name = module_path.replace('/', '.')
|
|
97
|
+
# Remove src. prefix if present
|
|
98
|
+
if '.src.' in module_name:
|
|
99
|
+
module_name = module_name.replace('.src.', '.')
|
|
100
|
+
return module_name
|
|
101
|
+
except ValueError:
|
|
102
|
+
return str(file_path.stem)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def find_python_modules() -> List[Path]:
|
|
106
|
+
"""
|
|
107
|
+
Find all Python modules (excluding tests).
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of Path objects
|
|
111
|
+
"""
|
|
112
|
+
if not PYTHON_DIR.exists():
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
modules = []
|
|
116
|
+
for py_file in PYTHON_DIR.rglob("*.py"):
|
|
117
|
+
# Skip test files
|
|
118
|
+
if '/test/' in str(py_file) or py_file.name.startswith('test_'):
|
|
119
|
+
continue
|
|
120
|
+
# Skip __pycache__
|
|
121
|
+
if '__pycache__' in str(py_file):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
modules.append(py_file)
|
|
125
|
+
|
|
126
|
+
return modules
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def build_dependency_graph() -> Dict[str, Set[str]]:
|
|
130
|
+
"""
|
|
131
|
+
Build dependency graph of all Python modules.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dict mapping module names to their dependencies
|
|
135
|
+
"""
|
|
136
|
+
graph = {}
|
|
137
|
+
|
|
138
|
+
for module_file in find_python_modules():
|
|
139
|
+
module_name = get_module_name(module_file)
|
|
140
|
+
imports = extract_all_imports(module_file)
|
|
141
|
+
|
|
142
|
+
# Filter to internal imports only
|
|
143
|
+
internal_imports = {
|
|
144
|
+
imp for imp in imports
|
|
145
|
+
if is_internal_import(imp, 'pace_dilemmas') or is_internal_import(imp, 'juggle_domains')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
graph[module_name] = internal_imports
|
|
149
|
+
|
|
150
|
+
return graph
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def find_circular_dependencies(graph: Dict[str, Set[str]]) -> List[tuple]:
|
|
154
|
+
"""
|
|
155
|
+
Find circular dependencies in module graph.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
graph: Dependency graph
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
List of (module_a, module_b) tuples representing circular deps
|
|
162
|
+
"""
|
|
163
|
+
circular = []
|
|
164
|
+
|
|
165
|
+
for module, deps in graph.items():
|
|
166
|
+
for dep in deps:
|
|
167
|
+
# Check if dep also imports module (direct circular)
|
|
168
|
+
if dep in graph and module in graph[dep]:
|
|
169
|
+
# Only report each circle once (canonical order)
|
|
170
|
+
if module < dep:
|
|
171
|
+
circular.append((module, dep))
|
|
172
|
+
|
|
173
|
+
return circular
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def find_cycles(graph: Dict[str, Set[str]], max_depth: int = 5) -> List[List[str]]:
|
|
177
|
+
"""
|
|
178
|
+
Find dependency cycles using DFS.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
graph: Dependency graph
|
|
182
|
+
max_depth: Maximum cycle length to detect
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of cycles (each cycle is a list of module names)
|
|
186
|
+
"""
|
|
187
|
+
cycles = []
|
|
188
|
+
|
|
189
|
+
def dfs(node, path, visited):
|
|
190
|
+
if node in path:
|
|
191
|
+
# Found a cycle
|
|
192
|
+
cycle_start = path.index(node)
|
|
193
|
+
cycle = path[cycle_start:] + [node]
|
|
194
|
+
if len(cycle) <= max_depth:
|
|
195
|
+
# Normalize cycle (start with smallest element)
|
|
196
|
+
min_idx = cycle.index(min(cycle))
|
|
197
|
+
normalized = cycle[min_idx:] + cycle[:min_idx]
|
|
198
|
+
if normalized not in cycles:
|
|
199
|
+
cycles.append(normalized)
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
if node in visited or node not in graph:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
visited.add(node)
|
|
206
|
+
path.append(node)
|
|
207
|
+
|
|
208
|
+
for neighbor in graph.get(node, set()):
|
|
209
|
+
dfs(neighbor, path, visited)
|
|
210
|
+
|
|
211
|
+
path.pop()
|
|
212
|
+
|
|
213
|
+
for start_node in graph.keys():
|
|
214
|
+
dfs(start_node, [], set())
|
|
215
|
+
|
|
216
|
+
return cycles
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@pytest.mark.coder
|
|
220
|
+
def test_no_circular_module_dependencies():
|
|
221
|
+
"""
|
|
222
|
+
SPEC-CODER-IMPORT-0001: No circular dependencies between modules.
|
|
223
|
+
|
|
224
|
+
Circular dependencies cause:
|
|
225
|
+
- Import errors
|
|
226
|
+
- Initialization issues
|
|
227
|
+
- Tight coupling
|
|
228
|
+
- Difficult testing
|
|
229
|
+
|
|
230
|
+
Given: All Python modules in python/
|
|
231
|
+
When: Building dependency graph
|
|
232
|
+
Then: No module imports another that imports it back
|
|
233
|
+
"""
|
|
234
|
+
graph = build_dependency_graph()
|
|
235
|
+
|
|
236
|
+
if not graph:
|
|
237
|
+
pytest.skip("No Python modules found to validate")
|
|
238
|
+
|
|
239
|
+
# Find direct circular dependencies (A → B, B → A)
|
|
240
|
+
circular = find_circular_dependencies(graph)
|
|
241
|
+
|
|
242
|
+
if circular:
|
|
243
|
+
pytest.fail(
|
|
244
|
+
f"\\n\\nFound {len(circular)} circular dependencies:\\n\\n" +
|
|
245
|
+
"\\n".join(f" {a} ↔ {b}" for a, b in circular[:10]) +
|
|
246
|
+
(f"\\n ... and {len(circular) - 10} more" if len(circular) > 10 else "")
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@pytest.mark.coder
|
|
251
|
+
def test_no_import_cycles():
|
|
252
|
+
"""
|
|
253
|
+
SPEC-CODER-IMPORT-0002: No dependency cycles (A → B → C → A).
|
|
254
|
+
|
|
255
|
+
Dependency cycles create:
|
|
256
|
+
- Complex initialization order
|
|
257
|
+
- Difficult refactoring
|
|
258
|
+
- Testing challenges
|
|
259
|
+
- Code smell
|
|
260
|
+
|
|
261
|
+
Given: All Python modules
|
|
262
|
+
When: Analyzing dependency chains
|
|
263
|
+
Then: No module depends on itself through intermediaries
|
|
264
|
+
"""
|
|
265
|
+
graph = build_dependency_graph()
|
|
266
|
+
|
|
267
|
+
if not graph:
|
|
268
|
+
pytest.skip("No Python modules found to validate")
|
|
269
|
+
|
|
270
|
+
cycles = find_cycles(graph, max_depth=5)
|
|
271
|
+
|
|
272
|
+
if cycles:
|
|
273
|
+
# Format cycles for display
|
|
274
|
+
formatted_cycles = []
|
|
275
|
+
for cycle in cycles[:5]: # Show first 5
|
|
276
|
+
chain = " → ".join(cycle)
|
|
277
|
+
formatted_cycles.append(f" {chain}")
|
|
278
|
+
|
|
279
|
+
pytest.fail(
|
|
280
|
+
f"\\n\\nFound {len(cycles)} dependency cycles:\\n\\n" +
|
|
281
|
+
"\\n".join(formatted_cycles) +
|
|
282
|
+
(f"\\n ... and {len(cycles) - 5} more" if len(cycles) > 5 else "") +
|
|
283
|
+
f"\\n\\nCycles create tight coupling and make refactoring difficult."
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@pytest.mark.coder
|
|
288
|
+
def test_no_test_imports_in_production():
|
|
289
|
+
"""
|
|
290
|
+
SPEC-CODER-IMPORT-0003: Production code doesn't import from test code.
|
|
291
|
+
|
|
292
|
+
Test code can import production code, but NOT vice versa.
|
|
293
|
+
|
|
294
|
+
Given: All Python production modules
|
|
295
|
+
When: Checking imports
|
|
296
|
+
Then: No imports from test/ directories
|
|
297
|
+
"""
|
|
298
|
+
violations = []
|
|
299
|
+
|
|
300
|
+
for module_file in find_python_modules():
|
|
301
|
+
# Skip if this is already a test file
|
|
302
|
+
if '/test/' in str(module_file):
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
imports = extract_all_imports(module_file)
|
|
306
|
+
|
|
307
|
+
for imp in imports:
|
|
308
|
+
# Check if importing from test directory
|
|
309
|
+
if '/test/' in imp or imp.startswith('test_') or '.test.' in imp:
|
|
310
|
+
rel_path = module_file.relative_to(REPO_ROOT)
|
|
311
|
+
violations.append(
|
|
312
|
+
f"{rel_path}\\n"
|
|
313
|
+
f" Import: {imp}\\n"
|
|
314
|
+
f" Issue: Production code imports from test code"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if violations:
|
|
318
|
+
pytest.fail(
|
|
319
|
+
f"\\n\\nFound {len(violations)} test import violations:\\n\\n" +
|
|
320
|
+
"\\n\\n".join(violations[:10]) +
|
|
321
|
+
(f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
|
|
322
|
+
f"\\n\\nProduction code should never import test code."
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@pytest.mark.coder
|
|
327
|
+
def test_imports_follow_layer_boundaries():
|
|
328
|
+
"""
|
|
329
|
+
SPEC-CODER-IMPORT-0004: Imports respect architectural boundaries.
|
|
330
|
+
|
|
331
|
+
Based on file path structure:
|
|
332
|
+
- domain/ can only import from domain/
|
|
333
|
+
- application/ can import from domain/, application/
|
|
334
|
+
- integration/ can import from domain/, integration/
|
|
335
|
+
- presentation/ can import from domain/, application/, presentation/
|
|
336
|
+
|
|
337
|
+
Given: All Python modules with layer structure
|
|
338
|
+
When: Checking imports
|
|
339
|
+
Then: Imports only from allowed layers
|
|
340
|
+
"""
|
|
341
|
+
violations = []
|
|
342
|
+
|
|
343
|
+
for module_file in find_python_modules():
|
|
344
|
+
module_path = str(module_file)
|
|
345
|
+
|
|
346
|
+
# Determine this module's layer
|
|
347
|
+
if '/domain/' in module_path:
|
|
348
|
+
current_layer = 'domain'
|
|
349
|
+
allowed_layers = ['domain']
|
|
350
|
+
elif '/application/' in module_path:
|
|
351
|
+
current_layer = 'application'
|
|
352
|
+
allowed_layers = ['domain', 'application']
|
|
353
|
+
elif '/integration/' in module_path or '/infrastructure/' in module_path:
|
|
354
|
+
current_layer = 'integration'
|
|
355
|
+
allowed_layers = ['domain', 'integration', 'infrastructure']
|
|
356
|
+
elif '/presentation/' in module_path:
|
|
357
|
+
current_layer = 'presentation'
|
|
358
|
+
allowed_layers = ['domain', 'application', 'presentation']
|
|
359
|
+
else:
|
|
360
|
+
# Can't determine layer, skip
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
imports = extract_all_imports(module_file)
|
|
364
|
+
|
|
365
|
+
for imp in imports:
|
|
366
|
+
# Skip external imports
|
|
367
|
+
if not (is_internal_import(imp, 'pace_dilemmas') or is_internal_import(imp, 'juggle_domains')):
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
# Check if import crosses boundary
|
|
371
|
+
import_layer = None
|
|
372
|
+
if '/domain/' in imp or '.domain.' in imp:
|
|
373
|
+
import_layer = 'domain'
|
|
374
|
+
elif '/application/' in imp or '.application.' in imp:
|
|
375
|
+
import_layer = 'application'
|
|
376
|
+
elif '/integration/' in imp or '/infrastructure/' in imp or '.integration.' in imp:
|
|
377
|
+
import_layer = 'integration'
|
|
378
|
+
elif '/presentation/' in imp or '.presentation.' in imp:
|
|
379
|
+
import_layer = 'presentation'
|
|
380
|
+
|
|
381
|
+
if import_layer and import_layer not in allowed_layers:
|
|
382
|
+
rel_path = module_file.relative_to(REPO_ROOT)
|
|
383
|
+
violations.append(
|
|
384
|
+
f"{rel_path}\\n"
|
|
385
|
+
f" Layer: {current_layer}\\n"
|
|
386
|
+
f" Import: {imp}\\n"
|
|
387
|
+
f" Target layer: {import_layer}\\n"
|
|
388
|
+
f" Violation: {current_layer} cannot import from {import_layer}"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
if violations:
|
|
392
|
+
pytest.fail(
|
|
393
|
+
f"\\n\\nFound {len(violations)} boundary violations:\\n\\n" +
|
|
394
|
+
"\\n\\n".join(violations[:10]) +
|
|
395
|
+
(f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
|
|
396
|
+
)
|