Skip to content

Repository files navigation

ideasframework

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.

Why TOML

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.

DDD strategic design — the three tiers

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.Grade become 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 Course type (downstream's own language; academics' model stays invisible),
  • an AcademicsPort to fetch the raw upstream value (typed unknown),
  • an AcademicsTranslator that converts raw → the local Course.

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.

Pipeline

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.

Metamodel concepts

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.

Workflows execute — pure core, effects at the edges

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 port

From this, codegen emits two things:

  • a pure reducer InvoiceLifecycleReduce(agg, command, effects) that returns the next aggregate (its status field advanced, set assignments applied) + emitted events — no IO;
  • an async command handler handleInvoiceLifecycle(command, id, deps) that wires the ports: load (repository) → reduce (pure) → savepublish events. The effectful builtins now() / uuid() come from injected Clock / IdGen ports.

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.

Packages

  • @ideascol/core — metamodel, TOML parser, expression language, normalize, validate
  • @ideascol/codegen — emit pure TypeScript from a Domain
  • @ideascol/diagram — emit Mermaid UML (class / context-map / state diagrams)
  • @ideascol/serialize — the inverse of the parser: a Domain/workspace back to TOML
  • @ideascol/cli — the ideas command
  • @ideascol/studio — a visual editor (ReactFlow canvas + TOML/code tabs) over the manifests

Usage

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/university

Examples:

  • examples/university — a workspace with two bounded contexts (academics upstream, billing downstream) 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.

Diagrams (UML)

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/docs

It emits, per workspace:

  • README.md — a context-map flowchart (contexts + import relationships, labeled ACL / 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.

Status

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, effectful now()/uuid() via ports) compiled to a pure reducer + an async command handler that runs load → 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 + an EventPublisher port); a downstream context that subscribes gets a generated _subscribe_<ctx>.ts handler interface, so integration stays async and decoupled (payloads typed unknown at the seam — narrow on type).
  • 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).

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages