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.
Files changed (184) hide show
  1. atdd/__init__.py +6 -0
  2. atdd/__main__.py +4 -0
  3. atdd/cli.py +404 -0
  4. atdd/coach/__init__.py +0 -0
  5. atdd/coach/commands/__init__.py +0 -0
  6. atdd/coach/commands/add_persistence_metadata.py +215 -0
  7. atdd/coach/commands/analyze_migrations.py +188 -0
  8. atdd/coach/commands/consumers.py +720 -0
  9. atdd/coach/commands/infer_governance_status.py +149 -0
  10. atdd/coach/commands/initializer.py +177 -0
  11. atdd/coach/commands/interface.py +1078 -0
  12. atdd/coach/commands/inventory.py +565 -0
  13. atdd/coach/commands/migration.py +240 -0
  14. atdd/coach/commands/registry.py +1560 -0
  15. atdd/coach/commands/session.py +430 -0
  16. atdd/coach/commands/sync.py +405 -0
  17. atdd/coach/commands/test_interface.py +399 -0
  18. atdd/coach/commands/test_runner.py +141 -0
  19. atdd/coach/commands/tests/__init__.py +1 -0
  20. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  21. atdd/coach/commands/traceability.py +4264 -0
  22. atdd/coach/conventions/session.convention.yaml +754 -0
  23. atdd/coach/overlays/__init__.py +2 -0
  24. atdd/coach/overlays/claude.md +2 -0
  25. atdd/coach/schemas/config.schema.json +34 -0
  26. atdd/coach/schemas/manifest.schema.json +101 -0
  27. atdd/coach/templates/ATDD.md +282 -0
  28. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  29. atdd/coach/utils/__init__.py +0 -0
  30. atdd/coach/utils/graph/__init__.py +0 -0
  31. atdd/coach/utils/graph/urn.py +875 -0
  32. atdd/coach/validators/__init__.py +0 -0
  33. atdd/coach/validators/shared_fixtures.py +365 -0
  34. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  35. atdd/coach/validators/test_registry.py +575 -0
  36. atdd/coach/validators/test_session_validation.py +1183 -0
  37. atdd/coach/validators/test_traceability.py +448 -0
  38. atdd/coach/validators/test_update_feature_paths.py +108 -0
  39. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  40. atdd/coder/__init__.py +1 -0
  41. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  42. atdd/coder/conventions/backend.convention.yaml +460 -0
  43. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  44. atdd/coder/conventions/commons.convention.yaml +460 -0
  45. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  46. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  47. atdd/coder/conventions/design.convention.yaml +327 -0
  48. atdd/coder/conventions/design.recipe.yaml +273 -0
  49. atdd/coder/conventions/dto.convention.yaml +660 -0
  50. atdd/coder/conventions/frontend.convention.yaml +542 -0
  51. atdd/coder/conventions/green.convention.yaml +1012 -0
  52. atdd/coder/conventions/presentation.convention.yaml +587 -0
  53. atdd/coder/conventions/refactor.convention.yaml +535 -0
  54. atdd/coder/conventions/technology.convention.yaml +206 -0
  55. atdd/coder/conventions/tests/__init__.py +0 -0
  56. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  57. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  58. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  59. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  60. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  61. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  62. atdd/coder/conventions/train.convention.yaml +325 -0
  63. atdd/coder/conventions/verification.protocol.yaml +53 -0
  64. atdd/coder/schemas/design_system.schema.json +361 -0
  65. atdd/coder/validators/__init__.py +0 -0
  66. atdd/coder/validators/test_commons_structure.py +485 -0
  67. atdd/coder/validators/test_complexity.py +416 -0
  68. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  69. atdd/coder/validators/test_design_system_compliance.py +413 -0
  70. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  71. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  72. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  73. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  74. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  75. atdd/coder/validators/test_import_boundaries.py +396 -0
  76. atdd/coder/validators/test_init_file_urns.py +593 -0
  77. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  78. atdd/coder/validators/test_presentation_convention.py +260 -0
  79. atdd/coder/validators/test_python_architecture.py +674 -0
  80. atdd/coder/validators/test_quality_metrics.py +420 -0
  81. atdd/coder/validators/test_station_master_pattern.py +244 -0
  82. atdd/coder/validators/test_train_infrastructure.py +454 -0
  83. atdd/coder/validators/test_train_urns.py +293 -0
  84. atdd/coder/validators/test_typescript_architecture.py +616 -0
  85. atdd/coder/validators/test_usecase_structure.py +421 -0
  86. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  87. atdd/conftest.py +126 -0
  88. atdd/planner/__init__.py +1 -0
  89. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  90. atdd/planner/conventions/appendix.convention.yaml +187 -0
  91. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  92. atdd/planner/conventions/component.convention.yaml +670 -0
  93. atdd/planner/conventions/criteria.convention.yaml +141 -0
  94. atdd/planner/conventions/feature.convention.yaml +371 -0
  95. atdd/planner/conventions/interface.convention.yaml +382 -0
  96. atdd/planner/conventions/steps.convention.yaml +141 -0
  97. atdd/planner/conventions/train.convention.yaml +552 -0
  98. atdd/planner/conventions/wagon.convention.yaml +275 -0
  99. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  100. atdd/planner/schemas/acceptance.schema.json +336 -0
  101. atdd/planner/schemas/appendix.schema.json +78 -0
  102. atdd/planner/schemas/component.schema.json +114 -0
  103. atdd/planner/schemas/feature.schema.json +197 -0
  104. atdd/planner/schemas/train.schema.json +192 -0
  105. atdd/planner/schemas/wagon.schema.json +281 -0
  106. atdd/planner/schemas/wmbt.schema.json +59 -0
  107. atdd/planner/validators/__init__.py +0 -0
  108. atdd/planner/validators/conftest.py +5 -0
  109. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  110. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  111. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  112. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  113. atdd/planner/validators/test_plan_wagons.py +174 -0
  114. atdd/planner/validators/test_train_validation.py +514 -0
  115. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  116. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  117. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  118. atdd/tester/__init__.py +1 -0
  119. atdd/tester/conventions/artifact.convention.yaml +257 -0
  120. atdd/tester/conventions/contract.convention.yaml +1009 -0
  121. atdd/tester/conventions/filename.convention.yaml +555 -0
  122. atdd/tester/conventions/migration.convention.yaml +509 -0
  123. atdd/tester/conventions/red.convention.yaml +797 -0
  124. atdd/tester/conventions/routing.convention.yaml +51 -0
  125. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  126. atdd/tester/schemas/a11y.tmpl.json +17 -0
  127. atdd/tester/schemas/artifact.schema.json +189 -0
  128. atdd/tester/schemas/contract.schema.json +591 -0
  129. atdd/tester/schemas/contract.tmpl.json +95 -0
  130. atdd/tester/schemas/db.tmpl.json +20 -0
  131. atdd/tester/schemas/e2e.tmpl.json +17 -0
  132. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  133. atdd/tester/schemas/event.tmpl.json +17 -0
  134. atdd/tester/schemas/http.tmpl.json +19 -0
  135. atdd/tester/schemas/job.tmpl.json +18 -0
  136. atdd/tester/schemas/load.tmpl.json +21 -0
  137. atdd/tester/schemas/metric.tmpl.json +19 -0
  138. atdd/tester/schemas/pack.schema.json +139 -0
  139. atdd/tester/schemas/realtime.tmpl.json +20 -0
  140. atdd/tester/schemas/rls.tmpl.json +18 -0
  141. atdd/tester/schemas/script.tmpl.json +16 -0
  142. atdd/tester/schemas/sec.tmpl.json +18 -0
  143. atdd/tester/schemas/storage.tmpl.json +18 -0
  144. atdd/tester/schemas/telemetry.schema.json +128 -0
  145. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  146. atdd/tester/schemas/test_filename.schema.json +194 -0
  147. atdd/tester/schemas/test_intent.schema.json +179 -0
  148. atdd/tester/schemas/unit.tmpl.json +18 -0
  149. atdd/tester/schemas/visual.tmpl.json +18 -0
  150. atdd/tester/schemas/ws.tmpl.json +17 -0
  151. atdd/tester/utils/__init__.py +0 -0
  152. atdd/tester/utils/filename.py +300 -0
  153. atdd/tester/validators/__init__.py +0 -0
  154. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  155. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  156. atdd/tester/validators/conftest.py +5 -0
  157. atdd/tester/validators/coverage_gap_report.py +321 -0
  158. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  159. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  160. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  161. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  162. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  163. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  164. atdd/tester/validators/test_contracts_structure.py +200 -0
  165. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  166. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  167. atdd/tester/validators/test_fixture_validity.py +372 -0
  168. atdd/tester/validators/test_isolation.py +487 -0
  169. atdd/tester/validators/test_migration_coverage.py +204 -0
  170. atdd/tester/validators/test_migration_criteria.py +276 -0
  171. atdd/tester/validators/test_migration_generation.py +116 -0
  172. atdd/tester/validators/test_python_test_naming.py +410 -0
  173. atdd/tester/validators/test_red_layer_validation.py +95 -0
  174. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  175. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  176. atdd/tester/validators/test_telemetry_structure.py +634 -0
  177. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  178. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  179. atdd-0.2.1.dist-info/METADATA +221 -0
  180. atdd-0.2.1.dist-info/RECORD +184 -0
  181. atdd-0.2.1.dist-info/WHEEL +5 -0
  182. atdd-0.2.1.dist-info/entry_points.txt +2 -0
  183. atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
  184. atdd-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,616 @@
