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,421 @@
1
+ """
2
+ Test use case structure follows best practices.
3
+
4
+ Validates:
5
+ - Use cases have single responsibility
6
+ - Use cases have proper input/output structure
7
+ - Use cases have execute/call method
8
+ - Use cases don't directly access database/API
9
+ - Use cases coordinate through ports/interfaces
10
+
11
+ Inspired by: .claude/utils/coder/usecase.py
12
+ But: Self-contained, no utility dependencies
13
+ """
14
+
15
+ import pytest
16
+ import re
17
+ import ast
18
+ from pathlib import Path
19
+ from typing import List, Tuple, Set
20
+
21
+
22
+ # Path constants
23
+ REPO_ROOT = Path(__file__).resolve().parents[3]
24
+ PYTHON_DIR = REPO_ROOT / "python"
25
+ DART_DIRS = [REPO_ROOT / "lib", REPO_ROOT / "dart"]
26
+ TS_DIRS = [REPO_ROOT / "supabase" / "functions", REPO_ROOT / "typescript"]
27
+
28
+
29
+ def find_usecase_files() -> List[Tuple[Path, str]]:
30
+ """
31
+ Find all use case files across languages.
32
+
33
+ Returns:
34
+ List of (file_path, language) tuples
35
+ """
36
+ usecase_files = []
37
+
38
+ # Python use cases
39
+ if PYTHON_DIR.exists():
40
+ for py_file in PYTHON_DIR.rglob("*_use_case.py"):
41
+ if '__pycache__' not in str(py_file):
42
+ usecase_files.append((py_file, 'python'))
43
+ for py_file in PYTHON_DIR.rglob("*usecase.py"):
44
+ if '__pycache__' not in str(py_file) and not py_file.name.endswith('_use_case.py'):
45
+ usecase_files.append((py_file, 'python'))
46
+
47
+ # Dart use cases
48
+ for dart_dir in DART_DIRS:
49
+ if dart_dir.exists():
50
+ for dart_file in dart_dir.rglob("*_usecases.dart"):
51
+ if '/build/' not in str(dart_file):
52
+ usecase_files.append((dart_file, 'dart'))
53
+ for dart_file in dart_dir.rglob("*_use_case.dart"):
54
+ if '/build/' not in str(dart_file):
55
+ usecase_files.append((dart_file, 'dart'))
56
+
57
+ # TypeScript use cases
58
+ for ts_dir in TS_DIRS:
59
+ if ts_dir.exists():
60
+ for ts_file in ts_dir.rglob("*-use-case.ts"):
61
+ if 'node_modules' not in str(ts_file):
62
+ usecase_files.append((ts_file, 'typescript'))
63
+ for ts_file in ts_dir.rglob("*-usecase.ts"):
64
+ if 'node_modules' not in str(ts_file):
65
+ usecase_files.append((ts_file, 'typescript'))
66
+
67
+ return usecase_files
68
+
69
+
70
+ def extract_python_classes(file_path: Path) -> List[str]:
71
+ """
72
+ Extract class names from Python file.
73
+
74
+ Args:
75
+ file_path: Path to Python file
76
+
77
+ Returns:
78
+ List of class names
79
+ """
80
+ try:
81
+ with open(file_path, 'r', encoding='utf-8') as f:
82
+ content = f.read()
83
+ except Exception:
84
+ return []
85
+
86
+ try:
87
+ tree = ast.parse(content)
88
+ except SyntaxError:
89
+ return []
90
+
91
+ classes = []
92
+ for node in ast.walk(tree):
93
+ if isinstance(node, ast.ClassDef):
94
+ classes.append(node.name)
95
+
96
+ return classes
97
+
98
+
99
+ def check_python_usecase_methods(file_path: Path) -> List[Tuple[str, List[str]]]:
100
+ """
101
+ Check Python use case classes for required methods.
102
+
103
+ Args:
104
+ file_path: Path to Python use case file
105
+
106
+ Returns:
107
+ List of (class_name, method_names) tuples
108
+ """
109
+ try:
110
+ with open(file_path, 'r', encoding='utf-8') as f:
111
+ content = f.read()
112
+ except Exception:
113
+ return []
114
+
115
+ try:
116
+ tree = ast.parse(content)
117
+ except SyntaxError:
118
+ return []
119
+
120
+ results = []
121
+
122
+ for node in ast.walk(tree):
123
+ if isinstance(node, ast.ClassDef):
124
+ methods = []
125
+ for item in node.body:
126
+ if isinstance(item, ast.FunctionDef):
127
+ methods.append(item.name)
128
+ results.append((node.name, methods))
129
+
130
+ return results
131
+
132
+
133
+ def check_for_direct_database_access(file_path: Path, language: str) -> List[str]:
134
+ """
135
+ Check if use case directly accesses database/API.
136
+
137
+ Args:
138
+ file_path: Path to use case file
139
+ language: File language (python, dart, typescript)
140
+
141
+ Returns:
142
+ List of violation messages
143
+ """
144
+ try:
145
+ with open(file_path, 'r', encoding='utf-8') as f:
146
+ content = f.read()
147
+ except Exception:
148
+ return []
149
+
150
+ violations = []
151
+
152
+ # Patterns that indicate direct database/API access
153
+ if language == 'python':
154
+ FORBIDDEN_PATTERNS = [
155
+ (r'import\s+psycopg2', 'Direct PostgreSQL import'),
156
+ (r'import\s+pymongo', 'Direct MongoDB import'),
157
+ (r'import\s+redis', 'Direct Redis import'),
158
+ (r'import\s+requests', 'Direct HTTP import (use repository)'),
159
+ (r'import\s+httpx', 'Direct HTTP import (use repository)'),
160
+ (r'from\s+sqlalchemy', 'Direct SQLAlchemy import'),
161
+ (r'from\s+django\.db', 'Direct Django DB import'),
162
+ ]
163
+ elif language == 'dart':
164
+ FORBIDDEN_PATTERNS = [
165
+ (r"import\s+['\"]package:sqflite", 'Direct SQLite import'),
166
+ (r"import\s+['\"]package:http/", 'Direct HTTP import (use repository)'),
167
+ (r"import\s+['\"]package:dio/", 'Direct HTTP client import (use repository)'),
168
+ (r"import\s+['\"]package:supabase/", 'Direct Supabase import (use repository)'),
169
+ ]
170
+ else: # typescript
171
+ FORBIDDEN_PATTERNS = [
172
+ (r"import.*?['\"]\@supabase/supabase-js['\"]", 'Direct Supabase import (use repository)'),
173
+ (r"import.*?['\"]axios['\"]", 'Direct HTTP import (use repository)'),
174
+ (r"import.*?['\"]node-fetch['\"]", 'Direct HTTP import (use repository)'),
175
+ (r"import.*?['\"]pg['\"]", 'Direct PostgreSQL import'),
176
+ ]
177
+
178
+ for pattern, message in FORBIDDEN_PATTERNS:
179
+ if re.search(pattern, content):
180
+ violations.append(message)
181
+
182
+ return list(set(violations))
183
+
184
+
185
+ def count_responsibilities(file_path: Path, language: str) -> int:
186
+ """
187
+ Count number of distinct responsibilities in use case file.
188
+
189
+ Heuristic: Count number of classes/functions that look like use cases.
190
+
191
+ Args:
192
+ file_path: Path to use case file
193
+ language: File language
194
+
195
+ Returns:
196
+ Number of responsibilities (1 is ideal)
197
+ """
198
+ if language == 'python':
199
+ classes = extract_python_classes(file_path)
200
+ # Filter to use case classes (exclude helpers)
201
+ usecase_classes = [c for c in classes if 'UseCase' in c or 'Command' in c or 'Query' in c]
202
+ return len(usecase_classes)
203
+
204
+ # For other languages, we'll be lenient and return 1
205
+ return 1
206
+
207
+
208
+ @pytest.mark.coder
209
+ def test_usecases_have_single_responsibility():
210
+ """
211
+ SPEC-CODER-USECASE-0001: Use cases have single responsibility.
212
+
213
+ Each use case file should contain ONE use case class.
214
+ Multiple use cases should be in separate files.
215
+
216
+ Single Responsibility Principle:
217
+ - One use case = one business workflow
218
+ - Clear, focused purpose
219
+ - Easy to test and maintain
220
+
221
+ Given: Use case files (*_use_case.py, *-use-case.ts, etc.)
222
+ When: Checking number of use case classes
223
+ Then: Each file has exactly one use case
224
+ """
225
+ usecase_files = find_usecase_files()
226
+
227
+ if not usecase_files:
228
+ pytest.skip("No use case files found to validate")
229
+
230
+ violations = []
231
+
232
+ for file_path, language in usecase_files:
233
+ count = count_responsibilities(file_path, language)
234
+
235
+ if count > 1:
236
+ rel_path = file_path.relative_to(REPO_ROOT)
237
+ violations.append(
238
+ f"{rel_path}\n"
239
+ f" Language: {language}\n"
240
+ f" Use Cases: {count}\n"
241
+ f" Issue: File contains {count} use cases, should have 1"
242
+ )
243
+
244
+ if violations:
245
+ pytest.fail(
246
+ f"\n\nFound {len(violations)} single responsibility violations:\n\n" +
247
+ "\n\n".join(violations[:10]) +
248
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
249
+ f"\n\nEach use case file should contain exactly one use case.\n" +
250
+ f"Split multiple use cases into separate files."
251
+ )
252
+
253
+
254
+ @pytest.mark.coder
255
+ def test_usecases_have_execute_method():
256
+ """
257
+ SPEC-CODER-USECASE-0002: Use cases have execute/call method.
258
+
259
+ Use cases should have a clear entry point:
260
+ - Python: execute(), __call__(), or run()
261
+ - Dart: call(), execute()
262
+ - TypeScript: execute(), run()
263
+
264
+ This makes use cases invokable and testable.
265
+
266
+ Given: Python use case files
267
+ When: Checking for execute methods
268
+ Then: Each use case has an entry point method
269
+ """
270
+ usecase_files = find_usecase_files()
271
+
272
+ # Only check Python for now (easier to parse)
273
+ python_usecases = [(f, l) for f, l in usecase_files if l == 'python']
274
+
275
+ if not python_usecases:
276
+ pytest.skip("No Python use case files found to validate")
277
+
278
+ violations = []
279
+
280
+ VALID_METHODS = {'execute', '__call__', 'run', 'handle'}
281
+
282
+ for file_path, _ in python_usecases:
283
+ class_methods = check_python_usecase_methods(file_path)
284
+
285
+ for class_name, methods in class_methods:
286
+ # Skip non-usecase classes
287
+ if 'UseCase' not in class_name and 'Command' not in class_name and 'Query' not in class_name:
288
+ continue
289
+
290
+ # Skip private/helper classes
291
+ if class_name.startswith('_'):
292
+ continue
293
+
294
+ # Check if has valid entry point
295
+ has_entry_point = any(method in VALID_METHODS for method in methods)
296
+
297
+ if not has_entry_point and len(methods) > 0:
298
+ rel_path = file_path.relative_to(REPO_ROOT)
299
+ violations.append(
300
+ f"{rel_path}\n"
301
+ f" Class: {class_name}\n"
302
+ f" Methods: {', '.join(methods)}\n"
303
+ f" Issue: Use case missing entry point (execute, __call__, run, handle)"
304
+ )
305
+
306
+ if violations:
307
+ pytest.fail(
308
+ f"\n\nFound {len(violations)} missing entry point violations:\n\n" +
309
+ "\n\n".join(violations[:10]) +
310
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
311
+ f"\n\nUse cases should have a clear entry point method:\n" +
312
+ f" - execute() - explicit, clear\n" +
313
+ f" - __call__() - makes use case invokable\n" +
314
+ f" - run() or handle() - also acceptable"
315
+ )
316
+
317
+
318
+ @pytest.mark.coder
319
+ def test_usecases_dont_directly_access_database():
320
+ """
321
+ SPEC-CODER-USECASE-0003: Use cases don't directly access database/API.
322
+
323
+ Use cases should coordinate through ports/interfaces:
324
+ - Don't import database libraries directly
325
+ - Don't import HTTP clients directly
326
+ - Use repository interfaces instead
327
+ - Use service interfaces for external APIs
328
+
329
+ Clean Architecture principle:
330
+ - Use cases are in Application layer
331
+ - Database/API access is in Integration layer
332
+ - Use cases depend on abstractions (ports)
333
+
334
+ Given: Use case files
335
+ When: Checking imports
336
+ Then: No direct database/API library imports
337
+ """
338
+ usecase_files = find_usecase_files()
339
+
340
+ if not usecase_files:
341
+ pytest.skip("No use case files found to validate")
342
+
343
+ violations = []
344
+
345
+ for file_path, language in usecase_files:
346
+ direct_access = check_for_direct_database_access(file_path, language)
347
+
348
+ if direct_access:
349
+ rel_path = file_path.relative_to(REPO_ROOT)
350
+ violations.append(
351
+ f"{rel_path}\n"
352
+ f" Language: {language}\n"
353
+ f" Violations:\n" +
354
+ "\n".join(f" - {v}" for v in direct_access)
355
+ )
356
+
357
+ if violations:
358
+ pytest.fail(
359
+ f"\n\nFound {len(violations)} direct database/API access violations:\n\n" +
360
+ "\n\n".join(violations[:10]) +
361
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
362
+ f"\n\nUse cases should not directly import database/API libraries.\n" +
363
+ f"Use repository interfaces instead (Dependency Inversion Principle)."
364
+ )
365
+
366
+
367
+ @pytest.mark.coder
368
+ def test_usecases_are_in_application_layer():
369
+ """
370
+ SPEC-CODER-USECASE-0004: Use cases are in application layer.
371
+
372
+ Use cases should live in the application layer:
373
+ - .../application/use_cases/
374
+ - .../application/usecases/
375
+ - .../usecases/ (if no explicit layers)
376
+
377
+ Not in:
378
+ - domain/ (pure business logic)
379
+ - presentation/ (UI/API)
380
+ - integration/ (database/external services)
381
+
382
+ Given: Use case files
383
+ When: Checking file paths
384
+ Then: All use cases in application layer
385
+ """
386
+ usecase_files = find_usecase_files()
387
+
388
+ if not usecase_files:
389
+ pytest.skip("No use case files found to validate")
390
+
391
+ violations = []
392
+
393
+ for file_path, language in usecase_files:
394
+ path_str = str(file_path).lower()
395
+
396
+ # Check if in application layer or usecases directory
397
+ in_application = '/application/' in path_str or '/usecases/' in path_str or '/use_cases/' in path_str
398
+
399
+ # Check if in wrong layer
400
+ in_domain = '/domain/' in path_str and '/application/' not in path_str
401
+ in_presentation = '/presentation/' in path_str
402
+ in_integration = '/integration/' in path_str or '/infrastructure/' in path_str or '/data/' in path_str
403
+
404
+ if (in_domain or in_presentation or in_integration) and not in_application:
405
+ rel_path = file_path.relative_to(REPO_ROOT)
406
+ violations.append(
407
+ f"{rel_path}\n"
408
+ f" Issue: Use case in wrong layer (should be in application/)"
409
+ )
410
+
411
+ if violations:
412
+ pytest.fail(
413
+ f"\n\nFound {len(violations)} layer placement violations:\n\n" +
414
+ "\n\n".join(violations[:10]) +
415
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
416
+ f"\n\nUse cases should be in application layer.\n" +
417
+ f"Expected paths:\n" +
418
+ f" - .../application/use_cases/\n" +
419
+ f" - .../application/usecases/\n" +
420
+ f" - .../usecases/ (if no explicit layers)"
421
+ )