# `Statifier.Telemetry`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.1.1/lib/statifier/telemetry.ex#L1)

The `:telemetry` bridge (ADR-0040, amended by ADR-0067) - the single
authoritative reference for every `[:statifier, :session, ...]` event any
stepping driver can emit. This module is the caller-agnostic emission half
of the ADR-0003 effect-interpreter role: it holds no state, drives no core
function, and is called from wherever a driver steps a chart -
`Statifier.Session` included, through the `Statifier.Session.Telemetry`
facade that pins `driver: :session`.

`events/0` returns every name below, built from the same literal-atom lists
the emitters use, so a consumer can `:telemetry.attach_many/4` to the whole
surface without hand-copying names out of this table.

## The prefix names the logical session, not a process (ADR-0067 decision 1)

The second segment of every event name, `:session`, names the logical SCXML
session - `datamodel["_sessionid"]`, the identity a position blob persists
and a resume preserves (ADR-0052/ADR-0060) - not the `Statifier.Session`
GenServer. The process was the first driver of a session, not the
definition of one: a host stepping the pure interpreter directly, with no
`Statifier.Session` anywhere, is stepping the same kind of thing, and its
events belong on the same prefix.

## Applicability across drivers (ADR-0067 decision 3)

Every event keeps one shape across every driver; not every driver emits
every event:

| Event | `Statifier.Session` | Process-less driver |
|---|---|---|
| `:init` | at process boot, `resumed` per ADR-0060 | once per logical run, at `Interpreter.initialize/2` time, `resumed: false`; never per load |
| `:halt` | yes | yes - the step whose outcome is terminal |
| `:terminate` | yes (GenServer contract) | never |
| `:macrostep, :start` / `:stop` | yes | yes - brackets each core advance call |
| `:interpret` | yes (ADR-0029 seam) | only if the driver exposes an equivalent injection seam |
| `:unroutable` | yes | yes - the driver routes effects too |
| `:effect, _` (11) | yes | yes |
| `:trace, _` (9) | yes, under `trace: true` | yes, under `trace: true` - the flag rides the position (ADR-0060), and the gate stays in the core |

`:init` marks initialization, not loading: a durable driver rehydrates a
position on every step, and an `:init` per load would both fire thousands
of times per logical run and misname "process boot", which does not exist
there - the durable `:init` marks the one `Interpreter.initialize/2` call
at run creation. `:terminate` names a GenServer callback and gets no
durable analog; ADR-0040 already directs "session finished" metrics at
`:halt`, driver-uniform. `:interpret` is conditional on a driver having an
ADR-0029-style injection seam; none exists outside `Statifier.Session`
today.

A span's start and stop halves must be emitted within one driver
invocation (ADR-0067 decision 5): `span_ref` is a `make_ref/0` reference,
node- and VM-local, and can never cross a persist boundary. A driver
brackets its in-memory core advance call with the two halves in the same
call - never a span opened before persist and closed after a later load.

## Measurements are numbers, metadata is everything else

Every measurement is a number - `:telemetry_metrics`-aggregatable. Every
identity - a session id, an effect struct, a resolved location, and every
constraint-3 index (`state_index`, `t_index`, `c_index`, `invoke_index`) -
is metadata, including the ones that happen to be integers: an opaque
index has no numeric meaning to average.

A `configuration` in metadata (on `:halt`, on the `:macrostep, :stop` event,
and on the `:effect, :done`/`:trace, :done` events below) is translated
from the raw `MapSet.t(non_neg_integer())` the machine state or the
`Done`/`Trace.Done` payload's own `configuration` field carries into a
`MapSet.t(String.t())` of state ids via `Statifier.Machine.id/2`, so a
subscriber never needs a `Machine` handle to read a configuration out of an
event - including the terminal one, where `MachineState.configuration` is
already empty and the payload's own field is the only carrier left.

## Span correlation