1
+ """
2
+ Test TypeScript code follows clean architecture and naming conventions.
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 frontend/backend conventions
10
+ - Files are in correct layers based on their suffixes
11
+
12
+ Conventions from:
13
+ - atdd/coder/conventions/frontend.convention.yaml
14
+ - atdd/coder/conventions/backend.convention.yaml
15
+
16
+ Inspired by: .claude/utils/coder/architecture.py
17
+ But: Self-contained, no utility dependencies
18
+ """
19
+
20
+ import pytest
21
+ import re
22
+ import yaml
23
+ from pathlib import Path
24
+ from typing import Dict, List, Set, Tuple
25
+
26
+
27
+ # Path constants
28
+ REPO_ROOT = Path(__file__).resolve().parents[4]
29
+ TS_DIRS = [
30
+ REPO_ROOT / "supabase" / "functions",
31
+ REPO_ROOT / "typescript",
32
+ REPO_ROOT / "frontend",
33
+ REPO_ROOT / "web",
34
+ ]
35
+ FRONTEND_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "frontend.convention.yaml"
36
+ BACKEND_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "backend.convention.yaml"
37
+
38
+
39
+ def load_conventions() -> Tuple[Dict, Dict]:
40
+ """
41
+ Load frontend and backend conventions from YAML files.
42
+
43
+ Returns:
44
+ Tuple of (frontend_convention, backend_convention) dicts
45
+ """
46
+ frontend = {}
47
+ backend = {}
48
+
49
+ if FRONTEND_CONVENTION.exists():
50
+ with open(FRONTEND_CONVENTION, 'r', encoding='utf-8') as f:
51
+ data = yaml.safe_load(f)
52
+ frontend = data.get('frontend', {})
53
+
54
+ if BACKEND_CONVENTION.exists():
55
+ with open(BACKEND_CONVENTION, 'r', encoding='utf-8') as f:
56
+ data = yaml.safe_load(f)
57
+ backend = data.get('backend', {})
58
+
59
+ return frontend, backend
60
+
61
+
62
+ def get_layer_component_suffixes(conventions: Dict) -> Dict[str, Dict[str, List[str]]]:
63
+ """
64
+ Extract layer -> component_type -> suffixes mapping from conventions.
65
+
66
+ Args:
67
+ conventions: Frontend or backend convention dict
68
+
69
+ Returns:
70
+ Dict like {
71
+ 'domain': {
72
+ 'entities': ['*.ts', '*-entity.ts'],
73
+ 'value_objects': ['*-vo.ts', '*.ts']
74
+ },
75
+ 'application': {...},
76
+ ...
77
+ }
78
+ """
79
+ result = {}
80
+
81
+ layers = conventions.get('layers', {})
82
+ for layer_name, layer_config in layers.items():
83
+ result[layer_name] = {}
84
+
85
+ component_types = layer_config.get('component_types', [])
86
+ for component_type in component_types:
87
+ name = component_type.get('name', '')
88
+ suffix_config = component_type.get('suffix', {})
89
+
90
+ # Get TypeScript suffixes
91
+ ts_suffixes = suffix_config.get('typescript', '')
92
+ if ts_suffixes:
93
+ # Parse comma-separated suffixes
94
+ suffixes = [s.strip() for s in ts_suffixes.split(',')]
95
+ result[layer_name][name] = suffixes
96
+
97
+ return result
98
+
99
+
100
+ def determine_layer_from_path(file_path: Path) -> str:
101
+ """
102
+ Determine layer from file path.
103
+
104
+ Args:
105
+ file_path: Path to TypeScript file
106
+
107
+ Returns:
108
+ Layer name: 'domain', 'application', 'presentation', 'integration', 'unknown'
109
+ """
110
+ path_str = str(file_path).lower()
111
+
112
+ # Check explicit layer directories
113
+ if '/domain/' in path_str or path_str.endswith('/domain.ts'):
114
+ return 'domain'
115
+ elif '/application/' in path_str or path_str.endswith('/application.ts'):
116
+ return 'application'
117
+ elif '/presentation/' in path_str or path_str.endswith('/presentation.ts'):
118
+ return 'presentation'
119
+ elif '/integration/' in path_str or '/infrastructure/' in path_str:
120
+ return 'integration'
121
+
122
+ # Check alternative patterns
123
+ if '/entities/' in path_str or '/models/' in path_str or '/value_objects/' in path_str:
124
+ return 'domain'
125
+ elif '/use_cases/' in path_str or '/usecases/' in path_str or '/handlers/' in path_str:
126
+ return 'application'
127
+ elif '/controllers/' in path_str or '/views/' in path_str or '/components/' in path_str:
128
+ return 'presentation'
129
+ elif '/adapters/' in path_str or '/repositories/' in path_str or '/clients/' in path_str:
130
+ return 'integration'
131
+
132
+ return 'unknown'
133
+
134
+
135
+ def extract_typescript_imports(file_path: Path) -> List[str]:
136
+ """
137
+ Extract import statements from TypeScript file.
138
+
139
+ Args:
140
+ file_path: Path to TypeScript file
141
+
142
+ Returns:
143
+ List of imported module paths
144
+ """
145
+ try:
146
+ with open(file_path, 'r', encoding='utf-8') as f:
147
+ content = f.read()
148
+ except Exception:
149
+ return []
150
+
151
+ imports = []
152
+
153
+ # Match: import { X } from 'Y'
154
+ from_imports = re.findall(r"import\s+.*?\s+from\s+['\"]([^'\"]+)['\"]", content)
155
+ imports.extend(from_imports)
156
+
157
+ # Match: import 'X'
158
+ direct_imports = re.findall(r"import\s+['\"]([^'\"]+)['\"]", content)
159
+ imports.extend(direct_imports)
160
+
161
+ # Match: const X = require('Y')
162
+ require_imports = re.findall(r"require\s*\(\s*['\"]([^'\"]+)['\"]\s*\)", content)
163
+ imports.extend(require_imports)
164
+
165
+ return imports
166
+
167
+
168
+ def infer_layer_from_import(import_path: str) -> str:
169
+ """
170
+ Infer layer from import path.
171
+
172
+ Args:
173
+ import_path: Import statement (e.g., "./domain/entities/user")
174
+
175
+ Returns:
176
+ Layer name or 'external' for third-party imports
177
+ """
178
+ import_lower = import_path.lower()
179
+
180
+ # Check for layer indicators in import path
181
+ if 'domain' in import_lower and ('entities' in import_lower or 'models' in import_lower or 'value_objects' in import_lower):
182
+ return 'domain'
183
+ elif 'application' in import_lower or 'use_case' in import_lower or 'usecase' in import_lower:
184
+ return 'application'
185
+ elif 'presentation' in import_lower or 'controller' in import_lower or 'component' in import_lower or 'view' in import_lower:
186
+ return 'presentation'
187
+ elif 'integration' in import_lower or 'infrastructure' in import_lower or 'adapter' in import_lower or 'repository' in import_lower or 'client' in import_lower:
188
+ return 'integration'
189
+
190
+ # Check if it's a relative import
191
+ if import_path.startswith('.'):
192
+ return 'unknown'
193
+
194
+ # Third-party or external (http://, https://, npm:, node:)
195
+ if import_path.startswith(('http://', 'https://', 'npm:', 'node:', '@')):
196
+ return 'external'
197
+
198
+ # Standard Deno imports
199
+ if 'deno.land' in import_path or 'esm.sh' in import_path:
200
+ return 'external'
201
+
202
+ # Third-party or standard library
203
+ return 'external'
204
+
205
+
206
+ def find_typescript_files() -> List[Path]:
207
+ """
208
+ Find all TypeScript files in configured directories.
209
+
210
+ Returns:
211
+ List of Path objects
212
+ """
213
+ ts_files = []
214
+
215
+ for ts_dir in TS_DIRS:
216
+ if not ts_dir.exists():
217
+ continue
218
+
219
+ for ts_file in ts_dir.rglob("*.ts"):
220
+ # Skip test files
221
+ if '/test/' in str(ts_file) or ts_file.name.startswith('test_'):
222
+ continue
223
+ # Skip .test.ts
224
+ if ts_file.name.endswith('.test.ts'):
225
+ continue
226
+ # Skip node_modules
227
+ if 'node_modules' in str(ts_file):
228
+ continue
229
+ # Skip .d.ts (type definitions)
230
+ if ts_file.name.endswith('.d.ts'):
231
+ continue
232
+
233
+ ts_files.append(ts_file)
234
+
235
+ return ts_files
236
+
237
+
238
+ def matches_suffix_pattern(filename: str, pattern: str) -> bool:
239
+ """
240
+ Check if filename matches a suffix pattern.
241
+
242
+ Args:
243
+ filename: File name (e.g., "user-service.ts")
244
+ pattern: Pattern (e.g., "*-service.ts")
245
+
246
+ Returns:
247
+ True if matches
248
+ """
249
+ # Convert glob pattern to regex
250
+ # *-service.ts -> .*-service\.ts$
251
+ # *.ts -> .*\.ts$
252
+ regex_pattern = pattern.replace('.', r'\.')
253
+ regex_pattern = regex_pattern.replace('*', '.*')
254
+ regex_pattern = f'^{regex_pattern}$'
255
+
256
+ return bool(re.match(regex_pattern, filename))
257
+
258
+
259
+ def determine_expected_layer_from_suffix(filename: str, conventions: Dict) -> Tuple[str, str]:
260
+ """
261
+ Determine expected layer and component type from filename suffix.
262
+
263
+ Args:
264
+ filename: File name (e.g., "user-service.ts")
265
+ conventions: Frontend or backend convention dict
266
+
267
+ Returns:
268
+ Tuple of (layer_name, component_type) or ('unknown', 'unknown')
269
+ """
270
+ layer_suffixes = get_layer_component_suffixes(conventions)
271
+
272
+ # First pass: check more specific patterns (skip generic *.ts)
273
+ for layer_name, component_types in layer_suffixes.items():
274
+ for component_type, suffixes in component_types.items():
275
+ # Sort suffixes by length descending (more specific first)
276
+ sorted_suffixes = sorted(suffixes, key=len, reverse=True)
277
+ for suffix_pattern in sorted_suffixes:
278
+ # Skip generic patterns
279
+ if suffix_pattern in ('*.ts', '*.tsx'):
280
+ continue
281
+ if matches_suffix_pattern(filename, suffix_pattern):
282
+ return layer_name, component_type
283
+
284
+ # Don't fall back to generic *.ts - causes too many false positives
285
+ return 'unknown', 'unknown'
286
+
287
+
288
+ def is_frontend_file(file_path: Path) -> bool:
289
+ """Check if file is in frontend (web/) directory."""
290
+ path_str = str(file_path)
291
+ return '/web/' in path_str or path_str.startswith('web/')
292
+
293
+
294
+ @pytest.mark.coder
295
+ def test_typescript_follows_clean_architecture():
296
+ """
297
+ SPEC-CODER-ARCH-TS-0001: TypeScript code follows 4-layer clean architecture.
298
+
299
+ Dependency rules differ by context:
300
+
301
+ Frontend (web/) - per frontend.convention.yaml:
302
+ - Domain → NOTHING (domain must be pure)
303
+ - Application → Domain, Integration (hooks can orchestrate both)
304
+ - Presentation → Application, Domain
305
+ - Integration → Application, Domain
306
+
307
+ Backend (supabase/) - per backend.convention.yaml:
308
+ - Domain → NOTHING (domain must be pure)
309
+ - Application → Domain only (use ports for integration)
310
+ - Presentation → Application, Domain
311
+ - Integration → Application, Domain
312
+
313
+ Given: TypeScript files in web/, supabase/functions/, etc.
314
+ When: Checking import statements
315
+ Then: No forbidden cross-layer dependencies per context
316
+ """
317
+ ts_files = find_typescript_files()
318
+
319
+ if not ts_files:
320
+ pytest.skip("No TypeScript files found to validate")
321
+
322
+ violations = []
323
+
324
+ for ts_file in ts_files:
325
+ layer = determine_layer_from_path(ts_file)
326
+
327
+ # Skip files we can't categorize
328
+ if layer == 'unknown':
329
+ continue
330
+
331
+ imports = extract_typescript_imports(ts_file)
332
+ is_frontend = is_frontend_file(ts_file)
333
+
334
+ for imp in imports:
335
+ target_layer = infer_layer_from_import(imp)
336
+
337
+ # Skip external imports (third-party libraries)
338
+ if target_layer == 'external' or target_layer == 'unknown':
339
+ continue
340
+
341
+ # Check dependency rules
342
+ violation = None
343
+
344
+ if layer == 'domain':
345
+ # Domain must not import from any other layer (both frontend and backend)
346
+ if target_layer in ['application', 'presentation', 'integration']:
347
+ violation = f"Domain layer cannot import from {target_layer}"
348
+
349
+ elif layer == 'application':
350
+ if is_frontend:
351
+ # Frontend: application CAN import integration (hooks orchestrate both)
352
+ # See frontend.convention.yaml: application -> [domain, integration]
353
+ if target_layer == 'presentation':
354
+ violation = f"Application layer cannot import from {target_layer}"
355
+ else:
356
+ # Backend: application can only import from domain (use ports)
357
+ if target_layer in ['presentation', 'integration']:
358
+ violation = f"Application layer cannot import from {target_layer}"
359
+
360
+ elif layer == 'integration':
361
+ # Integration can import from application (for ports) and domain
362
+ if target_layer == 'presentation':
363
+ violation = f"Integration layer cannot import from {target_layer}"
364
+
365
+ if violation:
366
+ rel_path = ts_file.relative_to(REPO_ROOT)
367
+ violations.append(
368
+ f"{rel_path}\n"
369
+ f" Layer: {layer}\n"
370
+ f" Import: {imp}\n"
371
+ f" Violation: {violation}"
372
+ )
373
+
374
+ if violations:
375
+ pytest.fail(
376
+ f"\n\nFound {len(violations)} architecture violations:\n\n" +
377
+ "\n\n".join(violations[:10]) +
378
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
379
+ )
380
+
381
+
382
+ @pytest.mark.coder
383
+ def test_typescript_domain_layer_is_pure():
384
+ """
385
+ SPEC-CODER-ARCH-TS-0002: TypeScript domain layer has no external dependencies.
386
+
387
+ Domain layer should only import:
388
+ - Standard library (no third-party)
389
+ - Other domain modules
390
+
391
+ Should NOT import:
392
+ - Third-party libraries (except type definitions)
393
+ - Application/Presentation/Integration layers
394
+ - Database/API libraries
395
+ - Deno/Node runtime libraries
396
+
397
+ Given: TypeScript files in domain/ directories
398
+ When: Checking imports
399
+ Then: Only standard imports and domain imports
400
+ """
401
+ ts_files = find_typescript_files()
402
+
403
+ if not ts_files:
404
+ pytest.skip("No TypeScript files found to validate")
405
+
406
+ # Allowed imports in domain layer
407
+ # Internal domain path aliases are allowed (e.g., @commons/domain)
408
+ ALLOWED_DOMAIN_IMPORTS = {
409
+ '@commons/domain', # Shared domain types and pure functions
410
+ }
411
+
412
+ violations = []
413
+
414
+ for ts_file in ts_files:
415
+ layer = determine_layer_from_path(ts_file)
416
+
417
+ # Only check domain layer
418
+ if layer != 'domain':
419
+ continue
420
+
421
+ imports = extract_typescript_imports(ts_file)
422
+
423
+ for imp in imports:
424
+ # Skip relative imports (internal to domain)
425
+ if imp.startswith('.'):
426
+ continue
427
+
428
+ # Check if it's external/third-party
429
+ if imp.startswith(('http://', 'https://', 'npm:', 'node:', '@')) or 'deno.land' in imp or 'esm.sh' in imp:
430
+ # Check if it's an allowed domain import (internal path alias)
431
+ if any(imp.startswith(allowed) for allowed in ALLOWED_DOMAIN_IMPORTS):
432
+ continue
433
+ rel_path = ts_file.relative_to(REPO_ROOT)
434
+ violations.append(
435
+ f"{rel_path}\n"
436
+ f" Import: {imp}\n"
437
+ f" Issue: Domain layer should not import external libraries"
438
+ )
439
+
440
+ if violations:
441
+ pytest.fail(
442
+ f"\n\nFound {len(violations)} domain purity violations:\n\n" +
443
+ "\n\n".join(violations[:10]) +
444
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
445
+ f"\n\nDomain layer should only import:\n" +
446
+ f" - Other domain modules (relative imports)\n" +
447
+ f" - Type definitions only (if necessary)"
448
+ )
449
+
450
+
451
+ @pytest.mark.coder
452
+ def test_typescript_component_naming_follows_conventions():
453
+ """
454
+ SPEC-CODER-ARCH-TS-0003: TypeScript components follow naming conventions.
455
+
456
+ Component naming rules from conventions:
457
+ - Controllers: *-controller.ts (presentation layer)
458
+ - Services: *-service.ts (domain layer)
459
+ - Repositories: *-repository.ts (integration layer)
460
+ - Use Cases: *-use-case.ts (application layer)
461
+ - Entities: *.ts or *-entity.ts (domain layer)
462
+ - DTOs: *-dto.ts (application layer)
463
+ - Validators: *-validator.ts (presentation/integration layer)
464
+ - Mappers: *-mapper.ts (integration layer)
465
+ - Clients: *-client.ts or *-api.ts (integration layer)
466
+ - Stores: *-store.ts (integration layer)
467
+ - Handlers: *-handler.ts (application layer)
468
+ - Guards: *-guard.ts (presentation layer)
469
+ - Middleware: *-middleware.ts (presentation layer)
470
+ - Ports: *-port.ts or *-interface.ts (application layer)
471
+ - Events: *-event.ts (domain layer)
472
+ - Exceptions: *-exception.ts or exceptions.ts (domain layer)
473
+
474
+ Given: TypeScript files with recognizable suffixes
475
+ When: Checking file locations
476
+ Then: Files are in correct layers per their suffixes
477
+ """
478
+ ts_files = find_typescript_files()
479
+
480
+ if not ts_files:
481
+ pytest.skip("No TypeScript files found to validate")
482
+
483
+ frontend_conv, backend_conv = load_conventions()
484
+
485
+ violations = []
486
+
487
+ for ts_file in ts_files:
488
+ actual_layer = determine_layer_from_path(ts_file)
489
+
490
+ # Skip files in unknown locations
491
+ if actual_layer == 'unknown':
492
+ continue
493
+
494
+ filename = ts_file.name
495
+
496
+ # Check against backend conventions
497
+ expected_layer, component_type = determine_expected_layer_from_suffix(filename, backend_conv)
498
+
499
+ # If not found in backend, try frontend
500
+ if expected_layer == 'unknown':
501
+ expected_layer, component_type = determine_expected_layer_from_suffix(filename, frontend_conv)
502
+
503
+ # If we found an expected layer and it doesn't match actual
504
+ if expected_layer != 'unknown' and expected_layer != actual_layer:
505
+ rel_path = ts_file.relative_to(REPO_ROOT)
506
+ violations.append(
507
+ f"{rel_path}\n"
508
+ f" Component Type: {component_type}\n"
509
+ f" Expected Layer: {expected_layer}\n"
510
+ f" Actual Layer: {actual_layer}\n"
511
+ f" Issue: File suffix indicates {expected_layer} layer but found in {actual_layer}"
512
+ )
513
+
514
+ if violations:
515
+ pytest.fail(
516
+ f"\n\nFound {len(violations)} component naming/placement violations:\n\n" +
517
+ "\n\n".join(violations[:10]) +
518
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
519
+ f"\n\nComponent suffixes must match their layer placement.\n" +
520
+ f"See:\n" +
521
+ f" - atdd/coder/conventions/frontend.convention.yaml\n" +
522
+ f" - atdd/coder/conventions/backend.convention.yaml"
523
+ )
524
+
525
+
526
+ @pytest.mark.coder
527
+ def test_typescript_layers_have_proper_component_organization():
528
+ """
529
+ SPEC-CODER-ARCH-TS-0004: Each layer has proper component type grouping.
530
+
531
+ Layer organization rules:
532
+ - Domain layer: entities/, value_objects/, services/, specifications/, events/, exceptions/
533
+ - Application layer: use_cases/, handlers/, ports/, dtos/, policies/, workflows/
534
+ - Presentation layer: controllers/, routes/, serializers/, validators/, middleware/, guards/, views/
535
+ - Integration layer: repositories/, clients/, caches/, engines/, formatters/, notifiers/, queues/, stores/, mappers/, schedulers/, monitors/
536
+
537
+ Given: TypeScript files organized in layers
538
+ When: Checking directory structure
539
+ Then: Component types are in correct subdirectories
540
+ """
541
+ ts_files = find_typescript_files()
542
+
543
+ if not ts_files:
544
+ pytest.skip("No TypeScript files found to validate")
545
+
546
+ frontend_conv, backend_conv = load_conventions()
547
+
548
+ # Build expected component type directories per layer
549
+ backend_layer_components = get_layer_component_suffixes(backend_conv)
550
+ frontend_layer_components = get_layer_component_suffixes(frontend_conv)
551
+
552
+ violations = []
553
+
554
+ for ts_file in ts_files:
555
+ layer = determine_layer_from_path(ts_file)
556
+
557
+ # Skip unknown layers
558
+ if layer == 'unknown':
559
+ continue
560
+
561
+ path_str = str(ts_file)
562
+ filename = ts_file.name
563
+
564
+ # Determine expected component type from suffix
565
+ expected_layer_backend, component_type_backend = determine_expected_layer_from_suffix(filename, backend_conv)
566
+ expected_layer_frontend, component_type_frontend = determine_expected_layer_from_suffix(filename, frontend_conv)
567
+
568
+ # Use whichever matched
569
+ if expected_layer_backend != 'unknown':
570
+ expected_layer = expected_layer_backend
571
+ component_type = component_type_backend
572
+ elif expected_layer_frontend != 'unknown':
573
+ expected_layer = expected_layer_frontend
574
+ component_type = component_type_frontend
575
+ else:
576
+ # Can't determine component type
577
+ continue
578
+
579
+ # Check if file is in a component type subdirectory
580
+ # Expected pattern: .../layer/component_type/file.ts
581
+ # e.g., .../domain/entities/user.ts
582
+ # or .../application/use_cases/create-user-use-case.ts
583
+
584
+ # Files commonly placed at layer root (no subdirectory required)
585
+ layer_root_allowed = [
586
+ 'exceptions.ts', 'errors.ts', # Exception definitions
587
+ 'types.ts', 'index.ts', # Type definitions and barrel exports
588
+ ]
589
+
590
+ # Skip validation for files commonly at layer root
591
+ if filename in layer_root_allowed:
592
+ continue
593
+
594
+ # Check if component type directory is in path
595
+ if f'/{component_type}/' not in path_str:
596
+ # Only flag if this is a clear architecture setup (has layer directory)
597
+ if f'/{layer}/' in path_str:
598
+ rel_path = ts_file.relative_to(REPO_ROOT)
599
+ violations.append(
600
+ f"{rel_path}\n"
601
+ f" Layer: {layer}\n"
602
+ f" Component Type: {component_type}\n"
603
+ f" Issue: Should be in {layer}/{component_type}/ subdirectory"
604
+ )
605
+
606
+ if violations:
607
+ pytest.fail(
608
+ f"\n\nFound {len(violations)} component organization violations:\n\n" +
609
+ "\n\n".join(violations[:10]) +
610
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
611
+ f"\n\nComponents should be organized in layer/component_type/ subdirectories.\n" +
612
+ f"Example: domain/entities/user.ts, application/use_cases/create-user-use-case.ts\n" +
613
+ f"See:\n" +
614
+ f" - atdd/coder/conventions/frontend.convention.yaml\n" +
615
+ f" - atdd/coder/conventions/backend.convention.yaml"
616
+ )