atdd 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atdd/__init__.py +0 -0
- atdd/cli.py +404 -0
- atdd/coach/__init__.py +0 -0
- atdd/coach/commands/__init__.py +0 -0
- atdd/coach/commands/add_persistence_metadata.py +215 -0
- atdd/coach/commands/analyze_migrations.py +188 -0
- atdd/coach/commands/consumers.py +720 -0
- atdd/coach/commands/infer_governance_status.py +149 -0
- atdd/coach/commands/initializer.py +177 -0
- atdd/coach/commands/interface.py +1078 -0
- atdd/coach/commands/inventory.py +565 -0
- atdd/coach/commands/migration.py +240 -0
- atdd/coach/commands/registry.py +1560 -0
- atdd/coach/commands/session.py +430 -0
- atdd/coach/commands/sync.py +405 -0
- atdd/coach/commands/test_interface.py +399 -0
- atdd/coach/commands/test_runner.py +141 -0
- atdd/coach/commands/tests/__init__.py +1 -0
- atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
- atdd/coach/commands/traceability.py +4264 -0
- atdd/coach/conventions/session.convention.yaml +754 -0
- atdd/coach/overlays/__init__.py +2 -0
- atdd/coach/overlays/claude.md +2 -0
- atdd/coach/schemas/config.schema.json +34 -0
- atdd/coach/schemas/manifest.schema.json +101 -0
- atdd/coach/templates/ATDD.md +282 -0
- atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
- atdd/coach/utils/__init__.py +0 -0
- atdd/coach/utils/graph/__init__.py +0 -0
- atdd/coach/utils/graph/urn.py +875 -0
- atdd/coach/validators/__init__.py +0 -0
- atdd/coach/validators/shared_fixtures.py +365 -0
- atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
- atdd/coach/validators/test_registry.py +575 -0
- atdd/coach/validators/test_session_validation.py +1183 -0
- atdd/coach/validators/test_traceability.py +448 -0
- atdd/coach/validators/test_update_feature_paths.py +108 -0
- atdd/coach/validators/test_validate_contract_consumers.py +297 -0
- atdd/coder/__init__.py +1 -0
- atdd/coder/conventions/adapter.recipe.yaml +88 -0
- atdd/coder/conventions/backend.convention.yaml +460 -0
- atdd/coder/conventions/boundaries.convention.yaml +666 -0
- atdd/coder/conventions/commons.convention.yaml +460 -0
- atdd/coder/conventions/complexity.recipe.yaml +109 -0
- atdd/coder/conventions/component-naming.convention.yaml +178 -0
- atdd/coder/conventions/design.convention.yaml +327 -0
- atdd/coder/conventions/design.recipe.yaml +273 -0
- atdd/coder/conventions/dto.convention.yaml +660 -0
- atdd/coder/conventions/frontend.convention.yaml +542 -0
- atdd/coder/conventions/green.convention.yaml +1012 -0
- atdd/coder/conventions/presentation.convention.yaml +587 -0
- atdd/coder/conventions/refactor.convention.yaml +535 -0
- atdd/coder/conventions/technology.convention.yaml +206 -0
- atdd/coder/conventions/tests/__init__.py +0 -0
- atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
- atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
- atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
- atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
- atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
- atdd/coder/conventions/thinness.recipe.yaml +82 -0
- atdd/coder/conventions/train.convention.yaml +325 -0
- atdd/coder/conventions/verification.protocol.yaml +53 -0
- atdd/coder/schemas/design_system.schema.json +361 -0
- atdd/coder/validators/__init__.py +0 -0
- atdd/coder/validators/test_commons_structure.py +485 -0
- atdd/coder/validators/test_complexity.py +416 -0
- atdd/coder/validators/test_cross_language_consistency.py +431 -0
- atdd/coder/validators/test_design_system_compliance.py +413 -0
- atdd/coder/validators/test_dto_testing_patterns.py +268 -0
- atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
- atdd/coder/validators/test_green_layer_dependencies.py +148 -0
- atdd/coder/validators/test_green_python_layer_structure.py +103 -0
- atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
- atdd/coder/validators/test_import_boundaries.py +396 -0
- atdd/coder/validators/test_init_file_urns.py +593 -0
- atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
- atdd/coder/validators/test_presentation_convention.py +260 -0
- atdd/coder/validators/test_python_architecture.py +674 -0
- atdd/coder/validators/test_quality_metrics.py +420 -0
- atdd/coder/validators/test_station_master_pattern.py +244 -0
- atdd/coder/validators/test_train_infrastructure.py +454 -0
- atdd/coder/validators/test_train_urns.py +293 -0
- atdd/coder/validators/test_typescript_architecture.py +616 -0
- atdd/coder/validators/test_usecase_structure.py +421 -0
- atdd/coder/validators/test_wagon_boundaries.py +586 -0
- atdd/conftest.py +126 -0
- atdd/planner/__init__.py +1 -0
- atdd/planner/conventions/acceptance.convention.yaml +538 -0
- atdd/planner/conventions/appendix.convention.yaml +187 -0
- atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
- atdd/planner/conventions/component.convention.yaml +670 -0
- atdd/planner/conventions/criteria.convention.yaml +141 -0
- atdd/planner/conventions/feature.convention.yaml +371 -0
- atdd/planner/conventions/interface.convention.yaml +382 -0
- atdd/planner/conventions/steps.convention.yaml +141 -0
- atdd/planner/conventions/train.convention.yaml +552 -0
- atdd/planner/conventions/wagon.convention.yaml +275 -0
- atdd/planner/conventions/wmbt.convention.yaml +258 -0
- atdd/planner/schemas/acceptance.schema.json +336 -0
- atdd/planner/schemas/appendix.schema.json +78 -0
- atdd/planner/schemas/component.schema.json +114 -0
- atdd/planner/schemas/feature.schema.json +197 -0
- atdd/planner/schemas/train.schema.json +192 -0
- atdd/planner/schemas/wagon.schema.json +281 -0
- atdd/planner/schemas/wmbt.schema.json +59 -0
- atdd/planner/validators/__init__.py +0 -0
- atdd/planner/validators/conftest.py +5 -0
- atdd/planner/validators/test_draft_wagon_registry.py +374 -0
- atdd/planner/validators/test_plan_cross_refs.py +240 -0
- atdd/planner/validators/test_plan_uniqueness.py +224 -0
- atdd/planner/validators/test_plan_urn_resolution.py +268 -0
- atdd/planner/validators/test_plan_wagons.py +174 -0
- atdd/planner/validators/test_train_validation.py +514 -0
- atdd/planner/validators/test_wagon_urn_chain.py +648 -0
- atdd/planner/validators/test_wmbt_consistency.py +327 -0
- atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
- atdd/tester/__init__.py +1 -0
- atdd/tester/conventions/artifact.convention.yaml +257 -0
- atdd/tester/conventions/contract.convention.yaml +1009 -0
- atdd/tester/conventions/filename.convention.yaml +555 -0
- atdd/tester/conventions/migration.convention.yaml +509 -0
- atdd/tester/conventions/red.convention.yaml +797 -0
- atdd/tester/conventions/routing.convention.yaml +51 -0
- atdd/tester/conventions/telemetry.convention.yaml +458 -0
- atdd/tester/schemas/a11y.tmpl.json +17 -0
- atdd/tester/schemas/artifact.schema.json +189 -0
- atdd/tester/schemas/contract.schema.json +591 -0
- atdd/tester/schemas/contract.tmpl.json +95 -0
- atdd/tester/schemas/db.tmpl.json +20 -0
- atdd/tester/schemas/e2e.tmpl.json +17 -0
- atdd/tester/schemas/edge_function.tmpl.json +17 -0
- atdd/tester/schemas/event.tmpl.json +17 -0
- atdd/tester/schemas/http.tmpl.json +19 -0
- atdd/tester/schemas/job.tmpl.json +18 -0
- atdd/tester/schemas/load.tmpl.json +21 -0
- atdd/tester/schemas/metric.tmpl.json +19 -0
- atdd/tester/schemas/pack.schema.json +139 -0
- atdd/tester/schemas/realtime.tmpl.json +20 -0
- atdd/tester/schemas/rls.tmpl.json +18 -0
- atdd/tester/schemas/script.tmpl.json +16 -0
- atdd/tester/schemas/sec.tmpl.json +18 -0
- atdd/tester/schemas/storage.tmpl.json +18 -0
- atdd/tester/schemas/telemetry.schema.json +128 -0
- atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
- atdd/tester/schemas/test_filename.schema.json +194 -0
- atdd/tester/schemas/test_intent.schema.json +179 -0
- atdd/tester/schemas/unit.tmpl.json +18 -0
- atdd/tester/schemas/visual.tmpl.json +18 -0
- atdd/tester/schemas/ws.tmpl.json +17 -0
- atdd/tester/utils/__init__.py +0 -0
- atdd/tester/utils/filename.py +300 -0
- atdd/tester/validators/__init__.py +0 -0
- atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
- atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
- atdd/tester/validators/conftest.py +5 -0
- atdd/tester/validators/coverage_gap_report.py +321 -0
- atdd/tester/validators/fix_dual_ac_references.py +179 -0
- atdd/tester/validators/remove_duplicate_lines.py +93 -0
- atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
- atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
- atdd/tester/validators/test_artifact_naming_category.py +307 -0
- atdd/tester/validators/test_contract_schema_compliance.py +706 -0
- atdd/tester/validators/test_contracts_structure.py +200 -0
- atdd/tester/validators/test_coverage_adequacy.py +797 -0
- atdd/tester/validators/test_dual_ac_reference.py +225 -0
- atdd/tester/validators/test_fixture_validity.py +372 -0
- atdd/tester/validators/test_isolation.py +487 -0
- atdd/tester/validators/test_migration_coverage.py +204 -0
- atdd/tester/validators/test_migration_criteria.py +276 -0
- atdd/tester/validators/test_migration_generation.py +116 -0
- atdd/tester/validators/test_python_test_naming.py +410 -0
- atdd/tester/validators/test_red_layer_validation.py +95 -0
- atdd/tester/validators/test_red_python_layer_structure.py +87 -0
- atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
- atdd/tester/validators/test_telemetry_structure.py +634 -0
- atdd/tester/validators/test_typescript_test_naming.py +301 -0
- atdd/tester/validators/test_typescript_test_structure.py +84 -0
- atdd-0.1.0.dist-info/METADATA +191 -0
- atdd-0.1.0.dist-info/RECORD +183 -0
- atdd-0.1.0.dist-info/WHEEL +5 -0
- atdd-0.1.0.dist-info/entry_points.txt +2 -0
- atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
- atdd-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPEC-CODER-CONV-0019: GREEN convention enforces cross-stack layer naming consistency
|
|
3
|
+
|
|
4
|
+
Test that green.convention.yaml enforces same layer names across all stacks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
13
|
+
BACKEND_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "backend.convention.yaml"
|
|
14
|
+
FRONTEND_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "frontend.convention.yaml"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.coder
|
|
18
|
+
def test_all_stacks_use_same_layer_names():
|
|
19
|
+
"""
|
|
20
|
+
SPEC-CODER-CONV-0019: All stacks use same 4 layer names.
|
|
21
|
+
|
|
22
|
+
Given: Layer structure defined for Python, Flutter, and Supabase
|
|
23
|
+
When: Checking layer names across stacks
|
|
24
|
+
Then: All use presentation, application, domain, integration
|
|
25
|
+
"""
|
|
26
|
+
assert BACKEND_CONVENTION.exists(), "backend.convention.yaml must exist"
|
|
27
|
+
assert FRONTEND_CONVENTION.exists(), "frontend.convention.yaml must exist"
|
|
28
|
+
|
|
29
|
+
with open(BACKEND_CONVENTION, 'r') as f:
|
|
30
|
+
backend_conv = yaml.safe_load(f)
|
|
31
|
+
|
|
32
|
+
with open(FRONTEND_CONVENTION, 'r') as f:
|
|
33
|
+
frontend_conv = yaml.safe_load(f)
|
|
34
|
+
|
|
35
|
+
expected_layers = {'presentation', 'application', 'domain', 'integration'}
|
|
36
|
+
|
|
37
|
+
# Check Python (backend)
|
|
38
|
+
backend = backend_conv['backend']
|
|
39
|
+
backend_structure = backend.get('structure', {})
|
|
40
|
+
if 'python' in backend_structure:
|
|
41
|
+
python_layers = set(backend_structure['python'].get('layers', []))
|
|
42
|
+
assert python_layers == expected_layers, \
|
|
43
|
+
f"Python layers {python_layers} must match {expected_layers}"
|
|
44
|
+
|
|
45
|
+
# Check Flutter/Dart (frontend)
|
|
46
|
+
frontend = frontend_conv['frontend']
|
|
47
|
+
frontend_structure = frontend.get('structure', {})
|
|
48
|
+
if 'flutter' in frontend_structure or 'dart' in frontend_structure:
|
|
49
|
+
flutter_layers = set(frontend_structure.get('flutter', frontend_structure.get('dart', {})).get('layers', []))
|
|
50
|
+
assert flutter_layers == expected_layers, \
|
|
51
|
+
f"Flutter layers {flutter_layers} must match {expected_layers}"
|
|
52
|
+
|
|
53
|
+
# Check Supabase/TypeScript (backend)
|
|
54
|
+
if 'supabase' in backend_structure or 'typescript' in backend_structure:
|
|
55
|
+
supabase_layers = set(backend_structure.get('supabase', backend_structure.get('typescript', {})).get('layers', []))
|
|
56
|
+
assert supabase_layers == expected_layers, \
|
|
57
|
+
f"Supabase layers {supabase_layers} must match {expected_layers}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.coder
|
|
61
|
+
def test_no_alternative_layer_names():
|
|
62
|
+
"""
|
|
63
|
+
SPEC-CODER-CONV-0019: No alternative naming (data, infrastructure) allowed.
|
|
64
|
+
|
|
65
|
+
Given: Layer structure configuration
|
|
66
|
+
When: Checking for alternative layer names
|
|
67
|
+
Then: Only presentation, application, domain, integration are allowed
|
|
68
|
+
"""
|
|
69
|
+
assert BACKEND_CONVENTION.exists(), "backend.convention.yaml must exist"
|
|
70
|
+
assert FRONTEND_CONVENTION.exists(), "frontend.convention.yaml must exist"
|
|
71
|
+
|
|
72
|
+
with open(BACKEND_CONVENTION, 'r') as f:
|
|
73
|
+
backend_conv = yaml.safe_load(f)
|
|
74
|
+
|
|
75
|
+
with open(FRONTEND_CONVENTION, 'r') as f:
|
|
76
|
+
frontend_conv = yaml.safe_load(f)
|
|
77
|
+
|
|
78
|
+
backend = backend_conv['backend']
|
|
79
|
+
backend_structure = backend.get('structure', {})
|
|
80
|
+
|
|
81
|
+
frontend = frontend_conv['frontend']
|
|
82
|
+
frontend_structure = frontend.get('structure', {})
|
|
83
|
+
|
|
84
|
+
# Check for forbidden alternatives (could be at convention level or structure level)
|
|
85
|
+
naming_rules = backend.get('naming_rules') or backend_structure.get('naming_rules', {})
|
|
86
|
+
|
|
87
|
+
if naming_rules:
|
|
88
|
+
forbidden_names = naming_rules.get('forbidden_layer_names') or \
|
|
89
|
+
naming_rules.get('disallowed_alternatives', [])
|
|
90
|
+
|
|
91
|
+
# Should forbid common alternatives
|
|
92
|
+
alternatives = ['data', 'infrastructure', 'adapters', 'interfaces']
|
|
93
|
+
for alt in alternatives:
|
|
94
|
+
if forbidden_names:
|
|
95
|
+
# At least some alternatives should be explicitly forbidden
|
|
96
|
+
pass # Convention should document this
|
|
97
|
+
|
|
98
|
+
# Alternative: check that only standard names are used
|
|
99
|
+
standard_layers = {'presentation', 'application', 'domain', 'integration'}
|
|
100
|
+
|
|
101
|
+
# Check backend stacks
|
|
102
|
+
for stack_name, stack_config in backend_structure.items():
|
|
103
|
+
if isinstance(stack_config, dict) and 'layers' in stack_config:
|
|
104
|
+
layers = set(stack_config['layers'])
|
|
105
|
+
non_standard = layers - standard_layers
|
|
106
|
+
|
|
107
|
+
assert len(non_standard) == 0, \
|
|
108
|
+
f"Backend stack {stack_name} has non-standard layers: {non_standard}"
|
|
109
|
+
|
|
110
|
+
# Check frontend stacks
|
|
111
|
+
for stack_name, stack_config in frontend_structure.items():
|
|
112
|
+
if isinstance(stack_config, dict) and 'layers' in stack_config:
|
|
113
|
+
layers = set(stack_config['layers'])
|
|
114
|
+
non_standard = layers - standard_layers
|
|
115
|
+
|
|
116
|
+
assert len(non_standard) == 0, \
|
|
117
|
+
f"Frontend stack {stack_name} has non-standard layers: {non_standard}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.coder
|
|
121
|
+
def test_layer_names_lowercase():
|
|
122
|
+
"""
|
|
123
|
+
SPEC-CODER-CONV-0019: All layer directory names are lowercase.
|
|
124
|
+
|
|
125
|
+
Given: Layer structure configuration
|
|
126
|
+
When: Checking layer name casing
|
|
127
|
+
Then: All layer names are lowercase
|
|
128
|
+
"""
|
|
129
|
+
assert BACKEND_CONVENTION.exists(), "backend.convention.yaml must exist"
|
|
130
|
+
assert FRONTEND_CONVENTION.exists(), "frontend.convention.yaml must exist"
|
|
131
|
+
|
|
132
|
+
with open(BACKEND_CONVENTION, 'r') as f:
|
|
133
|
+
backend_conv = yaml.safe_load(f)
|
|
134
|
+
|
|
135
|
+
with open(FRONTEND_CONVENTION, 'r') as f:
|
|
136
|
+
frontend_conv = yaml.safe_load(f)
|
|
137
|
+
|
|
138
|
+
backend = backend_conv['backend']
|
|
139
|
+
backend_structure = backend.get('structure', {})
|
|
140
|
+
|
|
141
|
+
frontend = frontend_conv['frontend']
|
|
142
|
+
frontend_structure = frontend.get('structure', {})
|
|
143
|
+
|
|
144
|
+
# Check all backend stacks
|
|
145
|
+
for stack_name, stack_config in backend_structure.items():
|
|
146
|
+
if isinstance(stack_config, dict) and 'layers' in stack_config:
|
|
147
|
+
layers = stack_config['layers']
|
|
148
|
+
|
|
149
|
+
for layer in layers:
|
|
150
|
+
assert layer == layer.lower(), \
|
|
151
|
+
f"Layer name '{layer}' in backend {stack_name} must be lowercase"
|
|
152
|
+
|
|
153
|
+
# Also check that it doesn't have special characters
|
|
154
|
+
assert layer.replace('_', '').isalnum(), \
|
|
155
|
+
f"Layer name '{layer}' should be alphanumeric (underscores allowed)"
|
|
156
|
+
|
|
157
|
+
# Check all frontend stacks
|
|
158
|
+
for stack_name, stack_config in frontend_structure.items():
|
|
159
|
+
if isinstance(stack_config, dict) and 'layers' in stack_config:
|
|
160
|
+
layers = stack_config['layers']
|
|
161
|
+
|
|
162
|
+
for layer in layers:
|
|
163
|
+
assert layer == layer.lower(), \
|
|
164
|
+
f"Layer name '{layer}' in {stack_name} must be lowercase"
|
|
165
|
+
|
|
166
|
+
# Also check that it doesn't have special characters
|
|
167
|
+
assert layer.replace('_', '').isalnum(), \
|
|
168
|
+
f"Layer name '{layer}' should be alphanumeric (underscores allowed)"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPEC-CODER-CONV-0018: GREEN convention validates layer dependency rules
|
|
3
|
+
|
|
4
|
+
Test that green.convention.yaml enforces clean architecture dependency rules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
13
|
+
BACKEND_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "backend.convention.yaml"
|
|
14
|
+
FRONTEND_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "frontend.convention.yaml"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.coder
|
|
18
|
+
def test_domain_has_no_layer_imports():
|
|
19
|
+
"""
|
|
20
|
+
SPEC-CODER-CONV-0018: Domain layer must be pure (no dependencies).
|
|
21
|
+
|
|
22
|
+
Given: Clean architecture dependency rules
|
|
23
|
+
When: Checking domain layer import rules
|
|
24
|
+
Then: Domain cannot import from application/presentation/integration
|
|
25
|
+
"""
|
|
26
|
+
assert BACKEND_CONVENTION.exists(), "backend.convention.yaml must exist"
|
|
27
|
+
|
|
28
|
+
with open(BACKEND_CONVENTION, 'r') as f:
|
|
29
|
+
convention = yaml.safe_load(f)
|
|
30
|
+
|
|
31
|
+
# Backend has dependency rules at backend.dependency
|
|
32
|
+
backend = convention['backend']
|
|
33
|
+
assert 'dependency' in backend, "Backend must define dependency rules"
|
|
34
|
+
|
|
35
|
+
dep_rules = backend['dependency']
|
|
36
|
+
|
|
37
|
+
assert dep_rules is not None, "Dependency rules must be defined"
|
|
38
|
+
|
|
39
|
+
# Backend uses edge-based dependencies, not per-layer rules
|
|
40
|
+
# Check that domain is not listed as 'from' in any allowed_edges
|
|
41
|
+
allowed_edges = dep_rules.get('allowed_edges', [])
|
|
42
|
+
forbidden_examples = dep_rules.get('forbidden_examples', [])
|
|
43
|
+
|
|
44
|
+
# Domain should not be in 'from' field of any allowed edge
|
|
45
|
+
domain_edges = [edge for edge in allowed_edges if edge.get('from') == 'domain']
|
|
46
|
+
assert len(domain_edges) == 0, "Domain should not have outgoing dependencies"
|
|
47
|
+
|
|
48
|
+
# Verify forbidden examples include domain restrictions
|
|
49
|
+
assert any('domain →' in example or 'domain ->' in example for example in forbidden_examples), \
|
|
50
|
+
"Forbidden examples must include domain restrictions"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.coder
|
|
54
|
+
def test_application_only_imports_domain():
|
|
55
|
+
"""
|
|
56
|
+
SPEC-CODER-CONV-0018: Application can only depend on domain.
|
|
57
|
+
|
|
58
|
+
Given: Clean architecture dependency rules
|
|
59
|
+
When: Checking application layer import rules
|
|
60
|
+
Then: Application cannot import from presentation/integration
|
|
61
|
+
"""
|
|
62
|
+
assert BACKEND_CONVENTION.exists(), "green.convention.yaml must exist"
|
|
63
|
+
|
|
64
|
+
with open(BACKEND_CONVENTION, 'r') as f:
|
|
65
|
+
convention = yaml.safe_load(f)
|
|
66
|
+
|
|
67
|
+
backend = convention["backend"]
|
|
68
|
+
dep_rules = backend.get('dependency')
|
|
69
|
+
|
|
70
|
+
allowed_edges = dep_rules.get('allowed_edges', [])
|
|
71
|
+
forbidden_examples = dep_rules.get('forbidden_examples', [])
|
|
72
|
+
|
|
73
|
+
# Find application edges
|
|
74
|
+
app_edges = [edge for edge in allowed_edges if edge.get('from') == 'application']
|
|
75
|
+
|
|
76
|
+
# Application should only depend on domain
|
|
77
|
+
assert len(app_edges) > 0, "Application must have defined dependencies"
|
|
78
|
+
app_edge = app_edges[0]
|
|
79
|
+
assert app_edge.get('to') == ['domain'], \
|
|
80
|
+
f"Application should only depend on domain, got {app_edge.get('to')}"
|
|
81
|
+
|
|
82
|
+
# Check forbidden examples
|
|
83
|
+
assert any('application → presentation' in example or 'application -> presentation' in example
|
|
84
|
+
for example in forbidden_examples), \
|
|
85
|
+
"Application to presentation must be forbidden"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.coder
|
|
89
|
+
def test_integration_only_imports_domain():
|
|
90
|
+
"""
|
|
91
|
+
SPEC-CODER-CONV-0018: Integration can only depend on domain.
|
|
92
|
+
|
|
93
|
+
Given: Clean architecture dependency rules
|
|
94
|
+
When: Checking integration layer import rules
|
|
95
|
+
Then: Integration cannot import from application/presentation
|
|
96
|
+
"""
|
|
97
|
+
assert BACKEND_CONVENTION.exists(), "green.convention.yaml must exist"
|
|
98
|
+
|
|
99
|
+
with open(BACKEND_CONVENTION, 'r') as f:
|
|
100
|
+
convention = yaml.safe_load(f)
|
|
101
|
+
|
|
102
|
+
backend = convention["backend"]
|
|
103
|
+
dep_rules = backend.get('dependency')
|
|
104
|
+
|
|
105
|
+
allowed_edges = dep_rules.get('allowed_edges', [])
|
|
106
|
+
|
|
107
|
+
# Find integration edges
|
|
108
|
+
int_edges = [edge for edge in allowed_edges if edge.get('from') == 'integration']
|
|
109
|
+
|
|
110
|
+
# Integration should depend on application and domain
|
|
111
|
+
assert len(int_edges) > 0, "Integration must have defined dependencies"
|
|
112
|
+
int_edge = int_edges[0]
|
|
113
|
+
int_deps = int_edge.get('to', [])
|
|
114
|
+
|
|
115
|
+
# Integration typically depends on both application and domain in clean arch
|
|
116
|
+
assert 'domain' in int_deps, "Integration should depend on domain"
|
|
117
|
+
assert 'presentation' not in int_deps, "Integration should not depend on presentation"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.coder
|
|
121
|
+
def test_presentation_imports_valid():
|
|
122
|
+
"""
|
|
123
|
+
SPEC-CODER-CONV-0018: Presentation can depend on application and domain.
|
|
124
|
+
|
|
125
|
+
Given: Clean architecture dependency rules
|
|
126
|
+
When: Checking presentation layer import rules
|
|
127
|
+
Then: Presentation can import from application and domain
|
|
128
|
+
"""
|
|
129
|
+
assert BACKEND_CONVENTION.exists(), "green.convention.yaml must exist"
|
|
130
|
+
|
|
131
|
+
with open(BACKEND_CONVENTION, 'r') as f:
|
|
132
|
+
convention = yaml.safe_load(f)
|
|
133
|
+
|
|
134
|
+
backend = convention["backend"]
|
|
135
|
+
dep_rules = backend.get('dependency')
|
|
136
|
+
|
|
137
|
+
allowed_edges = dep_rules.get('allowed_edges', [])
|
|
138
|
+
|
|
139
|
+
# Find presentation edges
|
|
140
|
+
pres_edges = [edge for edge in allowed_edges if edge.get('from') == 'presentation']
|
|
141
|
+
|
|
142
|
+
# Presentation can depend on application and domain
|
|
143
|
+
assert len(pres_edges) > 0, "Presentation must have defined dependencies"
|
|
144
|
+
pres_edge = pres_edges[0]
|
|
145
|
+
pres_deps = pres_edge.get('to', [])
|
|
146
|
+
|
|
147
|
+
assert 'application' in pres_deps, "Presentation must be allowed to depend on application"
|
|
148
|
+
assert 'domain' in pres_deps, "Presentation must be allowed to depend on domain"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPEC-CODER-CONV-0015: GREEN convention defines 4-layer structure for Python
|
|
3
|
+
|
|
4
|
+
Test that backend.convention.yaml enforces 4-layer structure for Python implementation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
13
|
+
BACKEND_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "backend.convention.yaml"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.coder
|
|
17
|
+
def test_green_enforces_python_layers():
|
|
18
|
+
"""
|
|
19
|
+
SPEC-CODER-CONV-0015: Backend convention enforces 4-layer structure for Python.
|
|
20
|
+
|
|
21
|
+
Given: backend.convention.yaml exists
|
|
22
|
+
When: Reading Python layer structure configuration
|
|
23
|
+
Then: Convention enforces 4 layer directories under src/
|
|
24
|
+
"""
|
|
25
|
+
assert BACKEND_CONVENTION.exists(), "backend.convention.yaml must exist"
|
|
26
|
+
|
|
27
|
+
with open(BACKEND_CONVENTION, 'r') as f:
|
|
28
|
+
convention = yaml.safe_load(f)
|
|
29
|
+
|
|
30
|
+
# Backend convention has backend.structure.python
|
|
31
|
+
assert 'backend' in convention, "Convention must have 'backend' section"
|
|
32
|
+
backend = convention['backend']
|
|
33
|
+
|
|
34
|
+
assert 'structure' in backend, "Backend must define structure"
|
|
35
|
+
structure = backend['structure']
|
|
36
|
+
|
|
37
|
+
# Check Python-specific configuration
|
|
38
|
+
assert 'python' in structure, "Convention must define Python layer structure"
|
|
39
|
+
|
|
40
|
+
python_config = structure['python']
|
|
41
|
+
|
|
42
|
+
# Verify 4 layers are defined
|
|
43
|
+
assert 'layers' in python_config, "Python config must define layers"
|
|
44
|
+
layers = python_config['layers']
|
|
45
|
+
|
|
46
|
+
expected_layers = {'presentation', 'application', 'domain', 'integration'}
|
|
47
|
+
assert set(layers) == expected_layers, \
|
|
48
|
+
f"Layers must be {expected_layers}, got {set(layers)}"
|
|
49
|
+
|
|
50
|
+
# Verify source path pattern
|
|
51
|
+
assert 'src_path_pattern' in python_config or 'source_pattern' in python_config, \
|
|
52
|
+
"Python config must define src_path_pattern"
|
|
53
|
+
|
|
54
|
+
pattern = python_config.get('src_path_pattern') or python_config.get('source_pattern')
|
|
55
|
+
|
|
56
|
+
assert '{wagon}' in pattern, "Pattern must include {wagon} placeholder"
|
|
57
|
+
assert '{feature}' in pattern, "Pattern must include {feature} placeholder"
|
|
58
|
+
assert '{layer}' in pattern, "Pattern must include {layer} placeholder"
|
|
59
|
+
assert 'src' in pattern, "Pattern must include 'src' directory"
|
|
60
|
+
|
|
61
|
+
# Verify it matches expected pattern
|
|
62
|
+
expected_pattern = "python/{wagon}/{feature}/src/{layer}/"
|
|
63
|
+
assert pattern == expected_pattern or pattern.startswith("python/{wagon}/{feature}/src/{layer}"), \
|
|
64
|
+
f"Pattern should be {expected_pattern}, got {pattern}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.coder
|
|
68
|
+
def test_python_component_layer_mapping():
|
|
69
|
+
"""
|
|
70
|
+
SPEC-CODER-CONV-0015: Component naming rules map to layers.
|
|
71
|
+
|
|
72
|
+
Given: Python layer structure with component types
|
|
73
|
+
When: Checking component-to-layer mapping
|
|
74
|
+
Then: Services in domain, repositories in integration, etc.
|
|
75
|
+
"""
|
|
76
|
+
assert BACKEND_CONVENTION.exists(), "backend.convention.yaml must exist"
|
|
77
|
+
|
|
78
|
+
with open(BACKEND_CONVENTION, 'r') as f:
|
|
79
|
+
convention = yaml.safe_load(f)
|
|
80
|
+
|
|
81
|
+
backend = convention['backend']
|
|
82
|
+
|
|
83
|
+
# Backend has layers with component_types that define which components go where
|
|
84
|
+
layers_def = backend.get('layers', {})
|
|
85
|
+
|
|
86
|
+
# Check that component types are defined per layer
|
|
87
|
+
assert 'domain' in layers_def, "Domain layer must be defined"
|
|
88
|
+
assert 'integration' in layers_def, "Integration layer must be defined"
|
|
89
|
+
assert 'presentation' in layers_def, "Presentation layer must be defined"
|
|
90
|
+
assert 'application' in layers_def, "Application layer must be defined"
|
|
91
|
+
|
|
92
|
+
# Verify component types exist in appropriate layers
|
|
93
|
+
domain_types = [ct['name'] for ct in layers_def['domain'].get('component_types', [])]
|
|
94
|
+
assert 'services' in domain_types, "Services should be in domain layer"
|
|
95
|
+
|
|
96
|
+
integration_types = [ct['name'] for ct in layers_def['integration'].get('component_types', [])]
|
|
97
|
+
assert 'repositories' in integration_types, "Repositories should be in integration layer"
|
|
98
|
+
|
|
99
|
+
presentation_types = [ct['name'] for ct in layers_def['presentation'].get('component_types', [])]
|
|
100
|
+
assert 'controllers' in presentation_types, "Controllers should be in presentation layer"
|
|
101
|
+
|
|
102
|
+
application_types = [ct['name'] for ct in layers_def['application'].get('component_types', [])]
|
|
103
|
+
assert 'use_cases' in application_types, "Use cases should be in application layer"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPEC-CODER-CONV-0017: GREEN convention defines 4-layer structure for Supabase
|
|
3
|
+
|
|
4
|
+
Test that green.convention.yaml enforces 4-layer structure for Supabase implementation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
13
|
+
BACKEND_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "backend.convention.yaml"
|
|
14
|
+
FRONTEND_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "frontend.convention.yaml"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.coder
|
|
18
|
+
def test_green_enforces_supabase_layers():
|
|
19
|
+
"""
|
|
20
|
+
SPEC-CODER-CONV-0017: Backend convention enforces 4-layer structure for Supabase.
|
|
21
|
+
|
|
22
|
+
Given: Supabase implementation uses supabase/functions/{wagon}/{feature}/
|
|
23
|
+
When: Reading Supabase layer structure configuration
|
|
24
|
+
Then: Convention enforces 4 layer directories under function root
|
|
25
|
+
"""
|
|
26
|
+
assert BACKEND_CONVENTION.exists(), "backend.convention.yaml must exist"
|
|
27
|
+
|
|
28
|
+
with open(BACKEND_CONVENTION, 'r') as f:
|
|
29
|
+
convention = yaml.safe_load(f)
|
|
30
|
+
|
|
31
|
+
backend = convention['backend']
|
|
32
|
+
assert 'structure' in backend, "Backend must define structure"
|
|
33
|
+
structure = backend['structure']
|
|
34
|
+
|
|
35
|
+
# Check Supabase/TypeScript-specific configuration
|
|
36
|
+
assert 'supabase' in structure or 'typescript' in structure, \
|
|
37
|
+
"Convention must define Supabase/TypeScript layer structure"
|
|
38
|
+
|
|
39
|
+
supabase_config = structure.get('supabase') or structure.get('typescript')
|
|
40
|
+
|
|
41
|
+
# Verify 4 layers are defined
|
|
42
|
+
assert 'layers' in supabase_config, "Supabase config must define layers"
|
|
43
|
+
layers = supabase_config['layers']
|
|
44
|
+
|
|
45
|
+
expected_layers = {'presentation', 'application', 'domain', 'integration'}
|
|
46
|
+
assert set(layers) == expected_layers, \
|
|
47
|
+
f"Layers must be {expected_layers}, got {set(layers)}"
|
|
48
|
+
|
|
49
|
+
# Verify source path pattern
|
|
50
|
+
assert 'src_path_pattern' in supabase_config or 'source_pattern' in supabase_config, \
|
|
51
|
+
"Supabase config must define src_path_pattern"
|
|
52
|
+
|
|
53
|
+
pattern = supabase_config.get('src_path_pattern') or supabase_config.get('source_pattern')
|
|
54
|
+
|
|
55
|
+
assert '{wagon}' in pattern, "Pattern must include {wagon} placeholder"
|
|
56
|
+
assert '{feature}' in pattern, "Pattern must include {feature} placeholder"
|
|
57
|
+
assert '{layer}' in pattern, "Pattern must include {layer} placeholder"
|
|
58
|
+
assert 'supabase/functions' in pattern or 'functions' in pattern, \
|
|
59
|
+
"Pattern must reference Supabase functions directory"
|
|
60
|
+
|
|
61
|
+
# Verify it matches expected pattern
|
|
62
|
+
expected_pattern = "supabase/functions/{wagon}/{feature}/{layer}/"
|
|
63
|
+
assert pattern == expected_pattern or \
|
|
64
|
+
pattern.startswith("supabase/functions/{wagon}/{feature}"), \
|
|
65
|
+
f"Pattern should reference {expected_pattern}, got {pattern}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.coder
|
|
69
|
+
def test_supabase_index_is_thin():
|
|
70
|
+
"""
|
|
71
|
+
SPEC-CODER-CONV-0017: Validates index.ts has no business logic.
|
|
72
|
+
|
|
73
|
+
Given: Supabase layer structure with index.ts at root
|
|
74
|
+
When: Checking index.ts constraints
|
|
75
|
+
Then: index.ts contains only adapter code (imports presentation only)
|
|
76
|
+
"""
|
|
77
|
+
assert BACKEND_CONVENTION.exists(), "backend.convention.yaml must exist"
|
|
78
|
+
|
|
79
|
+
with open(BACKEND_CONVENTION, 'r') as f:
|
|
80
|
+
convention = yaml.safe_load(f)
|
|
81
|
+
|
|
82
|
+
backend = convention['backend']
|
|
83
|
+
structure = backend.get('structure', {})
|
|
84
|
+
supabase_config = structure.get('supabase') or structure.get('typescript', {})
|
|
85
|
+
|
|
86
|
+
# Check for index.ts rules - might be under entrypoint_rules
|
|
87
|
+
index_rules = supabase_config.get('entrypoint_rules', {}).get('index_ts') or \
|
|
88
|
+
supabase_config.get('index_constraints')
|
|
89
|
+
|
|
90
|
+
if index_rules:
|
|
91
|
+
# index.ts should only import from presentation
|
|
92
|
+
assert 'allowed_imports' in index_rules or 'import_restrictions' in index_rules, \
|
|
93
|
+
"index.ts must have import restrictions"
|
|
94
|
+
|
|
95
|
+
allowed = index_rules.get('allowed_imports', [])
|
|
96
|
+
if allowed:
|
|
97
|
+
assert 'presentation' in allowed or './presentation' in str(allowed), \
|
|
98
|
+
"index.ts should only import from presentation layer"
|
|
99
|
+
|
|
100
|
+
# index.ts should not contain business logic
|
|
101
|
+
assert 'no_business_logic' in index_rules or \
|
|
102
|
+
index_rules.get('role') == 'thin_adapter', \
|
|
103
|
+
"index.ts must be documented as thin adapter with no business logic"
|