Bounded Context Scaffold¶
Overview¶
The scaffold module (ascetic_ddd.cli.scaffold) generates Python code for a
DDD Bounded Context from a declarative YAML domain model.
A single YAML file produces a complete directory tree: Aggregate roots with exporter/reconstitutor infrastructure, Value Object classes (identity, string, enum, composite), Domain Event dataclasses with exporters, and CQRS Command dataclasses with async handler stubs.
Generated code follows the patterns described in Design Patterns — encapsulated aggregates that expose state through the Mediator (exporter/reconstitutor) pattern, never through getters.
Why?¶
Setting up a new aggregate by hand involves creating 10-30 files with a lot
of boilerplate: the aggregate class, its exporter and reconstitutor interfaces,
value object classes with validation, domain events, commands, __init__.py
re-exports. Each file follows a strict structural convention.
The scaffold automates this initial setup. You describe the domain model declaratively — fields, value objects, events — and get a compilable, structurally correct codebase ready for customization. The generated code is a starting point, not a framework: you own it and modify it freely.
The YAML schema enforces DDD constraints at definition time:
Value objects use a class hierarchy (
IdentityVoDef,SimpleVoDef,EnumVoDef,CompositeVoDef), each with its own validation rules and generated structure.Commands use only primitive types — domain types do not leak into the application layer.
Collection fields generate
add_*methods on exporters (notset_*), enforcing the aggregate’s control over its internal collections.
Usage¶
python -m ascetic_ddd.cli scaffold \
-i domain-model.yaml \
-o ./output \
-p app.jobs
Flag |
Description |
|---|---|
|
Path to domain-model YAML file (required) |
|
Output directory for generated code (required) |
|
Base package name for imports, e.g. |
|
Custom templates directory (optional, see Custom templates) |
Programmatic usage:
from ascetic_ddd.cli.scaffold import scaffold
scaffold("domain-model.yaml", "./output", "app.jobs")
Complete YAML Example¶
aggregates:
Resume:
fields:
_id: ResumeId
_user_id: UserId
_title: Title
_description: Description
_specialization_ids: list[.specialization.values.SpecializationId]
_rate: Rate
_employment_types: list[EmploymentType]
_work_formats: list[WorkFormat]
_show_reputation: bool
_created_at: datetime
_updated_at: datetime
_is_active: bool
_experience: list[Experience] # collection entity
entities:
Experience:
fields:
resume_id: .resume.values.ResumeId
company_name: CompanyName
date_range: TimeRange
value_objects:
CompanyName:
type: str
constraints:
blank: false
max_length: 255
map:
- strip
TimeRange:
import: ascetic_ddd.seedwork.domain.values.TimeRange
value_objects:
ResumeId: # identity VO
type: int
identity: transient
UserId: # string VO with external reference
type: int
reference: external
constraints:
required: true
Title: # string VO with validation + strip
type: str
constraints:
blank: false
max_length: 255
map:
- strip
Description: # string VO with validation
type: str
constraints:
blank: false
map:
- strip
Rate: # composite VO
fields:
_rate_period: PaymentPeriod
_rate: ascetic_ddd.seedwork.domain.values.Money
constraints:
required: true
EmploymentType: # enum VO
type: Enum[str]
values:
FULL_TIME: "full_time"
PART_TIME: "part_time"
ONE_TIME: "one_time"
CONSULTING: "consulting"
MENTORING: "mentoring"
PaymentPeriod: # enum VO (used inside composite Rate)
type: Enum[str]
values:
HOURLY: "hourly"
MONTHLY: "monthly"
YEARLY: "yearly"
ONE_TIME: "one_time"
WorkFormat: # enum VO
type: Enum[str]
values:
OFFICE: "office"
HYBRID: "hybrid"
REMOTE: "remote"
domain_events:
ResumeCreated: # -> derives CreateResumeCommand
fields:
aggregate_id: ResumeId
user_id: UserId
title: Title
description: Description
specialization_ids: tuple[.specialization.values.SpecializationId, ...]
rate: Rate
employment_types: tuple[EmploymentType, ...]
work_formats: tuple[WorkFormat, ...]
show_reputation: bool
created_at: datetime
is_active: bool
event_version: 1 # metadata, not a domain field
Specialization:
fields:
_id: SpecializationId
_profile: SpecializationProfile # single entity
entities:
SpecializationProfile:
fields:
bio: str
level: str
value_objects:
SpecializationId:
type: int
identity: transient
external_references: # VOs from other bounded contexts
value_objects:
UserId:
type: int
reference: User
constraints:
required: true
This model generates files across two aggregates, including value objects of all four kinds (identity, string, enum, composite), entities (collection and single), a domain event with exporter, and a derived command with handler.
YAML Schema¶
Top-level structure¶
aggregates: # required, at least one
AggregateName:
fields: { ... }
value_objects: { ... }
entities: { ... }
domain_events: { ... }
external_references: # optional
value_objects:
TypeName:
type: int
reference: ExternalContext
Allowed top-level keys: aggregates, external_references.
Unknown keys raise ValueError.
Fields¶
Fields describe the internal state of an aggregate, an entity, a composite
VO, or a domain event. Declared as name: type pairs.
fields:
_id: ResumeId # VO reference
_title: Title # VO reference
_specialization_ids: list[.specialization.values.SpecializationId] # dotted path in collection
_rate: Rate # composite VO
_employment_types: list[EmploymentType]
_experience: list[Experience] # entity collection
_show_reputation: bool # primitive
_created_at: datetime # primitive
Underscore prefix (_id) denotes private aggregate state.
The prefix is stripped for parameter names in constructors and exporters
(_id -> id, _specialization_ids -> specialization_ids).
Primitive types: bool, int, str, float, datetime, Decimal.
Collection types: list[T], tuple[T, ...].
Collections generate add_* (singular form) methods on exporter interfaces
instead of set_*.
Inline dotted paths. A field type can be a dotted path referencing a VO from another aggregate or an external package. The parser creates a synthetic VO definition from the path — no explicit VO declaration is needed:
# Relative path (resolves within current bounded context's domain package)
_specialization_ids: list[.specialization.values.SpecializationId]
# Absolute path (external package)
_rate: ascetic_ddd.seedwork.domain.values.Money
The class name is extracted from the last segment; the module path is derived
by converting the class name to snake_case:
.specialization.values.SpecializationId → import from
.specialization.values.specialization_id.
Value Objects¶
A VO kind is determined by which keys are present in its definition.
Allowed keys: type, identity, fields, values, constraints, map,
reference, import. Unknown keys raise ValueError.
Identity VO¶
ResumeId:
type: int # int | str | uuid
identity: transient # transient | persistent
Discriminator: presence of the identity key.
Generated class extends IntIdentity, StrIdentity, or UuidIdentity
from ascetic_ddd.seedwork.domain.identity.
from ascetic_ddd.seedwork.domain.identity import IntIdentity
class ResumeId(IntIdentity):
pass
String VO¶
Title:
type: str
constraints:
blank: false # reject empty / whitespace-only (default: true)
max_length: 255 # reject strings over N chars (default: no limit)
map:
- strip # strip whitespace on init
Discriminator: no identity, no fields, no values.
Generated class validates constraints in __init__, exposes a value
property, implements __eq__, __hash__, and export(setter):
class Title:
def __init__(self, value: str) -> None:
if not value or not value.strip():
raise ValueError("Title cannot be empty")
if len(value) > 255:
raise ValueError("Title cannot exceed 255 characters")
self._value = value.strip()
def export(self, setter: typing.Callable[[str], None]) -> None:
setter(self._value)
Enum VO¶
EmploymentType:
type: Enum[str]
values:
FULL_TIME: "full_time"
PART_TIME: "part_time"
ONE_TIME: "one_time"
Discriminator: type value starts with Enum[.
Generated class extends str, Enum:
class EmploymentType(str, Enum):
FULL_TIME = "full_time"
PART_TIME = "part_time"
ONE_TIME = "one_time"
def export(self, setter: typing.Callable[[str], None]) -> None:
setter(self.value)
Composite VO¶
Rate:
fields:
_rate_period: PaymentPeriod
_rate: ascetic_ddd.seedwork.domain.values.Money
constraints:
required: true
Discriminator: presence of the fields key (without identity).
Generated as a class with an exporter interface and a separate exporter module:
class IRateExporter(metaclass=ABCMeta):
@abstractmethod
def set_rate_period(self, value) -> None: ...
@abstractmethod
def set_rate(self, value) -> None: ...
class Rate:
def export(self, exporter: "IRateExporter") -> None:
self._rate_period.export(exporter.set_rate_period)
self._rate.export(exporter.set_rate)
Composite VO fields can reference aggregate-level VOs, primitives, and
inline dotted paths (e.g. ascetic_ddd.seedwork.domain.values.Money).
When multiple composite VOs depend on each other, the parser uses
topological sorting (Kahn’s algorithm) to determine parse order.
Reference marker¶
Any VO can carry a reference key to document cross-aggregate or external
dependencies:
SpecializationId:
type: int
reference: Specialization # another aggregate in this context
UserId:
type: int
reference: external # external bounded context
The reference value is stored as metadata; the scaffold does not follow it.
Imported VO¶
There are two ways to reference VOs from external packages or other aggregates:
1. Inline dotted path in fields (preferred). Use a dotted path directly as the field type — no VO declaration needed:
fields:
_rate: ascetic_ddd.seedwork.domain.values.Money # absolute
_specialization_ids: list[.specialization.values.SpecializationId] # relative
The class name is the last path segment. The import module is derived by
converting the class name to snake_case. With --package app.jobs, the
relative path generates:
from app.jobs.domain.specialization.values.specialization_id import SpecializationId
2. Explicit import key on VO declaration. Use the import key with
package.ClassName format:
TimeRange:
import: ascetic_ddd.seedwork.domain.values.TimeRange
When import is specified, the scaffold does not generate a file for this VO.
The module path is derived from the class name:
ascetic_ddd.seedwork.domain.values.TimeRange →
from ascetic_ddd.seedwork.domain.values.time_range import TimeRange.
Relative imports. A . prefix resolves relative to the domain package
(i.e. {package_name}.domain). This works both in inline dotted paths and
in the import key.
The import key can be combined with other keys. For instance, a composite
imported VO (import + fields) will also import its exporter interface
and exporter class from the external package, following the same naming
convention as locally generated composite VOs.
Constraints reference¶
Key |
Applies to |
Default |
Description |
|---|---|---|---|
|
any VO |
|
Value must not be null |
|
string VO |
|
Empty / whitespace-only is allowed |
|
string VO |
|
Maximum string length |
Maps reference¶
The map key accepts a list of mapping names applied to the value on init:
Map |
Applies to |
Description |
|---|---|---|
|
string VO |
Strip leading/trailing whitespace |
Entities¶
Entities are child objects owned by an aggregate. They can hold their own value objects, fields, and even nested entities (recursive):
entities:
Experience:
fields:
resume_id: .resume.values.ResumeId
company_name: CompanyName
date_range: TimeRange
value_objects:
CompanyName:
type: str
constraints:
blank: false
max_length: 255
TimeRange:
import: ascetic_ddd.seedwork.domain.values.TimeRange
Collection entities are referenced from aggregate fields via list[EntityName].
They generate add_* methods on the aggregate exporter, an _experience = []
initialization in _make_empty, and list iteration in export:
fields:
_experience: list[Experience]
Single entities are referenced directly by name. They generate set_*
methods (not add_*), None initialization, and direct assignment:
fields:
_profile: SpecializationProfile
Entity fields can reference VOs from the parent aggregate scope via inline
dotted paths (e.g. .resume.values.ResumeId). Each entity generates its
own directory with the entity class, exporter, reconstitutor, and a
values/ subdirectory.
Domain Events¶
domain_events:
ResumeCreated:
fields:
aggregate_id: ResumeId
user_id: UserId
title: Title
event_version: 1 # metadata (default: 1), not a domain field
Generated as frozen dataclasses extending PersistentDomainEvent.
Each event gets an exporter interface and a separate exporter module.
The special key event_version inside fields is extracted as metadata and
excluded from the domain fields list.
Command derivation¶
Commands are derived automatically from domain events by suffix:
Event class name |
Derived command |
|---|---|
|
|
|
|
|
|
Command fields are the same as the event fields, but all VO types are mapped to their primitive equivalents. Domain types do not leak into the application layer:
@dataclass(frozen=True, kw_only=True)
class CreateResumeCommand:
aggregate_id: int # not ResumeId
user_id: int # not UserId
title: str # not Title
command_version: int = 1
Primitive mapping rules:
VO kind |
Primitive type |
|---|---|
Identity |
base type ( |
String |
|
Enum |
|
Composite |
|
Generated Structure¶
For a model with aggregates Resume and Specialization, package app.jobs:
output/
domain/
resume/
__init__.py
resume.py # aggregate root
resume_exporter.py # IResumeExporter implementation
resume_reconstitutor.py # IResumeReconstitutor implementation
values/
__init__.py # re-exports all VOs
resume_id.py # identity VO
title.py # string VO with validation
rate.py # composite VO
rate_exporter.py # composite VO exporter
employment_type.py # enum VO
...
events/
__init__.py
resume_created.py # frozen dataclass
resume_created_exporter.py # event exporter
experience/ # collection entity
__init__.py
experience.py
experience_exporter.py
experience_reconstitutor.py
values/
__init__.py
company_name.py
specialization/
__init__.py
specialization.py
specialization_exporter.py
specialization_reconstitutor.py
values/
...
specialization_profile/ # single entity
__init__.py
specialization_profile.py
specialization_profile_exporter.py
specialization_profile_reconstitutor.py
values/
__init__.py
application/
__init__.py
commands/
__init__.py # re-exports all commands
create_resume_command.py # frozen dataclass
create_resume_command_handler.py # async handler stub
Generated Code Patterns¶
Aggregate root¶
The aggregate extends EventiveEntity[PersistentDomainEvent] and
VersionedAggregate. It exposes its state exclusively through the
exporter/reconstitutor interfaces — no getters:
class Resume(EventiveEntity[PersistentDomainEvent], VersionedAggregate):
def export(self, exporter: "IResumeExporter") -> None:
super().export(exporter)
exporter.set_title(self._title)
for item in self._specialization_ids:
exporter.add_specialization_id(item)
def _import(self, provider: "IResumeReconstitutor") -> None:
super()._import(provider)
self._title = provider.title()
self._specialization_ids = list(provider.specialization_ids())
@classmethod
def reconstitute(cls, reconstitutor) -> typing.Self:
return super().reconstitute(reconstitutor)
Exporter interface¶
Scalar fields get set_* methods. Collection fields get add_* methods
with the field name singularized (specialization_ids -> add_specialization_id):
class IResumeExporter(IVersionedAggregateExporter, metaclass=ABCMeta):
@abstractmethod
def set_title(self, value) -> None:
raise NotImplementedError
@abstractmethod
def add_specialization_id(self, value) -> None:
raise NotImplementedError
Reconstitutor interface¶
class IResumeReconstitutor(IVersionedAggregateReconstitutor, metaclass=ABCMeta):
@abstractmethod
def title(self):
raise NotImplementedError
@abstractmethod
def specialization_ids(self):
raise NotImplementedError
Value Object export¶
Single-field VOs use a setter callback — the VO controls what gets exported:
class Title:
def export(self, setter: typing.Callable[[str], None]) -> None:
setter(self._value)
Composite VOs use an exporter interface with one method per field:
class Rate:
def export(self, exporter: "IRateExporter") -> None:
self._rate_period.export(exporter.set_rate_period)
self._rate.export(exporter.set_rate)
Command handler¶
Generated as an async stub raising NotImplementedError:
class CreateResumeCommandHandler:
async def __call__(self, command: CreateResumeCommand) -> typing.Any:
raise NotImplementedError
Architecture¶
YAML file
│
▼
ModelParser parser.py YAML → BoundedContextModel
│
▼
BoundedContextModel model.py dataclasses (TypeRef hierarchy)
│
▼
RenderWalker renderer.py walks model, renders Jinja2 templates
│ (merge=True: AST merge with existing)
│
├──[new file]──────► write directly
│
└──[existing file]─► ast_merge.py additive merge, then write
│
▼
*.py files templates/ 22 Jinja2 templates
Model (model.py)¶
TypeRef hierarchy — every field type is represented by a TypeRef
subclass, enabling polymorphic dispatch without enums:
TypeRef (base)
├── PrimitiveType(name) # bool, int, str, datetime, ...
├── VoRef(vo) # reference to ValueObjectDef
├── EntityRef(entity) # reference to EntityDef
└── CollectionType(kind, element: TypeRef) # list[T], tuple[T, ...]
ValueObjectDef hierarchy — VO kind is determined by class type, not by an enum:
ValueObjectDef (base)
├── SimpleVoDef # string-like VO with constraints
├── IdentityVoDef # identity VO (IntIdentity, StrIdentity, ...)
├── EnumVoDef # enum VO (str, Enum)
└── CompositeVoDef # composite VO with inner fields
Jinja2 templates dispatch on VO type via custom tests:
vo is composite_vo, vo is enum_vo.
The renderer dispatches template selection via VO_TEMPLATE_MAP[type(vo)].
CollectionKind — list, tuple (enum with str mixin).
Core dataclasses form a tree:
BoundedContextModel
├── external_value_objects: list[ValueObjectDef]
└── aggregates: list[AggregateDef]
├── fields: list[FieldDef]
│ └── type_ref: TypeRef
├── value_objects: list[ValueObjectDef] (subclass per kind)
├── entities: list[EntityDef]
│ ├── fields: list[FieldDef]
│ ├── value_objects: list[ValueObjectDef]
│ └── entities: list[EntityDef] (recursive)
├── domain_events: list[DomainEventDef]
│ └── fields: list[FieldDef]
└── commands: list[CommandDef] (derived from events)
└── fields: list[FieldDef]
Parser (parser.py)¶
ModelParser class with _vo_map and _entity_map as instance state —
maps class names to their definitions within the current scope.
Scope isolation (push/pop pattern):
Composite VO fields —
_vo_mapis shallow-copied so inner fields can reference aggregate-level VOs but additions don’t leak outward.Entity parsing —
_vo_mapand_entity_mapare shallow-copied; entity VOs are added to the copy.External references — parsed with a separate empty
_vo_map.
Two-pass VO parsing with topological sort:
Non-composite VOs are parsed first (identity, simple, enum).
Composite VOs are topologically sorted by field dependencies (Kahn’s algorithm) and parsed in dependency order. Circular dependencies raise
ValueError.
This ensures composite VOs can reference other composite VOs regardless of declaration order in YAML.
Type resolution (_resolve_type / _resolve_element_type):
Primitives →
PrimitiveTypeDotted paths (contain
.) →_resolve_import_refcreates or updates a synthetic VO, returnsVoRefEntity names →
EntityRefKnown VO names →
VoRefCollection wrappers (
list[T],tuple[T, ...]) →CollectionTypewrapping the resolved element type
VO classification follows a priority chain:
Has
identitykey →IdentityVoDeftypestarts withEnum[→EnumVoDefHas
fieldskey →CompositeVoDefOtherwise →
SimpleVoDef
YAML validation checks allowed keys at four levels (top-level, aggregate,
entity, value object) and raises ValueError with the offending key names.
Public facade:
from ascetic_ddd.cli.scaffold.parser import parse_yaml
model = parse_yaml("domain-model.yaml")
Renderer (renderer.py)¶
RenderWalker class with _visit_X methods, modeled after EvaluateVisitor
from ascetic_ddd.faker.domain.query. Per-aggregate state is captured in an
_AggregateContext dataclass (package path, directories, used VOs, field
lists).
All VO import paths (relative . prefixes) are resolved to absolute paths
by _resolve_vo_imports before template rendering — for aggregate used_vos,
value_objects, and domain event used_vos alike. Templates receive
pre-resolved absolute paths and use them directly.
Walk order:
BoundedContextModel
└── AggregateDef _visit_aggregate()
├── ValueObjectDef _visit_value_objects()
│ └── [composite] + exporter module
├── values/__init__
├── EntityDef _visit_entities() → _visit_entity()
│ ├── entity VOs _visit_value_object()
│ ├── values/__init__
│ ├── {entity}.py
│ ├── {entity}_exporter.py
│ ├── {entity}_reconstitutor.py
│ ├── __init__.py
│ └── [nested entities] (recursive)
├── _visit_aggregate_module()
│ ├── {agg}.py
│ ├── {agg}_exporter.py
│ ├── {agg}_reconstitutor.py
│ └── __init__.py
├── DomainEventDef _visit_domain_event()
│ ├── {event}.py
│ └── {event}_exporter.py
├── events/__init__
└── CommandDef _visit_command()
├── {cmd}_command.py
└── {cmd}_command_handler.py
All rendering goes through _render_template(tpl_name, path, **kwargs) —
a single method that loads the Jinja2 template, renders, writes the file,
and appends the path to the generated files list.
Public facades:
from ascetic_ddd.cli.scaffold.renderer import render_bounded_context
files = render_bounded_context(model, "./output", "app.jobs")
For additive merge with existing files (see AST Merge):
from ascetic_ddd.cli.scaffold.renderer import ast_render_bounded_context
files = ast_render_bounded_context(model, "./output", "app.jobs")
AST Merge (ast_merge.py)¶
When regenerating code for an existing codebase, ast_render_bounded_context
uses additive AST merge: the Jinja2 template renders to a string,
ast.parse() converts it to an AST, and merge_modules(existing, generated)
adds missing elements from the generated AST into the existing file’s AST.
The result is written back via ast.unparse().
This approach uses Jinja2 templates as the single source of truth — no separate AST builders needed.
What gets merged:
Element |
Action |
|---|---|
|
Add missing names to existing import, or add new import |
|
Add if absent |
Class (by name) |
Add if absent; merge members if present |
Field annotation ( |
Add if absent |
Method ( |
Add if absent; preserve existing body |
|
Add missing params |
|
Add missing assignments |
|
Add missing names |
What is not modified:
Existing method bodies (user business logic is preserved)
Existing class hierarchy / decorators
Existing import order
Limitation: ast.unparse() does not preserve comments or formatting.
After merge, the file is reformatted by Python’s AST unparsing.
Convenience wrapper:
from ascetic_ddd.cli.scaffold import ast_scaffold
ast_scaffold("domain-model.yaml", "./output", "app.jobs")
Naming (naming.py)¶
Pure functions for name transformations:
Function |
Example |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Templates¶
22 Jinja2 templates under ascetic_ddd/cli/scaffold/templates/:
Path |
Generates |
|---|---|
|
Shared macro: |
|
Shared macros: |
|
Aggregate root + interfaces |
|
Aggregate exporter |
|
Aggregate reconstitutor |
|
Aggregate / entity package |
|
Identity VO (extends |
|
String VO with validation |
|
Enum VO (extends |
|
Composite VO with exporter interface |
|
Composite VO exporter |
|
Values package re-exports |
|
Entity class + interfaces |
|
Entity exporter |
|
Entity reconstitutor |
|
Domain event + exporter interface |
|
Event exporter |
|
Events package |
|
Command dataclass |
|
Async handler stub |
|
Commands package re-exports |
|
Application package |
Jinja2 environment settings: trim_blocks, lstrip_blocks,
keep_trailing_newline. Custom filters: singularize (plural -> singular),
pluralize (singular -> plural), snake (CamelCase -> snake_case).
Custom tests: composite_vo, enum_vo (for VO type dispatch in templates).
Custom templates¶
The -t / --templates flag specifies a directory with custom Jinja2
templates. Templates found in this directory take priority over the built-in
ones; any template not present falls back to the default.
python -m ascetic_ddd.cli scaffold \
-i domain-model.yaml \
-o ./output \
-p app.jobs \
-t ./my-templates
To override a single template, create a file at the same relative path. For example, to customize string VO generation:
my-templates/
domain/
values/
string_vo.py.j2
The custom template receives the same context variables as the original (see Templates for the full list). All other templates continue to use the built-in versions.
Programmatic usage:
from ascetic_ddd.cli.scaffold import scaffold
scaffold("domain-model.yaml", "./output", "app.jobs",
templates_dir="./my-templates")
Limitations¶
Cross-aggregate VO sharing. Each aggregate defines its own VOs. When two aggregates share a type (e.g.
SpecializationId), use an inline dotted path in the field type:_specialization_ids: list[.specialization.values.SpecializationId]or an explicitimportkey on the VO definition (see Imported VO).No cyclic composite VO references. Composite VOs are topologically sorted within each aggregate. Circular dependencies among composite VOs raise
ValueError.Command derivation is suffix-based. Only
Created,Updated,Deletedevent suffixes are recognized. Events with other suffixes produce commands with the event name unchanged.Composite VO reconstruction is partial. The generated reconstitutor includes TODOs for composite VO fields that must be filled in manually.
Tests¶
python -m unittest discover -s ascetic_ddd/cli/scaffold/tests -p "test_*.py" -v
Module |
What it covers |
|---|---|
|
CamelCase conversion, collection detection, primitive classification |
|
YAML parsing, VO classification, type resolution, inline dotted paths, entity parsing, command derivation, validation errors |
|
Generated file contents, directory structure, entity rendering, custom templates, no f-strings in output |
|
AST merge: imports, classes, methods, |
|
AST merge mode: additive merge with existing files, user code preservation |
|
End-to-end: YAML -> compilable Python files |