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,297 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPEC-COACH-UTILS-0292: Detect consumer mismatches between wagon manifests and contract schemas
|
|
3
|
+
SPEC-COACH-UTILS-0293: Apply consumer synchronization updates with user approval
|
|
4
|
+
|
|
5
|
+
Tests validation and synchronization of consumer declarations between:
|
|
6
|
+
- Wagon manifests (plan/*/_*.yaml)
|
|
7
|
+
- Feature manifests (plan/*/*/*.yaml)
|
|
8
|
+
- Contract schemas (contracts/**/*.schema.json)
|
|
9
|
+
"""
|
|
10
|
+
import pytest
|
|
11
|
+
import yaml
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.platform
|
|
17
|
+
def test_detect_consumer_mismatches(tmp_path):
|
|
18
|
+
"""
|
|
19
|
+
SPEC-COACH-UTILS-0292: Detect consumer mismatches between wagon manifests and contract schemas
|
|
20
|
+
|
|
21
|
+
Given: Wagon manifests exist at plan/*/_*.yaml with optional consumers field
|
|
22
|
+
Feature manifests exist at plan/*/*/*.yaml with optional consumers field
|
|
23
|
+
Contract schemas exist at contracts/**/*.schema.json with x-artifact-metadata.consumers
|
|
24
|
+
Consumers in wagon manifests reference contracts as contract:domain:resource
|
|
25
|
+
Consumers in contract schemas follow pattern wagon:name or external:service
|
|
26
|
+
Some manifests may declare consumers that contracts don't list
|
|
27
|
+
Some contracts may list consumers that manifests don't declare
|
|
28
|
+
When: Running consumer validation between manifests and contracts
|
|
29
|
+
Then: All wagon manifests are scanned for consumer declarations
|
|
30
|
+
All feature manifests are scanned for consumer declarations
|
|
31
|
+
All contract schemas are scanned for x-artifact-metadata.consumers
|
|
32
|
+
Mismatches are detected in both directions
|
|
33
|
+
Direction 1 manifest→contract shows wagons/features declaring contracts not listing them as consumers
|
|
34
|
+
Direction 2 contract→manifest shows contracts listing wagon consumers not declared in manifest
|
|
35
|
+
Report shows three fix options 1-update manifests only 2-update contracts only 3-mutual sync both
|
|
36
|
+
No changes applied without user approval
|
|
37
|
+
"""
|
|
38
|
+
# Setup test directories
|
|
39
|
+
plan_dir = tmp_path / "plan"
|
|
40
|
+
plan_dir.mkdir()
|
|
41
|
+
contracts_dir = tmp_path / "contracts"
|
|
42
|
+
contracts_dir.mkdir()
|
|
43
|
+
|
|
44
|
+
# Create wagon manifest that declares consuming a contract
|
|
45
|
+
wagon_dir = plan_dir / "test_wagon"
|
|
46
|
+
wagon_dir.mkdir()
|
|
47
|
+
wagon_manifest = wagon_dir / "_test_wagon.yaml"
|
|
48
|
+
wagon_data = {
|
|
49
|
+
"wagon": "test-wagon",
|
|
50
|
+
"description": "Test wagon",
|
|
51
|
+
"consume": [
|
|
52
|
+
{"name": "contract:match:dilemma.current"}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
with open(wagon_manifest, 'w') as f:
|
|
56
|
+
yaml.dump(wagon_data, f, default_flow_style=False, sort_keys=False)
|
|
57
|
+
|
|
58
|
+
# Create feature manifest that declares consuming a contract
|
|
59
|
+
features_dir = wagon_dir / "features"
|
|
60
|
+
features_dir.mkdir()
|
|
61
|
+
feature_manifest = features_dir / "choose_option.yaml"
|
|
62
|
+
feature_data = {
|
|
63
|
+
"feature": "choose-option",
|
|
64
|
+
"description": "Choose dilemma option",
|
|
65
|
+
"consume": [
|
|
66
|
+
{"name": "contract:match:dilemma.paired"}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
with open(feature_manifest, 'w') as f:
|
|
70
|
+
yaml.dump(feature_data, f, default_flow_style=False, sort_keys=False)
|
|
71
|
+
|
|
72
|
+
# Create contract that DOES list the wagon as consumer (should match)
|
|
73
|
+
dilemma_dir = contracts_dir / "dilemma"
|
|
74
|
+
dilemma_dir.mkdir()
|
|
75
|
+
current_contract = dilemma_dir / "current.schema.json"
|
|
76
|
+
current_data = {
|
|
77
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
78
|
+
"$id": "match:dilemma.current",
|
|
79
|
+
"title": "CurrentDilemma",
|
|
80
|
+
"description": "Current dilemma contract",
|
|
81
|
+
"type": "object",
|
|
82
|
+
"x-artifact-metadata": {
|
|
83
|
+
"domain": "dilemma",
|
|
84
|
+
"resource": "current",
|
|
85
|
+
"api": {"method": "GET", "path": "/dilemmas/current"},
|
|
86
|
+
"producer": "wagon:pace-dilemmas",
|
|
87
|
+
"consumers": ["wagon:test-wagon"], # Matches wagon manifest
|
|
88
|
+
"dependencies": [],
|
|
89
|
+
"traceability": {
|
|
90
|
+
"wagon_ref": "plan/pace_dilemmas/_pace_dilemmas.yaml",
|
|
91
|
+
"feature_refs": ["feature:pace-dilemmas:select-dilemma"]
|
|
92
|
+
},
|
|
93
|
+
"testing": {
|
|
94
|
+
"directory": "contracts/dilemma/tests/",
|
|
95
|
+
"schema_tests": ["current_schema_test.json"]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
with open(current_contract, 'w') as f:
|
|
100
|
+
json.dump(current_data, f, indent=2)
|
|
101
|
+
|
|
102
|
+
# Create contract that DOES NOT list the feature as consumer (mismatch)
|
|
103
|
+
paired_contract = dilemma_dir / "paired.schema.json"
|
|
104
|
+
paired_data = {
|
|
105
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
106
|
+
"$id": "match:dilemma.paired",
|
|
107
|
+
"title": "PairedDilemma",
|
|
108
|
+
"description": "Paired dilemma contract",
|
|
109
|
+
"type": "object",
|
|
110
|
+
"x-artifact-metadata": {
|
|
111
|
+
"domain": "dilemma",
|
|
112
|
+
"resource": "paired",
|
|
113
|
+
"api": {"method": "GET", "path": "/dilemmas/paired"},
|
|
114
|
+
"producer": "wagon:pace-dilemmas",
|
|
115
|
+
"consumers": [], # MISSING wagon:test-wagon - this is a mismatch!
|
|
116
|
+
"dependencies": [],
|
|
117
|
+
"traceability": {
|
|
118
|
+
"wagon_ref": "plan/pace_dilemmas/_pace_dilemmas.yaml",
|
|
119
|
+
"feature_refs": ["feature:pace-dilemmas:pair-fragments"]
|
|
120
|
+
},
|
|
121
|
+
"testing": {
|
|
122
|
+
"directory": "contracts/dilemma/tests/",
|
|
123
|
+
"schema_tests": ["paired_schema_test.json"]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
with open(paired_contract, 'w') as f:
|
|
128
|
+
json.dump(paired_data, f, indent=2)
|
|
129
|
+
|
|
130
|
+
# Create contract that lists a consumer not declared in any manifest (reverse mismatch)
|
|
131
|
+
ux_dir = contracts_dir / "ux"
|
|
132
|
+
ux_dir.mkdir()
|
|
133
|
+
foundations_dir = ux_dir / "foundations"
|
|
134
|
+
foundations_dir.mkdir()
|
|
135
|
+
color_contract = foundations_dir / "color.schema.json"
|
|
136
|
+
color_data = {
|
|
137
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
138
|
+
"$id": "ux:foundations",
|
|
139
|
+
"title": "ColorFoundations",
|
|
140
|
+
"description": "Color foundations contract",
|
|
141
|
+
"type": "object",
|
|
142
|
+
"x-artifact-metadata": {
|
|
143
|
+
"domain": "ux",
|
|
144
|
+
"resource": "foundations",
|
|
145
|
+
"collection": True,
|
|
146
|
+
"member": "color",
|
|
147
|
+
"api": {"method": "GET", "path": "/ux/foundations/color"},
|
|
148
|
+
"producer": "wagon:maintain-ux",
|
|
149
|
+
"consumers": ["wagon:nonexistent-wagon"], # Not declared in any manifest!
|
|
150
|
+
"dependencies": [],
|
|
151
|
+
"traceability": {
|
|
152
|
+
"wagon_ref": "plan/maintain_ux/_maintain_ux.yaml",
|
|
153
|
+
"feature_refs": ["feature:maintain-ux:design-tokens"]
|
|
154
|
+
},
|
|
155
|
+
"testing": {
|
|
156
|
+
"directory": "contracts/commons/ux/foundations/tests/",
|
|
157
|
+
"schema_tests": ["color_schema_test.json"]
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
with open(color_contract, 'w') as f:
|
|
162
|
+
json.dump(color_data, f, indent=2)
|
|
163
|
+
|
|
164
|
+
# Import the validator (will be implemented)
|
|
165
|
+
from atdd.coach.commands.consumers import ConsumerValidator
|
|
166
|
+
|
|
167
|
+
# Run validation
|
|
168
|
+
validator = ConsumerValidator(tmp_path)
|
|
169
|
+
report = validator.detect_mismatches()
|
|
170
|
+
|
|
171
|
+
# Assertions - check that mismatches were detected
|
|
172
|
+
assert "manifest_to_contract" in report, "Should detect manifest→contract mismatches"
|
|
173
|
+
assert "contract_to_manifest" in report, "Should detect contract→manifest mismatches"
|
|
174
|
+
|
|
175
|
+
# Check manifest→contract mismatch (feature declares contract:match:dilemma.paired but contract doesn't list it)
|
|
176
|
+
manifest_to_contract = report["manifest_to_contract"]
|
|
177
|
+
assert len(manifest_to_contract) == 1, "Should find 1 manifest→contract mismatch"
|
|
178
|
+
mismatch = manifest_to_contract[0]
|
|
179
|
+
assert mismatch["manifest"] == str(feature_manifest.relative_to(tmp_path))
|
|
180
|
+
assert mismatch["contract"] == "contract:match:dilemma.paired"
|
|
181
|
+
assert "dilemma/paired.schema.json" in mismatch["contract_file"]
|
|
182
|
+
|
|
183
|
+
# Check contract→manifest mismatch (contract lists wagon:nonexistent-wagon but no manifest declares it)
|
|
184
|
+
contract_to_manifest = report["contract_to_manifest"]
|
|
185
|
+
assert len(contract_to_manifest) == 1, "Should find 1 contract→manifest mismatch"
|
|
186
|
+
mismatch = contract_to_manifest[0]
|
|
187
|
+
assert "ux/foundations/color.schema.json" in mismatch["contract_file"]
|
|
188
|
+
assert mismatch["consumer"] == "wagon:nonexistent-wagon"
|
|
189
|
+
|
|
190
|
+
# Verify no changes were made (validation only)
|
|
191
|
+
with open(paired_contract, 'r') as f:
|
|
192
|
+
unchanged_contract = json.load(f)
|
|
193
|
+
assert unchanged_contract["x-artifact-metadata"]["consumers"] == [], "Contract should be unchanged"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@pytest.mark.platform
|
|
197
|
+
def test_apply_consumer_sync_updates(tmp_path):
|
|
198
|
+
"""
|
|
199
|
+
SPEC-COACH-UTILS-0293: Apply consumer synchronization updates with user approval
|
|
200
|
+
|
|
201
|
+
Given: Consumer mismatches detected between manifests and contracts
|
|
202
|
+
User has selected fix direction 1-manifests 2-contracts 3-mutual
|
|
203
|
+
Target files exist and are valid YAML/JSON
|
|
204
|
+
When: Applying consumer synchronization updates
|
|
205
|
+
Then: If option 1 adds missing consumer references to wagon manifests only
|
|
206
|
+
If option 2 adds missing consumers to contract x-artifact-metadata.consumers only
|
|
207
|
+
If option 3 syncs both directions adding to manifests and contracts
|
|
208
|
+
Wagon manifest updates add contract references in format contract:domain:resource
|
|
209
|
+
Contract schema updates add consumers in format wagon:name or external:service
|
|
210
|
+
All updates preserve existing consumers no duplicates added
|
|
211
|
+
Updated files validate against respective schemas
|
|
212
|
+
YAML and JSON formatting preserved
|
|
213
|
+
Summary report shows all applied changes
|
|
214
|
+
"""
|
|
215
|
+
# Setup test directories
|
|
216
|
+
plan_dir = tmp_path / "plan"
|
|
217
|
+
plan_dir.mkdir()
|
|
218
|
+
contracts_dir = tmp_path / "contracts"
|
|
219
|
+
contracts_dir.mkdir()
|
|
220
|
+
|
|
221
|
+
# Create feature manifest missing consumer declaration
|
|
222
|
+
wagon_dir = plan_dir / "test_wagon"
|
|
223
|
+
wagon_dir.mkdir()
|
|
224
|
+
features_dir = wagon_dir / "features"
|
|
225
|
+
features_dir.mkdir()
|
|
226
|
+
feature_manifest = features_dir / "choose_option.yaml"
|
|
227
|
+
feature_data = {
|
|
228
|
+
"feature": "choose-option",
|
|
229
|
+
"description": "Choose dilemma option",
|
|
230
|
+
"consume": [] # Empty - will be updated
|
|
231
|
+
}
|
|
232
|
+
with open(feature_manifest, 'w') as f:
|
|
233
|
+
yaml.dump(feature_data, f, default_flow_style=False, sort_keys=False)
|
|
234
|
+
|
|
235
|
+
# Create contract missing consumer in metadata
|
|
236
|
+
dilemma_dir = contracts_dir / "dilemma"
|
|
237
|
+
dilemma_dir.mkdir()
|
|
238
|
+
current_contract = dilemma_dir / "current.schema.json"
|
|
239
|
+
current_data = {
|
|
240
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
241
|
+
"$id": "match:dilemma.current",
|
|
242
|
+
"title": "CurrentDilemma",
|
|
243
|
+
"description": "Current dilemma contract",
|
|
244
|
+
"type": "object",
|
|
245
|
+
"x-artifact-metadata": {
|
|
246
|
+
"domain": "dilemma",
|
|
247
|
+
"resource": "current",
|
|
248
|
+
"api": {"method": "GET", "path": "/dilemmas/current"},
|
|
249
|
+
"producer": "wagon:pace-dilemmas",
|
|
250
|
+
"consumers": [], # Empty - will be updated
|
|
251
|
+
"dependencies": [],
|
|
252
|
+
"traceability": {
|
|
253
|
+
"wagon_ref": "plan/pace_dilemmas/_pace_dilemmas.yaml",
|
|
254
|
+
"feature_refs": ["feature:pace-dilemmas:select-dilemma"]
|
|
255
|
+
},
|
|
256
|
+
"testing": {
|
|
257
|
+
"directory": "contracts/dilemma/tests/",
|
|
258
|
+
"schema_tests": ["current_schema_test.json"]
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
with open(current_contract, 'w') as f:
|
|
263
|
+
json.dump(current_data, f, indent=2)
|
|
264
|
+
|
|
265
|
+
# Import the validator
|
|
266
|
+
from atdd.coach.commands.consumers import ConsumerValidator
|
|
267
|
+
|
|
268
|
+
validator = ConsumerValidator(tmp_path)
|
|
269
|
+
|
|
270
|
+
# Test option 3: Mutual sync (both directions)
|
|
271
|
+
updates = [
|
|
272
|
+
{
|
|
273
|
+
"type": "manifest_to_contract",
|
|
274
|
+
"manifest_file": str(feature_manifest),
|
|
275
|
+
"contract_file": str(current_contract),
|
|
276
|
+
"contract_ref": "contract:match:dilemma.current",
|
|
277
|
+
"consumer_ref": "wagon:test-wagon"
|
|
278
|
+
}
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
summary = validator.apply_updates(updates, direction="mutual")
|
|
282
|
+
|
|
283
|
+
# Verify manifest was updated
|
|
284
|
+
with open(feature_manifest, 'r') as f:
|
|
285
|
+
updated_feature = yaml.safe_load(f)
|
|
286
|
+
assert len(updated_feature["consume"]) == 1, "Feature should have 1 consumer"
|
|
287
|
+
assert updated_feature["consume"][0]["name"] == "contract:match:dilemma.current"
|
|
288
|
+
|
|
289
|
+
# Verify contract was updated
|
|
290
|
+
with open(current_contract, 'r') as f:
|
|
291
|
+
updated_contract = json.load(f)
|
|
292
|
+
assert len(updated_contract["x-artifact-metadata"]["consumers"]) == 1
|
|
293
|
+
assert "wagon:test-wagon" in updated_contract["x-artifact-metadata"]["consumers"]
|
|
294
|
+
|
|
295
|
+
# Verify summary report
|
|
296
|
+
assert "applied" in summary
|
|
297
|
+
assert summary["applied"] == 2 # Both manifest and contract updated
|
atdd/coder/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Coder audits, conventions and schemas."""
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
recipe: adapter
|
|
2
|
+
pattern: "Adapter (implements port interface)"
|
|
3
|
+
category: integration
|
|
4
|
+
source: "GoF Design Patterns + Hexagonal Architecture"
|
|
5
|
+
utils: "architecture.py"
|
|
6
|
+
|
|
7
|
+
applies_when:
|
|
8
|
+
- "Port interface exists without implementation"
|
|
9
|
+
- "Need external system integration"
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- step: 1
|
|
13
|
+
what: "Verify port interface exists in application layer"
|
|
14
|
+
where: "application/ports/{resource}_repository.py"
|
|
15
|
+
example: |
|
|
16
|
+
class OrderRepository:
|
|
17
|
+
"""Port for order persistence."""
|
|
18
|
+
|
|
19
|
+
def save(self, order: Order) -> None:
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
|
|
22
|
+
def find_by_id(self, id: OrderId) -> Order:
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
|
|
25
|
+
- step: 2
|
|
26
|
+
what: "Implement adapter in integration layer"
|
|
27
|
+
where: "integration/{tech}_{resource}_repository.py"
|
|
28
|
+
naming: "{Technology}{ResourceName}Repository"
|
|
29
|
+
template: |
|
|
30
|
+
class PostgresOrderRepository(OrderRepository):
|
|
31
|
+
"""PostgreSQL adapter for OrderRepository port."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, db: Database):
|
|
34
|
+
self.db = db
|
|
35
|
+
|
|
36
|
+
def save(self, order: Order) -> None:
|
|
37
|
+
row = OrderMapper.to_row(order)
|
|
38
|
+
self.db.insert('orders', row)
|
|
39
|
+
|
|
40
|
+
def find_by_id(self, id: OrderId) -> Order:
|
|
41
|
+
row = self.db.find_one('orders', {'id': id.value})
|
|
42
|
+
if not row:
|
|
43
|
+
raise OrderNotFound(id)
|
|
44
|
+
return OrderMapper.to_domain(row)
|
|
45
|
+
|
|
46
|
+
verify:
|
|
47
|
+
- run: pytest -xvs tests/integration/test_postgres_order_repository.py
|
|
48
|
+
expect: GREEN
|
|
49
|
+
|
|
50
|
+
- step: 3
|
|
51
|
+
what: "Create mapper (isolate domain from infrastructure)"
|
|
52
|
+
where: "integration/mappers/{resource}_mapper.py"
|
|
53
|
+
purpose: "Prevent infrastructure types from leaking into domain"
|
|
54
|
+
template: |
|
|
55
|
+
class OrderMapper:
|
|
56
|
+
"""Map between Order domain entity and database row."""
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def to_row(order: Order) -> dict:
|
|
60
|
+
return {
|
|
61
|
+
"id": order.id.value,
|
|
62
|
+
"customer_id": order.customer_id.value,
|
|
63
|
+
"total_cents": order.total.cents,
|
|
64
|
+
"status": order.status.value,
|
|
65
|
+
"created_at": order.created_at.isoformat()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def to_domain(row: dict) -> Order:
|
|
70
|
+
return Order.reconstitute(
|
|
71
|
+
id=OrderId(row["id"]),
|
|
72
|
+
customer_id=CustomerId(row["customer_id"]),
|
|
73
|
+
total=Money.from_cents(row["total_cents"]),
|
|
74
|
+
status=OrderStatus(row["status"]),
|
|
75
|
+
created_at=datetime.fromisoformat(row["created_at"])
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
pattern_distinction:
|
|
79
|
+
adapter: "IMPLEMENTS the port interface"
|
|
80
|
+
strategy: "SELECTS which adapter to use at runtime"
|
|
81
|
+
mapper: "TRANSFORMS between domain and infrastructure types"
|
|
82
|
+
|
|
83
|
+
final_verify:
|
|
84
|
+
- "Adapter implements port interface"
|
|
85
|
+
- "Domain layer has NO infrastructure imports"
|
|
86
|
+
- "Mapper isolates type transformations"
|
|
87
|
+
- "Tests can swap implementations (in-memory vs real)"
|
|
88
|
+
- "All tests GREEN"
|