One thing the compiler has to say about one block (ADR-0004 decision 10).
Every finding names a block. There is no chart-level finding without an
owner, which is what lets an editor render findings as annotations on the
tree with no fallback presentation, and decision 5's totality is what
buys it for the :chart stage - the one stage whose findings arrive with
no idea that blocks exist.
The pipeline, and which stage produces what
| Stage | Errors it produces |
|---|---|
:document | :invalid_document - Document.validate/1's reason |
:resolve | :unknown_block_type, :block_type_too_new, :migration_failed, :composite_expansion_failed (ADR-0004's amendment of 2026-09-07, E1: StatifierBlocks.Composite.expand!/2 raises on a broken declaration and on a root expansion with no single root, and decision 1 forbids this pipeline to raise), :outcome_not_raisable (ADR-0002's Amendment of 2026-09-12, C2: a composite declared an outcome name nothing in its expansion can raise) |
:config | validate_config/1 findings, one per {key, message} pair, and the one config check that reads the datamodel document: core.on_event's declared-payload refusal (ADR-0002's amendment of 2026-09-06), reported in the same shape; and the one outcome name a root block may not declare, failed, which would take the state id ADR-0002's failure amendment (section 4 step 3) mints for the shared unhandled-failure final |
:structure | :slot_arity_violated, :undeclared_slot (ADR-0002 decision 6); assignability (ADR-0003) |
:emit | emit/2 findings, :invalid_role, :reserved_role, :invalid_outcome, :duplicate_binding (ADR-0004's foreach amendment, F6), :unspliced_child, :unknown_attribution, :conflicting_chart_use, :invalid_declaration, :duplicate_declaration (ADR-0004's host-declared-roots note), :self_reference (ADR-0004's subchart-src amendment), :sensitive_path_read (ADR-0002's secrets-rule amendment), :invalid_donedata_field (ADR-0013 decision 2) |
:chart | mapped statifier findings, both faults (decision 9) |
The pipeline stops at the first stage that produces errors and reports every error from that stage. Stopping rather than accumulating across stages is deliberate: a document with an unresolvable block type has no meaningful structural check to run, and reporting a cascade of consequences beside the cause is how an error panel becomes noise. Within a stage every finding is reported, because those are siblings rather than consequences.
The :document stage is not in decision 10's table, and it is not a new
decision either: decision 1 requires compile/3 never to raise, and
Document.to_json/1 raises on a document that fails ADR-0001's
structural rules. Checking first and reporting it as a finding is what
totality costs. It runs before :resolve because a document that is not
structurally a document has no blocks worth resolving.
fault: whose problem is this?
Decision 9 states the split for the :chart stage, where it is subtle
because the finding was raised against generated SCXML by a validator
that has never heard of blocks. The rule generalizes, and this module
applies the generalized form at every stage:
:author- a document edit fixes it. Every:configfinding, every:structurefinding (an author placed the block), and every:chartfinding whose owning span carries a config key.:package- a bug in this package or in a host's block type, and no edit to the document will help.:resolvefindings (the palette is the host's, not the author's), every:emitfinding that names no config key, and every:chartfinding whose owning span carries no config key: an author cannot express{:unresolved_target, id}, because the block vocabulary has no way to name a state id.
The editor renders the two differently, and "this cannot be fixed here" is the only honest message for the second.
severity
:error fails the compile; :warning rides on
StatifierBlocks.Compiled's warnings and does not. Warnings come from
three places:
- Upstream: every warning
Machine.warnings/1surfaces (st-ADR-0033), mapped at the:chartstage. - Decision 8's optional invoke-type lint, also
:chart. Decision 8 is explicit that the lint is never an error. - The
:emitstage's own advisories::draft_blocks_presentand:placeholder_block(ADR-0004's drafts amendment, D4),:shadowed_document_root(ADR-0001 decision 11f) and:deadline_lost_on_resume(ADR-0010's Note of 2026-09-02).
The emit advisories are deliberately absent from the stage table above, whose column is "Errors it produces".
[Correction 2026-09-02, sb-mg8v: this paragraph read "Upstream warnings
(st-ADR-0033) and decision 8's optional invoke-type lint are the two
sources of warnings". The :emit stage has raised advisories of its own
since then, so the count was wrong rather than the reasoning.]
config_value_span
Decision 9's last refinement, and the only field here that is about a
position inside a config value rather than about which config value.
It is nil on almost every finding, and every consumer must treat it
that way: config_key alone still answers "which field", and the span
only ever narrows an underline within that field.
StatifierBlocks.Compiler.Chart is the only producer, and its
moduledoc owns the criterion for when a finding gets one.
Summary
Types
Where inside the author's own config value the finding actually is: byte offsets into that value, 0-based, exclusive end.
Whose problem this is. See the moduledoc.
The pipeline stage that produced this finding.
Functions
reason's stable tag: the tuple's first element, or the reason itself
when it is already an atom.
Builds a finding.
Types
@type config_value_span() :: {non_neg_integer(), non_neg_integer()}
Where inside the author's own config value the finding actually is: byte offsets into that value, 0-based, exclusive end.
Decision 9's last refinement (StatifierBlocks.Compiler.Chart composes
it, and its moduledoc owns the criterion). nil on every finding that
is not a chart-stage content finding carrying a sub-expression span,
which is the overwhelming majority - a consumer with nothing to
underline falls back to the whole field, exactly as it did before this
field existed.
The start offset is the one decision 9 names; the end is what lets an editor underline the offending sub-expression rather than only put a caret in front of it.
@type fault() :: :package | :author
Whose problem this is. See the moduledoc.
@type stage() :: :document | :resolve | :config | :structure | :emit | :chart
The pipeline stage that produced this finding.
@type t() :: %StatifierBlocks.Compiler.Finding{ block_id: StatifierBlocks.Block.id() | nil, code: atom(), config_key: String.t() | nil, config_value_span: config_value_span() | nil, fault: fault(), message: String.t(), path: StatifierBlocks.Document.path() | nil, reason: term(), severity: :error | :warning, stage: stage() }
Functions
reason's stable tag: the tuple's first element, or the reason itself
when it is already an atom.
Builds a finding.
opts carries :block_id, :path, :config_key,
:config_value_span, :severity, :fault and :code. code
defaults to reason's own tag, which is the
stable atom an editor switches on while reason keeps carrying the
offending ids as data - the same split
Statifier.Validator.Error.code/1 makes upstream. fault defaults to
the stage's own rule, refined by whether a config key is present.