# `StatifierBlocks.Graph`
[🔗](https://github.com/riddler/statifier_blocks/blob/v0.44.1/lib/statifier_blocks/graph.ex#L1)

The publish-time check between a parent document and the children it
names (ADR-0008's amendment of 2026-09-22).

A compile of one document cannot see the child a `core.subchart` or a
`core.map` names, and at runtime a disagreement between the two is not
refused: a child outcome the parent routes on but the child no longer
finishes with leaves the parent's conditioned arm dead, and the child's
answer falls to the parent's unconditioned arm. The document graph is the
host's, so the host is what walks it. This module ships the **pairwise**
check the walk calls, over two `StatifierBlocks.Compiled` artifacts, in
the two directions A2 names:

  * `check/2` - forward, when a parent is published: every child the
    parent names is resolved through the host's resolver and judged
    against the parent.
  * `consumers_broken/2` - reverse, when a child is republished: the
    next child artifact is judged against every parent the host says
    currently names it (A3).

Both read only `StatifierBlocks.Compiled`'s `interface` field (A5). They
hold no store, start no process and perform no IO; the resolver is the
host's function and the only IO either direction touches.

## The three checks

  1. **Every child the parent names resolves** (A4). A resolver answering
     `{:error, :not_published}` is a finding on the referencing block's
     `chart` field.
  2. **Every outcome the parent routes on is declared by the child** (A1,
     A2). A `core.subchart` routes on the outcomes its author listed, or
     on `done` when the author listed none. `error` is exempt: the
     parent routes on it whether or not the author listed it, and a
     child reports it without declaring it. Anchored on the block's
     `outcomes` field.
  3. **Every done-data key the parent reads is declared by the child**
     (A1, A2). A `core.map` reads the members its `collect_type` marks
     required; a `core.subchart` reads none. Anchored on the block's
     `collect_type` field. Only presence is checked: whether a declared
     key has the type the parent expects stays
     `StatifierBlocks.BlockType.agrees?/3`'s dormant advisory. A
     `collect_type` that is a type name the parent could not resolve has
     no keys to check: the reference records the name as `unresolved`,
     and the pair reports the read as unchecked rather than passing it.
     That is a name the parent was compiled with no `:datamodel` to
     resolve (ADR-0008's second amendment of 2026-09-22), or one the
     `:datamodel` it was compiled with does not declare (ADR-0008's
     Amendment of 2026-10-19).

The other direction of the outcome rule - a child outcome the parent has
**no** arm for, such as one a child revision adds - is not refused here.
It falls to the parent's unconditioned arm, and
`StatifierBlocks.ViewModel.outcome_findings/3` stays the editor's
`:warning` about it.

## The findings

Every finding is a `StatifierBlocks.Finding` with source `:graph`,
anchored `{:config, block_id, key}` on the parent's referencing block,
under the field its author would change. A `core.subchart` or `core.map`
that a composite's expansion placed is not a block the author's stored
document holds, so the compile records its reference under the outermost
composite block, with the expanded member's id as `expanded_from`, and a
finding about it is anchored `{:block, composite_id}` on that composite's
chrome: the field the rule names is the member's, and no field on the
composite's form is to blame (ADR-0004's Amendment of 2026-10-19). The
anchor has no document arm,
so `consumers_broken/2` returns each finding paired with the parent's
document id, and its message names that document.

Every finding is severity `:error` but one: a done-data read left
unchecked because the parent's `collect_type` names a type the parent's
compile could not resolve is a `:warning` on the block's `collect_type`
field, and its message says which of the two reasons it was: the parent
was compiled without `:datamodel`, or with one that does not declare the
name. Either way the keys are unknown rather than in disagreement, so a
publish step refusing on `:error` findings does not refuse on it; passing
a `:datamodel` that declares the name, or writing the `collect_type`
inline, lets the keys be checked.

# `resolver`

```elixir
@type resolver() :: (String.t() -&gt;
                 {:ok, StatifierBlocks.Compiled.t()} | {:error, :not_published})
```

The host's publish-time resolver: a document id to that document's
currently published artifact, or `{:error, :not_published}`. It is not
the durable handler's start-time `resolve_chart/2`, and it is the only IO
`check/2` performs. `check/2` calls it once per distinct document id the
parent names, in the order the parent first names each.

# `check`

```elixir
@spec check(StatifierBlocks.Compiled.t(), resolver()) :: [StatifierBlocks.Finding.t()]
```

The forward check (ADR-0008 amendment A2, A4): judges `parent` against
every child it names, each resolved through `resolver`.

Returns `[]` when every child resolves, declares every outcome the parent
routes on and every done-data key the parent reads, and no read is left
unchecked. Otherwise one finding, source `:graph`, per failed rule, in the
parent's document order - each an `:error` but the last kind below:

  * a child the resolver answers `{:error, :not_published}` for -
    anchored `{:config, block_id, "chart"}`;
  * an outcome the parent routes on that the child does not declare,
    `error` exempt - anchored `{:config, block_id, "outcomes"}`;
  * a done-data key the parent reads that the child does not declare -
    anchored `{:config, block_id, "collect_type"}`;
  * a `:warning`, when the child resolves and the reference's
    `collect_type` names a type the parent was compiled with no
    `:datamodel` to resolve, or one the `:datamodel` it was compiled with
    does not declare, so its keys are unchecked - anchored
    `{:config, block_id, "collect_type"}`, its message naming which.

`block_id` is always the parent's referencing block. A reference a
composite's expansion placed is recorded under the composite block and
carries `expanded_from`, and each finding about it is anchored
`{:block, block_id}` instead of on a field. A resolver answering
anything else breaks its contract, and the call raises rather than
guessing what the host meant.

# `consumers_broken`

```elixir
@spec consumers_broken(StatifierBlocks.Compiled.t(), [StatifierBlocks.Compiled.t()]) ::
  [
    {String.t(), StatifierBlocks.Finding.t()}
  ]
```

The reverse check (ADR-0008 amendment A2, A3): judges `child_next`, the
child revision about to be published, against each of `parents`, the
published artifacts the host says currently name that child.

The child's document id is `child_next.record.document_id`, and only a
parent's references to that id are judged, so a parent that names other
documents only contributes nothing. Returns `[]` when every such parent
still finds every outcome it routes on and every done-data key it reads
declared by `child_next`, and no read is left unchecked; otherwise one
`{parent_document_id, finding}` pair per failed rule, parents in the
order given and each parent's findings in its document order. Each
finding is source `:graph`, anchored on the parent's referencing block -
`{:config, block_id, "outcomes"}` for an outcome, `{:config, block_id,
"collect_type"}` for a done-data key, or `{:block, block_id}` for a
reference a composite's expansion placed - and its message names the
parent document. Each is severity `:error`, except a `:warning` on
`"collect_type"` for a reference whose `collect_type` names a type the
parent was compiled with no `:datamodel` to resolve, or one the
`:datamodel` it was compiled with does not declare, so its keys are
unchecked. The resolver plays no part: both sides are in hand.

---

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