Code Generation
The compiler generates executable TypeScript code targeting the Emmett event sourcing framework. Each decider in a .ddd file produces a self-contained directory of generated files. The output directory is configurable and kept strictly separate from .ddd source files.
What Gets Generated Per Decider
For each decider block the generator emits:
| File | Contents |
|---|---|
{kebab-name}-state.ts | Discriminated union of all state variants |
{kebab-name}-events.ts | Discriminated union of all event variants |
{kebab-name}-decider.ts | decide, evolve, initialState functions and the command union |
{kebab-name}-wiring.ts | Emmett handle wiring that connects the decider to an event store |
{kebab-name}-command.ts | Smart constructor for each command that declares validate constraints |
index.ts | Barrel re-export for the decider directory |
Context-level shared files are also emitted once per bounded context:
| File | Contents |
|---|---|
types.ts | Branded type aliases from TypeDecl declarations |
errors.ts | Self-contained error class definitions |
result.ts | Result<T, E>, Ok, and Err utilities |
index.ts | Barrel re-export for the context directory |
File Structure
Given an output prefix of src/generated and a context named Registration, the generator emits:
src/generated/└── registration/ ├── types.ts ├── errors.ts ├── result.ts ├── index.ts └── user-registration/ # one directory per decider ├── user-registration-state.ts ├── user-registration-events.ts ├── user-registration-decider.ts ├── user-registration-wiring.ts ├── register-user-command.ts # smart constructor (if validate present) └── index.tsDirectory and file names are derived from the DSL identifier using kebab-case conversion. The top-level src/generated/index.ts re-exports every context barrel.
Generated Functions
decide(command, state)
Dispatches over command.type × state.type using nested switch statements. Returns Result<{Name}Event[], RejectionError> (or Result<{Name}Event[], RejectionError | TerminalStateError> when the decider has terminal states).
requireguards translate toif (!(condition)) return Err(new RejectionError('...'))statements.- A successful decision returns
Ok([...events]). - Exhaustive
defaultbranches throwExhaustiveCommandErrororExhaustiveStateError— these represent compiler bugs, not domain errors, and extendEmmettError.
evolve(state, event)
Dispatches over state.type × event.type. Returns the new state object. Field assignment is auto-resolved: event fields take priority over from-state fields. Explicit with assignments override auto-resolution. Exhaustive default branches throw ExhaustiveEventError.
initialState()
Returns the designated initial state literal. When the initial declaration includes field bindings, their values are inlined as TypeScript literals.
Smart constructors
For each command that declares validate constraints, a factory function is generated that returns Result<Command, ValidationError[]>. The constructor is the only way to create a valid command value — the raw type is not exported for direct construction.
Error Types
All error classes are emitted into errors.ts within the generated output directory. Generated code does not import from the @weltenwanderer/generator-emmett package at runtime; the error classes are self-contained in the output.
| Class | Base | Raised by | Meaning |
|---|---|---|---|
RejectionError | Error | decide | A require guard failed — domain rule rejection. No events are produced. |
PostconditionViolation | Error | Reserved | A postcondition (ensure) was violated after event application. |
TerminalStateError | Error | decide | A command was dispatched against a terminal state — no events are produced. |
ExhaustiveCommandError | EmmettError | decide | An unrecognised command type reached the dispatch switch — indicates a compiler bug. |
ExhaustiveEventError | EmmettError | evolve | An unrecognised event type reached the dispatch switch — indicates a compiler bug. |
ExhaustiveStateError | EmmettError | decide, evolve | An unrecognised state type reached the dispatch switch — indicates a compiler bug. |
RejectionError is the normal error path for domain logic. Exhaustive*Error classes signal invariant violations that should never occur in a correctly compiled system.
Terminal State Behavior
When a decider has terminal states, the generated decide() function handles them in the inner state switch. For every command, if state.type matches a terminal state, decide() returns Err(new TerminalStateError('Terminal state {Name} rejects all commands')) without emitting events.
TerminalStateError is distinct from RejectionError — consumers can distinguish terminal rejection from business rule rejection via instanceof. The return type of decide() widens to Result<{Name}Event[], RejectionError | TerminalStateError> when terminals are present.
Deciders without terminal states are unaffected — their decide() return type remains Result<{Name}Event[], RejectionError>.
Output Isolation
Generated files are always written to a configurable output prefix (e.g., src/generated). The generator never writes into the directory containing .ddd source files. Each generated file begins with the comment // Generated by Weltenwanderer — do not edit to make its origin unambiguous.