# `MishkaCmsCore.Runtime.PageBuilder.Op`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/page_builder/op.ex#L5)

One validated edit request — the only thing allowed to reach a
`MishkaCmsCore.Runtime.PageBuilder.Tree` combinator.

Every `handle_event` in the editor turns its raw params into an `Op` with `build/1` before the
session touches the tree (ADR-13). That is what makes the server the authority rather than a
transcription service: a tampered payload fails validation here, and a plugin that wants to veto
or rewrite an edit gets a typed struct on the `builder:node:before_*` hook instead of a bag of
strings.

## Why one struct and not twelve

ADR-13 sketches a struct per combinator. One struct with a validated `kind` and a per-kind
required-field check gives the same guarantee — nothing unvalidated reaches a combinator — with
one module to read and one hook payload shape for plugins to match on. It follows the house
precedent (`MishkaCmsCore.Dashboard.Widget` is likewise one struct with a `type`), and pattern
matching on `%Op{kind: :move}` reads the same as matching a `Move` struct would.

## Insertion is always "parse this template"

Dropping a palette component, inserting an example variant and pasting the clipboard are the same
operation: take HEEx, parse it, splice the subtree in. `:insert` therefore carries a `template`
string rather than a pre-built node, which means an example keeps its attributes and children (the
React editor used only `ast[0][0]` and dropped both) and a pasted subtree is validated by the same
parser as everything else.

# `t`

```elixir
@type t() :: %MishkaCmsCore.Runtime.PageBuilder.Op{
  bucket: atom() | nil,
  index: integer() | nil,
  kind: atom(),
  name: String.t() | nil,
  node_id: String.t() | nil,
  node_ids: list() | nil,
  parent_id: String.t() | nil,
  prop: String.t() | nil,
  tag: String.t() | nil,
  template: String.t() | nil,
  text: String.t() | nil,
  value: String.t() | nil,
  value_kind: atom() | nil
}
```

# `apply_to`

```elixir
@spec apply_to(t(), MishkaCmsCore.Runtime.PageBuilder.Tree.t()) ::
  {:ok, MishkaCmsCore.Runtime.PageBuilder.Tree.t()}
  | {:ok, MishkaCmsCore.Runtime.PageBuilder.Tree.t(), String.t()}
  | {:error, term()}
```

Applies a validated op to a tree.

Returns `{:ok, tree}`, `{:ok, tree, node_id}` for the ops that produce a node the caller wants to
select afterwards, or `{:error, reason}`. Containment is **not** checked here — the session does
that, so a plugin rewriting an op on the `before_*` hook cannot accidentally bypass it.

# `buckets`

```elixir
@spec buckets() :: [atom()]
```

The style buckets an override may be recorded in — every width crossed with every state.

# `build`

```elixir
@spec build(map()) :: {:ok, t()} | {:error, term()}
```

Builds an op from raw client params.

Accepts string or atom keys, coerces the handful of fields the wire sends as strings (`index`,
`kind`, `bucket`, `value_kind`) and then enforces the fields that kind actually needs — a `:move`
without a `node_id`, or a `:set_style` naming a bucket that does not exist, is refused here rather
than halfway through a combinator.

# `builder`

# `enforce_keys`

# `enforce_keys`

# `example`

A sample %MishkaCmsCore.Runtime.PageBuilder.Op{} populated from defaults + type-based fallbacks.

# `keys`

# `keys`

# `kinds`

```elixir
@spec kinds() :: [atom()]
```

Every op kind the editor can issue.

# `label`

```elixir
@spec label(t()) :: String.t()
```

A short label for the history sheet and the plugin payload — "Moved element", "Set class", …

# `mutating?`

```elixir
@spec mutating?(t()) :: boolean()
```

Whether an op changes the document (and so belongs in the undo history) — every kind currently
does, but the editor also routes selection and viewport changes through the same funnel, and those
must not consume a history slot.

---

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