# `PhoenixKitCatalogue.Catalogue.AttributeSets`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/v0.31.3/lib/phoenix_kit_catalogue/catalogue/attribute_sets.ex#L1)

Attribute SETS — the 2026-08-18 rework of the group/attribute system.

A set is one dimension from one vendor ("Ikea colors"), stored as a
MANAGED entities blueprint (created only through this module, hidden
from the generic entities admin); its data records are the values.
Items attach any number of sets through the catalogue-owned
`phoenix_kit_cat_item_attribute_sets` join (V177).

## The blueprint contract

    name:      "catalogue_set_<slug>"          (immutable identity)
    settings:  "managed_by"  => "catalogue"
               "locked_keys" => ["kind", "default_value_slug"]
               "catalogue"   => %{"kind" => "fixed" | "multi",
                                  "default_value_slug" => slug | nil}
    records:   slug = the value's stable key, title = display text,
               position = order, data = extras (per-set fields)

Everything else on the blueprint (display name, translations,
`fields_definition` extras like "price per liter") is freely editable.
`contract/1` validates the shape on every resolve; a broken contract
is surfaced (`{:error, :contract_broken}`), never guessed around.

## Enablement

Requires the entities module (`PhoenixKitEntities.enabled?/0`). Every
WRITE returns `{:error, :entities_disabled}` when it is off — same
loud-failure doctrine as the `:catalogue_pdf` queue guard. Reads
degrade quietly instead (`[]`, `nil`, `%{}`, `0`): UI callers render
empty rather than crash during a feature toggle.

Public surface re-exported from `PhoenixKitCatalogue.Catalogue`.

# `add_extra_field`

```elixir
@spec add_extra_field(struct(), map(), keyword()) ::
  {:ok, struct()} | {:error, term()}
```

Adds an extra field to the set's blueprint (`:label` required,
`:type` one of `extra_field_types/0`; `select` additionally needs
`:options`, a non-empty list). Every value can then carry data for
it. The key is derived from the label and is stable.

# `archive_set`

```elixir
@spec archive_set(struct(), keyword()) :: {:ok, struct()} | {:error, term()}
```

