IR format

IrExporter.Export(builtMachine) serializes a declaratively-authored machine to its IR: one canonical JSON document (<machine>.ir.json) that carries identity, structure, per-state context schema, per-trigger input schema, and every transition's guard and reducer as data. It is the single artifact the per-language generators consume, so the C# machine is the source and the IR is the contract. Output is canonical JSON, so the file is a stable golden.

Export requires a declarative machine: Export throws InvalidOperationException if the machine made no declarative call at all (nothing to serialize). It does not refuse a machine that mixes the styles. An edge whose guard or reducer is a C# delegate is exported without that guard or reduce, which reads as an unconditional edge that keeps the context; see Delegate vs declarative.

Top level

FieldTypeMeaning
idstringthe machine's stable id
versionnumberthe definition version
initialStatestringthe start state
initialContextobjectthe context a new snapshot starts with, from StartsAt(state, initialContext), so a generated runtime reproduces it exactly
statesstring[]every state, sorted (ordinal)
triggersstring[]every trigger, sorted (ordinal)
committedStatesstring[]states a soft autosave must not overwrite
contextobjectstate name to its context schema
inputsobjecttrigger name to its input schema (only triggers that declared WithInput<T>)
invariantsobjectstate name to a per-state policy rule (the .Requires(...) on top of the schema); omitted when the machine has none
transitionsobject[]the edges, sorted by (from, trigger, to)
differentialobjectthe test-only fuzzing inputs authored with .Differential(...): samples (per trigger), seeds (per state), and contexts (probes). Omitted when the machine declares none, and stripped from the generated runtime machine (it drives only the cross-language differential test).

A schema (under context or inputs) is { "fields": [ { "name", "type", "nullable", "constraints" } ] }, where type is one of string/number/boolean/array/object and constraints is an array of rules.

Transitions

Each transition carries its structure plus its guard and reducer as data:

FieldTypePresent when
from / trigger / tostringalways
guardrulethe edge has a declarative guard (When(Rule)); absent for no guard and for a delegate guard alike
guardMessagestringBecause(...) was set
reducereductionthe edge has a declarative reducer (Reduce(Reduction)); absent for no reducer and for a delegate reducer alike, and absent means the context is kept
effectobjectthe edge binds RunsOnce<T>; { "type": <TEffect full name>, "keyPrefix": <string> }

A rule is a tagged object keyed by rule (present, absent, ofType, nonEmpty, oneOf, compare, count, length, boolEquals, arrayOf, all, any, custom); a reduction is keyed by reduce (keep, clear, reset, set, custom). See the data model for each shape.

Example

The turnstile, exported:

{
  "id": "turnstile",
  "version": 1,
  "initialState": "Locked",
  "states": ["Locked", "Unlocked"],
  "triggers": ["Coin", "Push"],
  "committedStates": [],
  "context": {
    "Locked": { "fields": [] },
    "Unlocked": {
      "fields": [
        { "name": "paidWith", "type": "string", "nullable": false,
          "constraints": [ { "rule": "nonEmpty", "source": "context", "field": "paidWith" } ] }
      ]
    }
  },
  "inputs": {
    "Coin": { "fields": [ { "name": "coin", "type": "string", "nullable": false, "constraints": [] } ] }
  },
  "transitions": [
    { "from": "Locked", "trigger": "Coin", "to": "Unlocked",
      "guard": { "rule": "oneOf", "source": "input", "field": "coin", "values": ["quarter", "dollar"] },
      "guardMessage": "Only a quarter or a dollar is accepted.",
      "reduce": { "reduce": "set", "steps": [ { "field": "paidWith", "value": { "input": "coin" } } ] } },
    { "from": "Unlocked", "trigger": "Push", "to": "Locked", "reduce": { "reduce": "clear" } }
  ]
}