StatifierBlocks.Assignability (StatifierBlocks v0.27.0)

Copy Markdown View Source

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.

Summary

Types

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.

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.

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

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

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

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.

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

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.

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.

Functions

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.

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).

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

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.

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

The reason for one finding, derived rather than stored.

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

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.

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

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).

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.

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

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

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).

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).

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

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.

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.

Types

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()

@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()

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

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

kind()

@type kind() :: atom()

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

produces()

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

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

reason()

@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()

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

type_expr()

@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()

@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.

Functions

admits?(arg1, slot, arg2)

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?(palette, arg1, slot, arg2)

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?(palette, held, expected, declarations \\ %{}, opts \\ [])

@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(palette, document, target, candidate, ctx)

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(map)

@spec context(%{optional(:datamodel) => map() | nil, optional(any()) => 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(palette, finding, declarations \\ %{})

@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(palette, document, target, ctx)

@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(ref, config)

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(module, config)

@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 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(palette, ref, block)

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(palette, document, block, ctx)

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(palette, held, expected, producing_ref, declarations \\ %{})

@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(palette, document, ctx)

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(module, config, slot)

@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(palette, ref, block, slot)

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(palette, document, candidate, ctx)

@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(palette, document, candidate, ctx)

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(palette, document, ctx)

@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.