atdd 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. atdd/__init__.py +0 -0
  2. atdd/cli.py +404 -0
  3. atdd/coach/__init__.py +0 -0
  4. atdd/coach/commands/__init__.py +0 -0
  5. atdd/coach/commands/add_persistence_metadata.py +215 -0
  6. atdd/coach/commands/analyze_migrations.py +188 -0
  7. atdd/coach/commands/consumers.py +720 -0
  8. atdd/coach/commands/infer_governance_status.py +149 -0
  9. atdd/coach/commands/initializer.py +177 -0
  10. atdd/coach/commands/interface.py +1078 -0
  11. atdd/coach/commands/inventory.py +565 -0
  12. atdd/coach/commands/migration.py +240 -0
  13. atdd/coach/commands/registry.py +1560 -0
  14. atdd/coach/commands/session.py +430 -0
  15. atdd/coach/commands/sync.py +405 -0
  16. atdd/coach/commands/test_interface.py +399 -0
  17. atdd/coach/commands/test_runner.py +141 -0
  18. atdd/coach/commands/tests/__init__.py +1 -0
  19. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  20. atdd/coach/commands/traceability.py +4264 -0
  21. atdd/coach/conventions/session.convention.yaml +754 -0
  22. atdd/coach/overlays/__init__.py +2 -0
  23. atdd/coach/overlays/claude.md +2 -0
  24. atdd/coach/schemas/config.schema.json +34 -0
  25. atdd/coach/schemas/manifest.schema.json +101 -0
  26. atdd/coach/templates/ATDD.md +282 -0
  27. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  28. atdd/coach/utils/__init__.py +0 -0
  29. atdd/coach/utils/graph/__init__.py +0 -0
  30. atdd/coach/utils/graph/urn.py +875 -0
  31. atdd/coach/validators/__init__.py +0 -0
  32. atdd/coach/validators/shared_fixtures.py +365 -0
  33. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  34. atdd/coach/validators/test_registry.py +575 -0
  35. atdd/coach/validators/test_session_validation.py +1183 -0
  36. atdd/coach/validators/test_traceability.py +448 -0
  37. atdd/coach/validators/test_update_feature_paths.py +108 -0
  38. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  39. atdd/coder/__init__.py +1 -0
  40. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  41. atdd/coder/conventions/backend.convention.yaml +460 -0
  42. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  43. atdd/coder/conventions/commons.convention.yaml +460 -0
  44. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  45. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  46. atdd/coder/conventions/design.convention.yaml +327 -0
  47. atdd/coder/conventions/design.recipe.yaml +273 -0
  48. atdd/coder/conventions/dto.convention.yaml +660 -0
  49. atdd/coder/conventions/frontend.convention.yaml +542 -0
  50. atdd/coder/conventions/green.convention.yaml +1012 -0
  51. atdd/coder/conventions/presentation.convention.yaml +587 -0
  52. atdd/coder/conventions/refactor.convention.yaml +535 -0
  53. atdd/coder/conventions/technology.convention.yaml +206 -0
  54. atdd/coder/conventions/tests/__init__.py +0 -0
  55. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  56. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  57. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  58. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  59. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  60. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  61. atdd/coder/conventions/train.convention.yaml +325 -0
  62. atdd/coder/conventions/verification.protocol.yaml +53 -0
  63. atdd/coder/schemas/design_system.schema.json +361 -0
  64. atdd/coder/validators/__init__.py +0 -0
  65. atdd/coder/validators/test_commons_structure.py +485 -0
  66. atdd/coder/validators/test_complexity.py +416 -0
  67. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  68. atdd/coder/validators/test_design_system_compliance.py +413 -0
  69. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  70. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  71. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  72. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  73. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  74. atdd/coder/validators/test_import_boundaries.py +396 -0
  75. atdd/coder/validators/test_init_file_urns.py +593 -0
  76. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  77. atdd/coder/validators/test_presentation_convention.py +260 -0
  78. atdd/coder/validators/test_python_architecture.py +674 -0
  79. atdd/coder/validators/test_quality_metrics.py +420 -0
  80. atdd/coder/validators/test_station_master_pattern.py +244 -0
  81. atdd/coder/validators/test_train_infrastructure.py +454 -0
  82. atdd/coder/validators/test_train_urns.py +293 -0
  83. atdd/coder/validators/test_typescript_architecture.py +616 -0
  84. atdd/coder/validators/test_usecase_structure.py +421 -0
  85. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  86. atdd/conftest.py +126 -0
  87. atdd/planner/__init__.py +1 -0
  88. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  89. atdd/planner/conventions/appendix.convention.yaml +187 -0
  90. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  91. atdd/planner/conventions/component.convention.yaml +670 -0
  92. atdd/planner/conventions/criteria.convention.yaml +141 -0
  93. atdd/planner/conventions/feature.convention.yaml +371 -0
  94. atdd/planner/conventions/interface.convention.yaml +382 -0
  95. atdd/planner/conventions/steps.convention.yaml +141 -0
  96. atdd/planner/conventions/train.convention.yaml +552 -0
  97. atdd/planner/conventions/wagon.convention.yaml +275 -0
  98. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  99. atdd/planner/schemas/acceptance.schema.json +336 -0
  100. atdd/planner/schemas/appendix.schema.json +78 -0
  101. atdd/planner/schemas/component.schema.json +114 -0
  102. atdd/planner/schemas/feature.schema.json +197 -0
  103. atdd/planner/schemas/train.schema.json +192 -0
  104. atdd/planner/schemas/wagon.schema.json +281 -0
  105. atdd/planner/schemas/wmbt.schema.json +59 -0
  106. atdd/planner/validators/__init__.py +0 -0
  107. atdd/planner/validators/conftest.py +5 -0
  108. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  109. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  110. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  111. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  112. atdd/planner/validators/test_plan_wagons.py +174 -0
  113. atdd/planner/validators/test_train_validation.py +514 -0
  114. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  115. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  116. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  117. atdd/tester/__init__.py +1 -0
  118. atdd/tester/conventions/artifact.convention.yaml +257 -0
  119. atdd/tester/conventions/contract.convention.yaml +1009 -0
  120. atdd/tester/conventions/filename.convention.yaml +555 -0
  121. atdd/tester/conventions/migration.convention.yaml +509 -0
  122. atdd/tester/conventions/red.convention.yaml +797 -0
  123. atdd/tester/conventions/routing.convention.yaml +51 -0
  124. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  125. atdd/tester/schemas/a11y.tmpl.json +17 -0
  126. atdd/tester/schemas/artifact.schema.json +189 -0
  127. atdd/tester/schemas/contract.schema.json +591 -0
  128. atdd/tester/schemas/contract.tmpl.json +95 -0
  129. atdd/tester/schemas/db.tmpl.json +20 -0
  130. atdd/tester/schemas/e2e.tmpl.json +17 -0
  131. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  132. atdd/tester/schemas/event.tmpl.json +17 -0
  133. atdd/tester/schemas/http.tmpl.json +19 -0
  134. atdd/tester/schemas/job.tmpl.json +18 -0
  135. atdd/tester/schemas/load.tmpl.json +21 -0
  136. atdd/tester/schemas/metric.tmpl.json +19 -0
  137. atdd/tester/schemas/pack.schema.json +139 -0
  138. atdd/tester/schemas/realtime.tmpl.json +20 -0
  139. atdd/tester/schemas/rls.tmpl.json +18 -0
  140. atdd/tester/schemas/script.tmpl.json +16 -0
  141. atdd/tester/schemas/sec.tmpl.json +18 -0
  142. atdd/tester/schemas/storage.tmpl.json +18 -0
  143. atdd/tester/schemas/telemetry.schema.json +128 -0
  144. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  145. atdd/tester/schemas/test_filename.schema.json +194 -0
  146. atdd/tester/schemas/test_intent.schema.json +179 -0
  147. atdd/tester/schemas/unit.tmpl.json +18 -0
  148. atdd/tester/schemas/visual.tmpl.json +18 -0
  149. atdd/tester/schemas/ws.tmpl.json +17 -0
  150. atdd/tester/utils/__init__.py +0 -0
  151. atdd/tester/utils/filename.py +300 -0
  152. atdd/tester/validators/__init__.py +0 -0
  153. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  154. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  155. atdd/tester/validators/conftest.py +5 -0
  156. atdd/tester/validators/coverage_gap_report.py +321 -0
  157. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  158. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  159. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  160. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  161. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  162. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  163. atdd/tester/validators/test_contracts_structure.py +200 -0
  164. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  165. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  166. atdd/tester/validators/test_fixture_validity.py +372 -0
  167. atdd/tester/validators/test_isolation.py +487 -0
  168. atdd/tester/validators/test_migration_coverage.py +204 -0
  169. atdd/tester/validators/test_migration_criteria.py +276 -0
  170. atdd/tester/validators/test_migration_generation.py +116 -0
  171. atdd/tester/validators/test_python_test_naming.py +410 -0
  172. atdd/tester/validators/test_red_layer_validation.py +95 -0
  173. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  174. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  175. atdd/tester/validators/test_telemetry_structure.py +634 -0
  176. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  177. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  178. atdd-0.1.0.dist-info/METADATA +191 -0
  179. atdd-0.1.0.dist-info/RECORD +183 -0
  180. atdd-0.1.0.dist-info/WHEEL +5 -0
  181. atdd-0.1.0.dist-info/entry_points.txt +2 -0
  182. atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
  183. atdd-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,674 @@
