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.
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
@type mode() :: :authoring | :live
Which data source fed this pane: the merged fixture scope, or a live session.
@type opt() :: StatifierUI.DatamodelExplorer.Scope.opt() | {:scenario, String.t()}
Options shared by both constructors; Scope.opt() is forwarded as-is.
@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
@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.
- Builds the mode-independent tiers with
Scope.build/2, propagating its error. - Selects a scenario: the
:scenariooption when given, else the first ofFixtures.scenario_names/1(sorted), elsenilwhenfixturesisnilor holds no scenarios. A named:scenariothe 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. - Merges the selected scenario's datamodel onto the scope: a scenario
value naming an existing tier-1 entry replaces that entry's
value,shapeandlabeland leaves itstier:dataand itsd_indexintact; a scenario value naming nothing declared becomes a newtier: :scenarioentry. Scenario values are already decoded Elixir terms (the sidecar decodes at load,sidecar.ex:171-179), so this path never callsStatifierUI.Value.decode/1- doing so would re-interpret an already-decodedDateas 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.
@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).
- Refuses a
messageslist naming more than one session, the sameEventLog.build/1rule and error shape (docs/wire-format.md:96-102). An empty list returns an empty:livepane withsession: nil. truncated?istruewhen the lowestseqpresent is greater than0- the head of the stream was dropped, theEventLogprecedent.- Seeds the name set from
session.datamodel'sdatamodelpayload map, decoding each value withStatifierUI.Value.decode/1before inferring its shape - the whole point, since every<data>element reads{"$undefined": true}there. A decode failure yields an entry holding:undefinedplus one:undecodable_datamodel_valuediagnostic, never a failed build. - Classifies each seeded name against
session.start'sdatatable (:data, carrying that element'sd_index) and the fixed system variable names spec 5.10 declares (:system); anything else is:runtime. - Applies every
effect.datamodel_changein the producer's stamped{macrostep, microstep, seq}order (ADR-0011: never re-sorted bylocation_path, byd_index, or by name).new_value/prior_valueabsence reads as:undefined, matchingnormalizer.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 astier: :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_pathdiagnostic and the write is skipped rather than crashing.location_sourceis kept from the most recent write applied to that entry. macrostepon the pane is the highest macrostep any applied write carried (nilwhen none did). An entry ischanged?: truewhen at least one write stamped at that macrostep carried a decodedprior_valuedifferent from its decodednew_value- so a first binding, which has noprior_valueat all, counts as a change from:undefined. This is macrostep granularity, not round - chosen wheneffect.datamodel_changestill carried anilroundenvelope. sui-67d has since stampedroundon everyeffect.*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 theroundfield at all.shapeandlabelare 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.
@spec diagnostics(t()) :: [StatifierUI.Fixtures.diagnostic()]
This pane's diagnostics, bundle diagnostics ahead of any the pane's own build produced.
@spec entries(t()) :: [StatifierUI.DatamodelExplorer.Entry.t()]
This pane's entries, in tier order (:data, :system, :function,
:scenario in authoring mode).
@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.