One block, rendered: its resolved status, its presentation metadata, its slots (recursive), its form, and its own findings.
outcome is the outcome this block produces for the slot it sits in,
when the parent's type declared a slot_outcome_key for that slot
and this block's config holds a well-formed outcome name there; nil
otherwise, which is every block whose parent declared nothing. It is
resolved here rather than by a consumer so that the picture-drawing
side reads one field instead of a metadata key plus a config lookup,
and so it never learns that core.on_event is the type with an
outcome.
join_label is what the join marker under a side-by-side arrangement
reads: the string this block type's join_label callback returned for
this block's config, normalized by StatifierBlocks.BlockType.join_label/2,
and nil when the type declared none or the callback answered with
something the refusal set rejects (ADR-0002 amendment B). It is resolved
here for the same reason outcome is - the rendering side reads a string
and never learns that core.parallel is the type with a completion rule.
It is derived for every node, including the ones whose slots stack
and never draw a marker: a field that exists only sometimes is a field
every consumer has to remember to guard, and entry.layout already says
whether the marker is drawn. An unresolvable block reaches nil by the
ordinary route rather than by a special case - its entry is the
placeholder's, which declares no callback.
title is the author's own name for THIS block - the value of a
declared :string field keyed label - and nil when the type
declares no such field or the config holds nothing usable there, which
is every block in the core.* vocabulary. nil rather than "the
entry's label repeated" so that the card can tell the two apart: a
block with a name of its own reads as its name over its type, and a
block without one reads as its type alone rather than as its type
printed twice (ViewModel.title/1 and ViewModel.subtitle/1 are that
pair). It is derived here for the same reason outcome is - the
rendering side reads a string and never learns which key an author's
name lives under.
invoke_type is what this block's config carries under invoke_type,
when that is a non-empty string. It is a config key rather than a type
name, so a host type that calls out to a handler gets the same third
line core.invoke does by carrying the same key, and no component
learns that core.invoke exists. Whether the value is WELL FORMED is
validate_config/1's question and its answer arrives as a finding on
the same card; hiding the string while a finding complains about it
would be the card disagreeing with the form.
Summary
Types
@type status() :: :ok | {:unresolvable, term()}
@type t() :: %StatifierBlocks.ViewModel.Node{ block_id: StatifierBlocks.Block.id(), entry: StatifierBlocks.BlockType.palette_entry(), findings: [StatifierBlocks.Finding.t()], findings_count: non_neg_integer(), form: StatifierBlocks.ViewModel.Form.t() | nil, invoke_type: String.t() | nil, join_label: String.t() | nil, outcome: String.t() | nil, raw_config_json: String.t() | nil, slots: [StatifierBlocks.ViewModel.Slot.t()], status: status(), summary: [String.t()], title: String.t() | nil, type: StatifierBlocks.Block.type_name(), type_version: pos_integer() }