#Immutable state diagrams

    moonrockz/diagrams/state stores state diagrams without a syntax reader, renderer, host adapter or UI dependency. Constructors copy arrays. State IDs, region IDs, note IDs and transition IDs are distinct identities within a diagram. Equal labels and parallel transitions keep separate IDs.

    ///|
    test "construct nested concurrent regions and preserve their order" {
    let states = [
    @state.State::new("Session", role=Composite),
    @state.State::new("Idle", region="control", descriptions=["entry / log"]),
    @state.State::new("History", region="control", role=DeepHistory),
    @state.State::new("Listening", region="network"),
    ]
    let view = @state.StateDiagram::new(
    "session",
    states,
    [
    @state.Transition::new(
    "remember",
    "Idle",
    "History",
    label="resume [ready] / log",
    ),
    ],
    regions=[
    @state.Region::new("root"),
    @state.Region::new("control", owner="Session"),
    @state.Region::new("network", owner="Session", separator=Vertical),
    ],
    notes=[
    @state.Note::new(
    "detail",
    "Description only",
    targets=["Idle"],
    region="control",
    ),
    ],
    )
    states.clear()
    assert_eq(view.states.length(), 4)
    assert_eq(view.regions[1].members, ["Idle", "History"])
    assert_eq(view.regions[2].separator, Vertical)
    assert_eq(@state.validate(view), [])
    assert_eq(view.with_direction(RL).transitions, view.transitions)
    }

    Every state has one owning region. Each non-root region names a composite owner. A composite can own several ordered concurrent regions or an empty region. Region.members preserves member order. When a caller omits it, construction fills it from state order and State.region. Explicit membership must agree with those assignments. Horizontal and Vertical retain separator orientation. A composite's expanded=false hides descendants in the rendered view, while the source model and semantic transition endpoints stay intact.

    Role distinguishes Simple, Composite, Initial, Final, Choice, Fork, Join, ShallowHistory, DeepHistory, EntryPoint, ExitPoint, InputPin, OutputPin, ExpansionInput and ExpansionOutput. Unsupported(symbol) retains a source symbol for explicit renderer refusal. History retains its owning region and does not restore a prior state. Description lines retain entry, do, exit and internal behavior as ordered text. They never add transitions.

    Transitions retain declared source and target, ID, label, line style, source and target markers, minimum length, direction hint and optional UTF-16 source binding. Directions and marker types use the existing pure flowchart value types. Global layout direction does not reverse transition endpoints. Note.targets names states; Note.connection_targets names transitions for link notes. Neither kind of attachment adds a route edge.

    validate checks duplicate IDs, missing endpoints, ownership cycles, missing owners, membership, note targets and source spans. It validates drawn structure, not executable UML semantics. The caller controls node, transition, region, depth, diagnostic and work limits. state/invalid-limits rejects invalid options. state/model-limit, state/depth-limit, state/diagnostic-limit and state/work-limit report exhausted budgets. A nonempty diagnostic array is a failed validation result. No repair or successful partial model is implied.

    ///|
    test "reject invalid references and distinguish exhausted validation" {
    let view = @state.StateDiagram::new("invalid", [@state.State::new("A")], [
    @state.Transition::new("missing", "A", "B"),
    ])
    assert_true(
    @state.validate(view).any(d => d.code == "state/missing-endpoint"),
    )
    assert_eq(@state.validate(view, work_limit=1)[0].code, "state/work-limit")
    assert_eq(@state.fresh_id("A", ["A", "A:1"]).unwrap(), "A:2")
    }

    Pass Diagram::State(view) to a renderer. Source readers live in the Mermaid and PlantUML modules. Geometry, scenes, finite scenarios and queries remain separate APIs. All pure state operations run on wasm, wasm-gc, js and native. Run mise run test:state-consumer to verify fresh model, dedicated-reader and player consumers; append -- --published after release to use registry dependencies.

    Note

    pub struct Note {
    id : String
    text : String
    targets : ReadOnlyArray[String]
    connection_targets : ReadOnlyArray[String]
    placement : String
    region : String
    origin :
    SourceBinding
    ?
    } derive(Eq,
    Debug
    )

    Note::equal

    fn Note::equal(Note, Note) -> Bool

    Note::new

    fn Note::new(id : String, text : String, targets? : ArrayView[String], connection_targets? : ArrayView[String], placement? : String, region? : String, origin? :
    SourceBinding
    ) -> Note

    Note::not_equal

    fn Note::not_equal(x : Note, y : Note) -> Bool

    Note::to_repr

    Region

    pub struct Region {
    id : String
    owner : String?
    members : ReadOnlyArray[String]
    separator : Separator
    direction :
    Direction
    ?
    origin :
    SourceBinding
    ?
    } derive(Eq,
    Debug
    )

    Region::equal

    fn Region::equal(Region, Region) -> Bool

    Region::new

    fn Region::new(id : String, owner? : String, members? : ArrayView[String], separator? : Separator, direction? :
    Direction
    , origin? :
    SourceBinding
    ) -> Region

    Region::not_equal

    fn Region::not_equal(x : Region, y : Region) -> Bool

    Region::to_repr

    Role

    pub(all) enum Role {
    Simple
    Composite
    Initial
    Final
    Choice
    Fork
    Join
    ShallowHistory
    DeepHistory
    EntryPoint
    ExitPoint
    InputPin
    OutputPin
    ExpansionInput
    ExpansionOutput
    Unsupported(String)
    } derive(Eq,
    Debug
    )

    Source-independent state roles. History and boundaries describe drawn structure.

    Role::equal

    fn Role::equal(Role, Role) -> Bool

    Role::not_equal

    fn Role::not_equal(x : Role, y : Role) -> Bool

    Role::to_repr

    Separator

    pub(all) enum Separator {
    Horizontal
    Vertical
    } derive(Eq,
    Debug
    )

    Separator::equal

    fn Separator::equal(Separator, Separator) -> Bool

    Separator::not_equal

    fn Separator::not_equal(x : Separator, y : Separator) -> Bool

    State

    pub struct State {
    id : String
    label : String
    region : String
    role : Role
    descriptions : ReadOnlyArray[String]
    expanded : Bool
    direction :
    Direction
    ?
    origin :
    SourceBinding
    ?
    } derive(Eq,
    Debug
    )

    State::equal

    fn State::equal(State, State) -> Bool

    State::new

    fn State::new(id : String, label? : String, region? : String, role? : Role, descriptions? : ArrayView[String], expanded? : Bool, direction? :
    Direction
    , origin? :
    SourceBinding
    ) -> State

    State::not_equal

    fn State::not_equal(x : State, y : State) -> Bool

    State::to_repr

    State::with_descriptions

    fn State::with_descriptions(self : State, descriptions : ArrayView[String]) -> State

    State::with_label

    fn State::with_label(self : State, label : String) -> State

    StateDiagram

    pub struct StateDiagram {
    id : String
    states : ReadOnlyArray[State]
    transitions : ReadOnlyArray[Transition]
    regions : ReadOnlyArray[Region]
    notes : ReadOnlyArray[Note]
    direction :
    Direction

    title : String?
    accessibility_title : String?
    accessibility_description : String?
    } derive(Eq,
    Debug
    )

    StateDiagram::equal

    StateDiagram::new

    fn StateDiagram::new(id : String, states : ArrayView[State], transitions : ArrayView[Transition], regions? : ArrayView[Region], notes? : ArrayView[Note], direction? :
    Direction
    , title? : String, accessibility_title? : String, accessibility_description? : String) -> StateDiagram

    StateDiagram::not_equal

    fn StateDiagram::not_equal(x : StateDiagram, y : StateDiagram) -> Bool

    StateDiagram::with_direction

    StateDiagram::with_states

    fn StateDiagram::with_states(self : StateDiagram, states : ArrayView[State]) -> StateDiagram

    Transition

    Transition::equal

    fn Transition::equal(Transition, Transition) -> Bool

    Transition::new

    fn Transition::new(id : String, source : String, target : String, label? : String, style? :
    LineStyle
    , source_marker? :
    Marker
    , target_marker? :
    Marker
    , min_length? : Int, direction_hint? :
    Direction
    , origin? :
    SourceBinding
    ) -> Transition

    Transition::not_equal

    fn Transition::not_equal(x : Transition, y : Transition) -> Bool

    fresh_id

    fn fresh_id(stem : String, used : ArrayView[String], work_limit? : Int) -> Result[String,
    Diagnostic
    ]

    validate

    fn validate(model : StateDiagram, nodes_limit? : Int, edges_limit? : Int, regions_limit? : Int, depth_limit? : Int, diagnostics_limit? : Int, work_limit? : Int) -> ReadOnlyArray[
    Diagnostic
    ]

    Validate references and ownership, without evaluating state-machine behavior. Exhaustion uses state/model-limit, state/depth-limit or state/work-limit.