Drop-target enumeration (ADR-0005 decision 5): which slots would accept a dragged block, at slot granularity rather than gap granularity.
The stated reduction, from ADR-0003 positions to ADR-0005 slots
StatifierBlocks.Assignability.valid_targets/4 answers a finer question
than this module needs: every {block_id, slot_name, index} position
where a candidate may land. ADR-0005 decision 5 wants every {block_id, slot_name} slot that has at least one such position. This module -
concretely, droppable_slots/3 - bridges exactly those two functions, and
the bridge is existential quantification over the index:
{b, s} is droppable <=> exists i such that {b, s, i} is in valid_targets/4Decision 5's own text says "nothing in that list depends on the index
within the slot," and that sentence is literally true of rules 1, 3 and 4
but not of rule 2: Assignability.check/5 consults an upstream seam
and a downstream seam, both of which read the neighbours at index - 1
and index, so both move with the index. (The third seam check/5
consults, the vacated one, depends on the candidate's own current
position rather than on the target index, so it is constant across a
slot's gaps and is not part of what the reduction has to answer for.) The
reduction is sound anyway, for three reasons:
- The index-free half of rule 2 is preserved exactly. Kind
admission -
Assignability.admits?/3, ADR-0003 decision 3's structural gate - is a function of{parent module, parent config, slot, child kinds}with no index in it. A slot that fails kind admission fails it at every index, so existential quantification drops the whole slot, which is the same verdict a per-slot rule would give. This is the half rule 2 is really about: aninterruptsslot does not accept a step, at any index. - The index-dependent half is a seam check, and seams are validation, not admission. Decision 5 says the editor never blocks an edit for a validation reason outside its four rules, and ADR-0002 decision 6 already establishes that a document mid-edit is allowed to be invalid. A type mismatch at one gap of an otherwise acceptable slot is exactly that kind of finding, not a reason to darken the whole slot.
- The reduction is an over-approximation at gap granularity, never
an under-approximation. Highlighting a slot when at least one of
its gaps accepts the block can offer the author a gap that would
produce a
:type_mismatchfinding; it can never hide a gap that would have been clean. The failure mode is a finding the author can see and fix, not a legal arrangement they cannot reach.
The residue is real. Per-slot highlighting is a superset of per-position validity, and a drop at a particular gap may still produce an assignability finding. That is the documented cost of decision 5's per-slot granularity, not a defect introduced here.
droppable_slots/3's signature carries no Assignability.context(), so
this module calls valid_targets/4 with %{} - entry_type absent,
which resolves to :unknown, which ADR-0003 decision 5 makes the
permissive default.
Rules 3 and 4, which are this bead's alone
valid_targets/4 already enforces rules 1 and 2 (declared slots only,
kind admission). It does not filter by room and does not exclude the
candidate's own subtree - those two are added here, after the projection
to slots:
- Rule 3, room. Drop any
{b, s}whose arity inmodule.slots(config)is:exactly_oneor:zero_or_oneand whose current child count (asdocumentstores it) is 1 or more. A block already occupying such a slot excludes that slot for itself too; that only forbids moving a block to the position it already holds, which costs nothing. - Rule 4, subtree. Drop any
{b, s}wherebis the dragged block itself or a descendant of it. Computed once as aMapSetof ids walked from the dragged block the same wayDocument.blocks/1walks a document, so the filter is a membership test rather than a repeated path walk.
The droppable_slots_for/3 widening
droppable_slots/3 takes a Block.id(), per the record; a block id that
names nothing in document yields []. ADR-0005 decision 8 requires the
palette's "+" button to filter using "the same predicate," and a palette
insert has no block in the document yet to name by id - so this module
also exports droppable_slots_for/3, taking a %Block{} that need not
be in document at all. droppable_slots/3 is implemented as a lookup
followed by a call to droppable_slots_for/3: there is one
implementation, not two. This is the third documented widening this
module carries, alongside the two argued above.
Summary
Functions
Slots that would accept id's block: declared, kind-admitted (rule 1 and
the index-free half of rule 2), with room (rule 3), and outside the
block's own subtree (rule 4). Per-slot, not per-gap - see the moduledoc
for the reduction this projects from.
The same predicate as droppable_slots/3, taking a %Block{} directly
so a block not yet in document - the palette's "+" button, ADR-0005
decision 8 - can be asked the same question. droppable_slots/3 is a
lookup followed by a call to this function.
Functions
@spec droppable_slots( StatifierBlocks.Document.t(), StatifierBlocks.Palette.t(), StatifierBlocks.Block.id() ) :: [{StatifierBlocks.Block.id(), StatifierBlocks.Block.slot_name()}]
Slots that would accept id's block: declared, kind-admitted (rule 1 and
the index-free half of rule 2), with room (rule 3), and outside the
block's own subtree (rule 4). Per-slot, not per-gap - see the moduledoc
for the reduction this projects from.
id naming no block in document yields []: there is no block to drag,
so there is nothing to ask droppable_slots_for/3 about.
@spec droppable_slots_for( StatifierBlocks.Document.t(), StatifierBlocks.Palette.t(), StatifierBlocks.Block.t() ) :: [{StatifierBlocks.Block.id(), StatifierBlocks.Block.slot_name()}]
The same predicate as droppable_slots/3, taking a %Block{} directly
so a block not yet in document - the palette's "+" button, ADR-0005
decision 8 - can be asked the same question. droppable_slots/3 is a
lookup followed by a call to this function.