1
+ """
2
+ Test Python code follows clean architecture (4-layer).
3
+
4
+ Validates:
5
+ - Domain layer is pure (no imports from other layers)
6
+ - Application layer only imports from domain
7
+ - Presentation layer imports from application/domain
8
+ - Integration layer only imports from domain
9
+ - Component naming follows backend conventions
10
+ - Files are in correct layers based on their suffixes
11
+
12
+ Conventions from:
13
+ - atdd/coder/conventions/backend.convention.yaml
14
+
15
+ Inspired by: .claude/utils/coder/architecture.py
16
+ But: Self-contained, no utility dependencies
17
+ """
18
+
19
+ import pytest
20
+ import re
21
+ import yaml
22
+ from pathlib import Path
23
+ from typing import Dict, List, Tuple
24
+
25
+
26
+ # Path constants
27
+ REPO_ROOT = Path(__file__).resolve().parents[4]
28
+ PYTHON_DIR = REPO_ROOT / "python"
29
+ BACKEND_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "backend.convention.yaml"
30
+
31
+
32
+ def determine_layer_from_path(file_path: Path) -> str:
33
+ """
34
+ Determine layer from file path.
35
+
36
+ Args:
37
+ file_path: Path to Python file
38
+
39
+ Returns:
40
+ Layer name: 'domain', 'application', 'presentation', 'integration', 'unknown'
41
+ """
42
+ path_str = str(file_path).lower()
43
+
44
+ # Check explicit layer directories
45
+ if '/domain/' in path_str or path_str.endswith('/domain.py'):
46
+ return 'domain'
47
+ elif '/application/' in path_str or path_str.endswith('/application.py'):
48
+ return 'application'
49
+ elif '/presentation/' in path_str or path_str.endswith('/presentation.py'):
50
+ return 'presentation'
51
+ elif '/integration/' in path_str or '/infrastructure/' in path_str:
52
+ return 'integration'
53
+
54
+ # Check alternative patterns
55
+ if '/entities/' in path_str or '/models/' in path_str or '/value_objects/' in path_str:
56
+ return 'domain'
57
+ elif '/use_cases/' in path_str or '/usecases/' in path_str or '/services/' in path_str:
58
+ return 'application'
59
+ elif '/controllers/' in path_str or '/handlers/' in path_str or '/views/' in path_str:
60
+ return 'presentation'
61
+ elif '/adapters/' in path_str or '/repositories/' in path_str or '/gateways/' in path_str:
62
+ return 'integration'
63
+
64
+ return 'unknown'
65
+
66
+
67
+ def extract_python_imports(file_path: Path) -> list:
68
+ """
69
+ Extract import statements from Python file.
70
+
71
+ Args:
72
+ file_path: Path to Python file
73
+
74
+ Returns:
75
+ List of imported module paths
76
+ """
77
+ try:
78
+ with open(file_path, 'r', encoding='utf-8') as f:
79
+ content = f.read()
80
+ except Exception:
81
+ return []
82
+
83
+ imports = []
84
+
85
+ # Match: from X import Y
86
+ from_imports = re.findall(r'from\s+([^\s]+)\s+import', content)
87
+ imports.extend(from_imports)
88
+
89
+ # Match: import X
90
+ direct_imports = re.findall(r'^\s*import\s+([^\s;,]+)', content, re.MULTILINE)
91
+ imports.extend(direct_imports)
92
+
93
+ return imports
94
+
95
+
96
+ def infer_layer_from_import(import_path: str) -> str:
97
+ """
98
+ Infer layer from import path.
99
+
100
+ Args:
101
+ import_path: Import statement (e.g., "src.domain.entities")
102
+
103
+ Returns:
104
+ Layer name or 'external' for third-party imports
105
+ """
106
+ import_lower = import_path.lower()
107
+
108
+ # Check if it's a relative import - can't determine layer reliably
109
+ if import_path.startswith('.'):
110
+ # Exception: relative imports to 'ports' are application layer
111
+ if 'ports' in import_lower or '_port' in import_lower:
112
+ return 'application'
113
+ return 'unknown'
114
+
115
+ # Check for layer indicators in import path (order matters - more specific first)
116
+
117
+ # Ports are in application layer (interfaces), not integration
118
+ if 'ports' in import_lower or '_port' in import_lower:
119
+ return 'application'
120
+
121
+ # Domain layer
122
+ if '.domain.' in import_lower or '/domain/' in import_lower:
123
+ return 'domain'
124
+ if 'entities' in import_lower or 'models' in import_lower or 'value_objects' in import_lower:
125
+ return 'domain'
126
+
127
+ # Application layer
128
+ if '.application.' in import_lower or '/application/' in import_lower:
129
+ return 'application'
130
+ if 'use_case' in import_lower or 'usecase' in import_lower or 'use_cases' in import_lower:
131
+ return 'application'
132
+
133
+ # Presentation layer
134
+ if '.presentation.' in import_lower or '/presentation/' in import_lower:
135
+ return 'presentation'
136
+ if 'controller' in import_lower or 'handler' in import_lower:
137
+ return 'presentation'
138
+
139
+ # Integration layer (check after ports to avoid false positives)
140
+ if '.integration.' in import_lower or '/integration/' in import_lower:
141
+ return 'integration'
142
+ if 'infrastructure' in import_lower or 'adapter' in import_lower:
143
+ return 'integration'
144
+ # Only mark as integration if it has 'repository' but NOT 'port'
145
+ if 'repository' in import_lower and 'port' not in import_lower:
146
+ return 'integration'
147
+
148
+ # Third-party or standard library
149
+ return 'external'
150
+
151
+
152
+ def load_backend_convention() -> Dict:
153
+ """
154
+ Load backend convention from YAML file.
155
+
156
+ Returns:
157
+ Backend convention dict
158
+ """
159
+ if not BACKEND_CONVENTION.exists():
160
+ return {}
161
+
162
+ with open(BACKEND_CONVENTION, 'r', encoding='utf-8') as f:
163
+ data = yaml.safe_load(f)
164
+ return data.get('backend', {})
165
+
166
+
167
+ def get_layer_component_suffixes(convention: Dict) -> Dict[str, Dict[str, List[str]]]:
168
+ """
169
+ Extract layer -> component_type -> suffixes mapping from convention.
170
+
171
+ Args:
172
+ convention: Backend convention dict
173
+
174
+ Returns:
175
+ Dict like {
176
+ 'domain': {
177
+ 'entities': ['*.py'],
178
+ 'value_objects': ['*_vo.py', '*.py']
179
+ },
180
+ 'application': {...},
181
+ ...
182
+ }
183
+ """
184
+ result = {}
185
+
186
+ layers = convention.get('layers', {})
187
+ for layer_name, layer_config in layers.items():
188
+ result[layer_name] = {}
189
+
190
+ component_types = layer_config.get('component_types', [])
191
+ for component_type in component_types:
192
+ name = component_type.get('name', '')
193
+ suffix_config = component_type.get('suffix', {})
194
+
195
+ # Get Python suffixes
196
+ py_suffixes = suffix_config.get('python', '')
197
+ if py_suffixes:
198
+ # Parse comma-separated suffixes
199
+ suffixes = [s.strip() for s in py_suffixes.split(',')]
200
+ result[layer_name][name] = suffixes
201
+
202
+ return result
203
+
204
+
205
+ def matches_suffix_pattern(filename: str, pattern: str) -> bool:
206
+ """
207
+ Check if filename matches a suffix pattern.
208
+
209
+ Args:
210
+ filename: File name (e.g., "user_service.py")
211
+ pattern: Pattern (e.g., "*_service.py")
212
+
213
+ Returns:
214
+ True if matches
215
+ """
216
+ # Convert glob pattern to regex
217
+ # *_service.py -> .*_service\.py$
218
+ # *.py -> .*\.py$
219
+ regex_pattern = pattern.replace('.', r'\.')
220
+ regex_pattern = regex_pattern.replace('*', '.*')
221
+ regex_pattern = f'^{regex_pattern}$'
222
+
223
+ return bool(re.match(regex_pattern, filename))
224
+
225
+
226
+ def determine_expected_layer_from_suffix(filename: str, convention: Dict) -> Tuple[str, str]:
227
+ """
228
+ Determine expected layer and component type from filename suffix.
229
+
230
+ Args:
231
+ filename: File name (e.g., "user_service.py")
232
+ convention: Backend convention dict
233
+
234
+ Returns:
235
+ Tuple of (layer_name, component_type) or ('unknown', 'unknown')
236
+ """
237
+ layer_suffixes = get_layer_component_suffixes(convention)
238
+
239
+ # Check more specific patterns first (e.g., *_service.py before *.py)
240
+ # Sort by pattern length descending
241
+ for layer_name, component_types in layer_suffixes.items():
242
+ for component_type, suffixes in component_types.items():
243
+ # Sort suffixes by length descending (more specific first)
244
+ sorted_suffixes = sorted(suffixes, key=len, reverse=True)
245
+ for suffix_pattern in sorted_suffixes:
246
+ # Skip generic patterns - causes too many false positives
247
+ if suffix_pattern == '*.py':
248
+ continue
249
+ if matches_suffix_pattern(filename, suffix_pattern):
250
+ return layer_name, component_type
251
+
252
+ # Don't fall back to generic *.py - causes too many false positives
253
+ return 'unknown', 'unknown'
254
+
255
+
256
+ def find_python_files() -> list:
257
+ """
258
+ Find all Python files in python/ directory.
259
+
260
+ Returns:
261
+ List of Path objects
262
+ """
263
+ if not PYTHON_DIR.exists():
264
+ return []
265
+
266
+ python_files = []
267
+ for py_file in PYTHON_DIR.rglob("*.py"):
268
+ # Skip test files
269
+ if '/test/' in str(py_file) or py_file.name.startswith('test_'):
270
+ continue
271
+ # Skip __pycache__
272
+ if '__pycache__' in str(py_file):
273
+ continue
274
+ # Skip __init__.py (usually just imports)
275
+ if py_file.name == '__init__.py':
276
+ continue
277
+
278
+ python_files.append(py_file)
279
+
280
+ return python_files
281
+
282
+
283
+ @pytest.mark.coder
284
+ def test_python_follows_clean_architecture():
285
+ """
286
+ SPEC-CODER-ARCH-0001: Python code follows 4-layer clean architecture.
287
+
288
+ Clean architecture dependency rules:
289
+ - Domain → NOTHING (domain must be pure)
290
+ - Application → Domain only
291
+ - Presentation → Application, Domain
292
+ - Integration → Domain only
293
+
294
+ Forbidden dependencies:
295
+ - Domain → Application/Presentation/Integration
296
+ - Application → Presentation/Integration
297
+ - Integration → Application/Presentation
298
+
299
+ Given: Python files in python/
300
+ When: Checking import statements
301
+ Then: No forbidden cross-layer dependencies
302
+ """
303
+ python_files = find_python_files()
304
+
305
+ if not python_files:
306
+ pytest.skip("No Python files found to validate")
307
+
308
+ violations = []
309
+
310
+ for py_file in python_files:
311
+ layer = determine_layer_from_path(py_file)
312
+
313
+ # Skip files we can't categorize
314
+ if layer == 'unknown':
315
+ continue
316
+
317
+ imports = extract_python_imports(py_file)
318
+
319
+ for imp in imports:
320
+ target_layer = infer_layer_from_import(imp)
321
+
322
+ # Skip external imports (third-party libraries)
323
+ if target_layer == 'external' or target_layer == 'unknown':
324
+ continue
325
+
326
+ # Check dependency rules
327
+ violation = None
328
+
329
+ if layer == 'domain':
330
+ # Domain must not import from any other layer
331
+ if target_layer in ['application', 'presentation', 'integration']:
332
+ violation = f"Domain layer cannot import from {target_layer}"
333
+
334
+ elif layer == 'application':
335
+ # Application can only import from domain
336
+ if target_layer in ['presentation', 'integration']:
337
+ violation = f"Application layer cannot import from {target_layer}"
338
+
339
+ elif layer == 'integration':
340
+ # Integration can import from application (for ports) and domain
341
+ # See backend.convention.yaml line 402-403: integration -> [application, domain]
342
+ if target_layer == 'presentation':
343
+ violation = f"Integration layer cannot import from {target_layer}"
344
+
345
+ if violation:
346
+ rel_path = py_file.relative_to(REPO_ROOT)
347
+ violations.append(
348
+ f"{rel_path}\\n"
349
+ f" Layer: {layer}\\n"
350
+ f" Import: {imp}\\n"
351
+ f" Violation: {violation}"
352
+ )
353
+
354
+ if violations:
355
+ pytest.fail(
356
+ f"\\n\\nFound {len(violations)} architecture violations:\\n\\n" +
357
+ "\\n\\n".join(violations[:10]) +
358
+ (f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
359
+ )
360
+
361
+
362
+ @pytest.mark.coder
363
+ def test_domain_layer_is_pure():
364
+ """
365
+ SPEC-CODER-ARCH-0002: Domain layer has no external dependencies.
366
+
367
+ Domain layer should only import:
368
+ - Standard library (typing, dataclasses, etc.)
369
+ - Other domain modules
370
+
371
+ Should NOT import:
372
+ - Third-party libraries (except type hints)
373
+ - Application/Presentation/Integration layers
374
+ - Database/API libraries
375
+
376
+ Given: Python files in domain/ directories
377
+ When: Checking imports
378
+ Then: Only standard library and domain imports
379
+ """
380
+ python_files = find_python_files()
381
+
382
+ if not python_files:
383
+ pytest.skip("No Python files found to validate")
384
+
385
+ # Standard library modules (allowed in domain)
386
+ # Note: time is allowed for time.perf_counter() timing measurements (pure function)
387
+ ALLOWED_STDLIB = {
388
+ '__future__', 'typing', 'dataclasses', 'enum', 'abc', 'datetime', 'uuid',
389
+ 'collections', 'itertools', 'functools', 're', 'json', 'pathlib',
390
+ 'hashlib', 'warnings', 'types', 'random', 'math', 'decimal',
391
+ 'copy', 'operator', 'string', 'textwrap', 'io', 'struct', 'time'
392
+ }
393
+
394
+ violations = []
395
+
396
+ for py_file in python_files:
397
+ layer = determine_layer_from_path(py_file)
398
+
399
+ # Only check domain layer
400
+ if layer != 'domain':
401
+ continue
402
+
403
+ imports = extract_python_imports(py_file)
404
+
405
+ for imp in imports:
406
+ # Skip relative imports (internal to domain)
407
+ if imp.startswith('.'):
408
+ continue
409
+
410
+ # Get root module name
411
+ root_module = imp.split('.')[0]
412
+
413
+ # Check if it's allowed standard library
414
+ if root_module in ALLOWED_STDLIB:
415
+ continue
416
+
417
+ # Check if it's from contracts/ (neutral DTO boundary - allowed per dto.convention.yaml)
418
+ if root_module == 'contracts':
419
+ continue
420
+
421
+ # Check if it's domain import
422
+ if 'domain' in imp.lower():
423
+ continue
424
+
425
+ # Check if it's from the same package
426
+ if 'src' in imp or root_module in str(py_file):
427
+ continue
428
+
429
+ # Otherwise it's a violation
430
+ rel_path = py_file.relative_to(REPO_ROOT)
431
+ violations.append(
432
+ f"{rel_path}\\n"
433
+ f" Import: {imp}\\n"
434
+ f" Issue: Domain layer should not import external libraries"
435
+ )
436
+
437
+ if violations:
438
+ pytest.fail(
439
+ f"\\n\\nFound {len(violations)} domain purity violations:\\n\\n" +
440
+ "\\n\\n".join(violations[:10]) +
441
+ (f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
442
+ f"\\n\\nDomain layer should only import:\\n" +
443
+ f" - Standard library: {', '.join(sorted(ALLOWED_STDLIB))}\\n" +
444
+ f" - Other domain modules"
445
+ )
446
+
447
+ @pytest.mark.coder
448
+ def test_python_component_naming_follows_conventions():
449
+ """
450
+ SPEC-CODER-ARCH-PY-0003: Python components follow naming conventions.
451
+
452
+ Component naming rules from conventions (with flexible layer placement):
453
+ - Controllers: *_controller.py (presentation layer)
454
+ - Services: *_service.py (domain layer)
455
+ - Repositories: *_repository.py (integration layer)
456
+ - Use Cases: *_use_case.py (application layer)
457
+ - Entities: *.py (domain layer ONLY - pure business objects)
458
+ - DTOs: *_dto.py (application layer)
459
+ - Validators: *_validator.py (presentation|application|domain - depends on what they validate)
460
+ * presentation: input shape/format validation
461
+ * application: cross-cutting orchestration checks
462
+ * domain: business invariants (often inline, not separate files)
463
+ - Mappers: *_mapper.py (integration|application - depends on mapping responsibility)
464
+ * integration: boundary mappers (IO ↔ internal types)
465
+ * application: internal use-case transforms
466
+ - Clients: *_client.py (integration layer)
467
+ - Stores: *_store.py or *_storage.py (integration layer)
468
+ - Handlers: *_handler.py (application layer)
469
+ - Guards: *_guard.py (presentation layer)
470
+ - Middleware: *_middleware.py (presentation layer)
471
+ - Ports: protocols.py or *_port.py (application layer)
472
+ - Events: *_event.py (domain layer)
473
+ - Exceptions: *_exception.py or exceptions.py (domain layer)
474
+ - Engines: *_engine.py, *_analyzer.py, *_processor.py (integration layer)
475
+
476
+ Given: Python files with recognizable suffixes
477
+ When: Checking file locations
478
+ Then: Files are in correct layers per their suffixes
479
+ """
480
+ python_files = find_python_files()
481
+
482
+ if not python_files:
483
+ pytest.skip("No Python files found to validate")
484
+
485
+ backend_conv = load_backend_convention()
486
+
487
+ if not backend_conv:
488
+ pytest.skip("Backend convention file not found")
489
+
490
+ # Flexible layer rules: component type -> allowed layers
491
+ # These component types can legitimately appear in multiple layers depending on purpose
492
+ FLEXIBLE_LAYERS = {
493
+ 'validators': ['presentation', 'application', 'domain', 'integration'], # Validation at any boundary
494
+ 'mappers': ['integration', 'application'], # Depends on mapping responsibility
495
+ 'monitors': ['domain', 'application', 'integration'], # Domain: business state, Integration: infra
496
+ 'services': ['domain', 'application'], # Domain services (logic) and Application services (orchestration)
497
+ 'handlers': ['application', 'domain'], # Application: CQRS, Domain: domain event handlers
498
+ 'ports': ['application', 'integration'], # Application defines ports, Integration can have internal ports
499
+ 'engines': ['integration', 'domain'], # Integration: external, Domain: pure computation
500
+ 'formatters': ['integration', 'domain'], # Integration: output, Domain: value formatting
501
+ }
502
+
503
+ violations = []
504
+
505
+ for py_file in python_files:
506
+ actual_layer = determine_layer_from_path(py_file)
507
+
508
+ # Skip files in unknown locations
509
+ if actual_layer == 'unknown':
510
+ continue
511
+
512
+ filename = py_file.name
513
+
514
+ # Determine expected layer from suffix
515
+ expected_layer, component_type = determine_expected_layer_from_suffix(filename, backend_conv)
516
+
517
+ # Skip unknown component types
518
+ if expected_layer == 'unknown':
519
+ continue
520
+
521
+ # Skip generic "entities" matches from *.py pattern - too broad, causes false positives
522
+ # Only enforce entities rule if file is actually in domain/entities/ subdirectory
523
+ if component_type == 'entities' and actual_layer != 'domain':
524
+ # Check if file is in an entities subdirectory
525
+ if '/entities/' not in str(py_file):
526
+ continue # Skip - this is a false positive from generic *.py pattern
527
+
528
+ # Check if this component type has flexible layer rules
529
+ if component_type in FLEXIBLE_LAYERS:
530
+ allowed_layers = FLEXIBLE_LAYERS[component_type]
531
+ if actual_layer not in allowed_layers:
532
+ rel_path = py_file.relative_to(REPO_ROOT)
533
+ violations.append(
534
+ f"{rel_path}\n"
535
+ f" Component Type: {component_type}\n"
536
+ f" Allowed Layers: {', '.join(allowed_layers)}\n"
537
+ f" Actual Layer: {actual_layer}\n"
538
+ f" Issue: {component_type} must be in one of: {', '.join(allowed_layers)}"
539
+ )
540
+ # Otherwise, enforce strict layer placement
541
+ elif expected_layer != actual_layer:
542
+ rel_path = py_file.relative_to(REPO_ROOT)
543
+ violations.append(
544
+ f"{rel_path}\n"
545
+ f" Component Type: {component_type}\n"
546
+ f" Expected Layer: {expected_layer}\n"
547
+ f" Actual Layer: {actual_layer}\n"
548
+ f" Issue: File suffix indicates {expected_layer} layer but found in {actual_layer}"
549
+ )
550
+
551
+ if violations:
552
+ pytest.fail(
553
+ f"\n\nFound {len(violations)} component naming/placement violations:\n\n" +
554
+ "\n\n".join(violations[:10]) +
555
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
556
+ f"\n\nComponent suffixes must match their layer placement.\n" +
557
+ f"See: atdd/coder/conventions/backend.convention.yaml"
558
+ )
559
+
560
+
561
+ @pytest.mark.coder
562
+ def test_python_layers_have_proper_component_organization():
563
+ """
564
+ SPEC-CODER-ARCH-PY-0004: Each layer has proper component type grouping.
565
+
566
+ Layer organization rules:
567
+ - Domain layer: entities/, value_objects/, aggregates/, services/, specifications/, events/, exceptions/
568
+ - Application layer: use_cases/, handlers/, ports/, dtos/, policies/, workflows/
569
+ - Presentation layer: controllers/, routes/, serializers/, validators/, middleware/, guards/, views/
570
+ - Integration layer: repositories/, clients/, caches/, engines/, formatters/, notifiers/, queues/, stores/, mappers/, schedulers/, monitors/
571
+
572
+ Given: Python files organized in layers
573
+ When: Checking directory structure
574
+ Then: Component types are in correct subdirectories
575
+ """
576
+ python_files = find_python_files()
577
+
578
+ if not python_files:
579
+ pytest.skip("No Python files found to validate")
580
+
581
+ backend_conv = load_backend_convention()
582
+
583
+ if not backend_conv:
584
+ pytest.skip("Backend convention file not found")
585
+
586
+ violations = []
587
+
588
+ for py_file in python_files:
589
+ layer = determine_layer_from_path(py_file)
590
+
591
+ # Skip unknown layers
592
+ if layer == 'unknown':
593
+ continue
594
+
595
+ path_str = str(py_file)
596
+ filename = py_file.name
597
+
598
+ # Determine expected component type from suffix
599
+ expected_layer, component_type = determine_expected_layer_from_suffix(filename, backend_conv)
600
+
601
+ # If can't determine, skip
602
+ if expected_layer == 'unknown':
603
+ continue
604
+
605
+ # Check if file is in a component type subdirectory
606
+ # Expected pattern: .../layer/component_type/file.py
607
+ # e.g., .../domain/entities/user.py
608
+ # or .../application/use_cases/create_user_use_case.py
609
+
610
+ # Files commonly placed at layer root (no subdirectory required)
611
+ layer_root_allowed = [
612
+ 'exceptions.py', 'errors.py', # Exception definitions
613
+ '__init__.py', # Package init
614
+ 'types.py', 'protocols.py', # Type definitions and protocols
615
+ ]
616
+
617
+ # Skip validation for files commonly at layer root
618
+ if filename in layer_root_allowed:
619
+ continue
620
+
621
+ # Alternative directory patterns that are equivalent to convention patterns
622
+ # e.g., "api" is commonly used instead of "routes" or "controllers"
623
+ ALTERNATIVE_DIRS = {
624
+ 'routes': ['api', 'endpoints'], # FastAPI/Flask common patterns
625
+ 'controllers': ['api', 'handlers'], # API handlers pattern
626
+ 'use_cases': ['usecases', 'services'], # Common alternatives
627
+ 'services': ['usecases'], # Services often in usecases dir
628
+ 'validators': ['services'], # Validation services in services dir
629
+ 'handlers': ['services'], # Handler services in services dir
630
+ 'monitors': ['services', 'trackers'], # Monitoring/tracking services
631
+ 'engines': ['services', 'analyzers', 'processors'], # Computation engines
632
+ 'formatters': ['services', 'generators'], # Formatting/generation services
633
+ }
634
+
635
+ # Component types that can be at layer root (no subdirectory required)
636
+ # These are commonly placed directly in the layer directory
637
+ LAYER_ROOT_ALLOWED_COMPONENTS = [
638
+ 'handlers', # Domain/application handlers often at root
639
+ 'use_cases', # Small wagons may have single use case at root
640
+ 'formatters', # Simple formatters at domain root
641
+ 'services', # Simple services at layer root
642
+ ]
643
+
644
+ # Skip if component type is allowed at layer root and file is directly in layer
645
+ if component_type in LAYER_ROOT_ALLOWED_COMPONENTS:
646
+ # Check if file is directly in the layer directory (not in a subdirectory)
647
+ parent_dir = py_file.parent.name
648
+ if parent_dir == layer:
649
+ continue
650
+
651
+ # Check if component type directory (or alternative) is in path
652
+ dirs_to_check = [component_type] + ALTERNATIVE_DIRS.get(component_type, [])
653
+ found_valid_dir = any(f'/{dir_name}/' in path_str for dir_name in dirs_to_check)
654
+
655
+ if not found_valid_dir:
656
+ # Only flag if this is a clear architecture setup (has layer directory)
657
+ if f'/{layer}/' in path_str:
658
+ rel_path = py_file.relative_to(REPO_ROOT)
659
+ violations.append(
660
+ f"{rel_path}\n"
661
+ f" Layer: {layer}\n"
662
+ f" Component Type: {component_type}\n"
663
+ f" Issue: Should be in {layer}/{component_type}/ subdirectory"
664
+ )
665
+
666
+ if violations:
667
+ pytest.fail(
668
+ f"\n\nFound {len(violations)} component organization violations:\n\n" +
669
+ "\n\n".join(violations[:10]) +
670
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
671
+ f"\n\nComponents should be organized in layer/component_type/ subdirectories.\n" +
672
+ f"Example: domain/entities/user.py, application/use_cases/create_user_use_case.py\n" +
673
+ f"See: atdd/coder/conventions/backend.convention.yaml"
674
+ )