The two placement facts io/1 cannot carry, and the identity every other
surface asks about the shelf and the gap marker.
ADR-0002's amendment of 2026-08-31, section G12, states the two facts and
campaign-024 ruling R-b puts their enforcement in the compiler's
Structure stage; ADR-0004's amendment of the same date, section D3, names
the two codes. This module owns both, and owns nothing else: kind
admission, slot arity and the data-flow walk are unchanged and are still
StatifierBlocks.Assignability's and StatifierBlocks.SlotValidation's.
Why this module is small on purpose
:draft_shelf does the placement work. Every slot in the shipped core.*
vocabulary accepts [:step] or [:interrupt_handler], so a block
declaring only that kind is refused by all of them through the
intersection ADR-0003 decision 3 already performs, with no new rule, no
per-type list, and nothing in this module. What is left over is exactly
two things the intersection cannot express, and they are the only two
rules here:
| Finding | Fact |
|---|---|
:drafts_block_misplaced | a shelf is a direct child of the root block's body slot and nowhere else - a constraint on depth, which is a property of the document rather than of either block (G12a) |
:duplicate_drafts_block | a document carries at most one shelf - cardinality across a document, closest in shape to ADR-0001 decision 3's document-unique ids and checked the same way (G12b) |
Both refuse documents that would otherwise be admitted and neither changes the meaning of a document that is already valid, so relaxing either later is additive (G12c).
Why the type name and not the module
Every predicate here matches Block.type, the string ADR-0002 decision 1
puts in the document, rather than resolving a palette entry and comparing
modules. The record says "a core.drafts block" throughout and core. is
the reserved namespace decision 4 provides for, so the name is what is
being talked about; a name test needs no palette, cannot disagree with one,
and leaves the compiler and the editor with no dependency on a core type
module. A host that wants its own shelf mints its own kind, its own
container and its own structure rule, which ADR-0003's amendment of this
date already records as the intended path.
The second block, not the first
:duplicate_drafts_block names the second and every later shelf in
document order. The first is the one the author almost certainly means to
keep, and a finding on it would ask them to fix the block that is not the
problem - the same reasoning ADR-0001 decision 11f applies to a shadowed
root, arriving at a different rule for the same reason (D3).
Summary
Types
A placement violation, in the tuple shape StatifierBlocks.Compiler
adapts into a :structure finding. The first element is the code
ADR-0004's amendment of 2026-08-31, section D3, names.
Functions
The type name a shelf is stored under.
Whether this block is a gap marker.
Whether this type name is the gap marker's.
The type name a gap marker is stored under.
Whether {parent_id, slot} is the one position a shelf is admitted in:
the root block's body slot (G12a).
Whether this block is the shelf.
Whether this type name is the shelf's.
Every shelf in document, in Document.blocks/1 pre-order.
Every placement violation in document, in document order. :ok when
there are none.
Types
@type finding() :: {:drafts_block_misplaced, StatifierBlocks.Block.id()} | {:duplicate_drafts_block, StatifierBlocks.Block.id()}
A placement violation, in the tuple shape StatifierBlocks.Compiler
adapts into a :structure finding. The first element is the code
ADR-0004's amendment of 2026-08-31, section D3, names.
Functions
@spec drafts_type() :: StatifierBlocks.Block.type_name()
The type name a shelf is stored under.
@spec marker?(StatifierBlocks.Block.t()) :: boolean()
Whether this block is a gap marker.
@spec marker_type?(StatifierBlocks.Block.type_name()) :: boolean()
Whether this type name is the gap marker's.
@spec placeholder_type() :: StatifierBlocks.Block.type_name()
The type name a gap marker is stored under.
@spec root_body?( StatifierBlocks.Document.t(), StatifierBlocks.Block.id(), StatifierBlocks.Block.slot_name() ) :: boolean()
Whether {parent_id, slot} is the one position a shelf is admitted in:
the root block's body slot (G12a).
This is the admission half of the depth rule, and it has to exist as
well as the refusal half. Every container in the shipped vocabulary
declares slot_accepts [:step] or [:interrupt_handler], the root
included, so the ordinary intersection refuses a :draft_shelf
everywhere - at the one position the record admits it as well as at every
position it does not. G12a is what says the root's body admits one
anyway, and StatifierBlocks.Assignability consults this to say so.
Reaching it through assignability rather than through a filter on the
compiler's findings is deliberate: valid_targets/4 is the same
decision, so an author dragging a shelf onto the root's body is offered
the position for the same reason a shelf already there does not refuse.
Two answers would be two chances to disagree.
@spec shelf?(StatifierBlocks.Block.t()) :: boolean()
Whether this block is the shelf.
@spec shelf_type?(StatifierBlocks.Block.type_name()) :: boolean()
Whether this type name is the shelf's.
@spec shelves(StatifierBlocks.Document.t()) :: [StatifierBlocks.Block.t()]
Every shelf in document, in Document.blocks/1 pre-order.
A document is well-formed with none and with exactly one; more than one
is validate/1's business, not this function's.
@spec validate(StatifierBlocks.Document.t()) :: :ok | {:error, [finding()]}
Every placement violation in document, in document order. :ok when
there are none.
The two rules are independent and both are reported: a second shelf that is also nested inside a group carries both codes, which is ADR-0004 decision 10's "within a stage every finding is reported" - the two are siblings rather than one being a consequence of the other.