Author pure business logic in TOML manifests. The framework normalizes a manifest into a DDD-flavored domain metamodel, validates it, and generates dependency-free TypeScript — branded types, smart constructors with invariant checks, pure state-machine reducers, and port interfaces.
The manifest is a metalanguage: any problem you can model as entities, value objects, rules, flows, commands, and events maps onto it. The code underneath looks like a hand-written DDD domain.
Human-authored, comment-friendly, no whitespace traps, native typed scalars. TOML is the surface; internally everything normalizes to a JSON-ish AST, so a YAML/JSON front-end could be added without touching codegen.
The structure mirrors DDD's strategic layer. A directory is the boundary.
Workspace a whole system (directory of contexts)
Bounded Context hard boundary, own (subdirectory; context.toml)
ubiquitous language
Module grouping inside a context (one .toml file)
Aggregate consistency boundary ([entity] aggregateRoot = true)
university/ ← Workspace
workspace.toml ← optional metadata
academics/ ← Bounded Context (upstream)
context.toml ← [context] + [context.exports].types
catalog.toml ← Module "catalog"
billing/ ← Bounded Context (downstream)
context.toml ← declares an import of academics
invoicing.toml ← Module "invoicing"
The boundary rule (this is the DDD point):
- Inside a context, modules share types freely — qualified refs like
catalog.Gradebecome real imports between module files. - Across contexts, types are NOT shared directly. A downstream context must declare a context-map import, and may only consume types the upstream has explicitly exported. The validator rejects raw or private cross-context references.
# academics/context.toml (upstream controls what crosses)
[context]
name = "academics"
[context.exports]
types = ["Course", "Grade"] # everything else stays private
# billing/context.toml (downstream depends through a relationship)
[context]
name = "billing"
[[context.imports]]
from = "academics"
relationship = "anticorruption-layer" # or customer-supplier / conformist / shared-kernel
types = ["Course"]For an anticorruption-layer (or customer-supplier) import, codegen emits an
ACL module _acl_academics.ts containing:
- a billing-owned
Coursetype (downstream's own language; academics' model stays invisible), - an
AcademicsPortto fetch the raw upstream value (typedunknown), - an
AcademicsTranslatorthat converts raw → the localCourse.
So the upstream model can never leak into the downstream — exactly the
bounded-context isolation DDD prescribes. (conformist skips translation.)
Shared Kernel — when two contexts genuinely co-own a small model (rather
than one translating the other), declare a kernel: a directory with a
kernel.toml (sharedBy = ["academics", "billing"]) plus its module files. It
is the Venn overlap of the sharing contexts: codegen emits it once as
out/<kernel>/..., and the sharing contexts import its types directly by
relative path. The studio renders it as an overlap zone you can create and edit
visually.
Generated output mirrors the tiers:
out/university/
index.ts ← export * as academics / billing
academics/
_runtime.ts catalog.ts index.ts
billing/
_runtime.ts
_acl_academics.ts ← the anti-corruption layer
invoicing.ts
index.ts
A directory of plain .toml files (no context subdirs) loads as a single
bounded context; a single .toml file emits one self-contained module.
workspace dir ─┬─ workspace.toml ───────────────────> Workspace metadata
└─ <context>/ ─┬─ context.toml ──────> context map (imports/exports)
└─ *.toml ─normalize─> Module ─┐
│ per module
validateWorkspace ─────────────────┤ intra-ctx refs +
│ cross-ctx boundary
emitWorkspace ─────────────┴─> TS tree + ACLs
Zero external dependencies anywhere — the TOML parser, expression language, validator, and the runtime helpers in generated code are all hand-rolled.
| Concept | TOML key | Generates |
|---|---|---|
| Value Object | [valueObject.X] |
branded type + makeX() smart constructor |
| Entity | [entity.X] |
readonly interface (identity field) |
| Enum | [enum.X] |
string-literal union + const object |
| Command | [command.X] |
discriminated-union member |
| Event | [event.X] |
discriminated-union member |
| Workflow | [workflow.X] |
pure reducer + async command handler |
| Port | [port.X] |
interface (the impurity boundary) |
Invariants and transition guards are written in a tiny, total expression
language (amount >= 0, len(lines) > 0, command.amount > 0) — never
embedded TypeScript, so the model stays pure and portable.
A workflow transition can declaratively mutate the aggregate and stamp effectful values, all from the manifest:
[[workflow.InvoiceLifecycle.transitions]]
from = "Issued"
to = "Paid"
on = "PayInvoice"
guard = "command.amount > 0"
emits = ["InvoicePaid"]
set = { paidAt = "now()" } # effectful builtin, resolved via a Clock portFrom this, codegen emits two things:
- a pure reducer
InvoiceLifecycleReduce(agg, command, effects)that returns the next aggregate (itsstatusfield advanced,setassignments applied) + emitted events — no IO; - an async command handler
handleInvoiceLifecycle(command, id, deps)that wires the ports:load(repository) →reduce(pure) →save→publishevents. The effectful builtinsnow()/uuid()come from injectedClock/IdGenports.
So a command is executable end-to-end straight from the manifest. Guards and
invariants stay pure (effectful builtins are rejected there); only transition
assignments may use now()/uuid(), and the validator requires the backing
port to be declared. See examples/university/handler.ts
for a runnable handler driving the full invoice lifecycle.
@ideascol/core— metamodel, TOML parser, expression language, normalize, validate@ideascol/codegen— emit pure TypeScript from aDomain@ideascol/diagram— emit Mermaid UML (class / context-map / state diagrams)@ideascol/serialize— the inverse of the parser: aDomain/workspace back to TOML@ideascol/cli— theideascommand@ideascol/studio— a visual editor (ReactFlow canvas + TOML/code tabs) over the manifests
pnpm install
pnpm build
# workspace mode (dir of context subdirs): emits out/university/<context>/...
node packages/cli/dist/main.js gen examples/university -o examples/out
# single-module mode (file): emits one self-contained module
node packages/cli/dist/main.js gen examples/orders/orders.toml -o out/orders.ts
# validate only (enforces context boundaries)
node packages/cli/dist/main.js check examples/universityExamples:
examples/university— a workspace with two bounded contexts (academicsupstream,billingdownstream) wired through an anti-corruption layer, plus a runnable consumer that crosses the boundary.examples/orders— single-module manifest, generated output, and a runnable consumer.
ideas diagram turns the same metamodel into Mermaid UML so a stakeholder
can see the domain — no rendering toolchain needed (Mermaid renders in GitHub,
most markdown viewers, and many IDEs).
node packages/cli/dist/main.js diagram examples/university -o examples/docsIt emits, per workspace:
README.md— a context-map flowchart (contexts + import relationships, labeledACL/Conformist/ …) and links to each context.<context>.md— a class diagram of that context's types, with DDD stereotypes («aggregate root»,«value object»,«command»,«event»,«port»,«enumeration»), composition edges for field references, port dependency arrows, and invariants attached as note blocks — plus a state diagram for each workflow (states, transitions labeled by command + guard + emitted events).
See examples/docs/university. The generated Mermaid
is verified to render with @mermaid-js/mermaid-cli.
Verified end-to-end:
- DDD 3-tier — workspace → bounded context → module → aggregate, with the context map enforced (downstream consumes only explicitly exported types).
- Pure TypeScript codegen — branded types, smart constructors, anti-corruption layers, per-context file trees.
- Executable workflows — declarative state mutation (
set field = expr, effectfulnow()/uuid()via ports) compiled to a pure reducer + an async command handler that runsload → reduce → save → publish. - Mermaid UML — context-map, class, and state diagrams from the same model.
- Typed cross-context event contracts — an upstream context's events become
a published language (
_published.ts+ anEventPublisherport); a downstream context that subscribes gets a generated_subscribe_<ctx>.tshandler interface, so integration stays async and decoupled (payloads typedunknownat the seam — narrow ontype). - Visual studio —
@ideascol/studio, a ReactFlow canvas + TOML/code editors that reads and writes the same manifests (multi-project, live invariant validation, BPMN-style swimlanes, shared-kernel overlap zones).
Tested: pnpm test runs the full suite — 16 core/codegen/serialize tests
(parser, expression language, normalize, validate, context boundaries, kernel
imports, round-trips, and a check that the generated TypeScript compiles under a
strict tsconfig) plus 8 studio client-logic tests (TOML serialization round-trip
through the real core parser, id tagging/stripping, and canvas graph building).
Next: a JSON-schema export of the metamodel for editor tooling, and command-handler orchestration across aggregates (sagas).