What is known at a position: a map from datamodel path to type, carried through the document by a pre-order walk (ADR-0011 decision 1).
Nothing flows between adjacent blocks. Every value a block produces is written to a datamodel path by name and every value it reads is read from one, so the data-flow question at a position is a question about the paths the document has written on the way there - not about the block before it.
The type of a type
A type is one of the nine the datamodel document closes its set at, the
name of a record or shape that document declares, an opaque string a
host carries, {:list, type}, or :unknown. This module mints none of
them: every one arrives from a block's own declaration or from the
document. type_of/2 reads a declaration's spelling into
StatifierDatamodel.Types.t/0 and satisfies/3 hands the pair to
StatifierDatamodel.Types.satisfies/3, which is the read check
(ADR-0011 decision 3). There is no second one here, and no Compatibility
or Coverage module of this package's own.
The string "unknown" reads as :unknown rather than as an opaque string
spelled that way, so that StatifierDatamodel.Types.to_string/1 and
type_of/2 are inverse over the whole grammar. It is the one
reinterpretation of an expression a host could already have been carrying,
and it only ever admits: an opaque "unknown" compared by identity was
already satisfied against another "unknown", so nothing that passed
before is refused now.
The walk
Document.blocks/1's pre-order, carrying the environment forward. For a
block reached with environment env:
- each slot the block carries is walked from
env- the shelf is not entered (ADR-0003's amendment of 2026-08-31, A2), and each parked fragment is walked from an empty environment instead; - what the slots produce is merged per path by agreement (decision 4):
a path every slot holds at one type keeps it, a path some hold and
others do not - or hold differently - drops to
:unknown, and a path no slot holds is absent; - the block's own write signatures are applied to the merge.
A container with one slot merges to that slot's own answer, so a group
body's writes leave the group. A core.on_event in a group's interrupt
slot writes its captures on that arm alone, so a path only it holds leaves
the group at :unknown - decision 10's answer, arrived at by decision 4
rather than by a special case.
Why the walk terminates
It descends. at/3 asks StatifierBlocks.Document.fetch_path/2 for the
path from the root to the position it was given, and then walks down
that path once, carrying the environment: each step folds the siblings
before it and moves to a strictly deeper block, and the path is finite
because the document is a finite tree. Nothing here asks an ancestor for
its own position, so there is no recursion to bound and a document built of
nothing but empty sequences costs one step per level - which is also why
the answer stays cheap enough for the editor to compute on mousedown.
Signatures
A block declares what it reads and writes on its fields, not on itself (ADR-0011 decision 2), so a finding anchors on the field key the author has to change:
{:path, %{writes: T}}writesTat the path the field's value names;- a
{:path, opts}field with nowriteskey, and a field carryingdatamodel_path?: true, write:unknownthere - the path becomes known without becoming typed; - a
captureconfig map writes:unknownat each of its keys, one per pair; {:path, %{expects: T}}readsTthere.
io/1's consumes and produces are sugar over the subject path,
which the entry block's palette entry names with subject: (decision 6):
consumes is a read there and produces is a write there. A document whose
entry block declares no subject has no subject path, and the sugar is inert.
A produces of :unknown writes nothing rather than blanking the subject -
a container that says nothing about the subject leaves it alone, which is
the whole gain decision 4's per-path merge exists for.
Summary
Types
The environment with the block that wrote each entry, which is what
ADR-0011 decision 8's upstream_ref names. :slot_entry is the seed, or a
merge whose arms agreed on a type without agreeing on who put it there.
Caller-supplied, not stored in the document. :datamodel is the datamodel
document the declarations are read from; :entry_type seeds the subject
path for a document whose entry block declares no produces of its own.
One declared read or write: the field key it is declared on (or
:consumes / :produces for the sugar, and :capture for a capture
pair), the path it names, and the type.
Datamodel path to type. ADR-0011 decision 1's environment.
A position, as ADR-0001 decision 5 defines one.
A type at a path. One of StatifierDatamodel.Types.t/0's inhabitants as
a document spells it - a scalar name, a declared name, an opaque string -
or :unknown, or a list of one of those.
Functions
at/3, keeping the block that wrote each entry.
The environment at target, as a map from datamodel path to type.
The declarations ctx[:datamodel] carries, or an empty index.
Every read block declares, in config_schema/1 order with the
consumes sugar last.
The read check: StatifierDatamodel.Types.satisfies/3 over the two
spellings, and nothing else (ADR-0011 decision 3).
The environment the document opens with (ADR-0011 decision 2).
The datamodel path the document's subject lives at, or nil.
How a type expression is written for a human (ADR-0011 decision 9).
Reads a declared spelling into a type expression.
Every write block declares, in config_schema/1 order, then its capture
pairs, then the produces sugar (ADR-0011 decision 2).
Types
@type annotated() :: %{ optional(String.t()) => {type_expr(), StatifierBlocks.Block.id() | :slot_entry} }
The environment with the block that wrote each entry, which is what
ADR-0011 decision 8's upstream_ref names. :slot_entry is the seed, or a
merge whose arms agreed on a type without agreeing on who put it there.
Caller-supplied, not stored in the document. :datamodel is the datamodel
document the declarations are read from; :entry_type seeds the subject
path for a document whose entry block declares no produces of its own.
One declared read or write: the field key it is declared on (or
:consumes / :produces for the sugar, and :capture for a capture
pair), the path it names, and the type.
Datamodel path to type. ADR-0011 decision 1's environment.
@type target() :: {StatifierBlocks.Block.id(), StatifierBlocks.Block.slot_name(), non_neg_integer()}
A position, as ADR-0001 decision 5 defines one.
A type at a path. One of StatifierDatamodel.Types.t/0's inhabitants as
a document spells it - a scalar name, a declared name, an opaque string -
or :unknown, or a list of one of those.
Functions
@spec annotated( StatifierBlocks.Palette.t(), StatifierBlocks.Document.t(), target(), context() ) :: annotated()
at/3, keeping the block that wrote each entry.
ADR-0011 decision 8's {:type_mismatch, ...} names the block whose write
signature the read disagrees with, and this is where that name comes from.
@spec at( StatifierBlocks.Palette.t(), StatifierBlocks.Document.t(), target(), context() ) :: t()
The environment at target, as a map from datamodel path to type.
target is a position {parent_id, slot, index}: the answer is what the
block at that index sees, before its own writes are applied. Total - a
parent no block carries, or an index past the end of a slot, answers with
whatever the walk reached, never a raise.
@spec declarations(context()) :: StatifierDatamodel.Declarations.t()
The declarations ctx[:datamodel] carries, or an empty index.
StatifierDatamodel.Declarations.from_document/1 is total: a datamodel
that is not a document, or one with no types key, declares nothing rather
than failing, and a walk over it produces :unknown and not an exception.
@spec read_signatures( StatifierBlocks.Palette.t(), StatifierBlocks.Document.t(), StatifierBlocks.Block.t() ) :: [signature()]
Every read block declares, in config_schema/1 order with the
consumes sugar last.
A block with three path fields declares three reads and they are independent (ADR-0011 decision 2).
@spec satisfies(StatifierDatamodel.Declarations.t(), term(), term()) :: StatifierDatamodel.Types.reason()
The read check: StatifierDatamodel.Types.satisfies/3 over the two
spellings, and nothing else (ADR-0011 decision 3).
The palette's host relation is not asked here. It runs last, after this
returns not-satisfied, in StatifierBlocks.Assignability.assignable?/4.
@spec seed(StatifierBlocks.Palette.t(), StatifierBlocks.Document.t(), context()) :: t()
The environment the document opens with (ADR-0011 decision 2).
ctx[:entry_type] at the subject path, and nothing else. It is ADR-0003
decision 4's context key, kept meaning what it meant - the type entering the
document - now that the document has a path to hold it at. A document with
no entry block, or one whose palette entry declares no subject:, seeds
empty however the context is filled: with no subject path there is nowhere
for a subject type to be. Every read is then a read of a path the
environment does not hold, which is an advisory and not an error, so an
untyped document validates exactly as it did.
The entry block's own writes are not applied here. Decision 2 says the document opens with its subject path holding its subject type, and the walk is what puts it there: the entry block is the first position walked, so every position after it sees exactly that. Applying its writes ahead of the walk would additionally put them in front of the entry block's own reads, and decision 1 is explicit that a block's reads are checked before its own writes are applied - a block does not read what it is about to write.
@spec subject_path(StatifierBlocks.Palette.t(), StatifierBlocks.Document.t()) :: String.t() | nil
The datamodel path the document's subject lives at, or nil.
ADR-0011 decision 6: the subject: key on the entry block's
palette_entry/0 - the first block of the root's body slot. nil means
the document has no subject, and consumes and produces desugar to
nothing at all.
@spec type_label(StatifierDatamodel.Declarations.t(), term()) :: String.t()
How a type expression is written for a human (ADR-0011 decision 9).
A spelling that names a declaration in declarations renders that
declaration's label, which is the human-readable name the record asks a
finding and the Datamodel tab to carry so an author reads "Credit card
transaction" instead of a nominal name they have to go and look up. Every
other spelling renders exactly as it did before there were declarations:
one of the nine scalars as its own word, an opaque string a host carries as
itself, :unknown as unknown, and a list as what it holds.
It is a rendering and nothing else. No verdict reads it, nothing
branches on it, and a declaration whose label is absent renders its name
so a document that declares types without labelling them shows exactly what it showed before, rather than a blank where a name used to be.
iex> alias StatifierBlocks.Environment iex> declarations = StatifierDatamodel.Declarations.from_document(%{"types" => [ ...> %{"name" => "cards.credit_txn", "kind" => "record", ...> "label" => "Credit card transaction", "fields" => []}]}) iex> Environment.type_label(declarations, "cards.credit_txn") "Credit card transaction" iex> Environment.type_label(declarations, "myapp.card_txn") "myapp.card_txn" iex> Environment.type_label(declarations, :unknown) "unknown" iex> Environment.type_label(declarations, {:list, "cards.credit_txn"}) "list of Credit card transaction"
@spec type_of(StatifierDatamodel.Declarations.t(), term()) :: StatifierDatamodel.Types.t()
Reads a declared spelling into a type expression.
Total, and it defers to StatifierDatamodel.Types.parse/2 for everything
the grammar already covers. Two readings are this package's own: the atom
:unknown and the string "unknown" are both :unknown, and a
{:list, _} is the document's own list - ADR-0011 decision 14 puts no
cardinality on a read, so a list is checked as a list and its item type is
carried for a fan-out to bind, not for the check to descend into.
@spec write_signatures( StatifierBlocks.Palette.t(), StatifierBlocks.Document.t(), StatifierBlocks.Block.t() ) :: [signature()]
Every write block declares, in config_schema/1 order, then its capture
pairs, then the produces sugar (ADR-0011 decision 2).