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,660 @@
|
|
|
1
|
+
version: "1.0"
|
|
2
|
+
name: "DTO Convention"
|
|
3
|
+
description: "Data Transfer Objects and mapping patterns for wagon boundaries across all languages"
|
|
4
|
+
|
|
5
|
+
purpose: |
|
|
6
|
+
Defines how to implement contract boundaries in code across Python, Dart, TypeScript, and other languages.
|
|
7
|
+
|
|
8
|
+
DTOs are the CODE-SIDE mirror of contract JSON schemas (spec side).
|
|
9
|
+
They enable type-safe wagon-to-wagon communication without coupling wagons together.
|
|
10
|
+
|
|
11
|
+
Key principle: Wagons communicate via contracts (DTOs), never via direct imports of domain entities.
|
|
12
|
+
|
|
13
|
+
relationship:
|
|
14
|
+
mirrors: "contract.convention.yaml (spec side - language-agnostic JSON schemas)"
|
|
15
|
+
enforced_by: "boundaries.convention.yaml (forbidden cross-wagon imports)"
|
|
16
|
+
used_in: "green.convention.yaml (implementation guidance for wagon internals)"
|
|
17
|
+
generated_by: "coder agent (manual first, codegen later)"
|
|
18
|
+
|
|
19
|
+
architecture:
|
|
20
|
+
layers:
|
|
21
|
+
1_contract_spec:
|
|
22
|
+
what: "JSON Schema files"
|
|
23
|
+
location: "contracts/{theme}/{domain}/{resource}.schema.json"
|
|
24
|
+
authority: "contract.convention.yaml"
|
|
25
|
+
owner: "tester agent"
|
|
26
|
+
|
|
27
|
+
2_contract_dto:
|
|
28
|
+
what: "Language-specific data classes mirroring schemas"
|
|
29
|
+
location: "python/contracts/, dart/lib/contracts/, ts/contracts/"
|
|
30
|
+
authority: "dto.convention.yaml (this file)"
|
|
31
|
+
owner: "coder agent"
|
|
32
|
+
|
|
33
|
+
3_domain_model:
|
|
34
|
+
what: "Wagon-internal business logic entities"
|
|
35
|
+
location: "{wagon}/src/domain/entities/"
|
|
36
|
+
authority: "green.convention.yaml"
|
|
37
|
+
owner: "coder agent"
|
|
38
|
+
|
|
39
|
+
4_mapper:
|
|
40
|
+
what: "DTO ↔ Domain conversion functions"
|
|
41
|
+
location: "{wagon}/src/integration/dto_mapping.{ext}"
|
|
42
|
+
authority: "dto.convention.yaml (mapper_patterns section)"
|
|
43
|
+
owner: "coder agent"
|
|
44
|
+
|
|
45
|
+
flow: |
|
|
46
|
+
Producer Wagon:
|
|
47
|
+
domain_model → mapper.domain_to_dto() → DTO → (serialize to JSON)
|
|
48
|
+
|
|
49
|
+
Contract Boundary:
|
|
50
|
+
JSON payload (validated against contract schema)
|
|
51
|
+
|
|
52
|
+
Consumer Wagon:
|
|
53
|
+
(deserialize from JSON) → DTO → mapper.dto_to_domain() → domain_model
|
|
54
|
+
|
|
55
|
+
# ============================================================================
|
|
56
|
+
# PYTHON
|
|
57
|
+
# ============================================================================
|
|
58
|
+
|
|
59
|
+
languages:
|
|
60
|
+
python:
|
|
61
|
+
version_support: "3.11+"
|
|
62
|
+
|
|
63
|
+
dto_structure:
|
|
64
|
+
base_class: "dataclass from dataclasses"
|
|
65
|
+
immutability: "frozen=True recommended for DTOs"
|
|
66
|
+
location_pattern: "python/contracts/{theme}/{domain}/{resource}.py"
|
|
67
|
+
naming_convention: "{Resource}DTO"
|
|
68
|
+
urn_marker: "# urn: contract:{theme}:{domain}:{resource}.dto"
|
|
69
|
+
|
|
70
|
+
file_header_template: |
|
|
71
|
+
"""
|
|
72
|
+
# urn: contract:{theme}:{domain}:{resource}.dto
|
|
73
|
+
|
|
74
|
+
{Resource}DTO - Contract Data Transfer Object
|
|
75
|
+
|
|
76
|
+
Generated from: contracts/{theme}/{domain}/{resource}.schema.json
|
|
77
|
+
Contract: {theme}:{domain}:{resource}
|
|
78
|
+
Version: {version}
|
|
79
|
+
|
|
80
|
+
This DTO is a pure data structure with NO business logic.
|
|
81
|
+
It represents the contract boundary between wagons.
|
|
82
|
+
|
|
83
|
+
Producer: wagon:{producer_wagon}
|
|
84
|
+
Consumers: {consumer_list}
|
|
85
|
+
"""
|
|
86
|
+
from dataclasses import dataclass
|
|
87
|
+
from typing import Optional, List, Dict, Any
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class {Resource}DTO:
|
|
91
|
+
"""Contract DTO for {theme}:{domain}:{resource}"""
|
|
92
|
+
# fields derived from JSON schema
|
|
93
|
+
|
|
94
|
+
examples:
|
|
95
|
+
simple:
|
|
96
|
+
schema_file: "contracts/match/dilemma/current.schema.json"
|
|
97
|
+
dto_file: "python/contracts/match/dilemma/current.py"
|
|
98
|
+
code: |
|
|
99
|
+
"""
|
|
100
|
+
# urn: contract:match:dilemma.current.dto
|
|
101
|
+
|
|
102
|
+
CurrentDilemmaDTO - Contract Data Transfer Object
|
|
103
|
+
"""
|
|
104
|
+
from dataclasses import dataclass
|
|
105
|
+
from typing import Optional
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class FragmentDTO:
|
|
109
|
+
"""Fragment within a dilemma"""
|
|
110
|
+
id: str
|
|
111
|
+
label: str
|
|
112
|
+
description: str
|
|
113
|
+
kg_subject: str
|
|
114
|
+
domain_attribute: str
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class CurrentDilemmaDTO:
|
|
118
|
+
"""Current dilemma for player presentation"""
|
|
119
|
+
id: str
|
|
120
|
+
fragment_a: FragmentDTO
|
|
121
|
+
fragment_b: FragmentDTO
|
|
122
|
+
selection_metadata: Optional[dict] = None
|
|
123
|
+
presentation: Optional[dict] = None
|
|
124
|
+
|
|
125
|
+
type_mapping:
|
|
126
|
+
description: "JSON Schema types → Python types"
|
|
127
|
+
primitives:
|
|
128
|
+
string: "str"
|
|
129
|
+
integer: "int"
|
|
130
|
+
number: "float"
|
|
131
|
+
boolean: "bool"
|
|
132
|
+
"null": "None"
|
|
133
|
+
|
|
134
|
+
containers:
|
|
135
|
+
array: "List[T]"
|
|
136
|
+
object: "Dict[str, Any]"
|
|
137
|
+
|
|
138
|
+
optionality:
|
|
139
|
+
required_field: "field_name: str"
|
|
140
|
+
optional_field: "field_name: Optional[str] = None"
|
|
141
|
+
|
|
142
|
+
format_specific:
|
|
143
|
+
"date-time": "str # ISO 8601 string, not datetime object in DTO"
|
|
144
|
+
uuid: "str # UUID string, not UUID object in DTO"
|
|
145
|
+
uri: "str"
|
|
146
|
+
email: "str"
|
|
147
|
+
|
|
148
|
+
rationale: |
|
|
149
|
+
DTOs use simple types (str, int, float) rather than rich types (datetime, UUID, Decimal)
|
|
150
|
+
because DTOs cross boundaries and must serialize/deserialize cleanly.
|
|
151
|
+
Rich types belong in domain models, not DTOs.
|
|
152
|
+
|
|
153
|
+
mapper_patterns:
|
|
154
|
+
location: "{wagon}/src/integration/dto_mapping.py"
|
|
155
|
+
naming:
|
|
156
|
+
to_domain: "dto_to_domain()"
|
|
157
|
+
to_dto: "domain_to_dto()"
|
|
158
|
+
|
|
159
|
+
file_structure: |
|
|
160
|
+
"""
|
|
161
|
+
DTO Mapping - Integration Layer
|
|
162
|
+
|
|
163
|
+
Converts between contract DTOs and wagon domain models.
|
|
164
|
+
"""
|
|
165
|
+
from contracts.{theme}.{domain}.{resource} import {Resource}DTO
|
|
166
|
+
from {wagon}.src.domain.entities.{resource} import {Resource}
|
|
167
|
+
|
|
168
|
+
def dto_to_domain(dto: {Resource}DTO) -> {Resource}:
|
|
169
|
+
"""
|
|
170
|
+
Convert contract DTO to wagon domain model.
|
|
171
|
+
|
|
172
|
+
This is where you:
|
|
173
|
+
- Parse/validate rich types (str → UUID, str → datetime)
|
|
174
|
+
- Apply domain-specific business rules
|
|
175
|
+
- Enrich with wagon-specific context
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
dto: Contract DTO from producer wagon
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Domain model for this wagon's internal use
|
|
182
|
+
"""
|
|
183
|
+
return {Resource}(
|
|
184
|
+
id=UUID(dto.id), # str → UUID
|
|
185
|
+
# ... map fields, apply business logic
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def domain_to_dto(entity: {Resource}) -> {Resource}DTO:
|
|
189
|
+
"""
|
|
190
|
+
Convert wagon domain model to contract DTO.
|
|
191
|
+
|
|
192
|
+
This is where you:
|
|
193
|
+
- Serialize rich types (UUID → str, datetime → str)
|
|
194
|
+
- Project to contract shape (may omit internal fields)
|
|
195
|
+
- Ensure contract compliance
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
entity: Domain model from this wagon
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Contract DTO for consumer wagons
|
|
202
|
+
"""
|
|
203
|
+
return {Resource}DTO(
|
|
204
|
+
id=str(entity.id), # UUID → str
|
|
205
|
+
# ... map fields to DTO shape
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
examples:
|
|
209
|
+
full_mapper:
|
|
210
|
+
file: "python/resolve_dilemmas/src/integration/dto_mapping.py"
|
|
211
|
+
code: |
|
|
212
|
+
from uuid import UUID
|
|
213
|
+
from contracts.match.dilemma.current import CurrentDilemmaDTO, FragmentDTO
|
|
214
|
+
from resolve_dilemmas.src.domain.entities.dilemma import Dilemma
|
|
215
|
+
from resolve_dilemmas.src.domain.entities.fragment import Fragment
|
|
216
|
+
|
|
217
|
+
def dto_to_domain(dto: CurrentDilemmaDTO) -> Dilemma:
|
|
218
|
+
"""Convert CurrentDilemmaDTO to Dilemma domain model."""
|
|
219
|
+
return Dilemma(
|
|
220
|
+
id=UUID(dto.id),
|
|
221
|
+
fragment_a=fragment_dto_to_domain(dto.fragment_a),
|
|
222
|
+
fragment_b=fragment_dto_to_domain(dto.fragment_b),
|
|
223
|
+
# May add wagon-specific enrichment here
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def fragment_dto_to_domain(dto: FragmentDTO) -> Fragment:
|
|
227
|
+
"""Convert FragmentDTO to Fragment domain model."""
|
|
228
|
+
return Fragment(
|
|
229
|
+
id=UUID(dto.id),
|
|
230
|
+
label=dto.label,
|
|
231
|
+
description=dto.description,
|
|
232
|
+
kg_subject=dto.kg_subject,
|
|
233
|
+
domain_attribute=dto.domain_attribute,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def domain_to_dto(entity: Dilemma) -> CurrentDilemmaDTO:
|
|
237
|
+
"""Convert Dilemma domain model to CurrentDilemmaDTO."""
|
|
238
|
+
return CurrentDilemmaDTO(
|
|
239
|
+
id=str(entity.id),
|
|
240
|
+
fragment_a=fragment_domain_to_dto(entity.fragment_a),
|
|
241
|
+
fragment_b=fragment_domain_to_dto(entity.fragment_b),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def fragment_domain_to_dto(entity: Fragment) -> FragmentDTO:
|
|
245
|
+
"""Convert Fragment domain model to FragmentDTO."""
|
|
246
|
+
return FragmentDTO(
|
|
247
|
+
id=str(entity.id),
|
|
248
|
+
label=entity.label,
|
|
249
|
+
description=entity.description,
|
|
250
|
+
kg_subject=entity.kg_subject,
|
|
251
|
+
domain_attribute=entity.domain_attribute,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
validation:
|
|
255
|
+
dto_validation: "DTOs should be validated against JSON schema after deserialization"
|
|
256
|
+
mapper_validation: "Mappers should validate domain invariants when creating domain models"
|
|
257
|
+
testing: "Each mapper should have unit tests covering both directions (dto→domain, domain→dto)"
|
|
258
|
+
|
|
259
|
+
# ============================================================================
|
|
260
|
+
# DART
|
|
261
|
+
# ============================================================================
|
|
262
|
+
|
|
263
|
+
dart:
|
|
264
|
+
version_support: "3.0+"
|
|
265
|
+
|
|
266
|
+
dto_structure:
|
|
267
|
+
base_class: "none (plain immutable class with const constructor)"
|
|
268
|
+
immutability: "All fields final"
|
|
269
|
+
location_pattern: "dart/lib/contracts/{theme}/{domain}/{resource}.dart"
|
|
270
|
+
naming_convention: "{Resource}Dto"
|
|
271
|
+
|
|
272
|
+
file_header_template: |
|
|
273
|
+
/// # urn: contract:{theme}:{domain}:{resource}.dto
|
|
274
|
+
///
|
|
275
|
+
/// {Resource}Dto - Contract Data Transfer Object
|
|
276
|
+
///
|
|
277
|
+
/// Generated from: contracts/{theme}/{domain}/{resource}.schema.json
|
|
278
|
+
/// Contract: {theme}:{domain}:{resource}
|
|
279
|
+
/// Version: {version}
|
|
280
|
+
///
|
|
281
|
+
/// This DTO is a pure data structure with NO business logic.
|
|
282
|
+
/// It represents the contract boundary between wagons.
|
|
283
|
+
|
|
284
|
+
class {Resource}Dto {
|
|
285
|
+
const {Resource}Dto({
|
|
286
|
+
required this.field1,
|
|
287
|
+
required this.field2,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
final String field1;
|
|
291
|
+
final int field2;
|
|
292
|
+
|
|
293
|
+
Map<String, dynamic> toJson() => {
|
|
294
|
+
'field1': field1,
|
|
295
|
+
'field2': field2,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
factory {Resource}Dto.fromJson(Map<String, dynamic> json) => {Resource}Dto(
|
|
299
|
+
field1: json['field1'] as String,
|
|
300
|
+
field2: json['field2'] as int,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
examples:
|
|
305
|
+
simple:
|
|
306
|
+
schema_file: "contracts/match/dilemma/current.schema.json"
|
|
307
|
+
dto_file: "dart/lib/contracts/match/dilemma/current.dart"
|
|
308
|
+
code: |
|
|
309
|
+
/// # urn: contract:match:dilemma.current.dto
|
|
310
|
+
|
|
311
|
+
class FragmentDto {
|
|
312
|
+
const FragmentDto({
|
|
313
|
+
required this.id,
|
|
314
|
+
required this.label,
|
|
315
|
+
required this.description,
|
|
316
|
+
required this.kgSubject,
|
|
317
|
+
required this.domainAttribute,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
final String id;
|
|
321
|
+
final String label;
|
|
322
|
+
final String description;
|
|
323
|
+
final String kgSubject;
|
|
324
|
+
final String domainAttribute;
|
|
325
|
+
|
|
326
|
+
Map<String, dynamic> toJson() => {
|
|
327
|
+
'id': id,
|
|
328
|
+
'label': label,
|
|
329
|
+
'description': description,
|
|
330
|
+
'kg_subject': kgSubject,
|
|
331
|
+
'domain_attribute': domainAttribute,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
factory FragmentDto.fromJson(Map<String, dynamic> json) => FragmentDto(
|
|
335
|
+
id: json['id'] as String,
|
|
336
|
+
label: json['label'] as String,
|
|
337
|
+
description: json['description'] as String,
|
|
338
|
+
kgSubject: json['kg_subject'] as String,
|
|
339
|
+
domainAttribute: json['domain_attribute'] as String,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
class CurrentDilemmaDto {
|
|
344
|
+
const CurrentDilemmaDto({
|
|
345
|
+
required this.id,
|
|
346
|
+
required this.fragmentA,
|
|
347
|
+
required this.fragmentB,
|
|
348
|
+
this.selectionMetadata,
|
|
349
|
+
this.presentation,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
final String id;
|
|
353
|
+
final FragmentDto fragmentA;
|
|
354
|
+
final FragmentDto fragmentB;
|
|
355
|
+
final Map<String, dynamic>? selectionMetadata;
|
|
356
|
+
final Map<String, dynamic>? presentation;
|
|
357
|
+
|
|
358
|
+
Map<String, dynamic> toJson() => {
|
|
359
|
+
'id': id,
|
|
360
|
+
'fragment_a': fragmentA.toJson(),
|
|
361
|
+
'fragment_b': fragmentB.toJson(),
|
|
362
|
+
if (selectionMetadata != null) 'selection_metadata': selectionMetadata,
|
|
363
|
+
if (presentation != null) 'presentation': presentation,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
factory CurrentDilemmaDto.fromJson(Map<String, dynamic> json) => CurrentDilemmaDto(
|
|
367
|
+
id: json['id'] as String,
|
|
368
|
+
fragmentA: FragmentDto.fromJson(json['fragment_a'] as Map<String, dynamic>),
|
|
369
|
+
fragmentB: FragmentDto.fromJson(json['fragment_b'] as Map<String, dynamic>),
|
|
370
|
+
selectionMetadata: json['selection_metadata'] as Map<String, dynamic>?,
|
|
371
|
+
presentation: json['presentation'] as Map<String, dynamic>?,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
type_mapping:
|
|
376
|
+
description: "JSON Schema types → Dart types"
|
|
377
|
+
primitives:
|
|
378
|
+
string: "String"
|
|
379
|
+
integer: "int"
|
|
380
|
+
number: "double"
|
|
381
|
+
boolean: "bool"
|
|
382
|
+
"null": "null"
|
|
383
|
+
|
|
384
|
+
containers:
|
|
385
|
+
array: "List<T>"
|
|
386
|
+
object: "Map<String, dynamic>"
|
|
387
|
+
|
|
388
|
+
optionality:
|
|
389
|
+
required_field: "required this.fieldName"
|
|
390
|
+
optional_field: "this.fieldName"
|
|
391
|
+
|
|
392
|
+
format_specific:
|
|
393
|
+
"date-time": "String // ISO 8601 string"
|
|
394
|
+
uuid: "String // UUID string"
|
|
395
|
+
uri: "String"
|
|
396
|
+
email: "String"
|
|
397
|
+
|
|
398
|
+
mapper_patterns:
|
|
399
|
+
location: "{wagon}/lib/integration/dto_mapping.dart"
|
|
400
|
+
naming:
|
|
401
|
+
to_domain: "dtoToDomain()"
|
|
402
|
+
to_dto: "domainToDto()"
|
|
403
|
+
|
|
404
|
+
file_structure: |
|
|
405
|
+
import 'package:contracts/match/dilemma/current.dart';
|
|
406
|
+
import 'package:{wagon}/domain/entities/dilemma.dart';
|
|
407
|
+
|
|
408
|
+
Dilemma dtoToDomain(CurrentDilemmaDto dto) {
|
|
409
|
+
return Dilemma(
|
|
410
|
+
id: dto.id,
|
|
411
|
+
// ... map fields
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
CurrentDilemmaDto domainToDto(Dilemma entity) {
|
|
416
|
+
return CurrentDilemmaDto(
|
|
417
|
+
id: entity.id,
|
|
418
|
+
// ... map fields
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# ============================================================================
|
|
423
|
+
# TYPESCRIPT
|
|
424
|
+
# ============================================================================
|
|
425
|
+
|
|
426
|
+
typescript:
|
|
427
|
+
version_support: "5.0+"
|
|
428
|
+
|
|
429
|
+
dto_structure:
|
|
430
|
+
base_class: "interface (or type alias)"
|
|
431
|
+
immutability: "readonly properties"
|
|
432
|
+
location_pattern: "ts/contracts/{theme}/{domain}/{resource}.ts"
|
|
433
|
+
naming_convention: "{Resource}DTO"
|
|
434
|
+
|
|
435
|
+
file_header_template: |
|
|
436
|
+
/**
|
|
437
|
+
* # urn: contract:{theme}:{domain}:{resource}.dto
|
|
438
|
+
*
|
|
439
|
+
* {Resource}DTO - Contract Data Transfer Object
|
|
440
|
+
*
|
|
441
|
+
* Generated from: contracts/{theme}/{domain}/{resource}.schema.json
|
|
442
|
+
* Contract: {theme}:{domain}:{resource}
|
|
443
|
+
* Version: {version}
|
|
444
|
+
*/
|
|
445
|
+
|
|
446
|
+
export interface {Resource}DTO {
|
|
447
|
+
readonly field1: string;
|
|
448
|
+
readonly field2: number;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
examples:
|
|
452
|
+
simple:
|
|
453
|
+
schema_file: "contracts/match/dilemma/current.schema.json"
|
|
454
|
+
dto_file: "ts/contracts/match/dilemma/current.ts"
|
|
455
|
+
code: |
|
|
456
|
+
/**
|
|
457
|
+
* # urn: contract:match:dilemma.current.dto
|
|
458
|
+
*/
|
|
459
|
+
|
|
460
|
+
export interface FragmentDTO {
|
|
461
|
+
readonly id: string;
|
|
462
|
+
readonly label: string;
|
|
463
|
+
readonly description: string;
|
|
464
|
+
readonly kg_subject: string;
|
|
465
|
+
readonly domain_attribute: string;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export interface CurrentDilemmaDTO {
|
|
469
|
+
readonly id: string;
|
|
470
|
+
readonly fragment_a: FragmentDTO;
|
|
471
|
+
readonly fragment_b: FragmentDTO;
|
|
472
|
+
readonly selection_metadata?: Record<string, unknown>;
|
|
473
|
+
readonly presentation?: Record<string, unknown>;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
type_mapping:
|
|
477
|
+
description: "JSON Schema types → TypeScript types"
|
|
478
|
+
primitives:
|
|
479
|
+
string: "string"
|
|
480
|
+
integer: "number"
|
|
481
|
+
number: "number"
|
|
482
|
+
boolean: "boolean"
|
|
483
|
+
"null": "null"
|
|
484
|
+
|
|
485
|
+
containers:
|
|
486
|
+
array: "Array<T> or T[]"
|
|
487
|
+
object: "Record<string, unknown> or { [key: string]: unknown }"
|
|
488
|
+
|
|
489
|
+
optionality:
|
|
490
|
+
required_field: "fieldName: string"
|
|
491
|
+
optional_field: "fieldName?: string"
|
|
492
|
+
|
|
493
|
+
format_specific:
|
|
494
|
+
"date-time": "string // ISO 8601 string"
|
|
495
|
+
uuid: "string // UUID string"
|
|
496
|
+
uri: "string"
|
|
497
|
+
email: "string"
|
|
498
|
+
|
|
499
|
+
mapper_patterns:
|
|
500
|
+
location: "{wagon}/src/integration/dtoMapping.ts"
|
|
501
|
+
naming:
|
|
502
|
+
to_domain: "dtoToDomain()"
|
|
503
|
+
to_dto: "domainToDto()"
|
|
504
|
+
|
|
505
|
+
file_structure: |
|
|
506
|
+
import type { CurrentDilemmaDTO } from '@contracts/match/dilemma/current';
|
|
507
|
+
import { Dilemma } from '@{wagon}/domain/entities/dilemma';
|
|
508
|
+
|
|
509
|
+
export function dtoToDomain(dto: CurrentDilemmaDTO): Dilemma {
|
|
510
|
+
return new Dilemma({
|
|
511
|
+
id: dto.id,
|
|
512
|
+
// ... map fields
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function domainToDto(entity: Dilemma): CurrentDilemmaDTO {
|
|
517
|
+
return {
|
|
518
|
+
id: entity.id,
|
|
519
|
+
// ... map fields
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
# ============================================================================
|
|
524
|
+
# VALIDATION & ENFORCEMENT
|
|
525
|
+
# ============================================================================
|
|
526
|
+
|
|
527
|
+
validation:
|
|
528
|
+
dto_requirements:
|
|
529
|
+
- rule: "DTOs MUST be pure data structures with NO methods (except serialization)"
|
|
530
|
+
violation: "Adding business logic to DTO"
|
|
531
|
+
fix: "Move logic to domain model or mapper"
|
|
532
|
+
|
|
533
|
+
- rule: "DTOs MUST match contract schema exactly (fields, types, optionality)"
|
|
534
|
+
violation: "DTO diverges from schema"
|
|
535
|
+
fix: "Regenerate DTO from schema or update schema"
|
|
536
|
+
|
|
537
|
+
- rule: "DTOs MUST use simple types (str, int, float), not rich types (UUID, datetime)"
|
|
538
|
+
violation: "DTO has UUID field instead of str"
|
|
539
|
+
fix: "Use str in DTO, convert in mapper"
|
|
540
|
+
|
|
541
|
+
- rule: "DTOs MUST be immutable (frozen=True in Python, final in Dart, readonly in TS)"
|
|
542
|
+
violation: "Mutable DTO field"
|
|
543
|
+
fix: "Make all fields immutable"
|
|
544
|
+
|
|
545
|
+
mapper_requirements:
|
|
546
|
+
- rule: "Mappers MUST live in integration layer, never in domain"
|
|
547
|
+
violation: "Mapper in domain/entities/"
|
|
548
|
+
fix: "Move to {wagon}/src/integration/dto_mapping.{ext}"
|
|
549
|
+
|
|
550
|
+
- rule: "Mappers MUST handle both directions (dto→domain, domain→dto)"
|
|
551
|
+
violation: "Only one-way mapper"
|
|
552
|
+
fix: "Implement both conversion functions"
|
|
553
|
+
|
|
554
|
+
- rule: "Mappers MUST validate domain invariants when creating domain models"
|
|
555
|
+
violation: "Mapper creates invalid domain object"
|
|
556
|
+
fix: "Add validation in dto_to_domain()"
|
|
557
|
+
|
|
558
|
+
- rule: "Mappers MUST NOT call other wagons' code"
|
|
559
|
+
violation: "Mapper imports from another wagon"
|
|
560
|
+
fix: "Use composition or pass dependencies"
|
|
561
|
+
|
|
562
|
+
testing_requirements:
|
|
563
|
+
- "Each DTO MUST have serialization round-trip test (toJson/fromJson)"
|
|
564
|
+
- "Each mapper MUST have unit tests for both directions"
|
|
565
|
+
- "Mapper tests MUST verify domain invariants are enforced"
|
|
566
|
+
- "Integration tests MUST verify DTOs match contract schemas"
|
|
567
|
+
- "Integration tests MUST use ID comparison (not object identity) when asserting DTO→Entity conversions"
|
|
568
|
+
|
|
569
|
+
testing_patterns:
|
|
570
|
+
dto_entity_boundary_assertions:
|
|
571
|
+
problem: "After DTO→Entity conversion, object identity (in operator) fails"
|
|
572
|
+
reason: "Mapper creates new entity instances; DTO and Entity are different objects"
|
|
573
|
+
|
|
574
|
+
antipattern:
|
|
575
|
+
code: |
|
|
576
|
+
# ❌ WRONG: Object identity fails after DTO→Entity conversion
|
|
577
|
+
returned_entity = use_case.execute(dto_list)
|
|
578
|
+
assert returned_entity in dto_list # FAILS: different types/instances
|
|
579
|
+
|
|
580
|
+
why_fails: "Python 'in' operator uses __eq__ or identity; Entity ≠ DTO"
|
|
581
|
+
|
|
582
|
+
correct_pattern:
|
|
583
|
+
code: |
|
|
584
|
+
# ✅ CORRECT: Use ID comparison across DTO/Entity boundary
|
|
585
|
+
returned_entity = use_case.execute(dto_list)
|
|
586
|
+
dto_ids = {dto.id for dto in dto_list}
|
|
587
|
+
assert returned_entity.id in dto_ids # PASSES: ID is stable
|
|
588
|
+
|
|
589
|
+
rationale: "IDs are stable across DTO/Entity boundary per contract"
|
|
590
|
+
|
|
591
|
+
when_to_use:
|
|
592
|
+
- "Integration tests where use cases accept DTOs but return entities"
|
|
593
|
+
- "Tests validating that returned data came from input pool"
|
|
594
|
+
- "Pairing/selection algorithms that filter input lists"
|
|
595
|
+
|
|
596
|
+
examples:
|
|
597
|
+
- "Fragment pairing: assert dilemma.fragment_a.id in {f.id for f in fragments}"
|
|
598
|
+
- "Hot pool selection: assert selected.id in {f.id for f in warm_library}"
|
|
599
|
+
- "Domain filtering: assert choice.id in {c.id for c in available_choices}"
|
|
600
|
+
|
|
601
|
+
enforcement:
|
|
602
|
+
test_location: "atdd/coder/test_wagon_boundaries.py"
|
|
603
|
+
rules:
|
|
604
|
+
- "No cross-wagon imports of domain entities (use DTOs)"
|
|
605
|
+
- "No cross-wagon imports of use cases or controllers (never allowed)"
|
|
606
|
+
- "DTOs must live in neutral contracts/ namespace"
|
|
607
|
+
- "Mappers must live in wagon integration/ layer"
|
|
608
|
+
|
|
609
|
+
# ============================================================================
|
|
610
|
+
# GENERATION STRATEGY
|
|
611
|
+
# ============================================================================
|
|
612
|
+
|
|
613
|
+
generation:
|
|
614
|
+
phase_1_manual:
|
|
615
|
+
when: "Initial implementation (now)"
|
|
616
|
+
scope: "First 2-3 contracts (Fragment, Dilemma)"
|
|
617
|
+
process:
|
|
618
|
+
- "Read JSON schema"
|
|
619
|
+
- "Hand-write DTO following patterns above"
|
|
620
|
+
- "Hand-write mapper following patterns above"
|
|
621
|
+
- "Validate with tests"
|
|
622
|
+
goal: "Establish pattern and validate conventions"
|
|
623
|
+
|
|
624
|
+
phase_2_codegen:
|
|
625
|
+
when: "After pattern is validated"
|
|
626
|
+
scope: "Remaining contracts"
|
|
627
|
+
tools:
|
|
628
|
+
- "Custom generator reading contracts/**/*.schema.json"
|
|
629
|
+
- "Emits python/contracts/, dart/lib/contracts/, ts/contracts/"
|
|
630
|
+
- "Idempotent and safe to re-run"
|
|
631
|
+
considerations:
|
|
632
|
+
- "May use datamodel-code-generator as base for Python"
|
|
633
|
+
- "Need custom wrapper for URN comments and naming"
|
|
634
|
+
- "Should respect this convention file"
|
|
635
|
+
|
|
636
|
+
# ============================================================================
|
|
637
|
+
# CROSS-REFERENCES
|
|
638
|
+
# ============================================================================
|
|
639
|
+
|
|
640
|
+
references:
|
|
641
|
+
spec_authority: ".claude/conventions/tester/contract.convention.yaml"
|
|
642
|
+
enforcement: ".claude/conventions/coder/boundaries.convention.yaml"
|
|
643
|
+
implementation: ".claude/conventions/coder/green.convention.yaml"
|
|
644
|
+
testing: "atdd/coder/test_wagon_boundaries.py"
|
|
645
|
+
|
|
646
|
+
cross_convention_rules:
|
|
647
|
+
from_contract_convention:
|
|
648
|
+
- "JSON schema is source of truth for DTO structure"
|
|
649
|
+
- "Contract $id maps to DTO URN"
|
|
650
|
+
- "Contract versioning drives DTO versioning"
|
|
651
|
+
|
|
652
|
+
to_boundaries_convention:
|
|
653
|
+
- "DTOs are the ONLY allowed cross-wagon data type"
|
|
654
|
+
- "Use cases/controllers NEVER cross boundaries"
|
|
655
|
+
- "Qualified imports only (python/contracts/..., never from wagon)"
|
|
656
|
+
|
|
657
|
+
to_green_convention:
|
|
658
|
+
- "Domain models are wagon-internal"
|
|
659
|
+
- "Mappers live in integration layer"
|
|
660
|
+
- "Clean Architecture respected within wagons"
|