Skip to content

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:

FileContents
{kebab-name}-state.tsDiscriminated union of all state variants
{kebab-name}-events.tsDiscriminated union of all event variants
{kebab-name}-decider.tsdecide, evolve, initialState functions and the command union
{kebab-name}-wiring.tsEmmett handle wiring that connects the decider to an event store
{kebab-name}-command.tsSmart constructor for each command that declares validate constraints
index.tsBarrel re-export for the decider directory

Context-level shared files are also emitted once per bounded context:

FileContents
types.tsBranded type aliases from TypeDecl declarations
errors.tsSelf-contained error class definitions
result.tsResult<T, E>, Ok, and Err utilities
index.tsBarrel 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.ts

Directory 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).

  • require guards translate to if (!(condition)) return Err(new RejectionError('...')) statements.
  • A successful decision returns Ok([...events]).
  • Exhaustive default branches throw ExhaustiveCommandError or ExhaustiveStateError — these represent compiler bugs, not domain errors, and extend EmmettError.

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.

ClassBaseRaised byMeaning
RejectionErrorErrordecideA require guard failed — domain rule rejection. No events are produced.
PostconditionViolationErrorReservedA postcondition (ensure) was violated after event application.
TerminalStateErrorErrordecideA command was dispatched against a terminal state — no events are produced.
ExhaustiveCommandErrorEmmettErrordecideAn unrecognised command type reached the dispatch switch — indicates a compiler bug.
ExhaustiveEventErrorEmmettErrorevolveAn unrecognised event type reached the dispatch switch — indicates a compiler bug.
ExhaustiveStateErrorEmmettErrordecide, evolveAn 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.