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

What the builder needs from the review thread: the counter, and getting to an element.

The thread itself is drawn by Gervaz — `MishkaCmsCoreWeb.Templates.ReviewComments` reads
`MishkaCmsCore.Runtime.PageComment` through the `:for_editor` action and owns everything about
listing, filtering and paging. Nothing here duplicates that. This is the two things the EDITOR
needs that a table cannot answer.

## The counter

The red number beside Review is unresolved BUGS, and it is a `COUNT` — not `length/1` over a list
the sheet happens to have loaded. The sheet may be closed, may be showing "All", may have loaded
one page of fifty; none of that can be allowed to change the number. Asking the database is both
cheaper and the only answer that is always right.

## Getting to an element

A note's `node_id` is the editor's own `n<8>` handle, minted fresh by every `Tree.parse/1`. It is
a soft reference by construction: the element can be deleted, or the document re-parsed under it,
and the id will point at nothing. `locate/2` is the whole of what to do about that — `{:ok, id}`
when the element is still there, `:gone` when it is not — so that the caller shows a message
instead of selecting nil and every caller behaves the same way.

# `compose_defaults`

```elixir
@spec compose_defaults(map()) :: map()
```

The `defaults` the composer is seeded with — everything the editor already knows.

This is what makes the form two fields instead of seven, and what makes an element note and a page
note the same code path: with nothing selected, `node_id` is simply nil.

`node_label` is captured HERE, at compose time, and stored with the note, because the element it
names can be deleted afterwards.

# `fetch`

```elixir
@spec fetch(String.t(), map()) :: MishkaCmsCore.Runtime.PageComment.t() | nil
```

One note by id, with its author and resolver loaded, or `nil`.

Scoped to WHO IS ASKING, not to the id: `:get_any` bypasses multitenancy, so the id alone reached
every site's notes — and the id travels in a `phx-value-id` that anyone can type. A master reads
any note; a tenant reads its own site's; nobody else reads one at all.
`MishkaCmsCore.Runtime.PageBuilder.Snippets.delete/2` scopes its own by-id path the same way.

# `list`

```elixir
@spec list(map(), keyword()) :: [MishkaCmsCore.Runtime.PageComment.t()]
```

Every note on this document, newest first.

Used by the counter's siblings and by tests; the sheet itself reads through the Gervaz table.

# `locate`

```elixir
@spec locate(
  MishkaCmsCore.Runtime.PageComment.t() | map() | nil,
  MishkaCmsCore.Runtime.PageBuilder.Tree.t()
) :: {:ok, String.t()} | :page | :gone
```

Where a note points, if it still points anywhere.

  * `{:ok, node_id}` — the element is in the document and can be selected.
  * `:page` — the note was never anchored to an element; it is about the whole page.
  * `:gone` — it was anchored, and that element is no longer here.

Three answers rather than two, because "about the page" and "the element was deleted" deserve
different words in front of the reader, and a caller that cannot tell them apart will pick one and
be wrong half the time.

# `open_bugs`

```elixir
@spec open_bugs(map()) :: non_neg_integer()
```

How many unresolved bugs this document has.

Derived, always: it counts rows, so creating a bug, resolving one, reopening one and deleting one
all move it without anything having to remember to.

Returns 0 rather than raising when the read fails. A counter is chrome; a builder that will not
open because a badge could not be computed is a worse outcome than a badge that reads zero.

# `thread_params`

```elixir
@spec thread_params(map()) :: map()
```

The `path_params` the Gervaz thread is scoped by.

PREFIXED keys, and keyed by ATOM. Gervaz turns any path param whose name matches a resource
attribute into an equality filter and tests membership against `Ash.Resource.Info.attributes/1`,
whose names are atoms — so `site_id` would drop the global rows this read exists to include, and a
string key would match nothing at all.

---

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