RIDDL 2.0.0-rc.1 Released
RIDDL 2.0.0-rc.1 is now available.
RIDDL 2.0.0-rc.1 is now available. This release of the RIDDL compiler and language tooling includes the following changes:
What’s New
This is a release candidate. It is a real, immutable artifact, but nothing resolves to it by default — you have to ask for it by name:
brew install ossuminc/tap/riddlc@rc # not `riddlc`
npm install @ossuminc/riddl-lib@rc # not `latest`
"com.ossuminc" %% "riddl-language" % "2.0.0-rc.1"
Please exercise it against real models and report anything that looks wrong. The whole point of a release candidate is to find what our test suites and the two model corpora did not.
Migrating from 1.x? Start with the 1.x → 2.0 Migration Guide. It covers every breaking change below with before/after examples.
Executive Summary
RIDDL 2.0 makes models generatable. 1.x let you describe a system; 2.0 makes that description precise enough for a code generator — or an AI — to act on without guessing.
Three shifts get you there:
- A unified processor model. Every processor — context, entity, projector, repository, adaptor — can now declare ports and be wired into a streaming pipeline. Streaming stops being a separate world you opt into.
- Meaning where there was convention. A record is data, not a message. A projector handles events. A function is pure. Things that were merely usually true are now checked, so a generator can rely on them.
- Expressions instead of prose. Conditions, comparisons, constructors and function calls are now structured values rather than opaque strings — the difference between a model a generator can compile and one it must interpret.
Around that: 40-odd validation rules that catch modelling mistakes early, a build on sbt 2 and Scala 3.9, and a JSON surface that round-trips exactly so AI tools can read and write models losslessly.
Eleven breaking changes, all with clear migrations. Nine deprecated syntax forms still parse and warn, so a 1.x model keeps working while you migrate.
Language Changes
Syntax summaries. Each is elaborated in the Migration Guide.
New syntax
| Construct | Form | What it gives you |
|---|---|---|
| Generic processor | processor P as <shape> is { … } |
One keyword for every streaming component |
| Context intention | application context C is { … } |
Says what a context is for — and gets checked |
| Ports anywhere | inlet in is command C in any processor |
Wire entities and repositories directly into pipelines |
| Version | version V is "1.2.3" at nine scopes |
Versioning inherited down the model tree |
| Copyright | copyright C is "© 2026 …" at nine scopes |
Attribution a generator can emit into headers |
yields |
command Place yields event Placed is { … } |
Declares the response, so flows are traceable |
yield statement |
yield event Placed |
Replaces reply; clearer intent |
| Boolean expressions | when a.x == b.y and c.z != d.w |
Real conditions, not prose |
| Comparisons | == != < <= > >= |
Type-safe, reference-to-reference |
| Constructors | command Place(sku = s, qty = q) |
Build a message from values |
| Function calls | call function Total(items = i) |
Compose behaviour |
foreach |
foreach item in order.lines do … end |
Bounded iteration with a local |
get |
get input Form.field |
Read a value from a UI input or state |
put / return |
put v to output O · return v |
Explicit outputs |
prompt(…) value |
when prompt("the customer looks unhappy") |
Marks what an AI decides, distinct from a literal |
| Refusal step | step refuse … because "…" |
Model the unhappy path in an epic |
| Figma reference | figma "FILEKEY" node "12:34" |
Ties a UI element to its design |
| Attachments | attachment Doc is text/plain in file "…" |
Carry docs and ULIDs with a definition |
Comment before ??? |
// what goes here then ??? |
Documented stubs |
Deprecated but still parsing
Each emits [deprecated] and keeps working. All are removed in a future major.
| Old | New | Why |
|---|---|---|
state S is record R |
state S of record R is { … } |
is introduces a body everywhere else |
send … to inlet X |
send … to outlet X |
You send from an outlet |
reply |
yield |
Names what actually happens |
prompt "…" statement |
prompt("…") value |
A prompt is a value, usable anywhere |
bare string when "…" |
when prompt("…") |
Says explicitly that an AI decides |
source/sink/flow/merge/split |
processor P as source |
One keyword, shape ascribed |
gateway/service/external/wrapper options |
context intention prefix | An intention, not an option |
Abstract type |
Anything |
Says what it means |
anonymous nebula |
module M is { … } |
Named, so it can be referenced |
Breaking Changes
Eleven. Each says what breaks, what to do, and why it is worth doing.
1. A record is data, not a message
RecordRef no longer sits under the message hierarchy. A record is a data
shape; commands, events, queries and results are messages.
Migration: stop using a record where a message is expected — a handler
clause, a send, a tell. Use the message kind you mean.
Why: a generator needs to know whether something crosses a wire. Conflating the two meant it had to guess, and guessed wrong on persistence and transport.
2. A state’s data type must be a record
state S of record R — not an inline aggregate, not an arbitrary type.
Migration: name the aggregate as a record, then reference it.
Why: state is persisted. A named record gives the generator a schema to target and gives you one place to change it.
3. UI elements require an application context
input, output and group may only appear in a context declared
application context C is { … }.
Migration: add the application intention to any context holding UI.
Why: UI generation targets a different runtime from service generation. The intention says which, instead of leaving it to be inferred from contents.
4. Epic interactions must target an application context
A user interaction in an epic may only reach a context with the application
intention.
Migration: route interactions through the application context that owns the UI, rather than at a service or domain directly.
Why: epics describe what a user does. A user meets your system at its application boundary; modelling them poking a service directly hides a layer that has to exist.
5. Comparisons are reference-to-reference only
when order.total > limit.value — comparisons take two references. Numeric and
string literals are not permitted as operands.
Migration: name the value as a constant or a field and compare against
that.
Why: a literal in a comparison is a magic number with no name, no type and no single place to change it. Naming it makes the model self-documenting and the comparison type-checkable.
6. Refusals must precede effects
Within a statement list, error and require come before any send, tell,
yield, set or morph.
Migration: move refusals to the top of the clause.
Why: a refusal after an effect is unreachable — the effect already happened. Ordering makes the guard clause real rather than decorative.
7. Adaptors need an on other clause
An adaptor must say what it does with messages it does not specifically handle.
Migration: add on other is { … } — even just error "unsupported".
Why: an adaptor is a boundary. Silence at a boundary is how messages vanish without a trace.
8. Projectors are event-only
A projector may handle events. Not commands, not queries.
Migration: move command handling to an entity, query handling to a repository.
Why: a projector maintains a read model from a stream of facts. Letting it take commands made it a second entity with no lifecycle.
9. Functions must be pure
Effect statements — send, tell, yield, set, morph, become — are
banned in function bodies.
Migration: move effects to the calling handler; let the function compute and return.
Why: a pure function is safe to call from a condition, memoize, test in isolation and generate as a plain method.
10. Every processor is a streamlet
Contexts, entities, projectors, repositories and adaptors all carry inlets/outlets and an optional ascribed shape. Exactly one connector may attach to any port.
Migration: fan-in and fan-out are modelled by declaring multiple ports, not by attaching several connectors to one. The old shape keywords still parse.
Why: one connector per port means a pipeline’s topology is unambiguous, so it can be generated, drawn and reasoned about.
11. The JSON wire schema carries ordered contents
A container’s children travel in one contents array of $kind-tagged entries;
with { … } likewise through metadata.items. ProjectorDto.repository is now
repositories. Entries may carry $at: [offset, endOffset] source locations.
Migration: usually none — the old per-kind arrays are still read and report a deprecation. Only the writer changed.
Why: per-kind buckets could not express the order definitions were written in, so a comment at the top of a file came back at the bottom.
New Features
Modelling
Version and copyright as first-class definitions. Declare version and
copyright at any of nine scopes; each resolves by nearest declaring ancestor,
composing root-to-leaf. A generator can stamp accurate headers without you
repeating yourself.
Repositories at domain scope. A repository shared by several contexts no longer has to live inside one of them.
Connectors at domain scope. Wire pipelines that cross context boundaries where the crossing is visible.
Modules. module M is { … } is a flat, named collection of any top-level
definition — a unit you can reference and import. The anonymous nebula form is
deprecated in its favour.
A predefined Riddl standard module. Ships Anything, plus the terminators
BottomlessPit (a sink for anything) and ForeverEmpty (a source of nothing),
so a partial pipeline can be made complete without inventing placeholders.
BAST imports. import domain X from "file.bast", with selective and aliased
forms, so a large model can be split across compiled units.
Expression language
Boolean expressions in when, require and invariant, with and, or,
not and type-safe comparisons. Constructors to build a message from named
or positional arguments, checked for arity and type. Function calls as
values. foreach for bounded iteration. get to read from an input or
state. put/return for explicit outputs. prompt(…) to mark a value
an AI decides — compatible with any field type, and distinct from a literal.
Validation
Roughly forty new checks. The ones you will notice:
- Use-case witnessing — every step of an epic is traced against the structure that would carry it, so an epic that describes an impossible interaction says so.
- Streaming completeness — unattached ports, unreachable
telltargets, over-parallelised pipelines. - Saga discipline — steps confined to their domain, one failure point per
do-block,
compensateandparalleloptions. - Handler completeness — entity lifecycle, adaptor coverage, command→event.
- Hygiene — shadowed on-clauses, inconsistent nested terms, duplicated single-valued metadata, include hygiene, domains with no author.
- Translatability prediction (A40) — flags vague or arbitrary interactions an AI is unlikely to render faithfully, but only where there is prose to judge.
- Figma drift (A42) — an opt-in check that a referenced design node still exists.
Suggestions on every message. riddlc advise (or --provide-tips) attaches
a concrete next step to each diagnostic. Default output is unchanged.
Tooling
Fully reflective JSON. A model written to JSON and read back reproduces its source exactly, order included — 63 divergent fixtures to 0, and the external corpus from 185/187 to 189/189 on both round-trip identity and validation parity. This is what lets an AI safely read, edit and write models.
Source locations survive the JSON round trip, so diagnostics on a
JSON-authored model point at real lines instead of empty(1:1->1).
BAST reaches its 2.0 format with a restarted revision counter.
riddlc info reports the source commit, so you can tell exactly what a
binary was built from.
Build: sbt 2.0, projectMatrix layout, Scala 3.9.0-RC4, sbt-ossuminc 3.1.0.
Bugs Fixed
Correctness
- A projector’s
updates repositoryclause vanished on prettify — the reference lived in contents but nothing emitted it, so a round trip silently dropped the link between a projector and its store. - A body-less state lost its metadata —
state S of record R with { briefly … }dropped its documentation, because the metadata was emitted only alongside a closing brace that a body-less state does not have. 41 states across riddl-models were affected. - A schema’s types were reported unused — schema references live in fields, not contents, so nothing counted them.
- Nested
send/tellreferences went unresolved, producing false positives in message-flow analysis. - Two same-named ports on different processors were conflated, reporting one connector each as two connectors on one port.
attachment ULID is "…"could not be parsed at all. The construct had no fixture and no test anywhere, so a documented piece of syntax was unreachable.command Checkout()was unchecked against a message that has fields.show … to …steps were unvalidatable — the parser produced the empty relationship the validator rejects.- A comment could not introduce a
???stub, so a starter template had to drop its own explanation to parse. - A refusal-only command clause was reported incomplete, and the warning was silenced by adding unreachable code after the refusal — rewarding exactly the shape to avoid.
- Same-named siblings of different kinds passed silently —
type Thingbesideentity ThingmadeCtx.Thingambiguous. - Prettify emitted deprecated syntax, so canonicalising a clean model introduced deprecation warnings.
- Prettify emitted
String(7), which does not parse. - A negative
integerdiscarded its sign. - Definition keywords were accepted as bare identifiers.
Diagnostics
- Unreadable input threw a raw exception — a missing file printed a Java stack trace at the user. Load failures are now located messages.
- Parse-time warnings and deprecations were dropped on a successful parse,
so they appeared under
validateand nowhere else. They now surface under every command. - The error reporter could crash on a missing
}, replacing the real parse error with an internal exception. - Deprecations had no distinct label and were invisible in grouped output.
Quality
- Sixty-two test cases had never run. Four suites were dead — a fixture lambda on a plain spec, abstract suites with no concrete subclass, constructor parameters preventing discovery. Reviving them exposed 24 real failures, since fixed. The three failure modes are now documented so they are recognised rather than rediscovered.
- The corpus round-trip suite asserted
0 mustBe 0for months, driving off stale.bastartifacts whose every read failed and was skipped. It now drives from source and reports failures. - A grammar fixture had been failing CI for two days, missing from the validator’s skip list.
Known Limitations
- The Homebrew
riddlc@rcline and the npmrcdist-tag are built and unit-covered but have never been exercised against a live registry. Ifbrew install ossuminc/tap/riddlc@rcornpm install @ossuminc/riddl-lib@rcmisbehaves, that is the most likely cause. - GBNF is generated and statically linted but has never parsed RIDDL. Treat it as unverified.
- Include fragments are not covered by the standalone grammar validators (EBNF, GBNF); only entry points are.
isEmptyis comment-tolerant but nothing yet reports a container that holds only comments. The predicate is in place; the check is not.