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

May this block land in this slot? Answered by two independent gates -
structural admission by kind tag (ADR-0003 decision 3, untouched), and
data flow by the environment at the position, checked with the datamodel
document's own read check plus the palette's host relation last
(ADR-0011).

One implementation, consulted by both the editor and the compiler, because
both are passed the same palette.

## What the data-flow gate is a gate on

Nothing flows between adjacent blocks. ADR-0011 supersedes ADR-0003's seam:
a block declares the datamodel paths it reads and writes, and
`StatifierBlocks.Environment` carries a map from path to type through the
document in pre-order. The question at a position is whether the
environment there satisfies what the block reads, and the answer comes from
`StatifierDatamodel.Types.satisfies/3` - the one read check, defined in the
package that owns the document. This package defines no second one, and no
`Compatibility` or `Coverage` module of its own.

`consumes` and `produces` survive as sugar over the document's **subject
path**, which the entry block's palette entry names (decision 6), so a
palette that declared them against ADR-0003 keeps saying what it said.

## The contract

The five functions ADR-0003's "The relation as typespecs" section named
keep their arities, and two of them keep their arities while changing what
they are computed from:

    @spec check(Palette.t(), Document.t(), target(), Block.t(), context()) ::
            :ok | {:error, [finding()]}

    @spec valid_targets(Palette.t(), Document.t(), Block.t(), context()) :: [target()]

    @spec validate(Palette.t(), Document.t(), context()) :: :ok | {:error, [finding()]}

    @spec inbound_type(Palette.t(), Document.t(), target(), context()) ::
            type_expr() | :unknown

    @spec assignable?(Palette.t(), type_expr() | :unknown, type_expr() | :unknown) ::
            boolean()

`inbound_type/4` is now the environment's type **at the subject path**, and
answers `:unknown` for a document with no subject. `assignable?/3` keeps
ADR-0003 decision 6's ordering property - the host relation is consulted
only after the check has already failed, so it can only widen - with the
floor raised: the record-into-shape coverage step runs before the host is
asked, per ADR-0011 decision 3's narrowing of decision 6.

## The deliberate widening

This module ships more public functions than the records list, and none of
them is a second implementation of the relation: `io/2`, `kinds/2`,
`slot_accepts/3` and `admits?/3` are the structural gate's core-only
primitives, `kinds/3`, `slot_accepts/4` and `admits?/4` are the same three
questions asked with a palette in hand (ADR-0002's Note of 2026-09-08,
item 1),
`produces/4` resolves a declared `produces` including `{:passthrough,
slot}`, and `seam_reason/4`, `finding_reason/2`, `seam_reasons/3` and
`target_verdicts/4` read verdicts the deciding functions have already
reached and label or collect them.

