StatifierBlocks.Compiler.Finding (StatifierBlocks v0.22.0)

Copy Markdown View Source

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

StageErrors it produces
:document:invalid_document - Document.validate/1's reason
:resolve:unknown_block_type, :block_type_too_new, :migration_failed
:configvalidate_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
:structure:slot_arity_violated, :undeclared_slot (ADR-0002 decision 6); assignability (ADR-0003)
:emitemit/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)
:chartmapped 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 :config finding, every :structure finding (an author placed the block), and every :chart finding 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. :resolve findings (the palette is the host's, not the author's), every :emit finding that names no config key, and every :chart finding 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/1 surfaces (st-ADR-0033), mapped at the :chart stage.
  • Decision 8's optional invoke-type lint, also :chart. Decision 8 is explicit that the lint is never an error.
  • The :emit stage's own advisories: :draft_blocks_present and :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.

t()

Functions

reason's stable tag: the tuple's first element, or the reason itself when it is already an atom.

Types

config_value_span()

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

fault()

@type fault() :: :package | :author

Whose problem this is. See the moduledoc.

stage()

@type stage() :: :document | :resolve | :config | :structure | :emit | :chart

The pipeline stage that produced this finding.

t()

@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

code(reason)

@spec code(term()) :: atom()

reason's stable tag: the tuple's first element, or the reason itself when it is already an atom.

new(stage, reason, message, opts \\ [])

@spec new(stage(), term(), String.t(), keyword()) :: t()

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.