Skip to content

JSON IR

The IR serializes to JSON and back without losing a thing. Two methods bracket the round-trip, and a third rebinds behavior to the rehydrated definition.

ToJSON serializes a machine’s IR. It stamps the current schema version onto the document so every emitted definition is self-describing.

b, err := m.ToJSON()
if err != nil {
return err
}
// b is canonical JSON: stable key ordering, deterministic for golden diffs.

LoadFromJSON rehydrates the IR. It is generic over the same state, event, and context types the machine was forged with, and it returns an *IR: pure data, not yet a runnable machine.

ir, err := state.LoadFromJSON[Stage, Signal, Order](b)
if err != nil {
return err // e.g. a document declaring a newer schema major is refused
}

A higher schema major is rejected rather than guessed at; a higher minor and a pre-versioned document both load, and unknown keys are preserved verbatim so a load-then-save cycle never drops fields a newer producer emitted.

The IR carries behavior as named Refs, never as code. Provide binds every ref against a host registry and hands back a builder ready to Quench:

m := ir.Provide(reg).Quench()

If a ref does not resolve, it surfaces at Quench as the same typed error the fluent DSL raises for an unregistered binding: a JSON-authored machine and a code-authored one fail identically.

Every named hook in the IR is a Ref:

type Ref struct {
Name string `json:"name"`
Params map[string]any `json:"params,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
}

Name keys the registry. Params carry serializable configuration. Meta is a reserved, round-tripped extension namespace the kernel never inspects. Reach for the JSON IR whenever you need persistence, interchange, or tooling: anywhere the definition must outlive or travel beyond the process that forged it. See the IR and the split for the model.