Archives a set (soft-delete, 2026-09-11 direction): status flips to
`"archived"` through the owner bypass — `Managed.validate_mutation/3`
lets `on_behalf_of == owner` through ABOVE the identity-rename guard
that would otherwise refuse a managed blueprint's status change from
the generic entities admin. Allowed even while the set is attached to
items; that is the whole point of soft-delete over `delete_set/2`'s
hard `{:error, :set_in_use}` refusal — ties stay intact, values stay
resolvable (`resolve_set/2`'s `hidden_values`, §3c).

Idempotent: an already-archived set is returned as-is, no duplicate
activity row or broadcast — checked against a FRESH re-read
(`get_set/2`), not the caller's possibly-stale struct, same doctrine
as `update_set/3`. `ensure_enabled/0` still runs first, so a
disabled `entities` system refuses even the idempotent path — a
writer never reports success while its own foundation is off.

Unlike `update_set/3`, a re-read that comes back `nil` (uuid deleted,
or simply not a catalogue-owned set) is `{:error, :not_found}`, never
a fallback to the caller's struct: this writer changes `:status`,
exactly what `Managed.validate_mutation/3`'s identity-rename guard
protects, and `on_behalf_of` rides the owner constant unconditionally
above — falling back to an unverified struct would carry someone
else's entity past that guard on the owner bypass alone.

`on_behalf_of` rides the owner constant only, not the caller's
`opts` — same as every other writer in this module. That means
`:actor_uuid` is not forwarded to entities' own activity log, which
then attributes the change to the blueprint's original creator
rather than the acting admin; the catalogue's own
`"attribute_set.archived"` entry below still records the real actor
from `opts`.

# `attach_set`

```elixir
@spec attach_set(Ecto.UUID.t(), Ecto.UUID.t(), keyword()) ::
  {:ok, PhoenixKitCatalogue.Schemas.ItemAttributeSet.t()} | {:error, term()}
```

Attaches a set to an item (appends; no-op when already attached).

Runs under the per-set advisory lock shared with `delete_set/2` —
without it, an attach racing a delete could commit after the guard's
`set_attached?` check read false, leaving an instant orphan row
(panel finding, 2026-08-18 review).

# `attached_item_uuids`

```elixir
@spec attached_item_uuids([Ecto.UUID.t()]) :: MapSet.t()
```

Which of these items have at least one attached attribute set, with NO
resolve — no set/value lookups, just the join table. For callers that
only need the swatch indicator (presence) and not the "Attributes"
column's labels, this is one cheap query instead of `resolve_for_items/2`'s
per-set reads. Gated on `entities_enabled?/0` the same way, so the
swatch keeps disappearing along with the labels when entities is off.

# `attachment_counts`

```elixir
@spec attachment_counts([Ecto.UUID.t()]) :: %{
  optional(Ecto.UUID.t()) =&gt; non_neg_integer()
}
```

How many items attach each of the given sets: %{set_uuid => count}.

# `auto_migrate_legacy`

```elixir
@spec auto_migrate_legacy() :: :ok
```

Migrates any remaining legacy groups into sets, silently and safely —
there is no legacy UI once sets are live ("it should just migrate",
boss direction 2026-08-18), so this runs from the supervision-tree
startup task and again from the attributes page as a backstop (boot
can race the repo/settings, and entities can be enabled at runtime).

Never raises: any failure is logged and swallowed — a broken
migration must not take down boot or an admin page. Idempotent by
way of `migrate_groups_to_sets/1`.

# `backfill_managed_path`

```elixir
@spec backfill_managed_path() :: :ok
```

Idempotent backfill: stamps `settings["managed_path"]` on every existing
set blueprint that predates it — `create_set/2` writes it for new sets,
but sets provisioned before the 2026-09-11 direction need a one-time
top-up so entities' managed-blueprint badge can link back here. Runs
from `startup/0`; never raises, same doctrine as `auto_migrate_legacy/0`
— a broken write here must not take down boot. That doctrine covers the
BOOT, not the backfill itself: each set is written under its OWN
try/rescue (`backfill_one_managed_path/1`) so one bad set (unexpected
`settings`, a rejected write) is logged and skipped rather than aborting
the whole loop and leaving every set after it un-backfilled.

# `child_spec`

```elixir
@spec child_spec(keyword()) :: Supervisor.child_spec()
```

Registers the catalogue's blueprint delete guard with entities.
Ships as a supervision child via `PhoenixKitCatalogue.children/0`, so
it runs once per boot; deleting a set with item attachments is
refused at the entities write path.

# `contract`

```elixir
@spec contract(struct()) :: {:ok, map()} | {:error, :contract_broken}
```

Validates a set blueprint's catalogue contract. Returns
`{:ok, %{kind: atom, default: slug | nil}}` or
`{:error, :contract_broken}` — never a guessed fallback.

# `count_attached_items`

```elixir
@spec count_attached_items(Ecto.UUID.t(), keyword()) :: non_neg_integer()
```

Total match count for `list_attached_items/2` (same filters).

# `create_set`

```elixir
@spec create_set(map(), keyword()) :: {:ok, struct()} | {:error, term()}
```

Provisions a new set: a managed blueprint from the locked template.

`attrs`: `:name` (display, required), `:slug` (optional — derived
from the name when absent), `:kind` (`"fixed"`/`"multi"`, default
`"multi"`), `:description`.

# `create_value`

```elixir
@spec create_value(struct(), map(), keyword()) :: {:ok, struct()} | {:error, term()}
```

Adds a value to a set. `attrs`: `:label` (required), `:slug`
(derived from label when absent), `:extras` (map merged into the
record's data — cast against the blueprint's fields the same way
`update_value/4` casts them).

# `default_value_slug`

```elixir
@spec default_value_slug(struct()) :: String.t() | nil
```

The set's default value slug, or nil. See `kind/1`.

# `delete_set`

```elixir
@spec delete_set(struct(), keyword()) :: {:ok, struct()} | {:error, term()}
```

Deletes a set. Refused (`{:error, :set_in_use}`) while any item
attaches it — the same guard entities consults on its own delete path.

# `delete_value`

```elixir
@spec delete_value(struct(), struct(), keyword()) ::
  {:ok, struct()} | {:error, term()}
```

Deletes a value record. When the value is the set's default, the
default is cleared first so the contract never points at a ghost.

This is the programmatic HARD-delete path only — there is no UI
affordance for it here. The UI path for removing a value is the
entities trash (soft-delete via `EntityData.trash/2`, reversible; the
slug and its selections stay intact, see `resolve_set/2`'s
`hidden_values`, §3c). For a value hard-deleted some OTHER way (a
write that bypassed this function's own `prune_selection_slug/2`
sweep), the safety net is `prune_orphan_value_slugs/1`, run
automatically off entities' data-deletion PubSub event by
`AttributeSets.OrphanPruner`.

# `detach_set`

```elixir
@spec detach_set(Ecto.UUID.t(), Ecto.UUID.t(), keyword()) :: :ok
```

Detaches a set from an item (no-op when not attached).

# `enabled?`

```elixir
@spec enabled?() :: boolean()
```

True when the sets feature is live: the entities module is enabled
AND its package carries the Managed API (entities > 0.4.0) — on an
older package the whole feature degrades to `:entities_disabled`
rather than crashing on missing functions. UI surfaces branch on
this to decide sets-vs-legacy rendering.

# `extra_field_types`

```elixir
@spec extra_field_types() :: [String.t()]
```

Extra-field types the set editor offers (a curated entities subset).

# `filter_options`

```elixir
@spec filter_options(Ecto.UUID.t() | :all, keyword()) :: [map()]
```

The attribute filter's options for one catalogue: every set attached to
an item in it, with that set's values.

Only sets actually in use appear — a filter offering "Colour" for a
catalogue of screws is noise. Values come from the set itself rather
than from what is currently selected, so picking one that matches
nothing yet returns an honest empty list instead of hiding the option.

Shape: `[%{uuid:, name:, values: [%{slug:, title:}]}]`, named for what
the UI renders.

# `get_set`

```elixir
@spec get_set(Ecto.UUID.t(), keyword()) :: struct() | nil
```

Fetches one set by blueprint uuid (nil when missing/not a set).

# `get_value`

```elixir
@spec get_value(struct(), Ecto.UUID.t()) :: struct() | nil
```

Fetches one value record, scoped to the set (nil when foreign/missing).

# `kind`

```elixir
@spec kind(struct()) :: String.t()
```

The set's kind string (`"fixed"`/`"multi"`, tolerant default
`"multi"`). Public so UI layers read the contract through one
accessor instead of destructuring `settings["catalogue"]` — the
strict validating read stays `contract/1`.

