# `MishkaCmsCore.Runtime.PageComment`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/resources/page_comment.ex#L5)

A note left on a document in the visual editor — on one element, or on the page as a whole.

## Why this exists

Review notes lived on the editing session: a list on the `EditorSession` struct, in ETS, swept
after a day idle. `MishkaCmsCore.Runtime.PageBuilder.Comment`'s own moduledoc said so and named
the next step. A note that does not survive the afternoon is not a review; it is a sticky note on
a screen someone is about to close.

The field shape is that struct's, deliberately, so the swap is storage and not a rewrite: the
node it points at, the label captured beside it, who wrote it, whether it is resolved.

## What a comment is attached to

Two scopes, and the difference is one nullable column:

  * `node_id` set — a note on ONE element. The id is the editor's own `n<8>` handle, which is
    minted by `Tree.parse/1` and therefore changes every time the document is re-parsed. It is a
    soft reference on purpose and can never be a foreign key.
  * `node_id` nil — a note on the whole page.

`node_label` is captured at write time for exactly that reason. The element can be deleted, or the
document re-parsed under it, and the note still reads "on the `<h1>`" instead of pointing at
nothing. `MishkaCmsCore.Runtime.PageBuilder.Review.locate/2` is what turns a stale id back into a
selection, or into a plain "that element is gone".

## Who wrote it

`author_type` is `:human`, `:ai` or `:system`, and `author_id` is nullable, because the other two
have no user row. Nothing here implements AI review; what it does is make sure that when
something does, it is a new value in an existing column rather than a second comment system beside
this one. `resolved_by_id` is separate from `author_id` for the same reason — the thing that
resolves a note is often not the thing that raised it.

## Scope

`global? true` with a nullable `site_id`, matching `MishkaCmsCore.Runtime.Snippet`, because the
builder edits both tenant documents (pages, layouts, error pages) and global ones (components). A
comment on a global component belongs to no site.

# `t`

```elixir
@type t() :: %MishkaCmsCore.Runtime.PageComment{
  __lateral_join_source__: term(),
  __meta__: term(),
  __metadata__: term(),
  __order__: term(),
  aggregates: term(),
  author: term(),
  author_id: term(),
  author_master?: term(),
  author_type: term(),
  body: term(),
  calculations: term(),
  document_id: term(),
  id: term(),
  inserted_at: term(),
  master_mentions: term(),
  node_id: term(),
  node_label: term(),
  node_path: term(),
  parent_comment: term(),
  parent_comment_id: term(),
  replies: term(),
  resolved_at: term(),
  resolved_by: term(),
  resolved_by_id: term(),
  resolved_by_master?: term(),
  section: term(),
  site: term(),
  site_id: term(),
  status: term(),
  type: term(),
  updated_at: term()
}
```

# `apply_scope`

```elixir
@spec apply_scope(map(), map()) :: map()
```

Puts the editor's context onto the note the composer is about to save.

The author never restates which document they are on, which element is selected, or who they are —
the editor already knows all three, and asking would be a form with five fields where two will do.
It rides in on the form's `defaults`, which is also what makes a page-level note simply a note
with no `node_id`.

`site_id`, `author_id` and `author_type` are written only for a master. A tenant's `:create` does
not accept them: it takes `site_id` from the tenant and `author_id` from the actor.

# `default_short_name`

# `for_editor_arguments`

```elixir
@spec for_editor_arguments(Ash.Query.t(), map()) :: Ash.Query.t()
```

Puts the editor's own scoping onto a `:for_editor` query, from the table's `path_params`.

PREFIXED keys, for the reason `MishkaCmsCore.Runtime.Snippet` records: Gervaz turns any path param
whose name matches an attribute into an equality filter, and `site_id == X` would drop exactly the
global rows this read exists to include.

# `input`

```elixir
@spec input(values :: map() | Keyword.t()) :: map() | no_return()
```

Validates that the keys in the provided input are valid for at least one action on the resource.

Raises a KeyError error at compile time if not. This exists because generally a struct should only ever
be created by Ash as a result of a successful action. You should not be creating records manually in code,
e.g `%MyResource{value: 1, value: 2}`. Generally that is fine, but often with embedded resources it is nice
to be able to validate the keys that are being provided, e.g

```elixir
Resource
|> Ash.Changeset.for_create(:create, %{embedded: EmbeddedResource.input(foo: 1, bar: 2)})
|> Ash.create()
```

# `input`

```elixir
@spec input(values :: map() | Keyword.t(), action :: atom()) :: map() | no_return()
```

Same as `input/1`, except restricts the keys to values accepted by the action provided.

# `primary_key_matches?`

# `type_options`

```elixir
@spec type_options() :: [{String.t(), String.t()}]
```

Every comment type as a `{label, value}` option, in the order of `types/0`.

# `types`

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

Every comment type, in the order the form offers them.

---

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