# `StatifierBlocks.Compiler.Finding`
[🔗](https://github.com/riddler/statifier_blocks/blob/v0.44.1/lib/statifier_blocks/compiler/finding.ex#L1)

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), `:outcome_declaration_invalid` (ADR-0002's Amendment of 2026-09-18, `C9d`: a composite reading its outcomes from the instance's config answered a list this block cannot be given - not a list of names, a name twice, or a name whose derived outcome slot collides with a declared pass-through slot - or raised instead of answering; the package author's fault, and the two refusals a STATIC `:outcomes` list meets as its own module compiles), `:reserved_slot_name` (a slot on a DECLARING composite's own block spelled the way the compiler spells the slot it keeps that composite's expansion members under, whose children the resolved node would replace; an author's fault), `:undeclared_slot` and `:slot_arity_violated` (the SAME reason tuples the `:structure` row carries, drawn here for the one block that stage cannot see: a composite declaring no `outcomes` is replaced by its expansion outright (ADR-0002's `C3`), so Resolve is the last stage that holds it and the slots an author wrote on it; also the author's fault), `:minted_id_collision` (ADR-0002's Amendment of 2026-09-18, `C10`: a block an author placed in a composite block's pass-through slot or in a declaring composite's derived `on_<name>` slot, at any depth, carries an id that composite's expansion mints for one of its own members, which the Document stage's id-uniqueness check cannot see because the members do not exist until Resolve; also the author's fault) |
| `: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 `: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. Every `:resolve` finding except
    `:reserved_slot_name`, `:undeclared_slot`, `:slot_arity_violated` and
    `:minted_id_collision` (the palette is the host's, not the author's;
    those four, in the table above, are about a slot key an author typed
    (`:reserved_slot_name`), the children an author placed in a slot
    (`:undeclared_slot`, `:slot_arity_violated`) and a block id an author
    typed (`:minted_id_collision`), and a document edit fixes each), 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.

# `config_value_span`

```elixir
@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`

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

Whose problem this is. See the moduledoc.

# `stage`

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

The pipeline stage that produced this finding.

# `t`

```elixir
@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()
}
```

# `code`

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

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

# `new`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