# `list_attached_items`

```elixir
@spec list_attached_items(Ecto.UUID.t(), keyword()) :: [map()]
```

One page of the items attached to a set, name-ordered — the set
detail page's listing. Each row is `%{item: %Item{}, selected_slugs:
[...]}`; the slugs are the RAW attachment selection — callers
ghost-filter them against the set's current values with
`valid_selection/2`. Deleted items are excluded.

Options: `:search` (trimmed, matched on item name), `:limit`
(default 25), `:offset` (default 0).

# `list_attachments`

```elixir
@spec list_attachments(Ecto.UUID.t()) :: [
  PhoenixKitCatalogue.Schemas.ItemAttributeSet.t()
]
```

The item's attachments in order.

# `list_hidden_values_for`

```elixir
@spec list_hidden_values_for([Ecto.UUID.t()], keyword()) :: %{
  optional(Ecto.UUID.t()) =&gt; [struct()]
}
```

Hidden (archived/trashed) values for MANY sets at once:
`%{set_uuid => [record]}` — the batched twin of the archived/trashed
complement of `list_values_for/2`. Mirrors its batched call to
`EntityData.list_by_entities/2` (which understands `:include_trashed`)
instead of looping `list_by_entity/2` per set — that loop is exactly
the N+1 `resolve_for_items/2` used to reintroduce (each iteration
also paid its own sort-order lookup and the default `:entity`/
`:creator` preloads, neither of which this read needs).

# `list_sets`

```elixir
@spec list_sets(keyword()) :: [struct()]
```

Lists the catalogue's sets (managed blueprints), locale-resolved.

`:status` — `nil` (the default, absent) or `:archived` (archived
only), or `:all` (everything). There is no "trashed" here —
blueprints don't carry that status, only their value records do. Any
OTHER value raises rather than silently reading as "non-archived" —
the opposite of most typos (a stray string, an unrelated atom).

# `list_values`

```elixir
@spec list_values(struct() | Ecto.UUID.t(), keyword()) :: [struct()]
```

Lists a set's values in display order, locale-resolved.

# `list_values_for`

```elixir
@spec list_values_for([Ecto.UUID.t()], keyword()) :: %{
  optional(Ecto.UUID.t()) =&gt; [struct()]
}
```

Values for MANY sets at once: `%{set_uuid => [value]}`, archived excluded.

The batched twin of `list_values/2`. A listing that shows a preview of each
set's values must not call the singular form per row — the sets listing did,
and paged 25 at a time, so opening the Attributes tab cost 25 queries and
repeated them on every attribute or item broadcast.

`:limit` is PER SET. The batched entities API filters status in SQL, which is
what makes that limit mean what it says; the fallback for an older entities
pin cannot, so it over-fetches and trims — asking for 5 and getting 3 reads
as "there are only 3".

# `migrate_groups_to_sets`

```elixir
@spec migrate_groups_to_sets(keyword()) :: {:ok, map()} | {:error, term()}
```

Migrates the legacy group→attribute→value data into sets:

  * each `(group, attribute)` pair → one set blueprint, slug
    `catalogue_set_<group>_<attr-key>` (display "<Group> — <Attr>");
  * attribute values → records, slug = the old value key (stable, so
    existing order-line picks keep resolving), old `is_default` → the
    set's `default_value_slug`;
  * every item's single group assignment explodes into one attachment
    per attribute of that group, in attribute order — once per set: a
    migrated set records when its assignments were migrated
    (`settings.catalogue.assignments_migrated_at`), so a set detached
    from an item afterwards is not re-attached by a later run.

Idempotent: an existing blueprint with the target slug is reused (its
values/attachments are topped up, never duplicated), so re-running
after a partial failure is safe. Old tables are left untouched
(read-only by convention; dropped by a later core migration after
cutover). Returns `{:ok, %{sets: n, values: n, attachments: n}}`.

# `prune_orphan_attachments`

```elixir
@spec prune_orphan_attachments(Ecto.UUID.t()) :: non_neg_integer()
```

Removes attachments whose set blueprint no longer exists (called by
`AttributeSets.OrphanPruner` off entities PubSub delete events).

Guarded on enablement: with entities disabled, `get_set/1` returns
nil for EVERY uuid — without the guard a stray call during a feature
toggle would read that as "blueprint deleted" and destroy valid
attachments (panel finding, 2026-08-18 review).

# `prune_orphan_value_slugs`

```elixir
@spec prune_orphan_value_slugs(Ecto.UUID.t()) :: non_neg_integer()
```

Sweeps `data->'selected_value_slugs'` clean of any slug whose value was
HARD-deleted out of band — the belt for `AttributeSets.OrphanPruner`'s
subscription to entities' `{:data_deleted, entity_uuid, _}` event (§3b,
2026-09-11 direction). `delete_value/3` already sweeps its own slug
synchronously; this is the backstop for a value removed some other way
(a direct `EntityData.delete/2`, a repo-level delete).

Collects every slug the set's values still hold, **including archived
and trashed** (`EntityData.list_by_entity(uuid, include_trashed: true)`)
— a hidden-but-existing value's slug is NOT an orphan (that is
`resolve_set/2`'s `hidden_values` job, §3c); only a slug absent from
every record, live or hidden, is pruned. No-op (`0`) when `set_uuid`
isn't a catalogue set — the shared data-deletion topic fires for every
entities blueprint, not just sets. Returns the number of slug removals
performed — one per (attachment, orphan slug) pair, since each slug is
removed by its own atomic `UPDATE` — not the number of distinct
attachments touched: one attachment holding two orphan slugs counts twice.

# `remove_extra_field`

```elixir
@spec remove_extra_field(struct(), String.t(), keyword()) ::
  {:ok, struct()} | {:error, term()}
