StatifierUI.DatamodelExplorer (StatifierUI v0.1.0)

Copy Markdown View Source

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). sui-t36.8 owns the widget and the write affordance never arrives here.

Summary

Types

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

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

t()

Functions

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

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

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

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

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

Types

mode()

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

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

opt()

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

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

t()

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

Functions

build_authoring(machine, fixtures \\ nil, opts \\ [])

@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(messages, opts \\ [])

@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.

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(datamodel_explorer)

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

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

entries(datamodel_explorer)

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

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

entries(datamodel_explorer, tier)

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