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

The Forms group of the audit rail: what is wrong with the page's forms, and the tree edit that
puts each one right.

Every finding carries the shape the rail already renders — `node_id`, `group`, `severity`, `rule`,
`message`, `detail` — so a form check reads beside an accessibility one and its Fix button is the
same button. What is different is that most of these have a mechanical answer: `repair/3` hands
back the ops that fix the document, and the few with no edit behind them say why rather than
appearing to work.

## What is asked of each node

Not asked here at all: `MishkaCmsCore.Runtime.PageBuilder.Fields.form_node?/2` and
`MishkaCmsCore.Runtime.PageBuilder.Fields.field_node?/2` are the editor's one answer to both, so
what the insert funnel treats as a form, what `MishkaCmsCore.Runtime.PageBuilder.Containment`
refuses to nest, and what the audit reports on cannot disagree. This module used to carry its own
copy of each, and so did the other two.

## Two questions about a submit button, two answers

`form-no-submit` asks, per node and at any depth inside the form, whether anything sends it
(`Fields.submit_node?/3`): a bare `<button>`, a kit button left to its default, the CMS's own
`<.button>`. A button whose type is computed (`Fields.type_unread?/3`) may send it, so the rule
stays quiet rather than offer a second Send button. `submit-no-form` asks the reverse about a button outside every form, and there only a
written `type="submit"` or `"image"` counts (`Fields.explicit_submit?/3`), so a hero or dialog
button stays quiet.

## Why a wrap is an insert and a delete

`MishkaCmsCore.Runtime.PageBuilder.Op`'s `:wrap` builds a bare tag with no attributes, and a form
is nothing without its five. There is also no id to write them onto: the wrapper is minted inside
the combinator. So the field's own source is re-parsed, wrapped and bound in one go by
`FormPlacement.wrap/3`, inserted where the field stood, and the field deleted — two ops in one
`MishkaCmsCore.Runtime.PageBuilder.EditorSession.apply_ops/4` run, which is one undo step.

## A Fix that cannot act says so

Two of these have no edit behind them, and each says why in a sentence the author reads instead
of appearing to work: there is no form on the page to move a button into (`submit-no-form`);
unwrapping this form would strand the fields inside it (`form-in-form`). `repair/3` answers
`{:say, sentence}` for those, which is not the same as `:none` — `:none` is a stale click, and the
assistant is the right place for a document that has moved on. Everything else here has exactly
one right answer and takes it.

## A note for developers

`form-no-destination` is a `:note`, not counted against the page: a form nobody wrote code for
still checks its fields and thanks the visitor. A form that names no submit event and posts to its
own `action` has somewhere to go, so it raises nothing (`receiver/2`).

Its Fix finishes the form first, one edit per click: its name (`as`), then its source (`for`),
then its submit event. A form with all three answers `{:code, event, sentence}`: the form's submit
event, for the editor to open in the Code window, and the sentence to show someone who cannot
write code.

See `MishkaCmsCore.Runtime.PageBuilder.PageForms` for what declares a form in the first place, and
`MishkaCmsCore.Runtime.PageBuilder.PageForms.Naming` for the five names a seed derives.

# `findings`

```elixir
@spec findings(MishkaCmsCore.Runtime.PageBuilder.Tree.t(), map() | nil, map()) :: [
  map()
]
```

Every Forms finding this document raises, in document order, with the duplicate-name blockers
last.

`record` is the page being edited, read for the event handlers it declares; `session` is the
editing session, read for the palette and for the section — a section whose compiler emits no form
keeps only the rules HTML itself is wrong about.

# `forms?`

```elixir
@spec forms?([MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()], map()) :: boolean()
```

Whether these nodes hold a form at all.

The rail's passing rows are conditional on it: "every form reaches a handler" is not a tick a page
with no form has earned, it is a check that never ran — and on a section that compiles no form it
is not a check at all.

# `receiver`

```elixir
@spec receiver(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), map() | nil) ::
  :code | :plugin | :action | :none
```

What receives this form's submissions, the check `form-no-destination` makes:

  * `:code` when a handler `record` stores answers the form's submit event
    (`PageForms.answered?/2`) with code that is not the untouched default
    (`LiveViewCompiler.untouched_handler?/3`);
  * `:plugin` when no such handler does and a plugin listens on `page:form:submitted`;
  * `:action` when the form posts to its own `action`: with no submit event, or once its answers
    are checked (`PageForms.sends_elsewhere?/1`);
  * `:none` otherwise.

# `repair`

```elixir
@spec repair(map(), String.t(), String.t() | nil) ::
  {:op, map()}
  | {:ops, [map()]}
  | {:say, String.t()}
  | {:code, String.t(), String.t()}
  | :none
```

The ops that fix `rule` on `node_id`, the sentence to show when there is nothing this can edit, or
`:none`.

`{:ops, ops}` is a run applied as one history entry; `{:op, op}` is a single edit; `{:say,
sentence}` is a refusal the author reads, for the fixes whose answer is not in this document at
all. A rule this module does not answer for, and a node that is no longer in the tree, both give
`:none` — the caller falls back to the assistant, which is the right answer for a stale click.

# `repair`

```elixir
@spec repair(map(), String.t(), String.t() | nil, String.t() | nil) ::
  {:op, map()}
  | {:ops, [map()]}
  | {:say, String.t()}
  | {:code, String.t(), String.t()}
  | :none
```

The ops one of a finding's `fixes` makes, when it offers more than one: `"typing"` on
`form-no-submit` makes the form work as they type (`FormPlacement.how_ops/3`). Any other fix is
`repair/3`'s.

# `rule?`

```elixir
@spec rule?(term()) :: boolean()
```

Whether `rule` names one of this module's findings.

Fix on such a finding is answered by `repair/3`; Fix on any other is handed to the assistant.

---

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