```

Removes an extra field from the blueprint. Existing per-value data
for the key is left in place (harmless, invisible) — same doctrine
as entities' own field removal.

# `reorder_attachments`

```elixir
@spec reorder_attachments(Ecto.UUID.t(), [Ecto.UUID.t()], keyword()) ::
  :ok | {:error, term()}
```

Reorders an item's attachments to the given set_uuid order. No-op
(no writes, no activity row) when the order already matches — this
runs on every item save.

# `reorder_values`

```elixir
@spec reorder_values(struct(), [Ecto.UUID.t()], keyword()) :: :ok | {:error, term()}
```

Reorders a set's values to the given record-uuid order.

# `resolve_for_item`

```elixir
@spec resolve_for_item(Ecto.UUID.t(), keyword()) :: map()
```

Single-item convenience over `resolve_for_items/2`.

# `resolve_for_items`

```elixir
@spec resolve_for_items([Ecto.UUID.t()], keyword()) :: %{
  optional(Ecto.UUID.t()) =&gt; map()
}
```

Resolves the attached sets for many items in one batched pass: one
attachment query, plus one values listing and one hidden-values
listing, EACH batched across every distinct set in play — a 50-item
page with 6 sets costs those 3 queries once, not once per set (plus
one `get_set/2` lookup per distinct set, unbatched, unchanged by this).
Values are shared across items, so this is flat in item count too.

Returns `%{item_uuid => resolved}` where resolved is the v2 shape:

    %{schema_version: 2,
      sets: [%{uuid, key, name, kind, default,
               values:        [%{key, label, extras}],
               hidden_values: [%{key, label, extras}],
               fields:        [%{key, label, type}],
               selected: [slug]}]}

`:fields` mirrors the blueprint's extra-field definitions (what each
value's `extras` keys mean); `:selected` is the per-ATTACHMENT value
selection, intersected against `values ++ hidden_values` — a value
archived or trashed after being picked stays in `:selected` (§3c,
2026-09-11 direction: hidden, not gone), only a value gone for good
(`delete_value/3`, or the `OrphanPruner` backstop) ghosts out.
`:values` stays active-only — pickers must not offer a hidden value.
`:selected` exists ONLY on this batched read — `resolve_set/2`
resolves a bare set with no attachment context and carries no
`:selected` key.

Sets with a broken contract are skipped with a warning — a tampered
blueprint must not take item pages down, but it must not render
guessed data either.

# `resolve_set`

```elixir
@spec resolve_set(Ecto.UUID.t(), keyword()) :: map() | nil
```

Resolves ONE set to the v2 per-set shape
(`%{uuid, key, name, status, kind, default, values, hidden_values,
fields}`), or `nil` when the set is missing or its contract is broken.
No attachment context, so no `:selected` key — that exists only on
`resolve_for_items/2`'s per-item sets. Powers the item form's
attach-preview; the batched item reads go through `resolve_for_items/2`.

`:status` is the blueprint's own status (`"published"`/`"archived"`)
— a set stays resolvable while archived, so a consumer that badges an
already-attached archived SET (as opposed to an archived VALUE) reads
it here. `:values` is active-only (a picker's offer list);
`:hidden_values` carries the set's archived/trashed VALUES in the
SAME shape (§3c, 2026-09-11 direction) so a value hidden after being
selected stays resolvable — `valid_selection/2` accepts either list,
`:values` alone decides what a picker OFFERS.

# `restore_set`

```elixir
@spec restore_set(struct(), keyword()) :: {:ok, struct()} | {:error, term()}
```

Restores an archived set back to `"published"`. See `archive_set/2`
for the owner bypass, the idempotency check against a fresh re-read,
and the `on_behalf_of`/actor-attribution note. The target is
hardcoded, not "whatever status it had before archiving" — this
module only ever produces `"published"` or `"archived"`, so there is
no other prior state to restore TO. A `"draft"` status doesn't exist
here today; if one is ever introduced, restoring a draft-then-archived
set would wrongly publish it.

# `set_attached?`

```elixir
@spec set_attached?(Ecto.UUID.t()) :: boolean()
```

True when any item attaches the set (drives the delete guard).

# `set_attachment_selection`

```elixir
@spec set_attachment_selection(Ecto.UUID.t(), Ecto.UUID.t(), [String.t()], keyword()) ::
  :ok | {:error, term()}
