# `Enact.Resolve`
[🔗](https://github.com/svycal/enact/blob/v0.1.0/lib/enact/resolve.ex#L1)

Reference resolution — the pipeline step that turns payload references
(public-facing IDs like `usr_XXXX`) into loaded records.

Actions declare resolvers as data:

    def resolvers do
      [
        # scalar
        owner: {:owner_id, &fetch_owner/2},
        # path form → batch (one level deep)
        milestone_owners: {[:milestones, :owner_id], &fetch_milestone_owners/2}
      ]
    end

Resolution failures are field-level `:invalid` errors (`"not found"` on
the field), never `:not_found` — 404 belongs to the URL subject, and
"exists but not yours" must be indistinguishable from "doesn't exist".
All resolvers run and all failures are collected without
short-circuiting, tagged `validation: :resolution` so renderers can
distinguish reference errors from input-format errors without string
matching.

## Scalar fetcher contract

`(public_id, ctx) -> {:ok, struct} | :error | {:error, message}`

Skipped when the caller did not provide the field (an untouched
reference on PATCH survives) and on explicit nil-clears (nothing to
resolve). A provided reference resolves even when identical to the
current value: re-resolution is re-authorization, and it keeps the
execute-side contract total — whenever a non-nil reference appears in
`Enact.updates/2` output, the resolved record is in `ctx.assigns`. On
success the struct is stashed under the resolver name; `execute/2`
reads it there (`ctx.assigns.owner.id`) to translate public → internal
ids.

## Batch fetcher contract (path form)

`(ids, ctx) -> %{public_id => struct | {:error, message}}`

The unique non-nil ids across the embed's item changesets are collected
into one fetcher call (N items ≠ N queries). Ids absent from the result
map render the generic `"not found"` on the item at its correct index;
`{:error, message}` values render precise per-item messages. On success
the lookup map is stashed under the resolver name, keyed by public id —
the execute-side join is a map read. Paths are one level deep only.

## Error precision — the trust-anchor rule

Collapse is mandatory outside the trust anchor; precision is permitted
inside it. Missing and wrong-tenant must both produce bare `:error`
(generic `"not found"`) — if they render differently, probing IDs
reveals which exist. Precise messages (`{:error, "has been
deactivated"}`) are for records within the caller's own tenant only.
Safety comes from check ordering inside the fetcher: scope by the trust
anchor first; emit precise messages only about records the anchor-scoped
query returned.

---

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