Skip to content

Release 2.2.0

Choose a tag to compare

@reid-spencer reid-spencer released this 14 Sep 19:54
· 14 commits to main since this release

What's New

RIDDL 2.2.0 is a language release: three new pieces of syntax, a new message kind, and a
substantial tightening of what riddlc can say about the streaming and messaging structure of a
model. Every model that parsed on 2.1.1 still parses; the changes below are additive syntax and
new or corrected diagnostics. The BAST binary format moved to revision 25, so .bast files
written by 2.1.x must be regenerated with riddlc bastify.

Features

  • on quiescence <window> — a handler clause that fires when nothing has arrived at a
    processor instance within the window (on quiescence 30 minutes is { … }). The clock restarts
    on every handled message; inside a State's handler it is armed only while that state is
    active. Legal on any handler-bearing processor, at most one per handler. Before this, "no
    payment within 30 minutes, cancel the order" had to live in a projector correlation, and
    riddl-models counted 18 time-caused events raised by a command nothing could send.
  • send <message> to <portlet> at <instant> — a delivery scheduled for a time. at states an
    instant (a TimeStamp, DateTime or ZonedDateTime value), not a mechanism; the timer, delay
    queue or scheduler is the generator's choice. There is no cancellation construct — the idiom is
    to schedule to yourself and decide at fire time, and that loop connector is legal.
  • append and remove — collection statements. An event-sourced entity's fold can now say
    what it appends to and removes from a collection field of its state:
    append added.item to field Cart.Data.items, remove tag from field Cart.Data.tags (every
    element equal to the value), and the keyed form
    remove from field Cart.Data.items where itemId == removed.itemId (every element whose named
    field matches). The target must be a collection field; the value is typed against the element
    type; both statements obey every rule set obeys (scope, refusals-first, pure functions,
    event-sourcing). Arithmetic deliberately gets no syntax — RIDDL does none, and
    set field S.balance to prompt("balance + points") is the intended spelling.
  • Advisory — a new message kind. An advisory reports a structural fact that is consistent
    with the model as written and inconsistent with what such a declaration usually means; it never
    blocks code generation and never names a fix as required — the modeller may dismiss it by
    design. Severity 1 (with Style), not a warning: its own --show-advisories flag and
    show-advisories config key, default on, independent of -w. adaptor-direction-advisory had
    been a plain Warning that blocked generation; it is the first rule on the new kind.
  • Four counts about what an entity does with its journal. riddlc cannot judge whether an
    entity ought to be event-sourced, but it can count: entity-event-sourced-unread-history
    (one state, no transitions, no other processor reads its events — the journal has no reader),
    entity-crud-with-transitions-consumed (its inverse), entity-event-sourced-snapshot-events
    (every event carries the whole state and every fold copies it back) — all three advisories —
    and entity-event-sourced-prose-folds (Completeness: a fold written only as prose has no
    replay semantics). A set of a stated field from a prompt(…) counts as a stated fold.

Language rulings now enforced

  • Implied adaptor ports are abolished. A port is declared or absent, for every processor
    kind. A processor whose handlers receive messages but declares no inlet, or transmit but
    declares no outlet, is incomplete — a Missing warning naming the types
    (stream-processor-no-inlet / -no-outlet; an Entity keeps entity-no-inlet / -no-outlet)
    — and every rule that would read the missing side abstains until it exists, exactly as rules
    abstain from a ??? body. A connector endpoint that names an adaptor rather than one of its
    portlets is ref-wrong-kind again. Adaptors take whatever shape their ports give them (as merge on two inlets is legal).
  • The adaptor is the boundary for its pair, in both directions. A cross-context connector
    may terminate on an adaptor's declared portlet at either end, so an asking adaptor owns both
    legs of its ask — the request outlet and the reply inlet — and the reply has a legal path.
  • An ask needs a modelled path both ways (msg-ask-target-unreachable,
    msg-ask-reply-unreachable): the question leg and the reply leg must each be carried by
    connectors. The mechanism of the reply is the generator's; the path is the model's.
  • on other is case _. It fires for exactly the message types no on <message> clause
    handles, and it receives whatever its body — an error-only body is a refusal of those
    messages, which is business logic, not non-reception. A Context or Projector with an inlet and
    no handler at all is now reported (stream-inlet-not-received), the error-sink inlet excepted.
  • A tell into an unrelated domain is a modelling error (msg-tell-crosses-unrelated-domains)
    whose remedy is to restructure — put both domains under a common parent — since no connector
    between unrelated domains is legal and "add a connector" was advice that could not be followed.
  • stream-graph-cycle forbids an infinite message loop, not a connector ring: a message of
    type X that travels the network and re-enters the clause that transmits it. The
    schedule-to-yourself idiom is no longer flagged.
  • A handler-less processor is a stream tail whatever its shape — an opaque processor lets no
    rule assert what it does with a message.
  • A saga step that tells nothing is an Error (saga-step-no-tell): a step that effects nothing
    yet promises to compensate it is a self-contradiction. saga-no-timeout stays a
    Completeness warning but no longer blocks code generation.
  • A handler may declare at most one of each special on-clause (on other, on init,
    on term, on activate, on passivate, on quiescence) — two catch-alls with no rule for
    which runs used to validate clean.

Bug Fixes

  • Prettify wrote URL("https") as URL"https", which does not parse — a second copy of the
    type's own format had drifted. Fixed; both spellings round-trip.
  • find -type on-quiescence refused a kind the language produces; find -type append-statement
    / remove-statement likewise, caught by the release gate.
  • adaptor-direction-advisory now counts a far-context reference anywhere in the adaptor
    (handled types, transmitted operands, let ascriptions, portlet types), and resolves a
    qualified referent correctly; it used to reward the unmigrated shape.
  • Root2JsonCorpusTest stripped only one spelling of an embedded location, so the first
    corpus error carrying a file(line:col->col) read as a round-trip regression.

Improvements

  • validate reports advisories separately in its summary line and lists advisories on|off
    beside the warning classes.
  • The JavaScript ErrorInfo.kind union in index.d.ts now lists the kinds by the names riddlc
    actually renders (Severe, Error, Warning, Deprecation, Completeness, Usage,
    Missing, Style, Advisory, Tip, Info); the old spellings remain for compatibility.
  • The Messages scaladoc records the admission test between Missing and Completeness: Missing =
    the author owes something unwritten; Completeness = the written things do not connect.

Internal

  • BAST FORMAT_REVISION 24 → 25 (statement sub-kinds 22 append, 23 remove; the earlier bump
    to 24 in this cycle carried on quiescence and send … at). A revision-24 reader refuses a
    25 file cleanly.
  • adaptor-implied-outlet-ambiguous (five days old, from the implied-ports design) is retired;
    its code will not be reused. Two new rule ids for the collection statements, four for the
    event-sourcing counts, two for the incompleteness checks.
  • The Computational Model (RIDDL-Computational-Model.md) records every ruling above, with the
    reversed 2026-09-06 paragraphs kept as history.
  • CLAUDE.md was split: per-construct reference now lives in docs/claude/language-constructs.md
    and build/API notes in docs/claude/build-and-api.md.