StatifierBlocks.Edit.Targets (StatifierBlocks v0.1.0)

Copy Markdown View Source

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/4

Decision 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:

  1. 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: an interrupts slot does not accept a step, at any index.
  2. 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.
  3. 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_mismatch finding; 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 in module.slots(config) is :exactly_one or :zero_or_one and whose current child count (as document stores 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} where b is the dragged block itself or a descendant of it. Computed once as a MapSet of ids walked from the dragged block the same way Document.blocks/1 walks 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

droppable_slots(document, palette, id)

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.

droppable_slots_for(document, palette, block)

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.