The macrostep span's start and stop halves both carry `span_ref`, a
`make_ref/0` reference generated once per span and passed unchanged from
`macrostep_start/5` to the matching `macrostep_stop/8` - the
`telemetry_span_context` convention `:telemetry.span/3` uses, so a
subscriber (including an OTel bridge) pairs a span's two halves on
`span_ref` rather than on `(session_id, macrostep)`, which is unusable for
this because `macrostep` is not stable across the span (below) and ADR-0039
re-entry can nest an `:internal` span inside an `:event` span already in
flight, producing `start, start, stop, stop` with no other way to tell
which start belongs to which stop. Each span - including a nested one - gets
its own `span_ref`. Both halves also carry `monotonic_time`
(`System.monotonic_time/0`), matching `:telemetry_metrics`/OTel convention.

The start half deliberately carries no `macrostep`. The counter this span
opens against advances *inside* the core call the span brackets
(`MachineState.begin_macrostep/1`, called from `Interpreter.handle_event/2`
among others), so a `macrostep` read at the start half can be the
pre-increment value while the stop half reports the post-increment one -
the two would silently disagree on a pairing key. `span_ref` is the
correlation mechanism; the stop half is the only place `macrostep` is
authoritative for this span.

This bridge hand-rolls the span pair rather than calling
`:telemetry.span/3`: that helper wraps its span function in a `rescue`, so
it always emits a matching `:exception` event, and this bridge has no
`:exception` half to offer (below) - forwarding decisions the caller
already made deterministically, not evaluating one under a `rescue` of its
own would invent a failure mode the driver does not have.

## Two consumer-facing caveats

There is deliberately no fourth, `:exception`-named member of the
`:macrostep` span family alongside `:start`/`:stop` (previous section): a
crash mid-span - inside `Statifier.Session`'s `perform/3`, say - leaves
that span's start unmatched by any stop. A subscriber pairing on `span_ref`
should not expect every `span_ref` it sees on a start to arrive again on a
stop.

The `:initialize` span's start event is emitted *after*
`Interpreter.initialize/2` has already returned, for the `Statifier.Session`
driver (`Statifier.Session.init` needs the `machine_state` that call
produces before it can call `Statifier.Session.Telemetry.init/5`, which
happens first), so its `system_time` is not the wall-clock instant the span
actually opened at, even though `duration` on the matching stop is still
measured from a `System.monotonic_time/0` reading taken before that call.

## Locations are resolved at emission, for single-index effects only

