atdd 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atdd/__init__.py +6 -0
- atdd/__main__.py +4 -0
- atdd/cli.py +404 -0
- atdd/coach/__init__.py +0 -0
- atdd/coach/commands/__init__.py +0 -0
- atdd/coach/commands/add_persistence_metadata.py +215 -0
- atdd/coach/commands/analyze_migrations.py +188 -0
- atdd/coach/commands/consumers.py +720 -0
- atdd/coach/commands/infer_governance_status.py +149 -0
- atdd/coach/commands/initializer.py +177 -0
- atdd/coach/commands/interface.py +1078 -0
- atdd/coach/commands/inventory.py +565 -0
- atdd/coach/commands/migration.py +240 -0
- atdd/coach/commands/registry.py +1560 -0
- atdd/coach/commands/session.py +430 -0
- atdd/coach/commands/sync.py +405 -0
- atdd/coach/commands/test_interface.py +399 -0
- atdd/coach/commands/test_runner.py +141 -0
- atdd/coach/commands/tests/__init__.py +1 -0
- atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
- atdd/coach/commands/traceability.py +4264 -0
- atdd/coach/conventions/session.convention.yaml +754 -0
- atdd/coach/overlays/__init__.py +2 -0
- atdd/coach/overlays/claude.md +2 -0
- atdd/coach/schemas/config.schema.json +34 -0
- atdd/coach/schemas/manifest.schema.json +101 -0
- atdd/coach/templates/ATDD.md +282 -0
- atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
- atdd/coach/utils/__init__.py +0 -0
- atdd/coach/utils/graph/__init__.py +0 -0
- atdd/coach/utils/graph/urn.py +875 -0
- atdd/coach/validators/__init__.py +0 -0
- atdd/coach/validators/shared_fixtures.py +365 -0
- atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
- atdd/coach/validators/test_registry.py +575 -0
- atdd/coach/validators/test_session_validation.py +1183 -0
- atdd/coach/validators/test_traceability.py +448 -0
- atdd/coach/validators/test_update_feature_paths.py +108 -0
- atdd/coach/validators/test_validate_contract_consumers.py +297 -0
- atdd/coder/__init__.py +1 -0
- atdd/coder/conventions/adapter.recipe.yaml +88 -0
- atdd/coder/conventions/backend.convention.yaml +460 -0
- atdd/coder/conventions/boundaries.convention.yaml +666 -0
- atdd/coder/conventions/commons.convention.yaml +460 -0
- atdd/coder/conventions/complexity.recipe.yaml +109 -0
- atdd/coder/conventions/component-naming.convention.yaml +178 -0
- atdd/coder/conventions/design.convention.yaml +327 -0
- atdd/coder/conventions/design.recipe.yaml +273 -0
- atdd/coder/conventions/dto.convention.yaml +660 -0
- atdd/coder/conventions/frontend.convention.yaml +542 -0
- atdd/coder/conventions/green.convention.yaml +1012 -0
- atdd/coder/conventions/presentation.convention.yaml +587 -0
- atdd/coder/conventions/refactor.convention.yaml +535 -0
- atdd/coder/conventions/technology.convention.yaml +206 -0
- atdd/coder/conventions/tests/__init__.py +0 -0
- atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
- atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
- atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
- atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
- atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
- atdd/coder/conventions/thinness.recipe.yaml +82 -0
- atdd/coder/conventions/train.convention.yaml +325 -0
- atdd/coder/conventions/verification.protocol.yaml +53 -0
- atdd/coder/schemas/design_system.schema.json +361 -0
- atdd/coder/validators/__init__.py +0 -0
- atdd/coder/validators/test_commons_structure.py +485 -0
- atdd/coder/validators/test_complexity.py +416 -0
- atdd/coder/validators/test_cross_language_consistency.py +431 -0
- atdd/coder/validators/test_design_system_compliance.py +413 -0
- atdd/coder/validators/test_dto_testing_patterns.py +268 -0
- atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
- atdd/coder/validators/test_green_layer_dependencies.py +148 -0
- atdd/coder/validators/test_green_python_layer_structure.py +103 -0
- atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
- atdd/coder/validators/test_import_boundaries.py +396 -0
- atdd/coder/validators/test_init_file_urns.py +593 -0
- atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
- atdd/coder/validators/test_presentation_convention.py +260 -0
- atdd/coder/validators/test_python_architecture.py +674 -0
- atdd/coder/validators/test_quality_metrics.py +420 -0
- atdd/coder/validators/test_station_master_pattern.py +244 -0
- atdd/coder/validators/test_train_infrastructure.py +454 -0
- atdd/coder/validators/test_train_urns.py +293 -0
- atdd/coder/validators/test_typescript_architecture.py +616 -0
- atdd/coder/validators/test_usecase_structure.py +421 -0
- atdd/coder/validators/test_wagon_boundaries.py +586 -0
- atdd/conftest.py +126 -0
- atdd/planner/__init__.py +1 -0
- atdd/planner/conventions/acceptance.convention.yaml +538 -0
- atdd/planner/conventions/appendix.convention.yaml +187 -0
- atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
- atdd/planner/conventions/component.convention.yaml +670 -0
- atdd/planner/conventions/criteria.convention.yaml +141 -0
- atdd/planner/conventions/feature.convention.yaml +371 -0
- atdd/planner/conventions/interface.convention.yaml +382 -0
- atdd/planner/conventions/steps.convention.yaml +141 -0
- atdd/planner/conventions/train.convention.yaml +552 -0
- atdd/planner/conventions/wagon.convention.yaml +275 -0
- atdd/planner/conventions/wmbt.convention.yaml +258 -0
- atdd/planner/schemas/acceptance.schema.json +336 -0
- atdd/planner/schemas/appendix.schema.json +78 -0
- atdd/planner/schemas/component.schema.json +114 -0
- atdd/planner/schemas/feature.schema.json +197 -0
- atdd/planner/schemas/train.schema.json +192 -0
- atdd/planner/schemas/wagon.schema.json +281 -0
- atdd/planner/schemas/wmbt.schema.json +59 -0
- atdd/planner/validators/__init__.py +0 -0
- atdd/planner/validators/conftest.py +5 -0
- atdd/planner/validators/test_draft_wagon_registry.py +374 -0
- atdd/planner/validators/test_plan_cross_refs.py +240 -0
- atdd/planner/validators/test_plan_uniqueness.py +224 -0
- atdd/planner/validators/test_plan_urn_resolution.py +268 -0
- atdd/planner/validators/test_plan_wagons.py +174 -0
- atdd/planner/validators/test_train_validation.py +514 -0
- atdd/planner/validators/test_wagon_urn_chain.py +648 -0
- atdd/planner/validators/test_wmbt_consistency.py +327 -0
- atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
- atdd/tester/__init__.py +1 -0
- atdd/tester/conventions/artifact.convention.yaml +257 -0
- atdd/tester/conventions/contract.convention.yaml +1009 -0
- atdd/tester/conventions/filename.convention.yaml +555 -0
- atdd/tester/conventions/migration.convention.yaml +509 -0
- atdd/tester/conventions/red.convention.yaml +797 -0
- atdd/tester/conventions/routing.convention.yaml +51 -0
- atdd/tester/conventions/telemetry.convention.yaml +458 -0
- atdd/tester/schemas/a11y.tmpl.json +17 -0
- atdd/tester/schemas/artifact.schema.json +189 -0
- atdd/tester/schemas/contract.schema.json +591 -0
- atdd/tester/schemas/contract.tmpl.json +95 -0
- atdd/tester/schemas/db.tmpl.json +20 -0
- atdd/tester/schemas/e2e.tmpl.json +17 -0
- atdd/tester/schemas/edge_function.tmpl.json +17 -0
- atdd/tester/schemas/event.tmpl.json +17 -0
- atdd/tester/schemas/http.tmpl.json +19 -0
- atdd/tester/schemas/job.tmpl.json +18 -0
- atdd/tester/schemas/load.tmpl.json +21 -0
- atdd/tester/schemas/metric.tmpl.json +19 -0
- atdd/tester/schemas/pack.schema.json +139 -0
- atdd/tester/schemas/realtime.tmpl.json +20 -0
- atdd/tester/schemas/rls.tmpl.json +18 -0
- atdd/tester/schemas/script.tmpl.json +16 -0
- atdd/tester/schemas/sec.tmpl.json +18 -0
- atdd/tester/schemas/storage.tmpl.json +18 -0
- atdd/tester/schemas/telemetry.schema.json +128 -0
- atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
- atdd/tester/schemas/test_filename.schema.json +194 -0
- atdd/tester/schemas/test_intent.schema.json +179 -0
- atdd/tester/schemas/unit.tmpl.json +18 -0
- atdd/tester/schemas/visual.tmpl.json +18 -0
- atdd/tester/schemas/ws.tmpl.json +17 -0
- atdd/tester/utils/__init__.py +0 -0
- atdd/tester/utils/filename.py +300 -0
- atdd/tester/validators/__init__.py +0 -0
- atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
- atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
- atdd/tester/validators/conftest.py +5 -0
- atdd/tester/validators/coverage_gap_report.py +321 -0
- atdd/tester/validators/fix_dual_ac_references.py +179 -0
- atdd/tester/validators/remove_duplicate_lines.py +93 -0
- atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
- atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
- atdd/tester/validators/test_artifact_naming_category.py +307 -0
- atdd/tester/validators/test_contract_schema_compliance.py +706 -0
- atdd/tester/validators/test_contracts_structure.py +200 -0
- atdd/tester/validators/test_coverage_adequacy.py +797 -0
- atdd/tester/validators/test_dual_ac_reference.py +225 -0
- atdd/tester/validators/test_fixture_validity.py +372 -0
- atdd/tester/validators/test_isolation.py +487 -0
- atdd/tester/validators/test_migration_coverage.py +204 -0
- atdd/tester/validators/test_migration_criteria.py +276 -0
- atdd/tester/validators/test_migration_generation.py +116 -0
- atdd/tester/validators/test_python_test_naming.py +410 -0
- atdd/tester/validators/test_red_layer_validation.py +95 -0
- atdd/tester/validators/test_red_python_layer_structure.py +87 -0
- atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
- atdd/tester/validators/test_telemetry_structure.py +634 -0
- atdd/tester/validators/test_typescript_test_naming.py +301 -0
- atdd/tester/validators/test_typescript_test_structure.py +84 -0
- atdd-0.2.1.dist-info/METADATA +221 -0
- atdd-0.2.1.dist-info/RECORD +184 -0
- atdd-0.2.1.dist-info/WHEEL +5 -0
- atdd-0.2.1.dist-info/entry_points.txt +2 -0
- atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
- atdd-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|
+
)
|