# `Statifier.MachineState`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.0.0/lib/statifier/machine_state.ex#L1)

The reified interpreter position - every Appendix D global and loop
variable this core keeps, per `docs/observability.md` constraint 1
(ADR-0012): any `%MachineState{}` value is a complete, inspectable,
resumable position. `Statifier.Machine` is the compiled document this
struct walks; interpreter modules alias `Statifier.MachineState` as
`MachineState` and `Statifier.Machine.State` as `State`, never
abbreviating either further, because the two names are one character
apart and mean very different things - a compiled state versus a runtime
position.

## The configuration is full, not leaf-only (ADR-0005)

`configuration` holds every active state's index, ancestors included -
the same "full configuration" ADR-0005 commits to everywhere else in this
codebase. Leaf states (the ones a consumer usually wants: "what is the
machine actually doing") are `active_leaf_states/1`, a derived view over
`configuration`, never a second field to keep in sync.

## The external queue is deliberately absent

Appendix D's `main_event_loop` owns both the internal and the external
event queue. This struct only owns the internal one. The pure core takes
one external event per call (ADR-0003); the session that drives it owns
queueing the waiting external events. This is a mechanical deviation in
ADR-0002's sense - the semantics of processing one external event are
unchanged, only the storage of the *waiting* ones moves outward - so it
is legal, but the divergence must carry its reason at the port site: the
`main_event_loop` port (`Statifier.Interpreter.main_event_loop/1`)
repeats this comment at its own site, where it matters.

## `states_to_invoke` is every state entered since the last invoke pass

`states_to_invoke` mirrors Appendix D's `statesToInvoke` global: a
`MapSet` of state indexes, added to by `Statifier.Interpreter.ExitEntry.arrive/3`
(`enterStates`'s per-state entry body) and deleted from by
`Statifier.Interpreter.ExitEntry.exit_states/2` (`exitStates`'s
`for s in statesToExit: statesToInvoke.delete(s)`).

Population is unconditional - a state with no `<invoke>` children still
lands its index here on entry, exactly as `entered_states` below is
populated unconditionally and for the same reason: a field whose contents
depend on whether the document happens to carry `<invoke>` children has a
meaning a reader cannot state without the document in hand, and
ADR-0012 constraint 1 wants a `%MachineState{}` that inspects as a
complete position regardless of which document produced it.

The entry-order sort Appendix D's own pseudocode performs
(`statesToInvoke.sort(entryOrder)`) happens at the invoke pass itself, not
here - this field stores an unordered `MapSet`, the same pattern
`configuration` uses everywhere in this module.

It has two writers by design, matching the two Appendix D procedures that
touch it: `exit_states/2` deletes any state that exits before the invoke
pass runs, and the invoke pass empties whatever remains once it has walked
it. Neither is a counter function - unlike `macrostep`/`microstep`/`round`
above, membership here is not monotonic within a macrostep.

## `active_invocations` is `inv.invokeid` hoisted off compiled data

Appendix D stores an invocation's generated identifier on the invocation
object itself (`inv.invokeid`), read back by the cancel walks
(`cancelInvoke(inv)`) and the finalize pass
(`if inv.invokeid == externalEvent.invokeid: applyFinalize(...)`).
`Statifier.Machine.Invoke` is immutable compiled data - the same "nowhere
on the state to flip a mutable flag" situation `entered_states` above
describes for `s.isFirstEntry` - so the live invokeid is hoisted onto this
struct instead, keyed to match the pseudocode's own access pattern:

    active_invocations: %{{state_index, invoke_index} => invoke_id}

`state_index` and `invoke_index` together name one compiled `<invoke>`
element (`Statifier.Compiler.Expressions.owner_ref/0`'s `{:invoke, _, _}`
shape); document order for a walk over one state's invocations always
comes from the compiled `invoke` list itself, never from iterating this
map. This is the same ADR-0002 mechanical deviation `entered_states`
already declares: the semantics are unchanged (an invocation's id is still
readable by exactly the same lookups Appendix D performs), only the
storage moves off the immutable compiled document.

**This is not ADR-0027 decision 3's parent-held session table.** That
table maps an invokeid to `{child_session_id, pid, monitor_ref}` - process
identity the session layer owns. `active_invocations` holds none of that:
no pid, no monitor ref, no child session id, only the generated or
author-written invokeid string. The session's own table is built on top of
this one, not in place of it, by later work this struct does not depend on.

Entries are written by the invoke pass (one entry per invocation that
successfully started) and deleted by the cancel walks (a later phase's
`depart/2`/`exit_interpreter/1` seams) when the owning invocation is
cancelled.

## `entered_states` is `s.isFirstEntry` moved off the state

Appendix D tracks first entry as a mutable flag on the state itself,
`s.isFirstEntry`, set `false` the moment late binding consumes it
(`enterStates`, `appendix-d.txt:307-315`). A compiled `%Machine.State{}`
is immutable compile-time data in this port (`docs/architecture.md`
principle 4), so there is nowhere on the state to flip that flag - the set
moves to the runtime struct instead, keyed by `state_index`. This is an
ADR-0002 mechanical deviation: the semantics are unchanged (a state's
first entry is still detected exactly once, before any second entry could
read it), only the storage moves from the immutable compiled document to
the position that changes every microstep.

`entered_states` is populated unconditionally - every entered state's
index is added here, not only under `binding == :late` - even though only
late binding ever reads it (`Statifier.Interpreter.Datamodel.enter_state/2`
is a no-op under `:early`). Considered and rejected: gating the `MapSet.put`
on `machine.binding == :late`, which would save one put per state entry
under early binding at the cost of making the field's meaning conditional
on the document being interpreted - exactly the "not a complete,
inspectable position" failure ADR-0012 constraint 1 exists to prevent. A
`%MachineState{}` value must be a complete, inspectable, resumable
position regardless of which document produced it; `entered_states` is
that same field on every document, not only late-binding ones.

Not a substitute for `states_for_default_entry`
(`Statifier.Interpreter.ExitEntry.entry_set()`): that set is recomputed
fresh per `enter_states/2` call and answers "was this state entered via its
`<initial>` default this time", while `entered_states` accumulates across
the whole session and answers "has this state ever been entered before" -
a state re-entered through history restoration has no default entry at
all, so the two sets diverge exactly where late binding's "first time"
question needs the session-long answer.

## `invoke_counter` is the session-global platformid sequence (ADR-0008)

Spec 6.4.1 mandates a generated invoke id of the form
`stateid.platformid`, and its own MUST binds uniqueness to the
platformid alone - "platformid MUST be unique within the current
session" - not to the composite. `invoke_counter` is that platformid
source: one auto-increment for the whole session, read from and written
back to this struct by `Statifier.Interpreter.generate_invoke_id/3`
(`lib/statifier/interpreter.ex`), never per-state or per-`<invoke>`
element. A per-state counter would let `s0.1` and `s1.1` collide on
platformid `1` while still satisfying composite uniqueness, which 6.4.1
as written does not permit; one session-global counter makes every
platformid distinct by construction and also keeps the anonymous-state
fallback (`inv_<counter>`, no qualifier - `Machine.State.id` is `nil` for
a state with no author-written id) unique with no special case.

This field exists, instead of generating an id at the generation site,
because ADR-0003 wins where ADR-0008's identifier aesthetics collide with
the core's `(state, event) -> {state, [effect]}` contract: generating one
reads the wall clock and a CSPRNG, and `<invoke idlocation>` writes the
result into the datamodel, so minting it with entropy would make a
recorded run and its replay diverge in program state. A counter that is a
pure field on `%MachineState{}` replays identically and satisfies
ADR-0012 constraint 1 (inspectable, not hidden generator state). See
ADR-0008's 2026-08-15 amendment for the full argument, including why
session ids are still generated from entropy (uniqueness *across*
sessions, which entropy buys and a session-local counter cannot).

Starts at `0` in `new/2` (no invocation has generated an id yet) and is
incremented by `generate_invoke_id/3` immediately before use, so the
first generated platformid is `inv_1` - the same "increment before
read" idiom `begin_macrostep/1` etc. use below, though this field is not
part of that trio's enforced-by-review contract: it is written directly
at its one call site rather than through a dedicated `MachineState`
function, matching how `active_invocations` and `states_to_invoke` are
already mutated directly from `lib/statifier/interpreter.ex` and
`lib/statifier/interpreter/exit_entry.ex` rather than through setters.
An author-written `<invoke id="...">` is used verbatim and never
composed, so it does not advance this counter - only a generated id
consumes the sequence.

## `send_counter` is the session-global `send_` id sequence (ADR-0035)

Spec 3.14 leaves `<send>`'s generated-id format unconstrained ("The SCXML
processor MAY generate all other ids in any format, as long as they are
unique"), unlike `<invoke>`'s spec-pinned `stateid.platformid` form.
`send_counter` is the one auto-increment behind that free format: one
session-global counter, read from and written back to this struct at
`<send>`'s own generation site (a later phase's job, not this field's),
producing ids `send_1`, `send_2`, ... in the order the processor mints
them. Same reasoning as `invoke_counter` above for why this is a plain
counter and not a generated id: ADR-0003's core is
`(state, event) -> {state, [effect]}`, and generating one reads the wall
clock and a CSPRNG, which `<send idlocation>` writing the result into the
datamodel would turn into an observable replay divergence. A counter that
is a pure field on `%MachineState{}` replays identically instead.

**Not shared with `invoke_counter`.** ADR-0035 gives two reasons: the two
sequences answer to different governing clauses (`<invoke>`'s format is
pinned by 6.4.1; `<send>`'s is free per the 3.14 sentence above, so
coupling a free-format sequence to a pinned one buys nothing), and sharing
would let an `<invoke>` advance the send-id sequence, making a document's
`send_` ids depend on how many invocations happened first - still
deterministic and replayable, but no longer locally readable the way
`idlocation` wants them to be. See ADR-0035 for the full argument.

Starts at `0` in `new/2` (no send has generated an id yet). An
author-written `<send id="...">` is used verbatim and never advances this
counter, exactly as an author-written `<invoke id="...">` leaves
`invoke_counter` untouched - only a generated id consumes the sequence,
and a fresh id is generated on every execution of the element (3.14: "not
at load time but each time the element is executed"), never memoized on
it. No setter function, matching `invoke_counter`'s own precedent: written
directly at its one call site rather than through a dedicated
`MachineState` function.

## `timer_counter` is the session-global durable-timer ordinal (ADR-0059)

`%Effect.SendDelayed{}` and `%Effect.Cancel{}` both carry the position
fields `macrostep`/`microstep`/`round`/`c_index`/`owner`, and a
`<foreach>` body re-executes the same content node - the same `c_index` -
once per iteration, in the same microstep, under the same author-written
id. That collision is what a durable host's dedup key cannot resolve on
its own; `timer_counter` mints the disambiguator, read as `ordinal` onto
each effect at the moment it is built.

Starts at `0` in `new/2` (no durable-timer effect has been built yet). It
advances on **every** construction of a `%SendDelayed{}` or a `%Cancel{}`
- author-written id or not, inside a `<foreach>` or not - unlike
`send_counter`, which only a generated id consumes. It is never reset,
and the increment-before-read idiom is the same one `invoke_counter` and
`send_counter` use: the first minted ordinal is `1`. No setter function,
matching `invoke_counter`'s and `send_counter`'s own precedent - but
unlike either of those, this field is written directly at **two** call
sites rather than one: `lib/statifier/machine/content/send.ex`'s
delayed-send construction and `lib/statifier/machine/content/cancel.ex`'s
`<cancel>` construction, since one session-global sequence spans both
effect kinds (ADR-0059).

## `caller_context` is the current macrostep's caller (ADR-0063)

`caller_context :: term()` is transient per-macrostep fold state naming
the opaque host term the macrostep's triggering external event carried -
`nil` when nothing sent one (initialization, cancellation, an event sent
without a context). Every macrostep-opening core entry point overwrites
it, so it never holds a stale value, and there are exactly three
writers, all in `Statifier.Interpreter`: `handle_event/2` writes the
triggering event's `caller_context` at the macrostep's head, and
`initialize/2` and `cancel/1` write `nil` - their macrosteps have no
sending caller. Internal-event rounds never touch it: an internal event
was raised by the chart inside the same macrostep, so the macrostep's
attribution stands. The two durable-timer effect constructors
(`lib/statifier/machine/content/send.ex`'s delayed branch and
`lib/statifier/machine/content/cancel.ex`) copy it onto
`%Effect.SendDelayed{}`/`%Effect.Cancel{}` at the same sites that read
`timer_counter`. Because the field is pure fold state, replay re-mints
it byte-identically from the recorded events (ADR-0034). No setter
function, matching `timer_counter`'s precedent: written directly at its
three call sites. It is deliberately absent from `Statifier.Position`'s
export - a position is written at quiescence, where no macrostep is
open and the value attributes nothing (ADR-0063 decision 5).

## `running` and `status` differ only across `exit_interpreter`

`running` is Appendix D's `running` flag verbatim: the interpreter loop's
continue condition, set `false` when a top-level final is entered.
`status` is `:running | :done` and only becomes `:done` after
`exit_interpreter` has finished and the terminal `{:done, _}` effect has
been produced. The two therefore differ for exactly the window between
top-level final entry and the end of `exit_interpreter` - the window in
which `exit_interpreter` still has to run `onexit` content on a machine
that is no longer `running`. `new/2` produces `running: true,
status: :running`; a machine_state that has not been initialized yet is
indistinguishable from a running one, which is harmless because `new/2`
is only ever called by `Statifier.Interpreter.initialize/2`, immediately
before it enters the initial states.

## The counter contract

- `new/2` sets `macrostep: 0, microstep: 0, round: 0`. Zero means "no
  macrostep has begun", "no microstep of the current macrostep has
  begun", and "no round of the current macrostep's fold has begun",
  respectively; none of the three is ever the number of a real step or
  round.
- `begin_macrostep/1` is the **only** writer of `macrostep`: it increments
  it by one and resets both `microstep` and `round` to `0`. Its callers
  are `Statifier.Interpreter.initialize/2`, once (so the initialization
  macrostep is **macrostep 1**), and `Statifier.Interpreter.handle_event/2`,
  once per accepted external event (so the first external event is
  **macrostep 2**).
- `begin_microstep/1` is the **only** writer of `microstep`: it increments
  it by one. It is called once per *pseudocode microstep* - one
  exit/execute/enter round - so the first microstep of a macrostep is
  **microstep 1**. Its callers are `Statifier.Interpreter.initialize/2`,
  directly, once, immediately before its own `enter_states/2` call (the
  initial entry is itself a microstep even though the pseudocode's
  `enterStates([doc.initial.transition])` sits outside `microstep`), and
  `run_selected/3`, the private tail shared by every selection round, on
  the branch where the selected transition set is non-empty.
- A selection round that dequeues an internal event enabling no
  transitions does **not** advance `microstep`: no exit or entry
  happened, so there was no microstep. The consumed event is still
  visible, because the event-dequeued trace effect is emitted at the
  current counters.
- `begin_round/1` is the **only** writer of `round`: it increments it by
  one. Its single call site is `Statifier.Interpreter.microstep/1`'s
  head, both clauses included, so the first round of a macrostep's fold
  is **round 1** and every round the fold spends is counted, whether or
  not it advances `microstep` - which is what makes `round` defined even
  under `max_macrostep_rounds: :infinity`, where it counts up rather than
  deriving from the budget (ADR-0020). Effects emitted before the fold
  begins - `handle_event/2`'s own `EventDequeued` and everything
  `initialize/2` emits before entering the fold - are stamped `round: 0`
  for the same reason they are stamped `microstep: 0`: no round has
  begun yet.
- Cause metadata and trace effects are both stamped with the counters
  *as they stand at the moment of the stamp*, i.e. after the `begin_*`
  call for the step they belong to.

No later function may assign `macrostep`, `microstep`, or `round`
directly; the contract above is enforced by review (there being exactly
three writer functions, `begin_macrostep/1`, `begin_microstep/1`, and
`begin_round/1`), not mechanically.

## System variables live in the datamodel

Spec 5.10 places `_sessionid`, `_name`, `_ioprocessors`, and `_event`
alongside the author's own data in the datamodel - `datamodel` therefore
is not "the author's data only". `Statifier.Evaluator.SystemVariables`
owns the shape of each; this module owns *when* each is written, and each
has exactly one writer:

- `_sessionid`, `_name`, and `_ioprocessors` are written exactly once, by
  `new/2`, from `SystemVariables.initial/2` merged **over** the
  `:datamodel` option's map - an author-supplied datamodel can never
  shadow a system variable.
- `_event` is **seeded** to `nil` by `new/2` from the same
  `SystemVariables.initial/2` map, and thereafter written only by
  `put_event/2`. That is one writer per phase rather than two writers of
  one value: the seed is what makes `_event` *declared* for the session's
  whole lifetime with no value yet (spec 5.10, and see
  `SystemVariables.initial/2`'s own docs for why an absent key would say
  something different and wrong), and `put_event/2` is the only thing
  that ever gives it one. The same enforced-by-review status the counter
  contract above states for `macrostep`/`microstep` applies to that second
  half.

Every key in `datamodel` is a string, at every level, for every reachable
`%MachineState{}` value - not merely "in practice today" but by
construction. Every writer except the `:datamodel` option produces string
keys because every other key this codebase ever writes comes from an SCXML
id, which is a string by construction; the `:datamodel` option is the one
source whose keys a caller chooses, and `new/2` checks it (`checked_datamodel!/1`
below) before it ever reaches the struct. The invariant exists because the
expression context built over this map (`Statifier.Evaluator.context/1`)
binds each root by string name - a map that failed the invariant would
either crash at the first evaluation site or be silently rewritten under a
precedence rule the caller never saw, neither of which this module lets
happen.

Seeding the system variables in `new/2` rather than at
`interpret`'s own datamodel-initialization step is a mechanical deviation
from Appendix D's ordering, not a semantic one (ADR-0002):
`Statifier.Interpreter.initialize/2` calls `new/2` as its first statement,
so the variables are bound before anything can read them, and `new/2` is
the only writer of the struct's initial fields regardless.

## `==` is not a position-equality test

`internal_queue` is an `:queue.queue/0`. Two `:queue` values holding the
same events in the same order can differ structurally - the front/rear
split depends on the push/pop history - so `==` on two `%MachineState{}`
values is *not* a reliable "same position" test. A comparison that needs
to know whether two machine_states are at the same interpreter position (a
fold-to-quiescence-versus-step-by-step acceptance test, for one) must
compare a normalized view - `configuration`, `internal_events/1`,
`history_values`, the counters, `status` - never raw struct equality.
`internal_events/1` is exactly that normalized, inspection-and-assertion
view of the queue; no code outside this module touches `:queue` directly.

# `datamodel`

```elixir
@type datamodel() :: map()
```

The datamodel slot - a map today (`docs/datamodel.md:33-41`'s evaluation
context is a predicator context, i.e. a map), typed as `map()` rather
than `term()`/`any()` so that filling in real datamodel evaluation only
adds content, not shape, and dialyzer has something to check in the
meantime.

# `invoke_types`

```elixir
@type invoke_types() :: Statifier.Invoke.Types.t() | nil
```

The caller-declared registered `<invoke type>` set (ADR-0051), or `nil`.
`nil` means "the built-in set only": no `:invoke_handlers` were passed at
session start, so `Statifier.Invoke.Types.registered?/2` answers exactly
`Statifier.Send.Target.supported_invoke_type?/1` - `nil`, `"scxml"`, and
the bare `http://www.w3.org/TR/scxml/` URI. Unlike `t:routes/0`, this is
stamped once per session rather than per drive: the registered set is a
`start_link/2` option, fixed for the session's whole lifetime.

# `max_macrostep_rounds`

```elixir
@type max_macrostep_rounds() :: pos_integer() | :infinity
```

The round budget one macrostep's fold may spend - `pos_integer()`, or
`:infinity` for the spec's literal unbounded behavior (ADR-0019). Set once
in `new/2` and read-only thereafter, like `trace`. One round is one
`Statifier.Interpreter.microstep/1` call inside the fold, empty rounds
included; it is not a microstep count, because a livelocked fold can run
forever without advancing the microstep counter at all. The `round`
counter counts the same rounds this budget bounds, so at exhaustion
`round` equals the spent budget (ADR-0020).

# `routes`

```elixir
@type routes() :: Statifier.Send.Routes.t() | nil
```

The caller-declared route snapshot (ADR-0048), or `nil`. `nil` means "no
determination made": the driver stamped nothing before this drive, so
`Statifier.Machine.Content.Send`'s reachability arm makes no judgment and
the effect is emitted exactly as it was before ADR-0048 - the session's
`deliver/5` boundary stays the detector on that path (ADR-0048 decision
5). A non-`nil` value is a point-in-time claim, not a subscription: it
carries no obligation to track anything between writes.

# `t`

```elixir
@type t() :: %Statifier.MachineState{
  active_invocations: %{
    optional({non_neg_integer(), non_neg_integer()}) =&gt; String.t()
  },
  caller_context: term(),
  configuration: MapSet.t(non_neg_integer()),
  datamodel: datamodel(),
  entered_states: MapSet.t(non_neg_integer()),
  history_values: %{optional(non_neg_integer()) =&gt; MapSet.t(non_neg_integer())},
  internal_queue: :queue.queue(Statifier.Event.t()),
  invoke_counter: non_neg_integer(),
  invoke_types: invoke_types(),
  machine: Statifier.Machine.t(),
  macrostep: non_neg_integer(),
  max_macrostep_rounds: max_macrostep_rounds(),
  microstep: non_neg_integer(),
  round: non_neg_integer(),
  routes: routes(),
  running: boolean(),
  send_counter: non_neg_integer(),
  states_to_invoke: MapSet.t(non_neg_integer()),
  status: :running | :done,
  timer_counter: non_neg_integer(),
  trace: trace()
}
```

# `trace`

```elixir
@type trace() :: boolean()
```

Whether trace effects are emitted. A plain boolean, not a level: the
`Effect.trace/3` gate's contract is one field read and nothing built when
off, and a later level would arrive as a separate field so this one
never turns into a comparison.

# `active_leaf_states`

```elixir
@spec active_leaf_states(machine_state :: t()) :: MapSet.t(non_neg_integer())
```

The active *leaf* states - `configuration` filtered by
`Statifier.Machine.atomic?/2`. A `:final` is atomic - `kind` and
atomicity are independent facts (`Machine.atomic?/2`) - and therefore
appears in this view. `:history` pseudo-states never enter
`configuration` in the first place, so this filter never has to exclude
them - there is nothing history-shaped to filter out. The string-id
translation of this view belongs to a future API boundary, not this
module's: everything here stays integer indexes (ADR-0005).

This is `O(n)` in the configuration size and is meant for the API
boundary and for inspection, not for a per-microstep interpreter path.

# `begin_macrostep`

```elixir
@spec begin_macrostep(machine_state :: t()) :: t()
```

Begins a new macrostep: increments `macrostep` by one and resets both
`microstep` and `round` to `0`. The only writer of `macrostep` (the
counter contract above).

# `begin_microstep`

```elixir
@spec begin_microstep(machine_state :: t()) :: t()
```

Begins a new microstep: increments `microstep` by one, leaving
`macrostep` unchanged. The only writer of `microstep` (the counter
contract above) - called once per exit/execute/enter round, never for a
selection round that enabled no transitions.

# `begin_round`

```elixir
@spec begin_round(machine_state :: t()) :: t()
```

Begins a new round of this macrostep's fold: increments `round` by one,
leaving `macrostep` and `microstep` unchanged. The only writer of `round`
(the counter contract above) - called once per
`Statifier.Interpreter.microstep/1` invocation, empty rounds and the
terminal probe included, which is the same definition of a round that
`max_macrostep_rounds` uses (ADR-0020).

# `dequeue_internal`

```elixir
@spec dequeue_internal(machine_state :: t()) ::
  {:ok, Statifier.Event.t(), t()} | :empty
```

Dequeues the oldest event on the internal queue - `{:ok, event,
machine_state}` with the event removed, or `:empty` when the queue holds
nothing.

# `enqueue_internal`

```elixir
@spec enqueue_internal(machine_state :: t(), event :: Statifier.Event.t()) :: t()
```

Enqueues `event` on the internal queue, FIFO. The one and only writer of
`internal_queue` alongside `dequeue_internal/1`.

# `generate_session_id`

```elixir
@spec generate_session_id() :: String.t()
```

Mints a fresh `sess_` session id (ADR-0008 as amended): a 48-bit
big-endian millisecond timestamp followed by 80 bits of CSPRNG output,
Crockford base32-encoded. Public so `Statifier.Session` can resolve the
id it must put in an ADR-0048 route snapshot's own session set before
`Statifier.Interpreter.initialize/2` runs - a snapshot has to name the
declaring session's own id before that id is otherwise known, since
`new/2` would not generate it until `initialize/2` calls it.

# `internal_events`

```elixir
@spec internal_events(machine_state :: t()) :: [Statifier.Event.t()]
```

The pending internal events as a plain list, oldest first - the FIFO
order the queue's own opaque structure does not directly expose. This is
the inspection and assertion path for every test and every future
debugger; no interpreter code path needs it, since `dequeue_internal/1`
alone drives selection.

# `internal_queue_empty?`

```elixir
@spec internal_queue_empty?(machine_state :: t()) :: boolean()
```

Whether the internal queue holds no pending events - `internalQueue.isEmpty()`
(Appendix D), the post-invoke re-check's own test. Cheaper than
`internal_events/1 == []`: `:queue.is_empty/1` never materializes the
queue into a list, so this is the predicate the interpreter's hot path
uses, and `internal_events/1` stays the inspection-and-assertion view.

# `new`

```elixir
@spec new(machine :: Statifier.Machine.t(), opts :: keyword()) :: t()
```

A fresh position over `machine`: empty configuration, empty internal
queue, no history values, counters at zero, `running: true`,
`status: :running`. Does **not** enter any state - entering the initial
configuration is the not-yet-implemented `initialize/2`'s job, not this
constructor's.

Options: `:trace` (default `false`), `:datamodel` (default `%{}`),
`:session_id` (default a freshly generated `sess_` id, ADR-0008),
`:max_macrostep_rounds` (default `10_000`), `:routes` (default `nil`,
ADR-0048 - see the `t:routes/0` typedoc for what `nil` means), and
`:invoke_types` (default `nil`, ADR-0051 - see the `t:invoke_types/0`
typedoc for what `nil` means). All four system variables
(`SystemVariables.initial/2`) are merged **over** the `:datamodel`
option's map, so author-supplied data can never shadow a system variable.

`:datamodel` must be string-keyed at every level: raises `ArgumentError`
if any key, in the map itself or in a map nested inside it (directly or
inside a list, at any depth), is an atom other than `true`/`false` -
exactly the keys `Predicator.Context`'s own normalization would rewrite.
Boolean keys and non-atom keys (integers, for one) are accepted, and a
struct value's fields are never inspected, since predicator's walk does
not descend into one either.

A `nil` value anywhere in `:datamodel` means predicator's null (ADR-0037);
an embedder that means "declared, no value yet" passes `:undefined`
instead.

# `put_event`

```elixir
@spec put_event(machine_state :: t(), event :: Statifier.Event.t()) :: t()
```

`datamodel["_event"] = event` (Appendix D) - the one and only writer of
the `_event` system variable (spec 5.10.1). Both of `mainEventLoop`'s
assignments, the external one (`Statifier.Interpreter.handle_event/2`)
and the internal one (`Statifier.Interpreter.internal_round/1`), go
through here.

# `put_invoke_types`

```elixir
@spec put_invoke_types(machine_state :: t(), invoke_types :: invoke_types()) :: t()
```

Stamps `invoke_types` onto `machine_state` - ADR-0051's registered-type
snapshot, re-supplied by the driver rather than carried as durable position
state (`Statifier.Position.import/2` sets it `nil` for exactly this reason).

# `put_routes`

```elixir
@spec put_routes(machine_state :: t(), routes :: routes()) :: t()
```

Stamps `routes` onto `machine_state` - ADR-0048 decision 2's per-drive
snapshot write, called by `Statifier.Session` immediately before each
core drive. Pass `nil` to clear a snapshot back to "no determination
made" (`t:routes/0`).

# `raise_internal`

```elixir
@spec raise_internal(
  machine_state :: t(),
  name :: String.t(),
  origin :: Statifier.Event.Cause.origin(),
  opts :: keyword()
) :: t()
```

Raises an internal event: builds its `Cause` from `origin` and the
machine_state's own counters *as they stand right now*, builds the
`:internal` event, and enqueues it - so cause metadata cannot be
forgotten at a call site. This is the function `<raise>`'s
executable-content implementation (`Statifier.Machine.Content.Raise`)
calls. `done.state.*` is a `:platform` event per spec 5.10.1 and is
raised through `raise_platform/4` instead - both enqueue on this same
internal queue, `type` is provenance, not routing.

`origin` is `Cause.origin/0`: `<raise>` passes `{:content, c_index,
owner}` (the raising node and its owning onentry/onexit/transition
block). The round is stamped as it stood at the raise - same rule as the
counters.

# `raise_platform`

```elixir
@spec raise_platform(
  machine_state :: t(),
  name :: String.t(),
  origin :: Statifier.Event.Cause.origin(),
  opts :: keyword()
) :: t()
```

Raises a platform event: identical to `raise_internal/4` except it builds
an `Event.platform/3` event instead of `Event.internal/3` - so `type` is
stamped `:platform` per spec 5.10.1. This is the function
`Statifier.Interpreter.ExitEntry.raise_parent_completion/3` calls for
`done.state.*`: `Statifier.Event`'s moduledoc classifies `done.state.*`
as a platform-raised event, not one raised by executable content, and
`raise_internal/4` would stamp the wrong `type`.

Both `raise_internal/4` and this function enqueue on the *same* internal
queue - `type` is provenance, not routing - so nothing about ordering or
dequeue changes between the two.

`origin` is `Cause.origin/0`: `done.state.*` on entering a final state
passes `{:state, state_index}` (no content node backs it). The round is
stamped as it stood at the raise - same rule as the counters.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
