# `StatifierUI.DatamodelExplorer`
[🔗](https://github.com/riddler/statifier-ui/blob/v0.11.0/lib/statifier_ui/datamodel_explorer.ex#L1)

The datamodel explorer pane (`sui-t36.7`): one component, two data
sources. **Authoring mode** (`build_authoring/3`) merges the mode-
independent tiers `StatifierUI.DatamodelExplorer.Scope` builds - document
`<data id>` declarations (tier 1), the SCXML 5.10.1 system variables
(tier 2a), the predicator provider functions in scope (tier 2b) - with
one named fixture scenario (tier 3), the one tier that switches source
between modes (ADR-0003:82-88). **Live mode**'s `build_live/2` is
`sui-t36.7` Phase 3's addition to this same module.

This pane is a **projection, not an editor**. There is no write path in
either mode: authoring-mode tier-1 entries hold their declared source
text for display but are never evaluated (predicator is non-evaluative,
ADR-0004 upstream, adopted by ADR-0002), and live datamodel editing waits
for a recordable-channel design (statifier ADR-0029).

## The editing guard (`sui-8hg`, ADR-0012)

When a write path is eventually built - here or in whatever widget wraps
this pane - ADR-0012's flow-through clause forbids offering it over a
**projected** stream. `edit_disabled_reason/1` and `edit_disabled_reason/2`
are that clause in code, and `editable?/1` and `editable?/2` are the
boolean form. They answer *may* a value-editing affordance be offered,
not *does one exist*: today none does, and the guard is here so the
constraint is consulted rather than rediscovered.

Two reasons close the door. **The pane is projected** - `build_live/2`
carries `session.start`'s `projection` header onto the struct, and an
editor over a stream whose values the pane cannot see has nothing to seed
from and nothing meaningful to write over. **The entry is redacted** - its
value is the `:redacted` atom `StatifierUI.Value` decodes
`{"$redacted": true}` to, and an editor seeded from it would write the
sentinel back or silently write nothing. Both reasons name the profile or
the slot, so a host can say *why* rather than greying a control out
silently.

(`sui-t36.8` used to be named here as the owner of the widget and the
write affordance. It closed 2026-08-22 having shipped the inspector
assembly and the demo notebook only, with no write path, which left this
constraint ownerless; `sui-8hg` picked it up.)

# `mode`

```elixir
@type mode() :: :authoring | :live
```

Which data source fed this pane: the merged fixture scope, or a live session.

# `opt`

```elixir
@type opt() :: StatifierUI.DatamodelExplorer.Scope.opt() | {:scenario, String.t()}
```

Options shared by both constructors; `Scope.opt()` is forwarded as-is.

# `projection`

```elixir
@type projection() :: %{optional(String.t()) =&gt; String.t()}
```

`session.start`'s `projection` header, verbatim, when the stream this pane
folded carried one (ADR-0012). `nil` for an unprojected stream and for
authoring mode, which has no stream at all.

# `t`

```elixir
@type t() :: %StatifierUI.DatamodelExplorer{
  diagnostics: [StatifierUI.Fixtures.diagnostic()],
  entries: [StatifierUI.DatamodelExplorer.Entry.t()],
  macrostep: non_neg_integer() | nil,
  mode: mode(),
  projection: projection() | nil,
  scenario: String.t() | nil,
  scenario_names: [String.t()],
  session: String.t() | nil,
  truncated?: boolean()
}
```

# `build_authoring`

```elixir
@spec build_authoring(Statifier.Machine.t(), StatifierUI.Fixtures.t() | nil, [opt()]) ::
  {:ok, t()} | {:error, term()}
```

Builds an authoring-mode pane from a compiled `machine` and an optional
fixture bundle.

1. Builds the mode-independent tiers with `Scope.build/2`, propagating
   its error.
2. Selects a scenario: the `:scenario` option when given, else the first
   of `Fixtures.scenario_names/1` (sorted), else `nil` when `fixtures` is
   `nil` or holds no scenarios. A named `:scenario` the bundle does not
   hold is `{:error, {:unknown_scenario, name}}` - a typo in a host's
   switcher should say so rather than silently show the first scenario.
3. Merges the selected scenario's datamodel onto the scope: a scenario
   value naming an existing tier-1 entry replaces that entry's `value`,
   `shape` and `label` and leaves its `tier` `:data` and its `d_index`
   intact; a scenario value naming nothing declared becomes a new
   `tier: :scenario` entry. Scenario values are already decoded Elixir
   terms (the sidecar decodes at load, `sidecar.ex:171-179`), so this
   path never calls `StatifierUI.Value.decode/1` - doing so would
   re-interpret an already-decoded `Date` as a `$`-tagged map.

`entries/1` on the result concatenates `:data`, `:system`, `:function`,
then `:scenario`, each group in the order its builder produced. Bundle
diagnostics are carried onto the pane ahead of the scope's own, so a
sidecar that already had complaints does not lose them here.
`macrostep` is `nil`, `truncated?` is `false`, and `session` is `nil` -
none of those are meaningful outside live mode.

# `build_live`

```elixir
@spec build_live([StatifierUI.Trace.Message.t()], [opt()]) ::
  {:ok, t()} | {:error, {:mixed_sessions, [String.t()]}}
```

Builds a live-mode pane from `messages`, a session's captured effect
stream (in any order - this is a pure fold, not a subscription).

1. Refuses a `messages` list naming more than one session, the same
   `EventLog.build/1` rule and error shape
   (`docs/wire-format.md:96-102`). An empty list returns an empty `:live`
   pane with `session: nil`.
2. `truncated?` is `true` when the lowest `seq` present is greater than
   `0` - the head of the stream was dropped, the `EventLog` precedent.
3. Seeds the name set from `session.datamodel`'s `datamodel` payload map,
   decoding each value with `StatifierUI.Value.decode/1` **before**
   inferring its shape - the whole point, since every `<data>` element
   reads `{"$undefined": true}` there. A decode failure yields an entry
   holding `:undefined` plus one `:undecodable_datamodel_value`
   diagnostic, never a failed build.
4. Classifies each seeded name against `session.start`'s `data` table
   (`:data`, carrying that element's `d_index`) and the fixed system
   variable names spec 5.10 declares (`:system`); anything else is
   `:runtime`.
5. Applies every `effect.datamodel_change` in the producer's stamped
   `{macrostep, microstep, seq}` order (ADR-0011: never re-sorted by
   `location_path`, by `d_index`, or by name). `new_value`/`prior_value`
   absence reads as `:undefined`, matching `normalizer.ex:558-565`'s rule
   on the consumer side. `location_path`'s head names the root entry (a
   name the snapshot never carried is added as `tier: :runtime`); the
   remaining segments are applied into the root's decoded value,
   materializing a map for a string segment and a list for an integer
   segment when the container is missing or `:undefined`. A non-binary
   path head, an out-of-range index, or a segment that contradicts an
   existing container becomes an `:unresolvable_location_path` diagnostic
   and the write is skipped rather than crashing. `location_source` is
   kept from the most recent write applied to that entry.
6. `macrostep` on the pane is the highest macrostep any *applied* write
   carried (`nil` when none did). An entry is `changed?: true` when at
   least one write stamped at that macrostep carried a decoded
   `prior_value` different from its decoded `new_value` - so a first
   binding, which has no `prior_value` at all, counts as a change from
   `:undefined`. This is macrostep granularity, not round - chosen when
   `effect.datamodel_change` still carried a `nil` `round` envelope.
   sui-67d has since stamped `round` on every `effect.*` message, so
   round-granular marking is now a possible refinement here rather than
   an upstream wire change; this pane deliberately stays at macrostep
   granularity, and never reads the `round` field at all.
7. `shape` and `label` are computed once, from each entry's final decoded
   value, so an entry written more than once in one macrostep is
   labelled from its end state.
8. `projection` is `session.start`'s `projection` header verbatim, or
   `nil` when the stream carried none. It is read, never derived: a
   stream that happens to hold no redacted value is still projected if
   the header says so, and that is what `edit_disabled_reason/1` keys on
   (ADR-0012, `sui-8hg`).

Tier 2b (provider functions) is appended unchanged from the same source
`Scope` reads - `Statifier.Evaluator.Functions.base_context().functions` -
since it is a compile-time constant that needs no session; both modes
therefore show the same function list. `entries/1` on the result returns
`:data`/`:runtime`, then `:system`, then `:function`, each group sorted
by name; there is no tier 3 in live mode.

# `diagnostics`

```elixir
@spec diagnostics(t()) :: [StatifierUI.Fixtures.diagnostic()]
```

This pane's diagnostics, bundle diagnostics ahead of any the pane's own
build produced.

# `edit_disabled_reason`

```elixir
@spec edit_disabled_reason(t()) :: String.t() | nil
```

Why a value-editing affordance must not be offered over this pane, or
`nil` when nothing forbids one.

ADR-0012's flow-through clause in code. A projected pane always returns a
reason: an editor over values the pane cannot see has nothing to seed from
and nothing meaningful to write over. The returned string names the
profile and is written to be shown to a user, not logged.

There is no write path in this pane today (see the moduledoc). This
function exists so that whatever builds one asks first.

# `edit_disabled_reason`

```elixir
@spec edit_disabled_reason(t(), StatifierUI.DatamodelExplorer.Entry.t()) ::
  String.t() | nil
```

Why a value-editing affordance must not be offered for `entry` on this
pane, or `nil` when nothing forbids one.

The pane-level reason wins when the pane is projected. Otherwise a
**redacted** entry - one whose value is the `:redacted` atom
`StatifierUI.Value` decodes `{"$redacted": true}` to - carries its own
reason, because an editor seeded from the sentinel would write the
sentinel back or silently write nothing. A redacted slot is therefore
never presented as editable, whichever way it arrived.

# `editable?`

```elixir
@spec editable?(t()) :: boolean()
```

Whether a value-editing affordance may be offered over this pane at all -
`edit_disabled_reason/1` returning nothing.

# `editable?`

```elixir
@spec editable?(t(), StatifierUI.DatamodelExplorer.Entry.t()) :: boolean()
```

Whether a value-editing affordance may be offered for `entry` -
`edit_disabled_reason/2` returning nothing.

# `entries`

```elixir
@spec entries(t()) :: [StatifierUI.DatamodelExplorer.Entry.t()]
```

This pane's entries, in tier order (`:data`, `:system`, `:function`,
`:scenario` in authoring mode).

# `entries`

```elixir
@spec entries(t(), StatifierUI.DatamodelExplorer.Entry.tier()) :: [
  StatifierUI.DatamodelExplorer.Entry.t()
]
```

This pane's entries belonging to one `tier`, in the same relative order
`entries/1` returns them.

# `projected?`

```elixir
@spec projected?(t()) :: boolean()
```

Whether this pane folded a projected stream (ADR-0012).

True exactly when `session.start` carried a `projection` header. Authoring
mode is never projected - it has no stream.

    iex> {:ok, pane} = StatifierUI.DatamodelExplorer.build_live([])
    iex> StatifierUI.DatamodelExplorer.projected?(pane)
    false

# `projection_profile`

```elixir
@spec projection_profile(t()) :: String.t() | nil
```

The projection profile's name, or `nil` when the pane is not projected.

This is the string ADR-0012 asks a host to surface alongside the mode, so
a user asking "why can't I see this" has something to quote.

---

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