```

Stores the per-attachment value selection (`selected_value_slugs` in
the join row's reserved `data`) — the boss's two modes: ONE slug says
"this exact object is Red", several say "this object comes in these
options", empty clears the statement. Unknown slugs are dropped
against the set's current values; `{:error, :not_attached}` when the
item doesn't attach the set.

A hidden (archived/trashed) value is KEPT when the attachment already
selects it — a value hidden after being picked survives a save (§3c)
— but never ADDED: a slug naming a hidden value the row does not
already hold is dropped like an unknown one, the same rule the item
form's picker applies (hidden values are not offered for a new pick),
enforced here so no caller of the context can get around it.

# `set_uuids_matching_value`

```elixir
@spec set_uuids_matching_value([Ecto.UUID.t()], String.t()) :: [Ecto.UUID.t()]
```

Of the given sets, which have at least one VALUE whose label matches
`term`? Returns a list of set uuids — the listing search uses it
to find a set by what is IN it ("oak" finds the color set), not only
by its own name.

One batched query where the entities pin carries the API; the
fallback is the older global title search, intersected here. Archived
values are excluded, matching `list_values/2`.

# `update_extra_field`

```elixir
@spec update_extra_field(struct(), String.t(), map(), keyword()) ::
  {:ok, struct()} | {:error, term()}