Defaults, per ADR-0003 decision 5 (restated by ADR-0011 decision 3 as
`sd-ADR-0001`'s): an absent `io/1` callback, or a module that is not
loadable, is `%{}`; an absent `:kinds` key is `[:step]`; an absent
`:slot_accepts` entry for a given slot is `:any`; `:unknown` is permissive
in both directions.

# `context`

```elixir
@type context() :: StatifierBlocks.Environment.context()
```

Caller-supplied, not stored in the document. `StatifierBlocks.Environment`
reads both keys: `:datamodel` is the datamodel document the type
declarations come from, and `:entry_type` seeds the subject path.

# `finding`

```elixir
@type finding() ::
  {:kind_not_admitted, StatifierBlocks.Block.id(), StatifierBlocks.Block.id(),
   StatifierBlocks.Block.slot_name(), [kind()], [kind()] | :any}
  | {:type_mismatch, StatifierBlocks.Block.id(), upstream_ref(),
     type_expr() | :unknown, type_expr() | :unknown, String.t()}
```

ADR-0003 decision 8's vocabulary, with the one member ADR-0011 decision 8
adds: a `:type_mismatch` carries the **datamodel path** the read was
checked at, last. It is added rather than left to be re-derived because a
block may carry several read signatures on several paths, so a message
saying which two types disagreed without saying where is one an author
cannot act on. `:kind_not_admitted` gains none - a structural refusal is
about a slot and has no path to name.

# `io`

```elixir
@type io() :: %{
  optional(:kinds) =&gt; [kind()],
  optional(:consumes) =&gt; type_expr() | :unknown,
  optional(:produces) =&gt; produces(),
  optional(:slot_accepts) =&gt; %{
    optional(StatifierBlocks.Block.slot_name()) =&gt; [kind()] | :any
  }
}
```

The return shape of `c:StatifierBlocks.BlockType.io/1`. Every key optional.

# `kind`

```elixir
@type kind() :: atom()
```

Structural tag. `:step` and `:interrupt_handler` ship here; hosts may mint more.

# `produces`

```elixir
@type produces() ::
  type_expr() | :unknown | {:passthrough, StatifierBlocks.Block.slot_name()}
```

What a block declares it produces. See ADR-0003 decision 4.

# `reason`

```elixir
@type reason() ::
  :source_untyped
  | :target_untyped
  | :both_untyped
  | {:shape_not_satisfied, [String.t()]}
  | :not_assignable
  | {:fixable_by, StatifierBlocks.Block.id()}
```

Why a read came out the way it did (the 2026-08-29 amendment to ADR-0003
decision 8, with the arm ADR-0011 decision 8 adds). A reason **explains a
verdict; it never changes one** - see `seam_reason/4`.

  * `:both_untyped` - neither side declared a type, so the read was
    admitted without checking anything.
  * `:source_untyped` - the environment holds `:unknown` there and the read
    declared a type. Admitted, unchecked.
  * `:target_untyped` - the read declared `:unknown` and the environment
    holds a type. Admitted, unchecked.
  * `{:shape_not_satisfied, missing}` - the environment holds a record at
    the path, the read expects a shape, and the record does not cover the
    shape's required set. `missing` names the required fields it does not
    cover, in the shape's own field order, so a message can say which are
    absent without re-deriving them.
  * `:not_assignable` - refused: both sides typed, identity failed,
    coverage did not apply or did not hold, and the palette's relation did
    not widen. Nothing names a block to go and look at.
  * `{:fixable_by, block_id}` - the same refusal, where a block's write
    signature is what put the offending type at the path: `block_id` is the
    declaration an author would change.

# `target`

```elixir
@type target() ::
  {StatifierBlocks.Block.id(), StatifierBlocks.Block.slot_name(),
   non_neg_integer()}
```

A position, as ADR-0001 decision 5 defines one.

# `type_expr`

```elixir
@type type_expr() :: StatifierBlocks.Environment.type_expr()
```

A type as a declaration spells it. Opaque to this package - never parsed,
split or normalized here; `StatifierDatamodel.Types.parse/2` reads it
against the datamodel document, a list carries its item type, and an
inline unnamed shape carries its members.

# `upstream_ref`

```elixir
@type upstream_ref() :: StatifierBlocks.Block.id() | :slot_entry | :declaration
```

What put the held type at the path a `:type_mismatch` names (ADR-0011
decision 8, amended 2026-09-06). A block id where a block's write
signature put it; `:slot_entry` where the seed's subject type - or a merge
with no single writer to name - did; `:declaration` where the datamodel
document's own declared type did.

# `admits?`

```elixir
@spec admits?(
  {module(), StatifierBlocks.Block.config()},
  StatifierBlocks.Block.slot_name(),
  {module(), StatifierBlocks.Block.config()}
) :: boolean()
```

ADR-0003 decision 3's structural verdict for placing `child` in `slot` of
`parent`: `:any` admits everything, otherwise the slot's accepted kinds
and the child's own kinds must intersect.

**Core-only**, for the reason `kinds/2` gives. With a palette in hand, ask
`admits?/4`.

# `admits?`

```elixir
@spec admits?(
  StatifierBlocks.Palette.t(),
  {StatifierBlocks.Palette.type_ref(), StatifierBlocks.Block.t()},
  StatifierBlocks.Block.slot_name(),
  {StatifierBlocks.Palette.type_ref(), StatifierBlocks.Block.t()}
) :: boolean()
```

`admits?/3` asked with a palette in hand: the same intersection, over
`slot_accepts/4` and `kinds/3` rather than their core-only spellings
(ADR-0002's Note of 2026-09-08, item 1).

Each side is the `{ref, block}` pair `kinds/3` takes. There is still one
implementation of decision 3's intersection; this is the spelling of it a
caller holding a palette reaches, so admission refuses on the same kinds the
compiler compiles on.

# `assignable?`

```elixir
@spec assignable?(
  StatifierBlocks.Palette.t(),
  type_expr() | :unknown,
  type_expr() | :unknown,
  StatifierDatamodel.Declarations.t(),
  keyword()
) :: boolean()
```

May a value the environment holds as `held` be read where `expected` is
required?

ADR-0003 decision 6's ordered relation as ADR-0011 decision 3 narrows it.
The order is the contract:

  1. `StatifierDatamodel.Types.satisfies/3` - either side unknown, then
     identity, then a record covering a shape's required set;
  2. otherwise, and only otherwise, `palette.assignability`'s
     `assignable?/2`;
  3. a palette with no relation, or a module that is not loadable or does
     not export the callback, answers `false` - the floor a host cannot
     lower.

Reflexivity holds regardless of the host, because step 1 decides identity
before step 2 is reached, so the host callback can only widen the relation
and never narrow it. The floor is **higher** than ADR-0003 left it: a
record read as a shape it covers is admitted before the host is asked, so a
host that was widening records into shapes by hand can delete that half of
its module.

`declarations` is `StatifierDatamodel.Declarations.from_document/1`'s index
over the datamodel document's `types` key. With none - which is what every
caller that has no datamodel to hand passes - the check reduces to unknown
and identity, which is exactly what it decided before there were
declarations to read.

`opts` carries the strict switch and nothing else. `strict: true` - the
default is `false` - refuses a pair either side of which resolves to
`:unknown`: step 1's first decision answers `false` where it otherwise
answers `true`, and the host is **not** asked, because a host relation has
never been offered an unknown side and this option is not the place to
start offering it. No other step moves, so the strict relation is a subset
of the default one and reflexivity still holds for every typed pair.

The option is opt-in so that the floor stays exactly where every existing
caller found it; it exists for the caller that must not admit a value
nothing has typed - a pre-fill reading a datamodel document that declares
neither the record nor the shape it names (ADR-0002's Note of 2026-09-08,
item 4).

# `check`

```elixir
@spec check(
  StatifierBlocks.Palette.t(),
  StatifierBlocks.Document.t(),
  target(),
  StatifierBlocks.Block.t(),
  context()
) :: :ok | {:error, [finding()]}
```

ADR-0003 decision 7's one decision function: kind admission for
`candidate` at `target`, plus the reads whose verdict a placement at
`target` can change.

`candidate` not yet in `document` (`Document.fetch_path/2` returns
`:error`) is an **insert**: `candidate`'s own reads against the environment
at `target`, and the reads of whatever block currently sits at `target`'s
index against that environment with `candidate`'s writes applied (nothing
to check when the slot ends there).

`candidate` already in `document` is a **move**: the same two, plus what it
vacates - at its current position `{p, s, i}`, removing it makes `i - 1`
and `i + 1` adjacent, so the block at `i + 1` is checked against the
environment as it stood at `i`, which is the environment without
`candidate`'s writes (nothing to check when the slot has no `i + 1`).

Findings are decision 8's vocabulary verbatim, in this order when more than
one applies: kind admission first, then the reads at the insertion point,
then the vacated ones. A block carrying several read signatures contributes
one finding per unsatisfied read, each naming its own path, because the
signatures are independent (ADR-0011 decision 2). `:ok` when the list is
empty.

Degradation, per decision 5: a block that fails `Palette.resolve/2` - the
candidate, the parent, or any block involved - contributes the permissive
default rather than a finding.

# `context`

```elixir
@spec context(%{optional(:datamodel) =&gt; map() | nil, optional(any()) =&gt; any()}) ::
  context()
```

The context every data-flow question about a host's document is asked
with: the host's datamodel document, and nothing else.

`:entry_type` is deliberately absent. A surface embedding the editor is
not told what enters the document, and ADR-0011 decision 2 seeds empty in
that case - so this is one key, named once, and the drop check, the
environment walk and a datamodel view cannot drift apart by being handed
different ones.

Takes any map carrying a `:datamodel` key, which is the shape a host's
own assigns already have; `nil` there yields the empty context.

## Examples

    iex> StatifierBlocks.Assignability.context(%{datamodel: nil})
    %{}
    iex> StatifierBlocks.Assignability.context(%{datamodel: %{"types" => []}})
    %{datamodel: %{"types" => []}}

# `finding_reason`

```elixir
@spec finding_reason(
  StatifierBlocks.Palette.t(),
  finding(),
  StatifierDatamodel.Declarations.t()
) ::
  reason() | nil
```

The reason for one finding, derived rather than stored.

A `:type_mismatch` carries its producing ref, the held type, the expected
type and the path already, so its reason is `seam_reason/5` applied to what
the tuple holds. A reason kept *in* the finding would be a second copy of a
verdict the tuple plus the palette already determine, free to disagree with
it; derived, it cannot.

`:kind_not_admitted` answers `nil`: the structural gate's reason is its own
finding code, and this vocabulary is the data-flow gate's.

A caller with no datamodel document to hand gets the classification the
declarations-free check reaches, which is the one this package reached
before there were declarations at all.

# `inbound_type`

```elixir
@spec inbound_type(
  StatifierBlocks.Palette.t(),
  StatifierBlocks.Document.t(),
  target(),
  context()
) ::
  type_expr() | :unknown
```

The type flowing into `target`: the environment's type at the document's
**subject path**, at that position (ADR-0011 decisions 1 and 6).

ADR-0003 decision 4 computed this by walking to the previous sibling's
resolved `produces`, and ADR-0011 replaces the seam rather than the
question. A document with no subject path - no entry block, or an entry
block whose palette entry declares no `subject:` - has nothing flowing into
any position and answers `:unknown` everywhere, which is the permissive
default an untyped palette has always got.

Total: a `parent_id` no block in `document` carries, or an `index` past the
end of the slot's children, resolves to `:unknown` rather than raising.

Inside a `core.drafts` shelf the answer is `:unknown` at every index.
ADR-0003's amendment of 2026-08-31, section A2, is why, and ADR-0011
decision 1 carries it forward in its own vocabulary: the walk does not
enter the shelf, and each parked fragment is walked from an **empty**
environment, so a fragment reads nothing as known because nothing put it
there. The walk *inside* each fragment is untouched - a parked fragment is
a subtree whose own writes and reads are real, and they are checked exactly
as they would be anywhere else.

# `io`

```elixir
@spec io(StatifierBlocks.Palette.type_ref(), StatifierBlocks.Block.config()) :: io()
```

`module.io(config)`, or `%{}` when `io/1` is absent or `module` is not
loadable (ADR-0003 decision 5). Checked with `Code.ensure_loaded?/1` plus
`function_exported?/3`, the pattern `StatifierBlocks.Palette.resolve/2`
already uses.

# `kinds`

```elixir
@spec kinds(module(), StatifierBlocks.Block.config()) :: [kind()]
```

The block's `kinds`, defaulting to `[:step]` (ADR-0003 decision 5).

**Core-only.** This reads `io/2`, which is the `c:StatifierBlocks.BlockType.io/1`
callback, and a composite's derivation of that callback resolves its members
through `StatifierBlocks.Palette.core/0` alone (`StatifierBlocks.Composite`,
under "The callbacks are core-only, and a reader with a palette is not"). A
caller that holds a palette wants `kinds/3` instead: ADR-0002's Note of
2026-09-08, item 1, is the reason, and no palette argument is added to the
callback itself.

# `kinds`

```elixir
@spec kinds(
  StatifierBlocks.Palette.t(),
  StatifierBlocks.Palette.type_ref(),
  StatifierBlocks.Block.t()
) ::
  [kind()]
```

`kinds/2` asked with a palette in hand: `block`'s kinds with a composite's
members resolved through `palette`, exactly as `produces/4` resolves them
(ADR-0002's Note of 2026-09-08, item 1).

`ref` is `block`'s own palette entry and `block` the block as `resolve/2`
answered it, which is the pair `produces/4` already carries. A type that is
not a composite reads the same `io/1` callback it always did, so this
differs from `kinds/2` only where the difference is the point: a composite
whose expansion is rooted at a host type answers on the host palette's kinds
rather than falling back to `[:step]`.

# `produces`

```elixir
@spec produces(
  StatifierBlocks.Palette.t(),
  StatifierBlocks.Document.t(),
  StatifierBlocks.Block.t(),
  context()
) :: type_expr() | :unknown
```

`block`'s declared `produces`, resolved: a type expression resolves to
itself; `:unknown` resolves to `:unknown`; `{:passthrough, slot}` resolves
to the resolved `produces` of the last block in `slot`, or, when `slot` is
empty (or is not a slot `block` declares), `block`'s own inbound type.

A block that fails `Palette.resolve/2` contributes `:unknown` rather than a
finding (ADR-0003 decision 5's degradation rule).

Under ADR-0011 nothing in the data-flow gate consults this: a passthrough
container passes types through by carrying its slot's own writes out
through the merge, which is the environment doing it rather than a
declaration claiming it. It is kept because it reads a declaration a
palette may still carry, and reading a declaration is not the same as
deciding with it.

### Why this terminates

Every recursive step lands at a strictly earlier position in the document's
pre-order than the position that made the original call. Resolving
`{:passthrough, slot}` on block `B` either descends to the last child of
`B`'s own `slot` - later than `B`, but still strictly before whatever
position asked for `B`'s `produces`, since that position comes after `B`
and every descendant of `B` is ordered before `B`'s next sibling - or,
when the slot is empty, falls back to `B`'s own inbound type, which is
`B`'s previous sibling or, at index 0, `B`'s parent. Pre-order rank over a
finite tree is a well-founded measure with no infinite descending chain, so
this cannot cycle even for a tree of nothing but empty passthrough
sequences.

# `seam_reason`

```elixir
@spec seam_reason(
  StatifierBlocks.Palette.t(),
  type_expr() | :unknown,
  type_expr() | :unknown,
  upstream_ref(),
  StatifierDatamodel.Declarations.t()
) :: reason() | nil
```

Why the read of `expected` against a `held` type came out the way it did.

`producing_ref` is the block whose write signature put `held` at the path,
exactly as ADR-0011 decision 8's `:type_mismatch` tuple names it, or
`:slot_entry` when the seed - or a merge with no single writer to name -
is what the read disagrees with, or `:declaration` when the datamodel
document's own declared type is.

`nil` means **there is nothing to explain**: both sides are typed and the
read passed, by identity, by coverage, or because the palette's relation
widened it. Every other outcome names itself. The classification is total
and follows the read check's own order:

  1. both sides `:unknown` -> `:both_untyped`;
  2. `held` `:unknown` -> `:source_untyped`;
  3. `expected` `:unknown` -> `:target_untyped`;
  4. satisfied, or widened by the host -> `nil`;
  5. a record that does not cover the shape -> `{:shape_not_satisfied,
     missing}`;
  6. `producing_ref` is a block id -> `{:fixable_by, producing_ref}`;
  7. otherwise -> `:not_assignable`.

  Step 6 is why the two non-block refs land on step 7: neither
  `:slot_entry` nor `:declaration` names a block whose declaration an
  author would change, and for a `:declaration` the change is to the
  datamodel document rather than to anything in this one.

**This function decides nothing.** It reads a verdict `assignable?/4` has
already reached and labels it; nothing in this package branches a verdict
on what it returns, so the reason vocabulary stays strictly explanatory.
The first three arms sit on reads the permissive default *admitted*, and
adding them cannot refuse anything.

The two ADR-0003 refusing arms still differ only in whether the author has
somewhere to go, and under ADR-0011 that block is found by name rather than
by adjacency: it is the block whose write signature put the type at the
path, which is a more useful answer than the sibling ADR-0003 could name.

Structural refusals carry no reason from this vocabulary.
`{:kind_not_admitted, ...}` already names both kind sets in the finding
itself, so `finding_reason/2` answers `nil` for it rather than duplicating
it here.

# `seam_reasons`

```elixir
@spec seam_reasons(
  StatifierBlocks.Palette.t(),
  StatifierBlocks.Document.t(),
  context()
) :: [
  {StatifierBlocks.Block.id(), upstream_ref(), reason()}
]
```

Every read in `document` that has something to say about itself, in
`Document.blocks/1`'s pre-order: `{reading_block_id, producing_ref,
reason}`.

This is the queryable form of the cost ADR-0003's consequences state in
prose - "a partially typed palette permits what a fully typed one would
catch". The three untyped arms name exactly those reads, and they are the
reason this vocabulary is not merely a refusal vocabulary: a host auditing
its own palette's coverage asks here, gets back the reads that passed
without being checked, and declares the signatures that produced them.

A block declaring no read at all still contributes a row, against the
environment's type at the subject path - the block was reached without
saying anything about what it needs, which is precisely the coverage gap
this function exists to surface.

It changes no verdict and is not part of validation. Reads with nothing to
explain (typed on both sides and passing) are omitted rather than listed
with a `nil`.

# `slot_accepts`

```elixir
@spec slot_accepts(
  module(),
  StatifierBlocks.Block.config(),
  StatifierBlocks.Block.slot_name()
) ::
  [kind()] | :any
```

The accepted kinds for `slot` on `module`, defaulting to `:any` when the
slot has no entry in `io/1`'s `:slot_accepts` map (ADR-0003 decision 5).

**Core-only**, for the reason `kinds/2` gives. With a palette in hand, ask
`slot_accepts/4`.

# `slot_accepts`

```elixir
@spec slot_accepts(
  StatifierBlocks.Palette.t(),
  StatifierBlocks.Palette.type_ref(),
  StatifierBlocks.Block.t(),
  StatifierBlocks.Block.slot_name()
) :: [kind()] | :any
```

`slot_accepts/3` asked with a palette in hand, on the `{ref, block}` pair
`kinds/3` takes (ADR-0002's Note of 2026-09-08, item 1).

# `target_verdicts`

```elixir
@spec target_verdicts(
  StatifierBlocks.Palette.t(),
  StatifierBlocks.Document.t(),
  StatifierBlocks.Block.t(),
  context()
) :: [{target(), :ok | {:error, [finding()]}}]
```

Every position `valid_targets/4` enumerates, each paired with `check/5`'s
full verdict rather than filtered down to the accepting ones.

`valid_targets/4` is defined as this function keeping the `:ok` rows, so
there is one enumeration and one decision, not two. It is exposed for the
same reason the moduledoc's other widenings are: a caller that has to say
*why* a position was refused - `StatifierBlocks.Edit.Targets`, which
projects positions to slots and needs a reason for a slot it darkens -
must not re-derive the position set, because a second enumeration is a
second answer waiting to drift from this one.

Same order and the same determinism guarantee `valid_targets/4`
documents.

# `valid_targets`

```elixir
@spec valid_targets(
  StatifierBlocks.Palette.t(),
  StatifierBlocks.Document.t(),
  StatifierBlocks.Block.t(),
  context()
) :: [target()]
```

Every position in `document` where `candidate` may be dropped, per
ADR-0003's `valid_targets/4`: for every block in `Document.blocks/1`, for
every slot that block's module **declares** (`module.slots(config)`, not
the slot keys the stored document happens to carry), for every index from
`0` to that slot's current child count inclusive, the position is kept
when `check/5` returns `:ok` for it.

Deterministic: `Document.blocks/1` is already pre-order, `slots/1` is
visited in the order it declares slots, and indices ascend within each
slot - so the same call always returns the same list, in the same order.

A block whose type fails `Palette.resolve/2` contributes no positions:
there is no module to ask for a declared slot set, so this is an absence
of positions rather than a refusal.

# `validate`

```elixir
@spec validate(StatifierBlocks.Palette.t(), StatifierBlocks.Document.t(), context()) ::
  :ok | {:error, [finding()]}
```

Every finding in `document`, per ADR-0003's `validate/3`: every read
already present in the document, checked with the same rules `check/5`
checks one with - the whole-document counterpart to `check/5`'s
single-position query.

Walking every block in `Document.blocks/1` other than the root (which
occupies no slot) and checking its own reads against the environment at its
position visits every read in the document exactly once.

This is deliberately **not** `check/5` called with a block already sitting
at its own current position as the candidate: `check/5`'s downstream and
vacated reads are defined relative to a `target` the candidate is being
placed at or removed from, and a block that already occupies `target`
makes both of those check the document as though the block were somewhere
it is not. `validate/3` reaches the same two checks `check/5` runs for kind
admission and the block's own reads directly.

`:ok` when the document has no findings; otherwise `{:error, findings}`
with every block's findings concatenated, in `Document.blocks/1`'s
pre-order.

A block named in `ctx[:skip_blocks]` is passed over: it produces no
finding of its own and its declared writes leave no entry in the
environment (`StatifierBlocks.Environment`'s context documents why). The
walk continues past it - its siblings and its children are checked exactly
as they would have been - so this is an absence of one block's answers
rather than a shortened walk. `check/5` reads the same key through the
environment it builds, and no caller that has not refused a config sets
one.

---

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