An effect carrying exactly one resolvable index gets `metadata.location`,
resolved through `Statifier.Machine.content/2`, `Statifier.Machine.at/2`,
`Statifier.Machine.transition/2`, or `Statifier.Machine.data/2` as
appropriate, and carried as the `%Statifier.Parser.Location{}` struct
verbatim. Where no index resolves - an
effect with no index field at all, or an index field present but `nil` -
the two families answer differently, and the difference is the contract, not
an inconsistency to smooth over. A **core** effect still carries the key,
resolving to `location: nil`: every `core_shape/2` clause sets it
unconditionally, so `metadata.location` is safe to read without a
`Map.has_key?/2` guard. A **trace** effect omits the key entirely - there, a
key that can never hold a value is contract noise a consumer would otherwise
have to learn to ignore, so it is absent rather than present and `nil`
(ADR-0040 and its amendments). An effect carrying a *list* of
indexes carries the list in metadata
(nested in the `effect` struct) and its length as a `size` measurement,
with no `location` key, at any cardinality - zero, one, or many entries.
No `[:statifier, :session, :trace, _]` event ever carries a `location`
key, because every trace effect carrying a resolvable index carries it as
a list: a set-valued trace event names a phase (an exit set, an entry
set, an invoke-pass walk, a selection round's result), not a chart
element, so a location resolved from a list that happens to hold one
entry would describe a coincidence of the chart and the round rather than
a property of the event, and the rule would need a per-event footnote the
moment a second list field entered the picture (`Trace.InvokePass`
already carries two). Locations on this surface live exclusively on the
single-index core effect events and `:unroutable`, resolved by the rule
above (ADR-0040 Decision 4, amended). `cond_location` is never
resolved: no effect in the vocabulary is emitted from guard evaluation.

## Lifecycle and span events (7), emitted regardless of `trace`

| Event | Measurements | Metadata |
|---|---|---|
| `[:statifier, :session, :init]` | `system_time` | `driver`, `session_id`, `machine_name`, `trace`, `invoked_by`, `resumed` |
| `[:statifier, :session, :halt]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `reason`, `configuration` |
| `[:statifier, :session, :terminate]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `reason`, `status` |
| `[:statifier, :session, :macrostep, :start]` | `system_time`, `monotonic_time` | `driver`, `session_id`, `trigger`, `event_name`, `caller_context`, `span_ref` |
| `[:statifier, :session, :macrostep, :stop]` | `duration`, `macrostep`, `microsteps`, `rounds`, `monotonic_time` | `driver`, `session_id`, `trigger`, `outcome`, `event_name`, `configuration`, `caller_context`, `span_ref` |
| `[:statifier, :session, :interpret]` | `effect_count`, `macrostep`, `microstep` | `driver`, `session_id` |
| `[:statifier, :session, :unroutable]` | `macrostep`, `microstep` | `driver`, `session_id`, `effect`, `target`, `send_id`, `location` |

`[:statifier, :session, :unroutable]` names a routing failure the driver
itself detected (the `{:unroutable, effect}` instruction), not an effect
the core produced - a consumer alerting on it wants one name to attach to
rather than a filter across the nine effect names below.

`[:statifier, :session, :terminate]` fires from `Statifier.Session`'s
`terminate/2` and is therefore subject to the GenServer contract: it does
not fire on a brutal kill, and no process-less driver emits it at all.
`:halt` is the event to build a "session finished" metric on.

`duration` (on `:macrostep, :stop`) is `:native` units, per `:telemetry`'s
own convention - unit conversion (`System.convert_time_unit/3`) is left to
the consumer.

`caller_context` (on both macrostep halves here, and on the
`:send_delayed`/`:cancel` effect events below) is ADR-0063's opaque host
slot: the triggering external event's `caller_context`, `nil` for the
`:initialize`/`:cancel`/`:internal`/`:resume` triggers and for an event
sent without one. Identity, never a number, so it is metadata everywhere
and a measurement nowhere (ADR-0040's split). Both halves carry it for
the same reason both carry `trigger` and `event_name`: a consumer
attaching to only one half still attributes. On the two effect events
the value also rides in `metadata.effect` verbatim; the explicit key
keeps a bridge's read uniform with the macrostep events. A consumer
treats the term as opaque - something to parent or link with, never to
flatten into exported attributes.

## Core effect events (11), emitted regardless of `trace`

| Event | Measurements | Metadata |
|---|---|---|
| `[:statifier, :session, :effect, :send]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `effect`, `location`, `send_id`, `target`, `c_index`, `owner` |
| `[:statifier, :session, :effect, :send_delayed]` | `macrostep`, `microstep`, `round`, `delay_ms`, `ordinal` | `driver`, `session_id`, `effect`, `location`, `send_id`, `target`, `c_index`, `owner`, `caller_context` |
| `[:statifier, :session, :effect, :cancel]` | `macrostep`, `microstep`, `round`, `ordinal` | `driver`, `session_id`, `effect`, `location`, `send_id`, `c_index`, `owner`, `caller_context` |
| `[:statifier, :session, :effect, :invoke]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `effect`, `location`, `invoke_id`, `state_index`, `invoke_index` |
| `[:statifier, :session, :effect, :cancel_invoke]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `effect`, `location`, `invoke_id`, `state_index` |
| `[:statifier, :session, :effect, :autoforward]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `effect`, `location`, `invoke_id`, `state_index` |
| `[:statifier, :session, :effect, :budget_exhausted]` | `macrostep`, `microstep`, `round`, `budget` | `driver`, `session_id`, `effect`, `location` |
| `[:statifier, :session, :effect, :done]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `effect`, `location`, `configuration` |
| `[:statifier, :session, :effect, :log]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `effect`, `location`, `label`, `c_index`, `owner` |
| `[:statifier, :session, :effect, :datamodel_change]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `effect`, `location`, `location_path`, `location_source`, `new_value`, `prior_value`, `d_index`, `c_index`, `owner` |
| `[:statifier, :session, :effect, :datamodel_init]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `effect`, `location`, `datamodel` |

## Trace effect events (9), emitted only under `trace: true`

The trace family vanishes **in the core**, not at this bridge:
`Statifier.Effect.trace/3` expands to no effect at all when
`machine_state.trace` is `false`, so there is nothing here to forward.
This module contains no `if trace` of its own.

| Event | Measurements | Metadata |
|---|---|---|
| `[:statifier, :session, :trace, :event_dequeued]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `effect` |
| `[:statifier, :session, :trace, :transitions_selected]` | `macrostep`, `microstep`, `round`, `size` | `driver`, `session_id`, `effect` |
| `[:statifier, :session, :trace, :exit_set]` | `macrostep`, `microstep`, `round`, `size` | `driver`, `session_id`, `effect` |
| `[:statifier, :session, :trace, :content_executed]` | `macrostep`, `microstep`, `round`, `size` | `driver`, `session_id`, `effect` |
| `[:statifier, :session, :trace, :entry_set]` | `macrostep`, `microstep`, `round`, `size` | `driver`, `session_id`, `effect` |
| `[:statifier, :session, :trace, :macrostep_stable]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `effect` |
| `[:statifier, :session, :trace, :done]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `effect`, `configuration` |
| `[:statifier, :session, :trace, :invoke_pass]` | `macrostep`, `microstep`, `round`, `size` | `driver`, `session_id`, `effect` |
| `[:statifier, :session, :trace, :finalize_autoforward]` | `macrostep`, `microstep`, `round` | `driver`, `session_id`, `effect` |

`kind` (the fourth segment) is derived by a private, multi-clause function
pattern-matching each `Statifier.Effect.Trace.*` struct to a literal atom -
never `Module.split/1` composed with `String.to_atom/1`, which
`Credo.Check.Warning.UnsafeToAtom` forbids and which would make this
module's event list impossible to enumerate ahead of a call.

# `core_payload`

```elixir
@type core_payload() ::
  Statifier.Effect.Send.t()
  | Statifier.Effect.SendDelayed.t()
  | Statifier.Effect.Cancel.t()
  | Statifier.Effect.Invoke.t()
  | Statifier.Effect.CancelInvoke.t()
  | Statifier.Effect.Autoforward.t()
  | Statifier.Effect.BudgetExhausted.t()
  | Statifier.Effect.Done.t()
  | Statifier.Effect.Log.t()
  | Statifier.Effect.DatamodelChange.t()
  | Statifier.Effect.DatamodelInit.t()
```

One of the eleven core effect payload structs (`t:Statifier.Effect.core/0`, unwrapped).

# `driver`

```elixir
@type driver() :: atom()
```

The stepping driver that emitted an event. `:session` is reserved for this
library's own `Statifier.Session` process driver; an external driver names
itself with one stable atom of its own (ADR-0067 decision 4).

# `event_name`

```elixir
@type event_name() :: [atom(), ...]
```

One `:telemetry` event name this module can emit.

# `trace_payload`

```elixir
@type trace_payload() ::
  Statifier.Effect.Trace.EventDequeued.t()
  | Statifier.Effect.Trace.TransitionsSelected.t()
  | Statifier.Effect.Trace.ExitSet.t()
  | Statifier.Effect.Trace.ContentExecuted.t()
  | Statifier.Effect.Trace.EntrySet.t()
  | Statifier.Effect.Trace.MacrostepStable.t()
  | Statifier.Effect.Trace.Done.t()
  | Statifier.Effect.Trace.InvokePass.t()
  | Statifier.Effect.Trace.FinalizeAutoforward.t()
```

One of the nine trace payload structs (`t:Statifier.Effect.trace/0`, unwrapped).

# `effect`

```elixir
@spec effect(
  driver :: driver(),
  session_id :: String.t(),
  machine :: Statifier.Machine.t(),
  effect :: Statifier.Effect.t()
) :: :ok
```

Emits `[:statifier, :session, :effect, kind]` or
`[:statifier, :session, :trace, kind]`, dispatching on `effect`'s own tag.
`machine` resolves `location` per the moduledoc's single-index rule.

# `events`

```elixir
@spec events() :: [event_name()]
```

Every event name this module can ever emit - the 7 lifecycle/span names,
the 11 `[:statifier, :session, :effect, kind]` names, and the 9
`[:statifier, :session, :trace, kind]` names, built from
`@lifecycle_events`/`@effect_kinds`/`@trace_kinds`, the module's single
definition site for the vocabulary.

# `halt`

```elixir
@spec halt(
  driver :: driver(),
  session_id :: String.t(),
  reason :: :done | :cancelled | :budget_exhausted,
  machine_state :: Statifier.MachineState.t()
) :: :ok
```

Emits `[:statifier, :session, :halt]`.

# `init`

```elixir
@spec init(
  driver :: driver(),
  session_id :: String.t(),
  machine :: Statifier.Machine.t(),
  machine_state :: Statifier.MachineState.t(),
  invoked_by :: {pid(), String.t()} | nil,
  resumed :: boolean()
) :: :ok
```

Emits `[:statifier, :session, :init]`. `resumed` is ADR-0060's
metadatum - `true` when this session booted from a `:resume` option
rather than running `Statifier.Interpreter.initialize/2`. Additive: an
existing handler reading only the four original keys is unaffected by the
fifth.

# `interpret`

```elixir
@spec interpret(
  driver :: driver(),
  session_id :: String.t(),
  effect_count :: non_neg_integer(),
  machine_state :: Statifier.MachineState.t()
) :: :ok
```

Emits `[:statifier, :session, :interpret]`.

# `macrostep_start`

```elixir
@spec macrostep_start(
  driver :: driver(),
  session_id :: String.t(),
  trigger :: :initialize | :event | :cancel | :internal | :resume,
  event :: Statifier.Event.t() | nil,
  span_ref :: reference()
) :: :ok
```

Emits `[:statifier, :session, :macrostep, :start]`. `span_ref` is a
`make_ref/0` reference generated once by the caller and passed unchanged
to the matching `macrostep_stop/8` - the pairing mechanism a subscriber
needs (Decision 2 below), since `macrostep` cannot serve that role here:
the counter this span opens against advances *inside* the core call the
span brackets, so the start half is emitted before the increment and the
stop half after it. Carrying the pre-increment value on start would look
paired with the wrong stop on every span whose trigger's core call
advances the counter (`:event`, and any other trigger with the same
shape); carrying the post-increment value on start would require reading
it out of a machine state the start half does not otherwise need. Rather
than pick one and leave the other case's start event silently wrong, the
start half carries no `macrostep` at all - `span_ref` is the only
correlation mechanism, and it is exact by construction.

# `macrostep_stop`

```elixir
@spec macrostep_stop(
  driver :: driver(),
  session_id :: String.t(),
  trigger :: :initialize | :event | :cancel | :internal | :resume,
  machine_state :: Statifier.MachineState.t(),
  event :: Statifier.Event.t() | nil,
  outcome :: :quiescent | :done | :cancelled | :budget_exhausted,
  start_time :: integer(),
  span_ref :: reference()
) :: :ok
```

Emits `[:statifier, :session, :macrostep, :stop]`. `start_time` is a
`System.monotonic_time/0` reading taken at the matching
`macrostep_start/5`; `duration` is the difference, in `:native` units.
`span_ref` is the same reference passed to that `macrostep_start/5` call,
carried again here so a subscriber can pair this stop with its start
(Decision 2) - nested spans (an `:internal` span opened by ADR-0039
re-entry inside an `:event` span already in flight) each get their own
reference, so `start, start, stop, stop` is disambiguated by `span_ref`
rather than by call order.

# `terminate`

```elixir
@spec terminate(
  driver :: driver(),
  session_id :: String.t(),
  reason :: term(),
  status :: term(),
  machine_state :: Statifier.MachineState.t()
) :: :ok
```

Emits `[:statifier, :session, :terminate]`.

# `unroutable`

```elixir
@spec unroutable(
  driver :: driver(),
  session_id :: String.t(),
  machine :: Statifier.Machine.t(),
  effect :: Statifier.Effect.t()
) :: :ok
```

Emits `[:statifier, :session, :unroutable]` for an effect the driver could not route.

---

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