```

Updates an existing extra field: `:label` renames the display text
(the key — referenced by stored per-value data — never changes),
`:options` replaces a select field's option list (non-empty
required). The type is immutable after creation: stored values were
cast for it.

# `update_set`

```elixir
@spec update_set(struct(), map(), keyword()) :: {:ok, struct()} | {:error, term()}
```

Updates a set's unlocked surface: `:name` (display), `:description`,
`:kind`, `:default_value_slug`. Kind/default ride the owner bypass —
they are locked against GENERIC writes, not against this module.

# `update_value`

```elixir
@spec update_value(struct(), struct(), map(), keyword()) ::
  {:ok, struct()} | {:error, term()}
```

Updates a value: `:label` rewrites the display text (the slug — the
stable key — never changes), `:extras` merges into the record data.

Extras are cast per field type through the entities pipeline
(`FormBuilder.cast_field/2`): raw form strings coerce ("12.5" →
12.5, "" clears), invalid content returns `{:error, :invalid_value}`
and unknown keys `{:error, :unknown_field}` — never a silent junk
write.

# `valid_selection`

```elixir
@spec valid_selection(term(), map() | nil) :: [String.t()]
```

Filters stored selection slugs against a resolved set's CURRENT
values — THE single implementation of the ghost rule, shared with
every hydration path (the item form stages selections off raw
attachment rows).

The per-attachment selection is the boss's two modes (2026-08-19):
one slug = "this exact object is Red", several = "this object comes
in Red/Blue/Yellow", empty = no statement, the whole set applies.
The count IS the mode — nothing else is tracked. A value deleted
FOR GOOD after being ticked must not ghost through reads: unknown
slugs drop out, and a fully-ghosted selection degrades to `[]`
("whole set applies"), never to a vanished or mode-flipped set.

A value merely ARCHIVED or trashed is not a ghost (§3c, 2026-09-11
direction): its slug counts as valid when it appears in
`resolved_set`'s `:hidden_values` too, so a selection made before the
value was hidden survives the read. `:hidden_values` is optional on
the map — absent reads as `[]`, so a hand-built `%{values: [...]}`
(no hidden set in play) still works.

# `value_counts`

```elixir
@spec value_counts([Ecto.UUID.t()]) :: %{optional(Ecto.UUID.t()) =&gt; non_neg_integer()}
```

Value counts for many sets at once: `%{set_uuid => count}`, matching
`list_values/2`'s semantics (archived and trashed excluded). One
grouped query — the viewer must not COUNT per row.

# `value_match_counts`

```elixir
@spec value_match_counts(keyword()) :: %{optional(String.t()) =&gt; non_neg_integer()}
```

How many rows each attribute VALUE would still match, given the filter
already applied — the numbers beside the filter's checkboxes, and what
lets a value that leads nowhere be disabled instead of offered (Max,
2026-08-28).

Conditioned on the CURRENT selection, so once Blue is on, Oak shows how
many blue oak items there are. A dead combination is therefore visible
as a 0 before it is picked, rather than as an empty list afterwards.

Options: `:catalogue_uuid`, `:catalogue_uuids`, `:category_uuids`,
`:statuses`, `:search`, `:value_slugs` (the selection to condition on)
and `:count` — `:items` (default) or `:catalogues`, which counts
distinct catalogues for the index's version of the filter.

Every scope the LISTING is under has to be passed, or the promise
breaks the other way: a value offered as live because something
matches it SOMEWHERE, while the page the user is on has none of it,
is exactly the empty list this exists to prevent. `:search` narrows by
the same text predicate the item search uses.

One grouped query: the selection slugs are unnested from the
attachment's JSONB, so a page with fifteen values still asks once.

---

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