# Enact

A thin, behaviour-based action layer for application write operations. Enact standardizes the shape of every write:

```
load → authorize → cast → validate → resolve → execute → after_commit
```

The value is the uniform pipeline shape, the actor context, and the closed error taxonomy — not any novel validation or persistence machinery. **Enact orchestrates; Ecto does the work**: validation is Ecto changesets, input casting is Ecto embedded schemas, persistence is your existing schemas and changesets. No DSL, no parallel type system, no validation vocabulary.

## Installation

```elixir
def deps do
  [
    {:enact, "~> 0.1.0"}
  ]
end
```

Configure the default repo (or pass `repo:` per call):

```elixir
config :enact, repo: MyApp.Repo
```

## A complete action

```elixir
defmodule MyApp.Projects.Actions.CreateProject do
  use Enact.Action

  alias MyApp.Accounts
  alias MyApp.Projects.Inputs.ProjectInput
  alias MyApp.Projects.Project

  @impl Enact.Action
  def input, do: ProjectInput

  @impl Enact.Action
  def authorize(ctx), do: MyApp.Policy.can?(ctx.actor, :create_project)

  @impl Enact.Action
  def resolvers do
    [owner: {:owner_id, &fetch_owner/2}]
  end

  @impl Enact.Action
  def execute(changeset, ctx) do
    updates =
      changeset
      |> Enact.updates(ctx)
      |> Map.put(:owner_id, ctx.assigns.owner.id)

    %Project{org_id: ctx.actor.org.id}
    |> Project.changeset(updates)
    |> ctx.repo.insert()
  end

  @impl Enact.Action
  def after_commit(project, _ctx) do
    MyApp.Analytics.track(:project_created, project)
  end

  # the context owns the trust-anchor-scoped query; the fetcher adapts
  # its result to the resolver contract
  defp fetch_owner(public_id, ctx) do
    case Accounts.get_org_user(ctx.actor, public_id) do
      nil -> :error
      user -> {:ok, user}
    end
  end
end
```

Application callers go through a context one-liner that forwards to `Enact.run/3` (see the Phoenix guide). Action tests and IEx may call the runner directly:

```elixir
case Projects.create_project(params, actor: conn.assigns.current_scope) do
  {:ok, project} -> ...
  {:error, %Enact.Error{type: :invalid, changeset: changeset}} -> ...
end
```

The actor is always explicit and required — `actor: nil` raises, and every write path answers "as whom?". Anonymous callers pass an explicit anonymous actor (see `Enact.Actor`), permitted only by actions declaring `anonymous?: true`.

## Input schemas are just Ecto

Inputs are embedded-schema modules implementing the `Enact.InputSchema` behaviour — `changeset/3` heads per mode, a `fields/1` introspection manifest, and (for patch-mode use) a `from_subject/1` projection that lets `validate_required` and cross-field `get_field/2` rules work unmodified on PATCH:

```elixir
defmodule MyApp.Projects.Inputs.ProjectInput do
  use Ecto.Schema
  use Enact.InputSchema
  import Ecto.Changeset

  @primary_key false
  embedded_schema do
    field :name, :string
    field :slug, :string
    field :owner_id, :string
  end

  @all ~w(name slug owner_id)a
  @patch @all -- [:slug]
  # owner_id required, so the resolver always runs and execute can rely
  # on ctx.assigns.owner being present
  @required ~w(name slug owner_id)a

  @impl Enact.InputSchema
  def changeset(base, params, :create) do
    base
    |> cast_input(params, @all)
    |> validate_required(@required)
  end

  def changeset(base, params, :patch) do
    base
    |> cast_input(params, @patch)
    |> validate_required(@required)
  end

  @impl Enact.InputSchema
  def fields(:create), do: @all
  def fields(:patch), do: @patch

  @impl Enact.InputSchema
  def from_subject(project) do
    %__MODULE__{
      name: project.name,
      slug: project.slug,
      owner_id: MyApp.PublicIds.encode(:user, project.owner_id)
    }
  end
end
```

`Enact.updates/2` extracts exactly the fields the caller provided, with their casted values — so PATCH semantics fall out: omitted keys are untouched, explicit `null` clears, arrays replace wholesale. `Enact.Guardrails` mechanically enforces the input-schema invariants (no field defaults, no primary keys, no associations) on first run and in CI.

## Errors

Every failure is an `%Enact.Error{}` with one of five HTTP-shaped types: `:invalid` (422, carries the changeset), `:forbidden` (403), `:not_found` (404), `:conflict` (409), `:internal` (500). One renderer in your app handles all of them; actions never invent bespoke error atoms. Reference-resolution failures are field-level `"not found"` errors indistinguishable from validation failures — and cross-tenant probes are indistinguishable from nonexistent records.

## Dry runs and confirmation

`Enact.dry_run/3` runs everything up to (not including) execute and returns an `%Enact.Preview{}` — the exact updates map a real run would persist, the loaded subject, and a digest for confirmation flows:

```elixir
{:ok, preview} = Projects.update_project_dry_run(params, actor: actor)
# show preview.updates to the user...
{:ok, project} =
  Projects.update_project(params, actor: actor, confirm_digest: preview.digest)
```

A digest mismatch returns `:conflict` — "the user confirmed this exact change to this record" is a mechanical guarantee.

## Loading a subject

`Enact.subject/3` loads the action's subject and authorizes the actor. No body, no write. Use it when the GET needs the record (edit, archive confirmation, create-under-parent):

```elixir
{:ok, project} = Projects.update_project_subject(params, actor: actor)
```

Failures are `:not_found` or `:forbidden`. An action with no subject raises — use `authorized/3` for a new form:

```elixir
:ok = Projects.create_project_authorized(%{}, actor: actor)
```

## Telemetry

The runner emits `[:enact, :action, :run]`, `[:enact, :action, :dry_run]`, `[:enact, :action, :subject]`, and `[:enact, :action, :authorized]` events (plus matching `:error` events) with per-action, per-type metadata — observability and audit trails with zero action-author involvement.

## Testing

`Enact.Test` ships `assert_invalid/2`, `build_ctx/1`, and `errors_on/1` so host apps don't reinvent them.

## Documentation

- [Usage Rules](usage-rules.md) — the condensed do's and don'ts for writing actions; sync it into your agent instructions (CLAUDE.md / AGENTS.md) with [usage_rules](https://hex.pm/packages/usage_rules)
- [Change Detection](guides/change-detection.md) — how the validation base, presence-gated extraction, and PATCH fidelity actually work, and why `force_changes:`-style workarounds never appear
- [Phoenix Integration](guides/phoenix-integration.md) — actor/scope wiring, the reference FallbackController and error renderer, Inertia form posts, background jobs, telemetry
- [Recipes](guides/recipes.md) — worked examples: embedded data end-to-end, flattening embeds into columns, reading resolver assigns, MCP dry-run confirmation flows, empty-string-at-rest columns, partial updates on singular embeds
- [Testing Host Applications](guides/testing.md) — copy-paste templates for the host-side test obligations (cross-tenant sweep, PATCH/create matrices, projection completeness, resolver coverage, guardrails in CI)
- [Design Specification](spec.md) — the authoritative design, including the rationale for every decision
