# Surfex specification
<!-- surfex: surfex -->

Surfex keeps a specification, its tests and its code aligned. It reads source without
compiling it, versions every spec section, test and public definition, and keeps an
append-only log of which versions someone confirmed belong together (§11–§15). When
either side changes, the relation dangles until it is confirmed again. It also renders
**surface goldens**: markdown documents that CI regenerates and byte-compares, so a
document cannot disagree with the code without the build failing (§2, §10).

This document is the specification of Surfex itself. Surfex holds it to its own code and
tests with its own relation log: every public module and function implements a section,
every test hint is verified by a test, and every name cited here exists.

## 1. Scope and policies
<!-- surfex: scope -->

### 1.1 What Surfex is for
<!-- surfex: purpose -->

Surfex is a build-time tool. A project takes it as a dev/test dependency
(`only: [:dev, :test], runtime: false`), so it never reaches that project's consumers.

It serves two uses:

1. **Keeping a spec, its tests and its code aligned** (§11–§15). The spec, the tests and
   the code are scanned into versioned records. A log records which versions were
   confirmed to belong together, and `mix surfex.status` checks it: nothing changed
   since it was confirmed, every item described or deliberately excused, and every name
   the spec cites exists (§6–§8).
2. **Surface goldens** (§2, §10). A project scans its own source for whatever it
   catalogues and renders a golden with `Surfex.Golden`. The scanner is the project's; the
   notation is Surfex's.

How precisely Surfex can relate a spec depends on how the spec is written.
`guides/writing-specs.md` (§16) says how to write one that it relates well.

### 1.2 Policies
<!-- surfex: policies -->

- **No dependencies.** `deps: []`, stdlib only. Anything Surfex depends on, every project
  that takes it inherits.
- **Never compile what is scanned.** A gate built on Surfex runs before, and
  independently of, the code it inspects, so it cannot be fooled by a module that failed
  to build.
- **No project-specific code.** Surfex knows no project's vocabulary. What differs between
  projects is data: `.surfex.exs`, read as a `Surfex.Profile` and the log's own keys.
- **Goldens are a pure function of source.** No timestamps, dates or VCS data ever
  appear in one. A date makes a golden drift against itself on a quiet day.
- **Output never follows input order.** Every report, golden and suggestion is ordered
  by what it reports, not by the order its inputs came in: the log's lines arrive in
  whatever order a union merge left them, and a file system lists files as it likes. A
  golden that reordered with them would churn, and churn teaches people to regenerate
  without reading.
- **Loud over silent.** Configuration that cannot be right (an unknown key, a pattern
  that matches nothing, a scan that found nothing) raises before anything renders. A
  misconfigured check must never pass.
- **Published text is prose.** The spec, the README and the guides never carry script
  text outside a code fence: a scripted edit that writes its own source into them is
  caught by the tests, not by a reader (#83).
- **The test suite can't race itself.** The working directory belongs to the whole VM, so
  a test module that changes it doesn't run async.

```test:deterministic-output
the status report, the completeness report, a history and the suggestions are the same
whether the scans and the log's lines come in one order or another
```

## 2. Surface goldens
<!-- surfex: goldens -->

`Surfex.Golden` is the one renderer. `Surfex.Golden.render/1` takes a spec (a map) and
returns the golden's text.

```test:golden-renders
a golden spec renders to markdown: a header block naming what regenerates it, the prose,
the stats lines, and a table per group of typed cells; one spec always renders one text
```

### 2.1 Shape
<!-- surfex: golden-shape -->

1. **Header block**, three contiguous lines: `# <name>`, the one-line purpose, and the
   attribution ``Generated by `mix <task> --write` · gate `<gate>` (HARD|ADVISORY) — do not
   edit; a drift FAILS the gate.`` Free prose may follow.
2. **Stats lines**, each `**<lead>** · <label> <value> · …`.
   `Surfex.Golden.stat/2` builds one as data (lead, dimensions and rendered text), so a
   report can read the counts back without parsing markdown.
   `Surfex.Golden.stat_line/2` renders just the text.
3. **Tables.** The first column is the catalogued item and the last is conventionally
   `Locus`. A golden is either one flat table or `## <heading>` groups, each with its
   own table and optionally its own columns.

```test:golden-stats
a stats line reads **N noun** followed by each label and count, and a header block
names the golden, its purpose, the task that regenerates it and its gate
```

### 2.2 Cells are typed
<!-- surfex: golden-cells -->

A caller never writes notation. Each cell is typed, and the renderer formats it:

| Cell | Renders as |
|---|---|
| `{:code, text}` | `` `text` `` |
| `{:atom, a}` | `` `:a` `` |
| `{:locus, file, line}` | `` `file:line` ``; a `nil` line gives `` `file` ``; a `nil` file gives `—` |
| `{:version, hash}` | `` `hash` ``; `nil` gives `—` |
| `{:raw, text}` | `text` verbatim |
| `:absent` or `nil` | `—` |
| a string | the string verbatim |

Every backticked cell is a GFM code span that reads back as its text, whatever the text
holds. GFM splits a table row on `|` before it reads a code span, so each `|` is escaped
as `\|`. The delimiter is one backtick longer than the longest run of backticks in the
text. One space pads each side when the text starts or ends with a backtick, or starts
and ends with a space without being all spaces. surfex's own goldens build their
composite `{:raw, text}` cells by the same rule, and escape the `|` in their free text.

```test:golden-code-spans
a backticked cell holding a pipe, backtick runs or only backticks, split as a GFM table
row and read back by GFM's code-span rule, gives its text
```

### 2.3 Determinism
<!-- surfex: golden-determinism -->

Rendering is invariant under the order of a row map's keys and of the row list. Rows are
sorted by the spec's `:sort` function, or by default by the first column's natural key
(`Surfex.Golden.natural_key/1`: alternating text and integer chunks, so `Foo.9` sorts
before `Foo.10`). Two specs differing only in those orderings render identical bytes.

```test:golden-order
rows come out in natural-key order whatever order they went in, and a golden never
carries a date, a time or a commit
```

## 3. Reading source
<!-- surfex: reading-source -->
<!-- surfex: class informative: names the module; its primitives are specified in 3.1 to 3.3 -->

`Surfex.SourceScan` holds the primitives every scanner of Elixir source shares.

```test:source-not-compiled
source is read as text and parsed, never compiled: code no compiler would accept is
still read
```

### 3.1 Finding source
<!-- surfex: finding-source -->

- `Surfex.SourceScan.project_root/2` ascends from a starting directory to the first
  directory containing the given marker (`"mix.exs"` for a library), falling back to the
  starting directory. `Surfex.SourceScan.project_root/1` starts from the working
  directory. The marker has no default, because a default would be silently wrong for
  some layout. The starting directory exists so that nothing has to change the working
  directory, which belongs to the whole VM, to ask.
- `Surfex.SourceScan.lib_sources/1` lists the `.ex` files of every first-party `lib/`
  under a root, sorted. It serves a library and a multi-project tree alike, with nothing
  to configure. A `lib/` is first-party when:
  - **its parent has a `mix.exs`**, so it is a Mix project's own source, and not a
    `lib/` that a fixture or scratch tree happens to contain
  - **no directory between the root and it is `deps`, `_build`, `test`, `tmp`, or
    hidden.** Dependencies are Mix projects too, and so are some test fixtures, so the
    first rule alone is not enough.

  A looser rule would let the scratch trees a test run leaves behind into a golden,
  which would then drift on that machine and never in CI.

```test:finding-lib-sources
a project's root is found from a marker file; lib sources are the .ex files under a lib/
whose parent has a mix.exs, skipping deps, _build, test, tmp and hidden directories
```

### 3.2 Modules and definitions
<!-- surfex: definitions -->

- `Surfex.SourceScan.defmodules/1` returns every `defmodule` node in a quoted AST, in
  source order, nested ones included.
- `Surfex.SourceScan.hidden_module?/1` says whether a module declares `@moduledoc false`.
- `Surfex.SourceScan.line_range/1` gives the first and last source lines a node spans,
  from its metadata (parsed with `token_metadata: true`). Metadata never reaches a hash, so
  a line range is where something is, never what it is.
- `Surfex.SourceScan.defs/1` returns a module's **public definitions**, from its own body
  only (a nested module's definitions are that module's):
  - public means `def`, `defmacro`, `defdelegate` and `defguard`
  - excluded: `defp`, `defmacrop`, `defguardp`; anything under `@doc false`; an `@impl`
    callback with no `@doc` of its own; a definition whose name is computed
  - clauses are grouped by name and arity, and a later clause takes the first clause's
    visibility
  - a default argument declares every arity it generates
  - each function carries one hash over what it depends on (§3.3)

```test:public-definitions
a module's public definitions are listed with name, arity and kind; a default argument
declares each arity; @doc false and implicit @impl callbacks are hidden
```

### 3.3 Content versions
<!-- surfex: content-versions -->

`Surfex.SourceScan.definition_hash/1` is a definition's version: the first 8 hex characters
of a SHA-256 over its AST with **all** metadata removed. It accepts a quoted node or a
source string, and both give the same result for the same code.

Because metadata is gone, the hash is a function of structure. A blank line, a comment,
or re-indentation above a definition leaves its hash unchanged; changing its body changes
it. A line number reports movement, and this hash reports change. That is what lets a
golden locate items by path and version, not `file:line`, so two people editing
different items touch different rows.

A public function's version, from `Surfex.SourceScan.defs/1`, covers what the function
depends on, not only its own clauses. A change made through a helper is a change to the
function:
- **its own clauses**, with variables renamed by order of first appearance, so renaming
  a variable is not a change
- **every private definition it calls**, transitively: direct calls, piped calls
  (`x |> fee()` is `fee/1`), local captures (`&fee/1`), and calls that rely on default
  arguments. Recursion terminates.
- **the values assigned to every module attribute** it, or one of those callees, reads

Calls to other modules, and to the module's own public functions, are not followed:
those have their own rows and their own versions.

A public type's version, from `Surfex.SourceScan.types/1`, is the hash of its declaration
alone: changing one type changes it and nothing else's.

**A version covers definitions, not their documentation.** A function's or type's
version leaves out its `@doc` or `@typedoc`, so rewriting a docstring dangles nothing, and
surfex doesn't hold a published docstring to the spec: a `@doc` that states a contract
(its return values, its errors) can drift from the spec without any relation noticing.
Read the two together when either changes. A module's version does cover its
`@moduledoc` (above). Holding docstrings to the spec is planned with claim-level relations.

A module's version, from `Surfex.SourceScan.module_hash/1`, covers its **public surface**,
not its whole body:
- its `@moduledoc`
- its public definitions, as name, arity and kind
- its `@behaviour`s and `use`s
- its `defstruct` or `defexception` fields
- its `@type`, `@opaque`, `@callback` and `@macrocallback` declarations

Each list is sorted, so reordering is not a change. Function bodies, private definitions
and nested modules are left out: each public function has its own version, so a body edit
changes that function's version and not its module's, and a relation to the module
doesn't dangle for it.

```test:structure-versions
a definition's version changes with its structure and not with its position or layout;
a function's version follows its private helpers and the attributes they read; a
module's follows its public surface only
```

**An item's version covers everything it runs that isn't an item of its own.** A module
hides some of its public definitions from its docs, `@doc false` ones and `@impl`
callbacks with no `@doc` (§3.2), so they aren't items and have no version of their own.
The Elixir scanner (§5.2) folds them into the versions of what runs them, so changing one
dangles the relations that depend on it:
- **A module's version** covers its hidden definitions, with the private definitions they
  reach and the attributes they read: a GenServer's `handle_call/3` is part of its
  module's version. A module with no hidden definition keeps exactly the version
  `Surfex.SourceScan.module_hash/1` gives.
- **A public function's version** covers the hidden definitions it runs: those it calls
  locally, and, when it **dispatches through its own behaviour**, the module's hidden
  `@impl` callbacks. A function dispatches when it, or a private definition it reaches,
  calls a function of a module this one `use`s or declares as its `@behaviour`, an Elixir
  alias or an Erlang module (`GenServer.call/3`, `GenServer.start_link/3`,
  `:gen_statem.call/2`). So a GenServer's `commit/3`, which calls `GenServer.call`, covers
  its `handle_call/3`, and `start_link/1` covers its `init/1`. It is read from source and
  errs towards covering more, never less: a call to *another* server through the same
  behaviour counts too. Callbacks are known by their `@impl`.
- A documented callback is an item with its own version, so it isn't folded into anything;
  its own relations dangle when it changes.

**A hidden module's code is versioned in what runs it, wherever that is.** A module under
`@moduledoc false` has no items (§5.2), so none of its definitions, shown or hidden, has a
version of its own. Each item's version also covers the hidden modules' code it runs: a
function's, the code it calls, or that the hidden definitions it covers call; a module's,
the code its hidden definitions call. It follows that code in any module and any file,
transitively, through the private definitions it reaches and the attributes they read.
It stops at code an item versions: a public definition has its own version, and a shown
module's hidden definition is in its module's. So changing a helper module a Mix task's
`run/1` hands its work to dangles the task's relations, and changing the public function
the helper calls dangles only that function's. Code nothing public runs is in no version.
An item running no hidden module's code keeps the version above.

`Surfex.SourceScan.module_hash/1` and `Surfex.SourceScan.defs/1` are unchanged by this:
they are library functions a project's own goldens use, and the scanner composes the item
versions from them.

```test:hidden-versions
changing a hidden callback, or a private helper it reaches, changes its module's version
and that of each public function that dispatches through the module's behaviour or calls
it, and no other function's; a @doc false definition is part of its module's version; a
module with no hidden definition keeps its surface version
```

```test:hidden-module-versions
changing a hidden module's function, a private helper it reaches, or a hidden module it
calls changes the version of each public function and module running it, in any file,
and no other; changing a public function it calls changes that function's alone; a
hidden module has no items, and a module nested in it is scanned
```

## 4. Items
<!-- surfex: items -->

`Surfex.Item` is one thing a scanner found: the unit a citation resolves to, and the
source of a code record (§11).

| Field | Meaning |
|---|---|
| `kind` | the scanner's category (`:module`, `:function`, `:wire_field`, …) |
| `name` | unique within its parent, or globally when it has none |
| `file` | where it is declared, relative to the scanned root |
| `hash` | its content version (§3.3) |
| `value`, `detail` | a declared value, or scanner-specific extra, shown in the `Value` column |
| `parent` | the enclosing item's key, for a member |
| `type` | the key of the item a member is an instance of (a field holding a struct) |
| `aliases` | other names that cite it (§6.4) |
| `lines` | its first and last line in `file`, when the scanner knows them: never part of its key or its hash |
| `shape` | `true` for an item with no behaviour of its own (a type): no test run exercises it, so an `implements` relation to it is validated by judgement (§18). The scanner says so; surfex never infers it from a kind |
| `calls` | the keys of the code it calls, as far as its source says (§5.2). A fact about the source, never part of its key or its hash. Empty when the scanner doesn't know: the item is then reached only by a test calling it (§13.1) |
| `hidden` | code the item covers that isn't an item of its own (a module's hidden definitions, §3.3), keyed as a call names it (`Mix.Tasks.Foo.run/1`), each with its calls. Its version is in the item's; a call naming it reaches the item and goes on through its calls (§13.1). Empty for most items |
| `runs` | the hidden modules' code the item runs (§3.3), keyed as a call names it, each with its calls. It is no item's own: its version is in each item running it, and each carries the same entry. A call naming it reaches no item and goes on through its calls (§13.1). Empty for most items |
| `dispatch` | on a module, the dispatch functions declared in it (§11.10), each with what its table lists: `%{targets, unscanned}`. Never part of its key or its hash. Empty for every other item |

`Surfex.Item.key/1` is an item's identity: `parent.name` for a member, otherwise `name`.
The hash is never part of the key, so an edit changes a row's version and not its
identity.

```test:item-key
an item's key is its name, or parent.name when it has a parent
```

### 4.1 DSL items
<!-- surfex: dsl-items -->

In a project built with a DSL, much of the behaviour a spec describes is declared in
blocks inside a module (an action, a policy, a calculation) rather than written as `def`s,
so no Elixir item names it. Such a block is a **DSL item**: a `Surfex.Item` like any other,
given by a project scanner for the DSL beside the Elixir scanner's items (§5.3). Surfex
depends on no DSL: the project, or a companion package, writes that scanner with the
supported library API (§23). The contract fixes what it fills, so citations, relations,
reach, evidence and the triangle work for DSL items unchanged:

| Field | A DSL item's |
|---|---|
| `kind` | the DSL's own (`:action`, `:policy`, `:calculation`, …), read only for grouping and a profile's rules |
| `parent` | the module that declares the block |
| `name` | the DSL's name for the block, so its key is `Module.name` (§4). It is unique within the module and stable across edits that rename nothing: a block the DSL leaves unnamed takes a name from its own text (what it applies to), never from its position, or inserting a block renames its siblings |
| `file`, `lines` | the file declaring it, and the block's first and last line |
| `hash` | `Surfex.SourceScan.definition_hash/1` over the block's AST, so a change to the block changes its version and no sibling's, and moving it changes neither |
| `calls` | the keys of the functions and modules the block references (the changes, validations and preparations it names), so reach goes on from the block into them (§13.1). A call that drives the block, through the DSL's entry or a function it generates, is the scanner's to resolve to the block's key (§5.4) |
| `shape` | `true` for a pure declaration with no behaviour of its own, such as an attribute |
| `hidden`, `runs` | empty. **A DSL item carries no hidden definitions and runs no hidden code** (§3.3): its block is its whole definition and its `hash` covers it, and the code it uses has items of its own, reached through `calls` |

A key written as Elixir names a function without its arity (`MyApp.Shop.Order.checkout`)
is read under the Elixir scanner's profile (§5.2) with no profile keys of its own, and a
citation of it resolves as any item's key does (§6.4), so a subject section and a
contract fence can name a DSL item too. A key is an identity: a block named as a function
of its module (`checkout` beside `def checkout/1`) makes a citation of the name ambiguous,
and a key another scanner also gives is an error (§5.3).

```test:dsl-item-shape
a fixture DSL's scanner gives one item per block, keyed Module.name, with its kind, parent,
file and lines and no hidden code; its hash changes with the block, not with a sibling or
the layout; an action lists what its block references; a declaration is a shape; beside the
Elixir scanner its key is a code record and a citation of it resolves
```

## 5. Scanners
<!-- surfex: scanners -->

### 5.1 The contract
<!-- surfex: scanner-contract -->

`Surfex.Scanner` is the behaviour a scanner implements: `items(root, opts)` returns every
item under `root`. A scanner reads source and never compiles or loads it. The contract
is language-agnostic: a project whose code is in C implements it for C, and the rest is
unchanged.

```test:scanner-behaviour
a project scanner implements Surfex.Scanner, and a module that doesn't is refused,
named
```

### 5.2 The built-in Elixir scanner
<!-- surfex: elixir-scanner -->

`Surfex.Scanner.Elixir` implements it for Elixir.
`Surfex.Scanner.Elixir.items/2` (and `Surfex.Scanner.Elixir.items/1`, with default
options) returns:

| Item | Kind | Key | Aliases |
|---|---|---|---|
| a module | `:module` | `MyApp.Cart` | none |
| a public function (§3.2) | `:function` | `MyApp.Cart.add/2` | `MyApp.Cart.add` |
| a public macro or guard | `:macro` | `MyApp.Cart.is_cart/1` | `MyApp.Cart.is_cart` |
| a public type (`@type`, `@opaque`) | `:type` | `t:MyApp.Cart.t/0` | `MyApp.Cart.t` |

- A module under `@moduledoc false` is no item, and neither are its functions. A module
  nested in it is, since Elixir documents it separately. Its code is hidden code: each
  item running it lists it in `runs` (§4), `Module.fun/arity` with its calls, and has it
  in its version (§3.3). A test importing it is resolved against the definitions listed
  so, as against a scanned module's (§11).
- A type is keyed as ExDoc writes it, `t:` first, so a function of the same name keeps its
  own key; the alias it shares with that function cites both, as a family. A `@typep`,
  and a type under `@typedoc false`, is not public. A type is an item like any other: a
  project whose `require:` asks code for a relation relates its types, or excuses them
  by a class (`classes: [{"type", reason}]`, `rules: [%{class: "type", kinds: [:type]}]`).
  A type is a shape (§4): a spec section citing it relates to it by judgement (§18).
- A nested module is keyed by its full name, as Elixir names it.
- **A function or macro lists what it calls** (`calls`): the functions its clauses and the
  private helpers they reach call, as `Module.fun/arity`, and every module they name, as
  a test's calls are read (§11). A local call to one of the module's own public
  definitions is `Module.fun/arity`, and a `defdelegate` calls its target. Every arity of
  a function with default arguments lists the same calls. A module and a type list none.
- **A hidden definition is listed where it is called and kept on its module** (§3.3). A
  local call to one is `Module.fun/arity`, like a call to a public definition, and a
  function dispatching through its own behaviour calls the module's hidden callbacks. The
  module item's `hidden` (§4) lists each hidden definition, `Module.fun/arity`, with its
  calls, so reach goes through it.
- The `:paths` option (globs relative to the root) defaults to `lib/**/*.ex`: one
  project's own source. A poncho lists its members.

`Surfex.Scanner.Elixir.profile_defaults/1` gives the profile settings reading an Elixir
project's citations starts from, for its **roots**: `namespace:`, and every top-level
module the code scanner found (a Phoenix app's `MyAppWeb` beside `MyApp`), since the code
says what the project's names are (`Surfex.Status.Config.profile!/3`):
- `shape`: the roots' module and function names, with or without an arity
- `token`: the same names, found inside longer text. A name ends where a name ends: a
  root glued to more letters (`MyAppCollector`), a hyphen (`MyApp-Profile`) or a further
  segment that isn't a name is another word, not a citation of the root
- a `normalise` rule that drops a call's arguments, so `MyApp.Cart.add(cart, item)` names
  `MyApp.Cart.add`, and one that reads ExDoc's `t:MyApp.Cart.t/0` as `MyApp.Cart.t`: a
  type cited as `MyApp.Cart.t()` or `t:MyApp.Cart.t/0` resolves through its alias

A well-formed name under any root that resolves to nothing is an unresolved citation
(§6), so a stale name in the spec is reported rather than matched to a shorter one. A
name under no root (another library's, `Ecto.Changeset`) is not the project's to cite
unless `known_external:` says so.

```test:name-boundary
a token ends at a name's end, so a root glued to letters, a hyphen or a non-name segment
is no citation of it; every top-level module the code has is a root, so a stale name
under any root is unresolved rather than suggested against the root module
```

```test:elixir-scanner-items
the Elixir scanner reports every public module, definition and type under the paths it
is given, with nested modules named in full, and profile defaults for a namespace
```

```test:hidden-module-reach
an item running a hidden module's code lists it with its calls; a call naming it reaches
what it calls and no record for it, through other hidden modules, and a cycle ends
```

```test:code-calls
a function lists the code it calls: remote, aliased, piped and captured calls, through
its private helpers, a local call to the module's public function, and a delegate's
target; every default arity lists the same calls; a module lists none
```

```test:type-citations
a public type is cited as `Mod.t()` or `t:Mod.t/0` and resolves when declared; one not
declared, or private, is unresolved; a function of the same name keeps its own key
```

### 5.3 Several scanners
<!-- surfex: several-scanners -->

A project may have more than one kind of declaration worth an item: Elixir functions, and
the blocks a DSL declares inside its modules. `scanners:` in `.surfex.exs` lists the code
scanners, and their items are combined. Each entry is `:elixir` (the built-in scanner,
§5.2), a module implementing `Surfex.Scanner`, or either with its options,
`{scanner, opts}`:

```elixir
scanners: [:elixir, {MyApp.Surfex.DslScanner, paths: ["lib/**/*.ex"]}]
```

- Each item keeps what its scanner gave it: its `file`, `lines`, `hash` and `kind`.
  Surfex reads `kind` only for grouping and a profile's rules (§4), so two scanners'
  kinds need not know about each other.
- `scanner:` with `scanner_opts:` stays valid and is the one-element list
  `[{scanner, scanner_opts}]`. With neither, the list is `[:elixir]`. Giving both
  `scanner:` and `scanners:` is refused, as are an empty list, an entry of any other
  shape, a scanner named twice, and `scanner_opts:` beside `scanners:`.
- **Two scanners giving one key is an error naming both and the key.** A key is an
  item's identity (§4), and two scanners cannot both own it. Within one scanner, items
  sharing a key are told apart by kind, as before (§11).
- A task compiles the project before reading the code when any scanner is the project's
  own, and reads citations under the Elixir scanner's profile defaults (§5.2) when
  `:elixir` is among them.

`Surfex.Status.Config.scanners!/1` reads the list, and `Surfex.Status.Config.items/2` runs
it. `Surfex.Status.Config.project_scanner?/1` and `Surfex.Status.Config.elixir_scanner?/1`
say whether the project's own scanner, or the Elixir one, is among them.

```test:scanners-combined
the items of several scanners are combined, each keeping its scanner's file, lines, hash
and kind; scanner: is a one-element list; both keys, a malformed list or a scanner named
twice is refused; one key from two scanners raises naming both
```

### 5.4 Calls a scanner resolves
<!-- surfex: scanner-calls -->

A DSL item has no function name a call could carry: code reaches it through the DSL's
entry functions, naming the module and the block as arguments
(`MyDsl.run(MyApp.Shop.Order, :checkout, input)`), or through functions the DSL generates
for it. A call's id is `Module.fun/arity` (§11), so without help a test driving an action
reaches the entry and never the action. `Surfex.Scanner` has an optional callback for it:

```elixir
@callback calls(call :: Macro.t(), aliases :: %{atom => String.t()}, opts :: keyword) ::
            [item_key :: String.t()]
```

- The test scanner (§11) and the Elixir scanner (§5.2) ask each of the project's scanners
  that implements it (§5.3) about **every remote call they read**, and add the keys it
  answers to the caller's `calls`, beside the call's own `Module.fun/arity`. A scanner
  without it is never asked, so it changes nothing.
- `call` is the call's AST as written, except that a piped call has its piped value as
  its first argument, as it runs. A capture (`&MyDsl.run/3`) calls nothing and is not
  asked about. `aliases` maps each alias in scope at the call to the module it names, so
  the scanner resolves the module argument itself; `opts` is its own configuration.
- An answer is a list of item keys. A key no item has reaches nothing, as any call to
  unscanned code does. An answer of any other shape raises, naming the scanner.

So a test that drives an action reaches the action item, and through the item's own
`calls` the code its block references (§13.1). Reach, evidence (§17), the triangle and the
suggestions (§15) then follow for DSL items with no special case.
`Surfex.Status.Config.call_scanners/1` gives the scanners asked. The test scanner takes
them as `Surfex.Scan.ExUnit.records/4` and `Surfex.Scan.ExUnit.tests/4` do, and
`Surfex.Status.Config.items/2` hands them to `Surfex.Scanner.Elixir.items/2` (its
`:call_scanners` option); `Surfex.ExUnitFormatter` asks them when it records a run.

```test:dsl-calls
a test's call through a DSL's entry, piped or not, names the action a scanner's calls/3
answers, and a capture names none; a function's call does too; reach goes on through the
action's calls; a run records the action's version; the triangle closes for a claim
related to it, and doesn't without calls/3; a malformed answer raises naming the scanner
```

## 6. Citations
<!-- surfex: citations -->

`Surfex.Cite` finds a specification's citations of the code and resolves them.
`Surfex.Cite.citations/3` returns every citation in the profile's sources, sorted by
file, line and span.

```test:citation-status-kinds
each citation ends in exactly one status: resolved, ambiguous, unresolved, external or
a documented absence
```

### 6.1 No new notation
<!-- surfex: citation-notation -->

A specification already cites code in prose: a code span naming a function, a struct, a
constant or a file. Those spans are the machine-readable form. **A span is a citation
when it resolves.** A span that does not resolve but has the profile's shape (a claim
about the code) is a broken citation. Anything else in backticks is prose.

### 6.2 Statuses
<!-- surfex: citation-statuses -->

Every citation has exactly one status:

| Status | Meaning |
|---|---|
| `:resolved` | it names one item, or one family (§6.4), or one item per step of a member path (§6.5) |
| `:ambiguous` | it names several unrelated items. Reported, never guessed |
| `:unresolved` | it has the shape of a claim about the code and names nothing |
| `:external` | it names something real outside the scanned tree (`known_external`) |
| `:documented_absence` | a section names it *because* the code lacks it (`documented_absences`) |

### 6.3 What is read
<!-- surfex: citation-reading -->

- `Surfex.Cite.sources/2` lists the source files: the profile's globs under a root, minus
  its exclusions, de-duplicated and sorted.
- **Code spans**, line by line, credited to the nearest heading above them (a markdown
  heading, or the `defmodule` in an `.ex` source), or `(preamble)` before the first. A
  heading's anchor (`{#id}`, §11) is not part of its name.
- **Subject headings** (§6.6) cite the items their section is about.
- **Table cells** under the profile's `table_columns`, backticked or not. A cell holding a
  code span is already cited as one.
- **Fenced code blocks**: shape-matching tokens are credited to `(code block)`. Inside a
  fence nothing else is read, so a `# comment` in a code block is not a heading.

```test:citation-sections
a citation is credited to the nearest heading above it (or the defmodule in an .ex
source); inside a fence nothing but shaped tokens is read; sources are globbed, excluded
and sorted
```

### 6.4 Resolution
<!-- surfex: citation-resolution -->

A span is normalised (trimmed, then the profile's rewrites in order) and then resolved
by the first rule that applies:

1. a documented absence for this name in this file
2. a known external name
3. a member of a subject of an enclosing section (§6.6)
4. an item key (`Surfex.Cite.index/2`): one item resolves, several are ambiguous
5. an alias: every item sharing it is cited together as a family
6. a member path (§6.5)
7. the profile's shape: unresolved
8. otherwise, the tokens inside the span that have the shape, or the `known_shape` and
   resolve, each cited in its own right

An alias differs from a shared key. Two items sharing a key are two unrelated things with
one name, so the citation is ambiguous. Items sharing an alias are a family on purpose:
citing `MyApp.Cart.add` means the function, whatever its arity.

A member is reached through its parent (`struct.field`), a subject, or a member path,
**never by its bare name**: a bare `offset` naming three structs' fields is not a claim
about any one of them.

```test:citation-resolves
a span resolves through absences, externals, subject members, item keys, aliases and
member paths, in that order; a bare member name never resolves on its own
```

### 6.5 Member paths
<!-- surfex: member-paths -->

`field.member` resolves the longest prefix that names an item, then each further step as
a member of the previous item's `type`, or of the item itself when it has no type. Every
step is cited. A step that does not name exactly one item makes the path fall through
to the shape rule.

### 6.6 Subject sections
<!-- surfex: subject-sections -->

A profile subject (a file, a heading pattern, item keys) declares that a section is
*about* those items. Its heading cites them. A bare member name anywhere in the section,
until the next heading of the same or higher level, resolves as that item's member, and
inside such a section the member wins over a global of the same name. A subject key
that names nothing makes the heading's citation unresolved. A contract fence (§11.1) in a
subject section is about the subject's one item where nothing nearer names a module (§11.9).

## 7. Coverage
<!-- surfex: coverage -->

`Surfex.Coverage.verdict/3` adjudicates an item nothing describes: is the silence
expected, or is it a gap? The relation log asks it when suggesting an `excuses` relation
(§15) and when judging whether an excuse is stale (§13). Given the set of item keys that
count as cited (described), the verdict is:

- `:cited` when the item's key is in it
- a **gap** for a `never_excused` kind that is not cited, whatever the rules say
- otherwise `{:expected, class}` for the first rule that matches (kinds, an optional name
  pattern, and optionally "only while its parent is cited")
- otherwise a **gap**

Rules are by class, never by item. A new helper falls into its class and stays quiet,
while a new entry point matches no rule and is a gap. Each class carries its reason, so
silence is always attributable. `Surfex.Coverage.verdict/3` reads only the rules and the
`never_excused` kinds: a whole `Surfex.Profile`, or the map `Surfex.Profile.coverage!/1`
gives.

**A verdict belongs to an item, not to a key.** Two items may share a key (a citation of
it is then ambiguous), and each is judged on its own.

```test:coverage-verdict
an item is cited, excused by the first rule that matches, or a gap; a never-excused
kind is a gap whatever the rules say; the rules can come from a profile or its coverage
map
```

## 8. Profiles
<!-- surfex: profiles -->

`Surfex.Profile` is everything project-specific about reading a spec's citations and
excusing code, as data with no functions. `Surfex.Profile.new!/1` builds one and validates
it, and `Surfex.Profile.keys/0` lists its keys:

| Key | Meaning |
|---|---|
| `sources` (required) | globs of the files whose prose cites the code |
| `exclude` | path prefixes to drop from them: an excluded file is no part of the spec, contributing neither sections nor citations (`Surfex.Scan.Markdown.files/3` is the one answer to which files are the spec) |
| `shape` (required) | regex: what a claim about the code looks like |
| `token` | regex whose first group finds names inside longer text |
| `known_shape` | regex: tokens trusted only when they resolve |
| `normalise` | `{regex, replacement}` rewrites of a span |
| `subjects` | `%{file, heading, items}` subject sections (§6.6) |
| `table_columns` | header names whose cells are citations |
| `file_targets` | file names citable in their own right; `:item_files` adds every item's file |
| `known_external` | `%{name => reason}` |
| `documented_absences` | `%{{name, file} => reason}` |
| `classes` | `[{class, reason}]` |
| `rules` | `%{class, kinds, name, parent_cited, parent}` excusal rules; `parent` is a regex on a member's parent module (an item with no parent, such as a module, never matches it), so a module family's members are excused by family |
| `never_excused` | kinds that are the spec's subject matter |

It raises, naming the key, on an unknown key, a missing required key, a wrong type, a
rule naming an undeclared class, and a rule that would excuse a never-excused kind.

A project writes these keys in `.surfex.exs`, alongside the relation log's own (§13.2).
For the built-in Elixir scanner, `shape`, `token` and `normalise` default from the
project's namespace (§5.2).

```test:exclude-both
a file under exclude: contributes neither spec sections nor citations; excluding every
file is an empty spec, which is an error
```

```test:rule-parent
a rule's parent: excuses the members of a module family and no other module's members of
the same name; it never matches an item with no parent; it composes with the rule's other
keys, must be a regex, and a rule without it keeps its class's version
```

```test:profile-validated
a profile rejects an unknown key, a missing required key, a wrong type, and a rule that
names an unknown class or excuses a never-excused kind
```

## 9. The v0.2 trace, removed
<!-- surfex: traces -->

Versions 0.2 and 0.3 had a **trace**: `SPEC_TRACE.md`, rendered from the spec's citations,
giving every item a verdict (cited, excused by a class, or GAP). It was removed in 0.4.0.
It could show that the spec and the code cite each other, but not that anyone reconciled
them after either changed: regenerating the golden cleared its signal. The relation log
records that reconciliation, and each of the trace's checks lives on:

| The trace | The relation log |
|---|---|
| a citation | a suggested `implements` relation (§15) |
| cited | a current relation (§13) |
| excused by a class | an `excuses` relation (§11, §15) |
| GAP | an unmet id under `require:` (§13.1) |
| an unresolved or ambiguous citation | a broken citation, failing the check (§13.1) |
| drift since the golden | a dangling relation, naming the side that changed |

A `.surfex.exs` that still has a trace-only key (`output`, `purpose`, `task`, `gate`,
`hardness`, `item_noun`, `columns`, `groups`, `locus_prefix`, `not_catalogued`, `prose`,
`require_citation`, `file_labels`) raises, naming the keys and why (§13.2). So does a
`:trace` entry in `goldens:`.

## 10. Gating goldens
<!-- surfex: gating -->

### 10.1 The gate
<!-- surfex: gate -->

`Surfex.Gate` is the write-or-check step every golden shares:

- `Surfex.Gate.run/4` writes a golden, or checks the committed file against a fresh
  render. It returns the failures, one message each: a missing file, or a drift naming
  what regenerates it. It returns none when the file is current or was just written.
- `Surfex.Gate.drift/2` compares committed bytes with a fresh render. Identical texts
  give `nil`. Otherwise it names the rows that changed, appeared and went. A table is
  told apart by its `##` heading and its header row. A row is keyed by the cells that
  identify it, read from the table: its `Item` column when that names every row once on
  both sides, else the fewest leading columns that do, else the whole row with its
  occurrence. Rows are compared whole, and a changed row names each cell that differs
  with its old and new text, `State :current → :retired`. Cells are split as GFM splits
  them, on each `|` not escaped as `\|`. A change outside every row is reported as a
  change to prose or a stats line. The committed side is parsed from its bytes, never
  re-derived.
- `Surfex.Gate.config!/1` evaluates a `.surfex.exs`-style data file, which must return a
  keyword list.

```test:gate-drift
the gate writes a golden or checks it: a missing file and a drift fail, naming the rows
that changed, appeared or went, or that only the prose changed
```

```test:gate-drift-rows
a row is keyed by the cells that identify it, so rows sharing a first cell stay apart; a
hand-edited cell is named under Changed with its old and new text, and an escaped `\|`
stays inside its cell
```

### 10.2 Project goldens
<!-- surfex: project-goldens -->

`Surfex.Surface` is the behaviour for a golden only the project can scan for (the
endpoints a router declares, the actions a model defines): `spec(opts)` returns a
`Surfex.Golden` spec, which is rendered and gated like any golden.

```test:project-golden
a module implementing Surfex.Surface is rendered and gated like any golden, and a module
that doesn't is named
```

### 10.3 Every golden at once
<!-- surfex: all-goldens -->

`Surfex.Goldens` runs every golden a project lists in `.surfex.exs`'s `goldens:` key.
Each entry is one of:
- `:status` or `{:status, output}`: the relation status as a golden (§13.3),
  `RELATIONS.md` by default. Its drift fails here; whether the relations are healthy is
  `mix surfex.status`'s question.
- `:completeness` or `{:completeness, output}`: the completeness report as a golden (§20),
  `COMPLETENESS.md` by default. Its drift fails here; whether the project is complete
  enough is `mix surfex.status`'s question, under `completeness: [min: N]`.
- `{output, module, opts}`: a `Surfex.Surface`

With no key, the list is `[:status]`.

- `Surfex.Goldens.entries!/1` validates the list. It raises naming an entry that is
  neither form, the removed `:trace` entry (§9), an empty list, and two entries writing
  one file.
- `Surfex.Goldens.needs_compile?/2`: only project code needs the project compiled, meaning
  a surface module, or a project scanner the status reads.
- `Surfex.Goldens.run/6` renders every entry, writes or checks each, and returns every
  drift in entry order. A module that does not implement `Surfex.Surface` raises, naming
  it.

```test:goldens-entries
goldens: defaults to [:status]; each entry is validated, :trace is refused with its
reason, and every golden's drift is reported in one run
```

### 10.4 The task
<!-- surfex: golden-tasks -->

`Mix.Tasks.Surfex.Goldens` is `mix surfex.goldens`:

- Without `--write`, the task checks. It fails if any golden is missing or has drifted.
- `--write` regenerates. It writes every golden, drifted or not, so a drift shows up in
  the diff, and succeeds, saying how many it wrote. Regenerating is how a drift is
  resolved, so it never fails for one; checking is what fails.
- `--config PATH` reads a definition other than `.surfex.exs`.
- The namespace citations are read under defaults to the app's name camelized. The task
  compiles the project only when `Surfex.Goldens.needs_compile?/2` says so.
- Every failure of every golden is reported in one run.

Surfex gates itself this way: CI runs `mix surfex.goldens`, which gates `RELATIONS.md`,
and, after its tests, `mix surfex.status --verify --evidence --validated`, which checks
the relation log itself (§13.3, §17): every relation holds, every confirmation by evidence
is borne out, and every relation was validated by the process (§18). Surfex's own
triangle is closed and gated (`triangle: :fail`): every section something implements
states its claims as test hints, each verified by a tagged test that exercises the
implementing code.

```test:goldens-task
mix surfex.goldens checks every golden and fails on a drift; --write regenerates them
all, even when some have drifted, and succeeds
```

## 11. Scan records
<!-- surfex: scan-records -->

Sections 11 onwards describe the relation log: pure scanners report what the spec, the
tests and the code are now, and an append-only log records which versions of them
someone has confirmed belong together.

`Surfex.Scan` is a scan record: `{kind, id, hash, location}`, plus `role` and `within`
for spec records.
- **kind:** `:spec` or `:code`
- **id:** the identity: a code item's key (`MyApp.Cart.add/2`), or a spec unit's file
  and name (`spec.md#Carts/Adding items`, `spec.md#cart-add`)
- **hash:** the content version, 8 hex characters
- **location:** the file and first and last lines
- **role:** what part of the source a record is: for spec, `:section`, `:block` or
  `:test_hint` (below); for code, the item's kind (`:module`, `:function`, …); `nil` for
  tests and classes
- **within:** what it sits in: the section or block of a block or hint, or the parent of
  a code item (a function's module); `nil` when nothing encloses it. It is a fact about
  the source, not a relation.

**Scanners only ever produce scan records.** They never read or write relations, and a
record is a pure function of the source. Location is never part of a relation: moving
code, or adding text above a section, changes where something is and not what it is.

`Surfex.Scan.code/1` makes code records from any scanner's items (§5), with the item's
key, version (§3.3) and line range, its kind as `role`, its parent as `within` and its
`calls`.
The id is the item's key. When two items share a key (a C function and a constant both
called `twin`), each id carries its kind, `twin (function)` and `twin (const)`, so each is
a relation end of its own. `Surfex.Scan.for_item/2` finds an item's record whatever its
id. A citation of a shared key stays ambiguous (§6.2). `Surfex.Scan.definition/1` is
what a code record is a version of: its file, lines and version. A function with default
arguments is one definition reported as several items (`f/1`, `f/2`), and they share it. `Surfex.Status.derive/4` raises on
two records with one kind and id, naming both locations: no record may shadow another.

`Surfex.Scan.ExUnit` is the test scanner. It makes one record of kind `:test` per ExUnit
`test`, and per ExUnitProperties `property`, read from source without compiling it.
`Surfex.Scan.ExUnit.records/2` reads every file matching a list of globs under a root,
and `Surfex.Scan.ExUnit.tests/2` scans one text. A property is a test in everything
below: its id, version, tags and calls, and its runs are evidence (§17), found by its
lines as a test's are. Its `check all` generators are part of its body, so weakening one
changes its version.
- **Its id** is the module, the `describe` if any, and the test's name:
  `MyApp.CartTest: adding: rejects a closed cart`. A test defined in a comprehension
  keeps its name as written (`counts #{name}`), since the names it generates are only
  known when compiled. Two tests with one id are told apart as `~2`, `~3`.
- **Its hash** is a function's version (§3.3): its body, the private helpers it calls
  (transitively) and the module attributes they read, with variables normalised. Every
  `setup` that applies to it (the module's and its `describe`'s, including named
  callbacks) and the generators of a comprehension that defines it are also part of it.
  A private helper defined inside a `describe` block counts like one at the module's top
  level, as ExUnit compiles it there.
  Weakening a test through a helper, a setup or its table of cases changes its version.
- **Its location** is the file and the test's lines.
- **What it declares**: a test says what it verifies with ExUnit's own tags.
  `@tag verifies: "id"` (or a list) applies to the next test, `@describetag` to a
  `describe`, and `@moduletag` to a module. Each becomes `{:verifies, id}` in the
  record's `declares`. They combine as ExUnit merges tags: the most specific level that
  sets `verifies` replaces the others, a test's own `@tag` over its `describe`'s over its
  module's, and a tag that doesn't set `verifies` leaves it as it was. A test that also
  verifies its module's requirement names it again in its own list. Since a tag is
  ordinary ExUnit, `mix test --only verifies:id` runs the tests whose tag is that id, and
  surfex records each of them as declaring it. A list is where the two part. ExUnit
  compares a list tag as one string, its ids run together, so `--only` selects a test
  tagged `["a", "b"]` by neither `a` nor `b`, while surfex reads both. A test that
  verifies two requirements is either recorded against both (a list) or selectable by
  `--only` (one id), not both. The record's `unselectable` lists the declared ids its
  runner can't select it by: for a list, each id that isn't the whole list run together
  (so a one-id list is selectable by its id); for a string, none. Status reports them
  (§13.1). A `verifies` value that isn't a literal string or list of strings raises,
  naming the file and line.

```test:unselectable-ids
a test's record lists the declared ids mix test --only can't select it by: every id of a
list of several, none of a string or a one-id list, whichever level's tag it took
```
- **What it deviates from**: a test whose requirement the code is known not to meet yet
  says so with `@tag deviation: "#119"`, beside the ExUnit `skip:` that keeps the suite
  green. The value is a reference to what will fix the code, free text: a project's
  tracker is its own business. It is read at the same levels as `verifies` and merged the
  same way, key by key: a test's own `@tag` over its `describe`'s over its module's, so a
  tag setting only `verifies` leaves an inherited `deviation` as it was. The record's
  `deviation` is the reference, or `nil`. It names no unit itself: the test deviates from
  what it declares it verifies, and status lists those units as deviating (§13.1). A
  `deviation` value that isn't a literal string raises, naming the file and line.

```test:deviation-tag
a test's deviation: tag is read as its reference at its own, its describe's or its
module's level, the most specific level that sets it winning, independently of verifies;
a value that isn't a literal string raises
```

`mix surfex.test UNIT…` runs a requirement's tests as surfex reads them, list members
included. Each UNIT is any reference a declaration may use, resolved as a declaration is
(`Surfex.Scan.resolve/2`). The tests are every test whose declarations resolve to one of
them. With `--within`, they also include every test declaring a unit inside one: a block,
hint or row whose `within` chain reaches it. The task runs `mix test` in a child process
with each test's file and first line (`test/cart_test.exs:12`), which ExUnit's location
filter selects exactly, then any arguments after `--`. It exits with that run's status,
and the run's evidence is recorded as any run's is (§17). A test defined in a
comprehension shares its line with its siblings, so they all run. A unit that is unknown
or ambiguous, or that no test declares, is an error before anything runs: running nothing
would pass in silence. `--list` prints each selected test's id and location, and runs
nothing. `Surfex.Select.tests/3` makes the selection and `Surfex.Select.args/1` the
locations, both pure functions of the scans.

`mix surfex.test --deviations` runs every test that declares a deviation (below,
`Surfex.Select.deviations/1`, a pure function of the scans) in place of a unit's tests,
skips included. A deviation test is skipped beside its tag, and the run must include it
for its evidence to say whether it passes, which is how a stale deviation is found (§17).
A project checks its deviations on demand, or in a scheduled job. ExUnit includes a
skipped test only by a filter on its skip reason, and a location given as `FILE:LINE`
replaces every include, so the run names each test's files, `--only location:FILE:LINE`
for each test and `--include skip:REASON` for each reason among them
(`Surfex.Select.deviation_args/1`). The test record's `skip` is that reason, read like
`verifies`: a literal string, `"true"` for `@tag :skip`, or unreadable when computed. A
reason filter would also run any other test skipped for the same reason, so another test
in their files skipped for a deviation test's reason is an error naming both, as is a
deviation test whose reason isn't a literal: either way the run wouldn't be exactly the
deviation tests. It takes no unit (naming one is an error), and no test declaring a
deviation is an error before anything runs. `--list` and `--` work as for a unit.

```test:test-deviations
mix surfex.test --deviations runs exactly the tests declaring a deviation, with their skips
included, so a skipped one runs and a test skipped for another reason doesn't; --list runs
nothing; a unit with it, no deviation test, another test skipped for a deviation test's
reason, or a reason that isn't a literal, is an error before anything runs
```

```test:test-selection
mix surfex.test selects the tests declaring a unit, a list's members included, and with
--within the tests of the units inside it; it runs them by location, refuses an unknown,
ambiguous or undeclared unit, and --list runs nothing
```
- **What it calls**: the record's `calls` lists the functions the test's body and the
  private helpers it reaches call, as `Module.fun/arity`, and every module it calls or
  names as a value (a module handed to a helper is exercised through it). Aliases are resolved
  where they're declared, as the compiler sees them: a module's apply throughout it, a
  `describe`'s to its tests and the helpers defined in it (not to another describe), and
  one in a test or helper body to that body, from its line on. A
  piped call counts the piped value, and a capture (`&Mod.fun/2`) is a call. A local
  call the module doesn't define comes from one of its imports, `import A` or `import
  Base.{A, B}` (each member), whose `only:` or `except:` admits it. Given the project's
  code records (`Surfex.Scan.ExUnit.records/3`), a call through an import of a scanned
  module is a call to the function that module defines, and names the module too, as a
  qualified call does; one it doesn't define (ExUnit's `assert`) comes from elsewhere
  and isn't listed under it. Through a module that isn't scanned, it is listed as that
  module's `Module.fun/arity`, which reaches nothing. Setups are
  left out: they prepare a test, and what a test tests is what it calls itself. These
  are facts about the source; which of them are code the project has is for
  `Surfex.Suggest` to find (§15).

```test:test-import-calls
a local call resolves through an import in either form, A or Base.{A, B}, as its only:
or except: admits it; through a scanned module only to a function it defines, naming the
module too, and through one not scanned to the candidate; a function captured in a list
is a call
```

```test:property-tests
a property is scanned as a test: its id, its declarations, what it calls, and a version
that changes with its generators
```

`Surfex.Scan.Classes` is the class scanner. `Surfex.Scan.Classes.records/1` makes one
record of kind `:class` per class in `.surfex.exs`. A class names a kind of code the spec
deliberately doesn't describe, and says why (`classes: [{name, reason}]`). Its rules say
which items fall into it (`rules: [%{class:, kinds:, name:, parent_cited:}]`). These are
the profile's keys (§8), validated the same way by `Surfex.Profile.coverage!/1`.
- **Its id** is the class's name.
- **Its hash** covers its reason and every rule naming it (kinds, name pattern,
  `parent_cited`), in any order. Rewording the reason or changing a rule changes it.
- **Its location** is `.surfex.exs`, with no lines, since the config is evaluated data.

It is pure: a function of the config alone. `Surfex.Status.Config.scans/2` includes class
records when `classes:` is present.

A declaration is a claim in the source, not a relation. `Surfex.Scan.resolve/2` finds
the spec unit it names: a full id (`spec.md#cart-add`, `spec.md#Carts/Adding items`), or
a bare anchor, block or hint id (`cart-add`) looked up across every spec file. A bare
id never names a heading path. A `§`-id may be cited with its sign: `§Api.3.2` finds
`Api.3.2`. It gives `{:error, :unknown}` when nothing has the id,
and `{:error, {:ambiguous, ids}}` when more than one file does.
Where many references resolve against one scan (every test's declarations, in a status),
`Surfex.Scan.resolver/1` indexes the spec units once, by full id and by bare id (a
`Surfex.Scan.Resolver`, built by `Surfex.Scan.Resolver.new/1`), and
`Surfex.Scan.resolve/2` takes that resolver in place of the scans
(`Surfex.Scan.Resolver.resolve/2`): each reference is then a lookup, not a pass over every
record, and resolves exactly as it would against the scans.

```test:declaration-resolver
a resolver built once from the scans resolves every reference as the scans do: full and
bare ids, a §-id with its sign, unknown and ambiguous references, and never a heading path
by its bare name
```

`Surfex.Scan.Markdown` is the spec scanner for markdown. `Surfex.Scan.Markdown.records/2`
reads every file matching a list of globs under a root (`Surfex.Scan.Markdown.records/4`
leaves out those under an `exclude:` prefix, §8, and reads headings by `heading_ids:`);
`Surfex.Scan.Markdown.sections/2` scans one text. Both give every unit (sections, marked blocks and test hints) in the
order they start.
- **A section** is a heading and the lines under it, up to the next heading of any
  level. Subsections are sections of their own, so editing one changes only its own
  version. Text before the first heading is the section `(preamble)`, when it has any.
- **Its id** is the one its author wrote, when there is one, and never comes from where
  the section stands. The first of these that the heading has:
  1. **An id comment**, `<!-- surfex: cart-add -->`, on the line after the heading or at
     the end of it (`## Adding items <!-- surfex: cart-add -->`): the id is
     `spec.md#cart-add`. On the line after, the comment is also a block's opening marker,
     and it opens a block, as it always has, when a closing `<!-- /surfex -->` comes
     before the next marker, heading or the end of the file. Otherwise it is the heading's
     id. A heading with an id on both lines is an error.
  2. **An anchor**, `{#cart-add}` at the end of the heading, does the same.
  3. **The project's heading grammar**, `heading_ids:` in `.surfex.exs`, for ids the authors
     already write into their headings: `:section_sign` reads a leading `§ID`
     (`## §Api.3.2 — Limits` is `spec.md#Api.3.2`), `:section_number` a leading
     `1.`, `1.2` or `1.2.3`, and a regex with a named capture `id` anything else. A
     section number's last part may carry a lowercase letter when its dot follows:
     `## 2a. Model checking` is `spec.md#2a`, while `## 3d rendering` is no number. A spec
     that writes ids in more than one form lists its grammars,
     `heading_ids: [~r/^\d+\. (?<id>D-\d+)/, :section_number]`, and the first that matches
     a heading reads it. A heading none matches falls through.
  4. **The path of headings** down to it, `spec.md#Carts/Adding items`. Two sections with
     the same path in one file are told apart as `~2`, `~3`, in order. Only a path is ever
     numbered: a written id that repeats is an error.

  An id that was written survives renaming the heading or any heading above it. The comment
  and the anchor are not part of the heading's text, so subsections' paths don't include
  them; a grammar's id is, since it is the text the author wrote.
- **Its hash** covers its own body, **not its heading**, with runs of whitespace
  collapsed. Rewording the body changes it. Renaming the heading, reflowing a
  paragraph or adding blank lines doesn't, so a renamed section keeps its version
  under a new id, which is what lets a tool recognise the move.
- **Its location** runs from the heading's line to its last non-blank line.
- A line inside a fenced code block is never a heading; the block is body, and counts in
  the section's version. Fences follow CommonMark. A fence opens on three or more
  backticks or tildes, indented at most three spaces (a backtick fence whose info string
  holds a backtick doesn't open). It closes only on a line of the same character, at least
  as long, with nothing after it but whitespace, so a longer fence can quote a shorter
  one. A fence that never closes runs to the end of the file.

Finer units sit inside a section. Each is a record of its own, with its own version,
and is **left out of the version of what it sits in**, so editing one changes only its
own:
- **A marked block** is one requirement: the lines between an opening marker, the HTML
  comment `<!-- surfex: ID -->`, and a closing one, `<!-- /surfex -->`, each on a line
  of its own. Its id is `file#ID`, its role `:block`, and it sits in its section. The
  comments don't render, so the spec reads as prose. A block can't nest in another
  block or span a heading.
- **A test hint** says how to test something: a fenced code block whose info string is
  exactly `test:ID` or `test ID`. Its id is `file#ID`, its role `:test_hint`, and it sits
  in the block or section around it. Its version covers the fence's content. It renders,
  since readers should see test guidance. A `test` fence without an id is an ordinary
  code block.
- **A marked table** holds one claim per row. The line `<!-- surfex: table ID -->`, on its
  own line before a table (blank lines between are fine), makes the table a unit, with id
  `file#ID` and role `:table`. Its version covers the header and the separator row only.
  Each row is a unit too, with role `:row`, sitting in the table:
  - its id is `file#ID/KEY`, where KEY is the row's key cell: the first column, or the
    column `key: Header` names in the marker (`<!-- surfex: table modes key: Mode -->`);
  - KEY is the cell as written, without its code and emphasis marks and with whitespace
    collapsed, so `` `:word` `` is `:word`, and it may hold spaces;
  - its version covers the row's cells, so editing one row changes only that row.

  The table ends at the first line that isn't a table line. A table without the marker is
  body, as before.

  The marker may name its columns further, with options in either order after the id, each
  a header or a bracketed list of them:
  - **`key: [Function, Key]`** keys a row on several columns, for a table whose row is
    identified by a pair: KEY is their cells, each as above, joined by `/`
    (`file#options/render/:width`). No part may be empty, and two rows with one KEY are the
    duplicate-id error.
  - **`ignore: [Measured]`** leaves columns out of each row's version, for cells that record
    a measurement rather than state a claim, so refreshing them dangles nothing. Use it for
    nothing else: an edit to an ignored cell is invisible to every relation. A key column
    can't be ignored. The table's version, its header, still names every column.

  A marker's text after its id must be these options, so a misspelt one is an error, not
  silently body.

**Where the spec renders.** A forge's CommonMark renders every form above as intended, but
for the anchor, which shows as text. ex_doc's markdown (EarmarkParser) renders as intended
only the id comment on the line after the heading, `test:ID` hints, marked blocks and
marked tables. It shows an id comment at the end of a heading, and an anchor, as heading
text, and renders a `test ID` hint (an info string holding a space) as inline text that
swallows the heading after it. A spec published with ex_doc uses the forms it renders.

Written ids, block, hint and table ids, and row ids share one namespace per file with the
section ids. All but a row's KEY are letters, digits and `.` `_` `-` `:`, starting with a
letter or digit. Markers and hint fences inside a fenced code block are text. A spec that
uses none of these scans exactly as it did before they existed. Every problem raises,
naming the file and line:
- an unclosed block or hint;
- a table marker with no table after it or with text that isn't an option, a table with no
  separator row, a `key:` or `ignore:` naming no column, an ignored key column, or a row
  with an empty key;
- a block that nests or spans a heading;
- a close marker with no block;
- a heading with an id comment on the line after it and a written id of its own;
- a malformed id;
- a duplicate id, naming both lines.

```test:table-rows
a marked table is a unit, and each row one keyed file#TABLE/KEY by its key cell (the first
column, or key: Header), without code or emphasis marks; editing a row changes only that
row's version; a marker with no table, no separator row, an unknown key column, an empty
or repeated key is an error naming the line
```

```test:table-columns
key: [A, B] keys each row file#TABLE/a/b on several columns, and an empty part or a repeated
pair is an error; ignore: [C] leaves C's cells out of each row's version, so editing one
changes nothing, while editing any other cell still does; an ignored key column, an unknown
column or marker text that isn't an option is an error naming the line
```

```test:heading-ids
a heading's id is its id comment, else its anchor, else what the project's heading grammar
reads from it (§ids, section numbers, a regex, or the first of a list that matches), else
its path; a section number may end in a letter before its dot (2a.); a written id survives
a renamed heading, is never numbered ~2, and a repeat is an error naming both lines, even
when two grammars read it
```

```test:heading-id-after
an id comment on the line after a heading is its id, with the version and subsection paths
an id at the end of the heading gives; a <!-- /surfex --> before the next marker or heading
makes it a block's opening marker instead, as before; an id on both lines is an error naming
the line after
```

```test:hint-info-forms
a fence whose info string is test:ID is the hint ID, with the version the same fence as
test ID has; test: with nothing after it, or test:ID followed by more words, is an ordinary
code block
```

```test:earmark-forms
through ex_doc's markdown (ExDoc.Markdown.Earmark), the forms it is said to render (the id
comment after a heading, test:ID hints, marked blocks and tables, an id comment of its own
line before a paragraph or list item, a marked list of written ids, a marked contract
fence) show no marker text, and every heading in the text is a heading; the id comment at
the end of a heading, the anchor, test ID hints, an id comment inside a list item, one
starting a paragraph and an elixir contract fence are shown to break as said
```

```test:section-versions
given a section
when its body is reworded, its version changes
when its heading is renamed, a paragraph reflowed or blank lines added, it doesn't
when a subsection is edited, only the subsection's version changes
```

```test:fence-rule
a # line inside a fence (backticks or tildes, closed only by the same character at
least as many times) stays in its section's body and counts in its version;
outside a fence it is a heading
```

```test:unit-versions
editing a marked block, a test hint, or a section's own prose changes that unit's
version and no other's
```

```test:test-versions
a test's version changes when its body, a private helper it calls, an attribute it
reads, a setup that applies to it, or its comprehension's cases change;
not when a variable is renamed, or a setup it doesn't get changes
```

```test:scan-records-pure
scanners produce scan records only, each a pure function of the source: spec units,
code items, tests and classes
```

```test:test-alias-scope
a test's calls resolve each alias where it's declared: a module's throughout, a describe's
for its tests and helpers but not a sibling describe, a body's from its line on; every
alias form
```

### 11.1 Claim forms
<!-- surfex: claim-forms -->

A claim is often smaller than a section: one rule in a list, one paragraph. Its author
names it, and it is a unit of its own with role `:rule`, sitting in its section or block.
A hint inside a rule sits in the rule. **No id ever comes from where a claim stands:** an
ordinal would dangle every claim after an insertion.

- **A named paragraph or list item.** An id comment on a line of its own, directly before
  a paragraph or a list item, names it: `<!-- surfex: no-override -->` and then the item
  or paragraph on the next line is `file#no-override`. The comment is a block's opening
  marker too. It opens a block, as it always has, when a `<!-- /surfex -->` closes it
  before the next marker, heading or the end of the file; on the line after a heading it
  is the heading's id (§11); otherwise it names what follows it. An id comment that starts
  an item or paragraph names it the same way: `- <!-- surfex: no-override --> Anonymous
  requests never …`.
- **A marked list.** `<!-- surfex: rules NAME -->` on its own line before a list (blank
  lines between are fine) makes each of the list's top-level items a rule. An item's id is
  its id comment, else the id the project's `rule_ids:` grammar reads from the start of its
  text, in the forms `heading_ids:` takes (§11): `:section_sign` reads `1. §Tenancy.1.4 …`
  as `Tenancy.1.4`, and `~r/^(?<id>P\d+)/` reads `P3`. NAME names the list in errors. The
  list ends at the first line that is no part of its items, or an item of another list
  (another bullet, or another ordered delimiter).
- **Where a claim ends.** A paragraph runs to a blank line, a heading, a marker, a fence or
  a list item. An item takes its nested lines (every line indented to its content) and a
  lazy continuation of its text. It ends at a less indented line after a blank one,
  another item at its level or above, a marker, a fence or a heading. A rule can't hold
  another.
- **Its version** covers its text and nested lines, without its list marker or id
  comment, with whitespace collapsed, so renumbering a list changes none. Like every finer
  unit, a rule is left out of the version of what it sits in, so rewording one changes
  that rule's version and no other.
- **Bold is never a claim.** A bold-lead sentence with no id comment is its section's
  text, whatever it says. Bold is emphasis far more often than it is a claim.

**A contract fence** holds the one place where naming code in the spec is the contract: a
module's signatures. A fenced block after the line `<!-- surfex: contract -->` (blank lines
between are fine), or opened as ```` ```elixir contract ````, yields one unit of role
`:contract` per function its `@spec`, `@callback` or `@macrocallback` lines name. Where
a signature ends is §11.8's.
- **Its id** is `file#Module.name/arity`, read by parsing the signature as Elixir, with the
  module §11.9 resolves: `spec/17-public-api.md#MyApp.Feed.feed/2`. Several signatures of
  one function (a multi-clause spec) are one unit, at the first one's place. The record's
  `contract` is the function it is the contract of, `MyApp.Feed.feed/2`.
- **Its version** is its signatures' text, so changing a signature dangles exactly that
  unit's relations. The fence's other lines (comments, code) and its fence lines are the
  text of what it sits in.

The bare marker is a block's opening marker too, and stays a block where a
`<!-- /surfex -->` closes it (§11). A signature with no module, a marker with no fence after
it, a fence holding no signature, a signature that doesn't parse, and one id in two fences
of one file are errors naming the line. Whether a contract unit's citation of its own
function yields `implements` is plan item D.2's to decide.

**Folding path headings.** With `fold_paths: true` in `.surfex.exs`, a heading with no id
(no id comment, anchor or `heading_ids:` id) inside a section that has one is **part of
that section's unit**, through any such headings between them:
- it isn't a unit of its own;
- its heading line and body count in the id'd section's version, which runs to its last
  line, so editing or renaming it changes that section's version;
- the units inside it (hints, blocks, rules, rows, contracts) sit in the id'd section.

A heading with no id'd heading above it stays a path unit, as before: an index page, or a
project that writes no ids. A sub-heading meant as a unit of its own gets an id. Folding
is the default wherever the spec writes ids (`heading_ids:`): a path heading under an
id'd heading is part of that unit's own text (§13.4). `fold_paths: false` keeps path sub-headings as units, and without `heading_ids:`
nothing folds. A value other than `true` or `false` is an error.

```test:fold-default
with heading_ids: and no fold_paths:, a path heading under an id'd heading folds;
fold_paths: false keeps it a unit; without heading_ids: nothing folds
```

```test:fold-paths
with fold_paths: true a heading with no id inside an id'd section is no unit: its text and
body count in that section's version and what sits in it sits in that section; one with no
id'd heading above stays a path unit; without it every heading is a unit as before; a
value that isn't a boolean is an error
```

**A project's citation forms.** Ids are file-scoped, and a project cites them across files
in its own forms: `§03.4` (chapter 3, section 4), `spec/05 §4.3`. `unit_refs:` in
`.surfex.exs` lists those forms, each a regex with named captures: `id`, the unit's id in
its file, and `file`, the spec file's path with or without `.md`, or `chapter`, the number
a spec file's name starts with (`spec/03-wire-format.md` is chapter 3, `03` and `3` alike).
`~r/^§(?<chapter>\d\d)\.(?<id>[\w.]+)$/` makes `§03.4` resolve to
`spec/03-wire-format.md#4`.
- `Surfex.Scan.Resolver` reads them: `Surfex.Scan.resolver/2` takes the scans and the
  forms, and `Surfex.Scan.resolve/2` tries the forms before its exact and bare-id lookups.
  The first form that matches decides: one unit is found; none falls through to the ids;
  several (two files of one chapter) is ambiguous. `Surfex.Scan.Resolver.unit_refs!/1`
  validates them, and a form with no `id`, or neither `file` nor `chapter`, is an error.
- So a test's `verifies:` may use the project's form. `Surfex.Status.Config.scans/2` gives
  such a declaration as the full id it names, so everything reading declarations resolves
  it as the id; one that names nothing stays as written, and is reported as any unknown
  declaration is.
- `mix surfex.test` and the recording tasks that take a unit (`relate`, `validate`,
  `confirm`, `annotate`, `retire`, `move`, `resolve`, `mark`, their `--file` batches
  included) take the form too.

Prose citations of spec units aren't checked as citations: §6 checks citations of the code,
and checking the spec's citations of itself is plan item D's.

```test:unit-refs
unit_refs: forms resolve a project's citations by chapter or file to file-scoped units, an
unknown chapter or id is unknown and two files of one chapter ambiguous; a form without id
and file or chapter is an error; a test's declaration in the form names its unit, and
surfex.test and the recording tasks take the form
```

Rule ids share the file's namespace (§11), so a repeated one is the duplicate-id error.
`Surfex.Status.units/1` counts rules and contracts with the other roles, and the reports
show them once a spec has one. A spec that names no claim scans exactly as before. An id
comment of its own line that was once an unclosed block is now a claim's id. When no
paragraph or item follows it, it is still the error it was. Also an error, naming the line:
- an item of a marked list with no id, or a marker with no list after it;
- a rule inside a rule;
- a list item with two ids (a comment of its own line, and one starting it);
- a malformed id.

**Where these render.** ex_doc's markdown renders an id comment of its own line before a
paragraph or list item, and the rules marker, as comments. Before an item, the comment
needs a blank line above it, which splits the list in two where it isn't the first item.
It shows an id comment anywhere inside a list item as text, and an id comment that starts
a paragraph swallows the paragraph and the heading after it. A forge's CommonMark reads
a line that starts with a comment as raw HTML, so the rest of that line loses its
markdown. A spec names its claims with the comment on its own line, or with a marked list
of written ids. The contract marker renders as a comment; an `elixir contract` info string
holds a space, and ex_doc renders it as a `test ID` hint: as text swallowing the heading
after it.

```test:contract-fences
a contract fence (after the contract marker, or opened elixir contract) is one unit per
function its @spec, @callback and @macrocallback lines name, keyed file#Module.name/arity;
changing one signature changes only its unit, the fence's other lines are its section's
text; a bare marker a close marker follows is a block; a signature with no module, a fence
with no signature, a marker with no fence, a signature that doesn't parse and one id in two
fences are errors naming the line; status counts contracts
```

```test:rule-items
a marked list's top-level items are rules, each by its id comment or the id rule_ids: reads
(a § id, a regex, the first of a list), sitting in their section; an item starting with an
id comment, or after one of its own line, is a rule in any list; a hint inside a rule sits
in it; a rule's version is its text and nested lines without its list marker or id comment;
an item with no id, a marker with no list, a repeated id, a rule inside a rule and an item
with two ids are errors naming the line; status counts rules
```

```test:rule-paragraphs
an id comment of its own line before a paragraph, or starting it, makes the paragraph a
rule whose version is its own text, left out of its section's; bold with no id comment is
the section's text; a comment a close marker follows is a block, and one naming nothing is
the block error it was
```

```test:no-positional-ids
inserting a claim at the top of every claim form (a named paragraph, a marked list, a named
item) changes no other unit's id or version
```

### 11.2 List-shaped table keys
<!-- surfex: list-keys-section -->

A marked table's row key (§11) that reads as a list, such as `1001, 1005 legacy hilite`, is
almost always two things in one row, so their relations can't be told apart.
`Surfex.Status.list_keys/1` lists every row whose key holds `, `, ` / ` or ` and `, with
its table and the separator it holds, in file and line order. The key parts of a
composite key (`key: [A, B]`) are joined by a bare `/`, which is no separator. The status
reports them in a list of their own, and the JSON as `list_keys`. It is a **warning**, never
a failure, since a key may hold a comma on purpose. The scan is unchanged: the rows are
units as before.

```test:list-keys
a row key holding ", ", " / " or " and " is listed with its table and separator, in the text
report and the JSON, and the status doesn't fail; a composite key or a plain one isn't
listed, and the rows are units as before
```

### 11.3 Calls a test states through a framework
<!-- surfex: framework-calls -->

Some tests reach their code through a framework rather than by naming it: a request to a
route the router maps, an event sent to the view a request mounted, a message sent to a
named process. Each resolves deterministically from source, so it is a **call** in the
test's record, beside the calls §11 lists, and reach, the triangle, evidence (§13.1, §17)
and the suggestions (§15) follow it. These are a test's calls only: a definition's calls
(§5.2), and so every code version, stay as they were.

**Routes.** The project's router is read from source, never compiled
(`Surfex.Status.Config.routes/2`): the router modules `router:` in `.surfex.exs` names
(a module or a list), or, without it, each module an endpoint (`use Phoenix.Endpoint`)
plugs that is a router (`use Phoenix.Router`, or `use App, :router`), from the Elixir
scanner's `:paths`. `router: false` turns this off; a named router the source doesn't
define, and a `router:` of any other shape, is refused. Its routes are read in order:
`scope` joins paths and aliases as Phoenix does (`scope "/carts", MyAppWeb` then
`live "/:id", CartLive.Show` is `MyAppWeb.CartLive.Show` at `/carts/:id`), and
`live_session` and `pipe_through` change neither. A `live` route is a `GET`; `get`,
`post`, `put`, `patch`, `delete`, `options` and `head` routes name a controller and an
action. Other forms (`resources`, `forward`) aren't read.
- `live(conn, PATH)` and each verb's request (`get(conn, PATH)`, …), local or qualified
  (`Phoenix.LiveViewTest.live/2`) and piped or not, resolve to the first route, in the
  router's order, of that verb whose path matches. A live route calls the LiveView
  module, its `mount/3` and its `handle_params/3`; a controller route calls the
  controller module and its action (`Controller.action/2`).
- `PATH` is a literal string, or a string or `~p` with interpolations. A literal segment
  matches the same segment or a param (`:id`); a segment holding an interpolation matches
  only a param, since what it holds is known only when the test runs; a glob (`*path`)
  takes the rest. The query string is no part of the match. A path in a variable, or one
  no route matches, is no call.
- `live_isolated(conn, Module, …)` calls `Module` and its `mount/3`.

**LiveView events.** In one body (the test's, or a helper's), a variable bound by
`{:ok, view, _html} = REQUEST`, where the request resolves to a LiveView, is that view.
`render_click`, `render_submit`, `render_change`, `render_keydown`, `render_keyup`,
`render_blur`, `render_focus` and `render_hook` given the view, or an `element/2,3` or
`form/2,3` of it, call the view's `handle_event/3`. A component the element targets
(`phx-target`) isn't known from the test, so the call names the view's.

**Messages.** `GenServer.call(Module, msg)` and `GenServer.cast(Module, msg)`, with a
module as the server, call that module's `handle_call/3` and `handle_cast/2`, and name
`Module`. A server given any other way (a pid, a `{:global, …}` name) is no call.

**Captures through a helper.** A capture handed to a helper (`growth(&MyApp.f/1, …)`)
is already the test's own call (§11), so reach follows it whether or not the helper is
scanned.

`Surfex.Scan.ExUnit.records/5` and `Surfex.Scan.ExUnit.tests/5` take the routes;
`Surfex.Status.Config.scans/2` and `Surfex.ExUnitFormatter` pass the project's.

```test:route-calls
the router is found from the endpoint or named by router:, its scopes joined in order;
a request through a live route calls the view's mount and handle_params, through a
controller route the action; a literal segment matches a param, an interpolated one only
a param, a glob the rest; an unknown or variable path is no call; reach follows
```

```test:event-calls
an event given a view bound from a resolved live request, or an element or form of it,
calls the view's handle_event/3; one to any other view is no call
```

```test:message-calls
GenServer.call and cast with a module call its handle_call/3 or handle_cast/2, and any
other server is no call; a capture handed to a helper is the test's call
```

### 11.4 Checks
<!-- surfex: checks -->

Some claims are verified by a gate, not a test: a lint rule that no function rescues, a
security scanner, a project's own task comparing a layout table with a compiled header.
`checks:` in `.surfex.exs` declares them, each a command that can fail and the claims it
verifies:

```elixir
checks: [
  [id: "no-rescue", run: "mix credo --strict --only Credo.Check.Warning.Rescue",
   verifies: ["plat-3"], inputs: [".credo.exs", "lib/**/*.ex"]],
  [id: "layout", run: "mix my_app.layout", verifies: ["spec/03-wire.md#layout/*"],
   inputs: ["lib/my_app/wire.ex", "c_src/wire.h"]]
]
```

`Surfex.Scan.Checks` makes one scan record of kind `:check` per check
(`Surfex.Scan.Checks.records/2`), and `Surfex.Status.Config.scans/2` includes them:
- **Its id** is the check's `id`: letters, digits and `.` `_` `-`, starting with a letter
  or digit, so never a test's.
- **Its version** covers its declaration (`run`, `verifies` and `inputs`) and the content
  of every file its `inputs` globs match. Changing the rule, what it verifies or what it
  inspects is a new version, and its relations dangle; no other file changes it. The
  record's `check` holds the command, the declaration's own version and each input file's
  version, from which `Surfex.Scan.Checks.version/2` makes its version.
- **What it declares** is its `verifies`, each `{:verifies, ref}`, resolved as a test's
  declaration is (§11, and `unit_refs:`, §11.1). `TABLE/*` declares every row of a marked
  table at once. A reference that resolves to nothing is broken (§13.1), as a test's is.
- **Its location** is `.surfex.exs`, with no lines.

`Surfex.Scan.Checks.declared!/1` validates the key: each check is a keyword list of `id`
(unique), `run` (a command), `verifies` (a reference or a list of them) and, optionally,
`inputs` (a list of globs), and nothing else. Anything else raises, naming it, as does an
input glob that matches no file: a misspelt input would leave the version blind to what
the check inspects.

```test:check-records
a check is a record of kind check, its version changing with its command, its verifies,
its input globs and its inputs' content, and not with other files; TABLE/* declares each
row; a malformed check, a repeated id or an input matching no file raises
```

A check verifies, as a test does, and does nothing else. A `verifies` relation runs from
a test or a check to a spec unit (§12.1), and status treats a check's as a test's: it is
suggested from the declaration (§15), unrecorded while the log lacks it and undeclared
once the check stops declaring it (§13.1), the check named `check ID` in those lists. A
check has no calls, so it takes no `tests` relation, and it carries no `implements`: the
code a claim is about is related to the claim by hand. The triangle (§13) counts a check
verifying a unit as a verifying test that exercises the code in the files it inspects,
and completeness (§20) counts a unit a check verifies by a validated relation as
verified. Status lists the checks, with what they verify, in its text, its JSON
(`checks`) and `RELATIONS.md`, where a project has any.

```test:check-status
a check's declared verifies is suggested, unrecorded until recorded and undeclared once
dropped; a unit only a check verifies has no triangle gap for the code in its inputs and
counts as verified in completeness; status, its JSON and the golden list the checks
```

`mix surfex.check [ID…]` runs checks, every one or those named, with the system's shell
in the project's root (`Surfex.Check.run/3`), and records each result as evidence
(§17.1) in the file the test formatter writes, or in `--evidence PATH`. Exit status 0
passed and anything else failed; the task then fails, so it is the gate as well. An
unknown id, or a project with no checks, is an error before anything runs
(`Surfex.Check.select/2`). `--list` names them and runs nothing.

`mix surfex.test UNIT` (§11) also names the checks verifying a unit: `--list` lists them
after the tests, and a run says which there are. `--checks` runs them after the tests,
recording their evidence as `mix surfex.check` does, and fails if a test or a check
fails. With it, a unit that only checks verify is no error.

```test:check-run
mix surfex.check records each check's result, passed or failed by its exit status, at its
declaration's version with its inputs' versions, and fails when one failed; an unknown id
is an error; surfex.test --checks runs the checks verifying a unit, and --list names them
```

### 11.5 Where a declaration counts
<!-- surfex: declaration-place -->

A `verifies` declaration counts only from the test or property block it applies to (§11),
or from a declared check (§11.4). Nothing else asserts anything: a tag on a module with no
test, or in a helper under `test/support/`, would credit a requirement for where a file
sits. The test scanner gives a declaration only to the test records it applies to, so such
a tag verifies nothing. It is still reported, so a misplaced tag is visible rather than
silently ignored. `Surfex.Scan.ExUnit.inert/2` finds them:
- in the files `tests:` names, a `@tag` setting `verifies` that no test follows in its
  block (the module's, a describe's or a comprehension's), and a `@describetag` or
  `@moduletag` setting it over a describe or module that defines no test;
- every `verifies` tag, at any level, in a file under `test/support/`, which is compiled
  as support code and never run as a test, unless `tests:` scans that file.

Each is its file and line, the ids it names, and why. `Surfex.Status.Config.load/3` passes
them to `Surfex.Status.derive/4` as `inert:`, and status lists them as **inert** in its
text, its JSON (`inert`) and `RELATIONS.md` (by file, where there are any). It is
information, never a failure: a tag that verifies nothing fails nothing, and the
requirement it names is judged by the tests that do verify it.

```test:inert-tags
a verifies tag no test follows, a describetag or moduletag over no test, and any verifies
tag under test/support is inert, listed with its file, line, ids and reason; a tag a test
takes, a comprehension's included, is not; status lists them and doesn't fail on them
```

### 11.6 A project's own declaration forms
<!-- surfex: declared-forms-section -->

A project may already record what its tests verify in a form of its own, an ExUnit tag of
another name or a module attribute, often with something appended to each id, such as a
hash of the section's text. Rewriting every tag as `verifies:` would be a one-shot import,
redone whenever the form changes. Instead `declares:` in `.surfex.exs` names the forms the
test scanner reads as `verifies` declarations:

```elixir
declares: [verifies: [{:tag, :validates, strip: ~r/@[0-9a-f]+$/}]]
```

- `{:tag, KEY}` is an ExUnit tag key, read at `@tag`, `@describetag` and `@moduletag` and
  merged level by level as `verifies:` is (§11).
- `{:attribute, NAME}` is a module attribute before a test, `@validates ["…"]`, applying
  to the next test as a `@tag` does.
- `strip: REGEX`, optional, is removed from each id, so `@tag validates:
  ["Engine.15.2@abc123"]` declares `Engine.15.2`. Each id is then a declaration like any
  other: in the project's citation form it resolves through `unit_refs:` (§11.1), it is
  suggested, recorded and confirmed as one (§15, §17), and a form outside a test is inert
  (§11.5).
- Such ids can't be selected by `mix test --only verifies:id`, which reads the `verifies`
  tag alone, so each is listed in the test's `unselectable` (§13.1). `mix surfex.test`
  selects by declaration, so it runs them.
- `Surfex.Status.Config.declares!/1` reads and validates the key: anything but
  `[verifies: [form, …]]`, a form of another shape, a name ExUnit or surfex reads itself
  (`verifies`, `tag`, `moduletag`, …), or an option other than a regex `strip:` is refused,
  naming it.

The form stays the project's: nothing is rewritten, and a change to it is a change to the
config. A bare `§` mention in a test's prose or a string is **not** read: a mention is not
a claim.

```test:declared-forms
a declared tag form at every level and a declared attribute form before a test are read as
verifies declarations with their strip pattern removed, unselectable by --only; without
the form they declare nothing; a bare § mention is no declaration; a form no test takes is
inert; a value that can't be read raises naming its key
```

```test:declared-forms-config
declares: is validated, refusing a malformed form; a declared id in the project's citation
form resolves through unit_refs:, is counted unselectable, and is selected by surfex.test
```

### 11.7 Spec-unit classes and deferrals
<!-- surfex: spec-classes -->

Code that is described elsewhere has an excusal: a class with a reason (§8). Spec text has
its own: a unit that deliberately asserts nothing a test could fail on (a decision record,
prior art, an architecture map, how to read the spec) is **classed**, and a unit that
states what isn't built yet is **deferred** by the issue that will build it. Both are read
from the source, never recorded in the log.

**Classes, declared once.** `spec_classes:` in `.surfex.exs` names the project's classes,
each with a reason, in the shape of code's `classes:`:

```elixir
spec_classes: [
  {"informative", "explains or points; asserts nothing a test or check could fail on"},
  {"decision", "a decision record: why, not what", files: ["spec/13-decisions.md"]},
  {"process", "how the project is developed", headings: ~r/^Process\b/},
  {"informative", "explains or points; asserts nothing a test or check could fail on",
   marker: ~r/^spec-coverage: informative$/}
]
```

- A class is a **name** (letters, digits, `-`, `_`) and a **reason**, both required. One
  name may be declared several times, with one reason.
- `files:` (globs, relative to the root) and `headings:` (a regex over a heading's text as
  written, without its id comment or anchor) make it a **pattern class**: every section
  that matches is classed, and with both, a section must match both.
- `marker:` (a regex over an own-line HTML comment's inner text) reads a project's existing
  comment as this class's marker, an **alias**. The comment then belongs to no unit's text,
  like any marker.
- A class with none of these is selected only by surfex's own marker.
- `Surfex.Status.Config.spec_classes!/2` reads and validates the key. A malformed class, an
  empty reason, a bad name, an option other than these three, one name with two reasons,
  and a `files:` glob that matches no spec file are refused, naming them: a misspelt
  pattern would otherwise class nothing in silence. `Surfex.Status.Config.scans/2` passes
  the classes to `Surfex.Scan.Markdown.sections/3` as `spec_classes:`.

**The markers,** each on a line of its own, in a unit's own text:

```markdown
## 2. Architecture
<!-- surfex: arch -->
<!-- surfex: class informative: the module map is orientation; the rules after it are claims -->

## 6. View abstraction
<!-- surfex: deferred #412: past v1 -->
```

- `<!-- surfex: class NAME -->` classes the section it sits in, with the class's reason.
  `<!-- surfex: class NAME: REASON -->` adds the author's own reason for this unit.
- `<!-- surfex: deferred REF -->` or `<!-- surfex: deferred REF: REASON -->` defers the
  section or marked block it sits in. REF is an issue, `#123` or `group/project#123`, or
  the spec unit that records the decision (§13.13): a deferral with neither would never
  end, and that is what a class is for. There is no pattern form; each deferral names its
  own.
- A marker may follow a heading's id comment on the next line. Like every marker it is
  left out of the unit's version, so classing or deferring a unit changes no version.

```test:class-markers
an own-line class marker classes its section with the class's reason and its own; it may
follow the heading's id comment; the section's version is the one it has without the
marker; the preamble can be classed
```

```test:deferred-marker
a deferral marker defers its section, or the marked block it sits in, with its reference
and reason; a cross-project reference is read whole; the unit's version is unchanged
```

**Which class a section has.** A section's own marker wins, a deferral marker included:
no pattern classes a deferred section. Otherwise the first declared pattern class that
matches it, as code's `rules:` do (§7). An alias is a marker. `Surfex.Scan` holds the
result on the record: `class` (`%{name, reason, note, by}`, `by` being `:marker`,
`:pattern` or `:alias`, and `note` the marker's own reason) and `deferred`
(`%{ref, reason}`).

```test:class-patterns
files: classes every section of the files it matches, headings: only the sections whose
heading matches, and both only where both match; a marker beats a pattern; the first
pattern declared that matches wins
```

```test:class-marker-alias
with marker: ~r/^spec-coverage: informative$/ that comment classes its section by alias and
leaves the version as it was; without the alias it is section text, as before
```

**Classes are for sections.** A rule, a row, a hint, a contract and a marked block are
claims: the way to excuse one is not to name it. Every problem raises, naming the file and
line:
- a class marker naming a class `spec_classes:` doesn't declare;
- a class marker in a marked block or in a rule, or a deferral marker in a rule;
- two class or deferral markers in one unit, a class and a deferral among them;
- a deferral whose reference is neither an issue nor a spec unit (§13.13);
- a marker under a heading `fold_paths:` folds into its ancestor (§11.1), which would class
  the ancestor its author didn't mark.

```test:class-marker-errors
an undeclared class; a class marker in a block or a rule; two markers in one section; a
class and a deferral in one unit; a deferral without a well-formed reference; a marker
under a folded heading: each raises naming the file and line; spec_classes: refuses a
malformed class, an unknown option, two reasons for one name and a glob matching no spec
file
```

**Where they render.** Both markers are own-line HTML comments, like the id comment after
a heading, so ex_doc's markdown renders them as nothing, alone or after an id comment, and
keeps every heading.

```test:class-markers-render
through ex_doc's markdown (ExDoc.Markdown.Earmark), a class marker and a deferral marker,
alone after a heading or after its id comment, show no marker text and every heading
stays a heading
```

What a class or a deferral changes in the status is §13.6's.

### 11.8 Where a contract signature ends
<!-- surfex: contract-signature-extent -->

A contract signature (§11.1) runs from its `@spec`, `@callback` or `@macrocallback` line
for as long as its text doesn't parse as Elixir yet: while a `{`, `(`, `[` or `<<` is open,
or a `::` waits for its type, it takes the next line whatever its indentation, so a map
closed by a `}` back at the head's column is one signature. Once it parses, it takes the
lines indented deeper than its head, such as a `when` or a `| nil` continuation, and no
others. It never runs into the next `@spec`, `@callback`, `@macrocallback`, `@type`,
`@typep` or `@opaque` line, and it ends at the fence's end. A signature still unfinished
there, or one that never parses, such as a sketch `%{id:, …}`, is the error naming its
first line that §11.1 lists.

```test:signature-extent
a contract signature whose closing }, ), ] or >> sits at its head's column is one unit
through that line; once a signature parses only deeper-indented lines join it; a sketch
that never parses raises naming its first line, and an unfinished one stops at the next
@spec, @callback, @macrocallback, @type, @typep or @opaque line; every fence of
typespecs that parses whole reads without a parse error
```

### 11.9 Which module a contract signature is about
<!-- surfex: contract-module-resolution -->

A contract signature (§11.1) is keyed by the function it states, so two functions of one
name in two modules are two units, and a callback is a unit apart from a function of the
same name. A signature's module is, nearest first:
1. the module its head names, `@spec MyApp.open(port, opts) :: …`, as written;
2. else the `defmodule M do` around it inside the fence, whose `end` at its column closes
   it; a `defmodule` inside another joins its name, `A.B`;
3. else the marker's `module:` (`<!-- surfex: contract module: MyApp.Feed -->`);
4. else the nearest enclosing heading that names a module, climbing past headings that
   name none: the items a profile subject gives it (§6.6), else the module alias it writes
   alone in backticks, as `` ## 1. `MyApp.Feed`: the feed `` does.

A `@spec` is keyed `file#Module.name/arity`, a `@callback` `file#Module.callback:name/arity`
and a `@macrocallback` `file#Module.macrocallback:name/arity`. The kind holds no space, and
no module or function name holds a `:`, so a callback's id never collides with a module's.
A behaviour that also defines the function it declares as a callback has both units. The
record's `contract` is `Module.name/arity` for every kind.

These stay errors naming the line: one id in two fences of one file, which names the line
it first had, since the same function stated twice in one file is a restatement that
drifts; a signature none of the four places, and one whose nearest naming heading names
several modules. A heading's modules are asked only of a signature that needs them, so a
qualified signature under such a heading reads.

```test:contract-modules
an unqualified signature takes the module of the defmodule around it in the fence (nested
ones join), else the marker's module:, else the nearest heading naming one, by a subject
or a backticked alias, climbing past headings that name none; a callback and a spec of one
name and arity are two units; one id twice in a file raises naming both lines; a signature
no module places, or whose heading names several, raises naming its line
```

### 11.10 Calls through a declared dispatch function
<!-- surfex: dispatch-calls -->

Some code runs its handlers through a table: a function looks the handler up in a map of
operations and calls it with `apply(module, fun, args)`. No call in the source names a
handler, so without help a test driving the dispatcher reaches none of them, and each
handler earns (§13.5) only through a review naming the path (§18.2). The project author
knows the dispatcher exists, so the project declares it once, and surfex reads the table
from source, never compiled and never traced at run time:

- `dispatch: [{Module, :fun, arity}]` in `.surfex.exs`
  (`Surfex.Status.Config.dispatch!/1`) names the dispatch functions. Anything but a list
  of such triples is refused, as is a dispatcher the scanned source doesn't define with
  `def`, one in a `@moduledoc false` module, and the key without the Elixir scanner among
  the scanners (§5.3), which is what reads it.
- A dispatcher's **targets** are the literal entries its module writes inside a module
  attribute's value (not a typespec's or the docs'), a map literal or a keyword literal
  (not a `do:` block): `{Module, :fun}` names every arity of `Module.fun` the scanned
  source defines with `def` or `defdelegate`, `{Module, :fun, arity}` and
  `&Module.fun/arity` that arity. Aliases resolve as the module declares them, and
  `__MODULE__` is the module. A module nested in it is its own. An entry naming no function
  the scanned source defines is listed as **unscanned** and reaches nothing.
- A call that names a dispatcher, remote, local inside its module or a capture, also calls
  each target and names its module, in a definition's calls (§5.2) and a test's (§11)
  alike. So reach, the triangle, evidence (§13.1, §17), the suggestions (§15) and earning
  (§13.5) follow it with no special case.
- A scanner's `calls/3` (§5.4) still takes precedence: where it answers for a call to a
  dispatcher, its answer is what that call reaches, and the table adds nothing.
- The reading travels on the dispatcher's module item and code record (`dispatch`, §4).
  `Surfex.Status.derive/4` gives `dispatch`, one entry per dispatcher, `%{dispatcher,
  targets, unscanned, unresolved}`. A dispatcher whose module lists no literal entry is
  **unresolved**: a call to it reaches nothing beyond it. It is reported, never failing.
  `Surfex.Status.Report.text/1` lists each dispatcher with what it reaches, the JSON
  gives `dispatch`, and the status golden holds a `dispatch` table and counts the
  dispatchers in its header, each only where `dispatch:` declares one.

```test:dispatch-reach
a test driving an endpoint whose code calls a declared dispatcher reaches each handler its
module's map lists, and the handler's module; a {Module, :fun} entry names every defined
arity and an &Module.fun/arity capture its arity; without dispatch: it reaches none
```

```test:dispatch-unresolved
a dispatcher whose table isn't literal reaches nothing beyond it and is listed unresolved,
an entry naming unscanned code is listed unscanned, and the status doesn't fail; text,
JSON and golden list dispatchers only where dispatch: declares one; a malformed dispatch:
or an undefined dispatcher is refused
```

```test:dispatch-calls-precedence
where a scanner's calls/3 answers for a call to a dispatcher, the call reaches its answer
and not the table; a call it doesn't answer still reaches the table
```

## 12. The relation log
<!-- surfex: relation-log -->

The relation log records judgements: that two things relate, at two versions. It is the
only state surfex keeps. Scanners never read or write it (§11).

### 12.1 Entries
<!-- surfex: log-entries -->

`Surfex.Log.Entry` is one entry:

| Field | Meaning |
|---|---|
| `id` | SHA-256 of the entry's canonical line without `id`: stable, and changed by any edit |
| `at` | when it was recorded, UTC ISO 8601: orders the history |
| `commit` | HEAD when it was recorded: context only, since squash merges can make it unreachable |
| `parents` | the entry ids it follows for the same relation; two means it resolves a fork |
| `op` | `relate`, `retire`, `mark` or `observe` (`Surfex.Log.Entry.ops/0`) |
| `type` | a relation's: `implements`, `refines`, `depends_on`, `tests`, `verifies` (a test or a check verifies a spec unit, §11.4) or `excuses` (`Surfex.Log.Entry.types/0`); or a mark's: `needs_update` (`Surfex.Log.Entry.mark_types/0`); or an observation's: `red_green` or `baseline` (`Surfex.Log.Entry.observation_types/0`) |
| `ends` | a relation's two `{kind, id, hash}`, of the kinds `Surfex.Log.Entry.kinds/0` lists (`spec`, `code`, `test`, `class`, `check`) and the grammar below allows. A `null` hash is a **planned** end: an id that didn't exist when the relation was recorded (§14). At most one end is planned. A mark's one end: the spec unit, at its hash. An observation's one end: the test, at its version, or for a `red_green`, a check at its declaration's version (§17.1) |
| `by` | who recorded it |
| `note` | why, optionally |
| `basis` | how the entry validates the relation (§18): `evidence`, `review`, `judgement`, `baseline` (§18.1) or `proposed` (`Surfex.Log.Entry.bases/0`); absent when it validates nothing, which keeps every older entry's canonical line and id |
| `through` | how a reviewed test exercises code no call reaches (§18.2), free text; only on a `relate` of an `implements` with basis `review`, and absent otherwise, which keeps every other entry's line and id |
| `migrates` | the section (or table) a claim's `implements` moves from, recorded by a migration (§14.1); only on a `relate` of an `implements` with basis `proposed` or `baseline`, and absent otherwise, which keeps every other entry's line and id |

- **An observation** is a fact the log keeps about one test version, not a relation:
  `op: observe`, type `red_green`, one `test` end at its version (or a check's, §17.1), basis `evidence`. It
  records that the version **discriminated** (§17): it failed against one version of its
  code and later passed against another, with the two runs in its note. It is never
  withdrawn: a changed test is a new version, which the observation doesn't speak for.
  `Surfex.Log.Entry.relation?/1` tells a relation's entries from a mark's or an
  observation's.
- **A mark** is a statement about one spec unit, not a relation: `op: mark`, type
  `needs_update`, one `spec` end at the unit's version, and a note (required) saying what is
  wrong in the result. A `retire` of the same type and end, naming the mark as its parent,
  withdraws it. `Surfex.Log.Entry.mark?/1` tells a mark's entries from a relation's.
- **A relation is its type and its two ends' kinds and ids** (`Surfex.Log.Entry.relation/1`).
  For the undirected types (`implements`, `excuses`), ends are sorted, so A↔B and B↔A are
  one relation. The directed types (`Surfex.Log.Entry.directed/0`: `depends_on`,
  `refines`, `tests`, `verifies`) keep their ends in the order given, from → to, because "A depends on
  B" is not "B depends on A". `Surfex.Log.Entry.relation/3` gives the relation of a type
  between two ends without an entry, oriented the same way.
- **The grammar.** What a well-formed entry is, the log enforces itself, rather than
  trusting its writers. A malformed entry from a bad merge, a hand edit or a writer's bug
  is refused where it is read, not left to surface as a confusing state far away:

  | Type | Op | Ends | Basis |
  |---|---|---|---|
  | `implements` | relate, retire | code ↔ spec | `proposed`, `evidence`, `review`, `judgement`, `baseline`; or none (legacy) |
  | `verifies` | relate, retire | test or check → spec | `proposed`, `evidence`, `review`, `judgement`, `baseline`; or none (legacy) |
  | `tests` | relate, retire | test → code | none, `evidence`, `judgement`, `baseline` |
  | `refines` | relate, retire | spec → spec | none, `judgement` |
  | `depends_on` | relate, retire | code → code | none, `judgement` |
  | `excuses` | relate, retire | class ↔ code | `proposed`, `judgement`; or none (legacy) |
  | `needs_update` | mark, retire | one spec | none |
  | `red_green` | observe | one test or check | `evidence` |
  | `baseline` | observe | one test | `baseline` |

  A `retire` has the ends of what it retires and never a basis. **None (legacy)** admits
  entries written before bases existed (0.4 and earlier): they decode, and status reports
  an `implements` or `verifies` among them unvalidated (§13.1). `Surfex.Record` never
  writes one: every `implements` and `verifies` it records carries a basis. An
  `implements` on `judgement` is grammatical, but validates only a shape's relation (§18):
  the grammar sees one entry, and whether its code has behaviour is the scan's to say. A
  violation names the rule ("implements ends must be code ↔ spec, got test, test"), and
  at decode the entry too. `mix surfex.log --verify` lists every violation with its file
  and line.
- `Surfex.Log.Entry.build/1` makes an entry from its fields and computes its id, or says
  which field or rule is broken. `Surfex.Log.Entry.new!/1` raises instead.
- **The canonical line is JSON**, keys in the order above (`Surfex.Log.Entry.encode/1`). It
  is readable by anything, and parsing it never evaluates code.
- `Surfex.Log.Entry.decode/1` reads a line back, or says why it isn't an entry: a missing
  or unknown field, or an id that no longer matches the content, meaning the line was
  edited. `Surfex.Log.Entry.decode!/1` raises instead. Text that is not JSON at all always
  raises, since the file is then corrupt. `Surfex.Log.Entry.json/1` reads one JSON value
  with `null` as `nil`.

```test:mark-entry
a mark is one spec end with op mark and type needs_update, refused with two ends or a
non-spec end; entries recorded before marks existed keep their lines and ids
```

```test:entry-tamper
an entry whose line is edited no longer decodes under its id,
and verifying the log reports it
```

```test:entry-canonical
an entry's canonical line is JSON with fixed keys, its id a hash of that line; undirected
ends are sorted; the vocabulary of ops, types, kinds and bases is fixed; a basis is
written only when set, so an entry without one keeps its line and id
```

```test:grammar-ends
each type joins only its kinds of end, and a directed type only from → to; a verifies runs
from a test or a check; a mark is one spec end and an observation one test end, or a check
for red_green; there is no config kind
```

```test:grammar-bases
each type carries only its bases, with none admitted as legacy for implements, verifies
and excuses; a tests relation is never reviewed; a retire and a mark carry no basis; an
observation carries exactly its own
```

```test:through-entry
through is written after basis only when set, so an entry without it keeps its line and
id; an entry other than a reviewed implements carrying it, or a blank one, is refused
```

```test:migrates-entry
migrates is written last only when set, so an entry without it keeps its line and id; an
entry other than a proposed or baselined implements relate carrying it, or a blank one, is
refused
```

```test:grammar-loud
a line breaking the grammar is refused at decode, naming the entry and the rule, and
verifying the log lists it with its file and line; every entry in surfex's own log fits,
with its id unchanged
```

### 12.2 The log
<!-- surfex: log-segments -->

`Surfex.Log` keeps the entries in `.surfex/` under the project root
(`Surfex.Log.dir/1`). **Nothing is ever edited or removed.** A newer entry for a relation
supersedes an older one and names it as a parent, and any question about history is
answered from the log alone.

- **Segments:** entries are appended to `surfex.log`. `Surfex.Log.break/1` closes it as the
  next `surfex_N.log` and starts a new one.
- **Headers:** every file starts with `{"segment": N, "previous": HASH}`, where `HASH` is
  the SHA-256 of the previous segment's sorted entry ids (`null` for the first). Headers
  are not judgements, so `Surfex.Log.rechain/1` may rewrite them. That's needed only when
  two branches each started a segment and a merge combined them.
- `Surfex.Log.init/1` creates the log and adds `.surfex/*.log merge=union` to
  `.gitattributes`, so git merges concurrent appends by keeping both sides' lines.
- `Surfex.Log.append/2` adds entries to the open segment.
- `Surfex.Log.load/1` reads every segment, drops duplicate ids, and orders entries by `at`
  and then `id`. Every checkout derives the same order, whatever order the lines arrive
  in, which is what makes the union merge correct.
- `Surfex.Log.verify/1` reports every edited line, every parent no entry has (a removed
  line), and every segment header that doesn't match the segments before it (truncation).

`Mix.Tasks.Surfex.Log` is `mix surfex.log`, with exactly one of `--init`, `--break`,
`--verify` (fails on any problem) or `--rechain`. It never edits or removes an entry.

```test:union-merge
two branches that each append to the log merge without a conflict,
and the merged log loads to one state
```

```test:log-append-only
the log only appends: segments load together with duplicates dropped, a break starts a
new segment, and verifying finds an edited line, a removed parent or a truncation
```

## 13. Relation status
<!-- surfex: status -->

`Surfex.Status` derives the state of every relation from the scans (§11) and the log
(§12). `Surfex.Status.derive/2`, `Surfex.Status.derive/3` (with a `require:` policy) and
`Surfex.Status.derive/4` (with options: `planned: :allow | :fail`, `triangle: :report |
:fail`, `validated:` (§18), `evidence:` (§17), `citations:` (§6), `coverage:` (§13.1),
`deviations: :allow | :fail` (§13.1)) are
pure: the same scans, log and options always give the same status.

```test:status-pure
the status is derived from the scans and the log alone: the same inputs always give the
same status
```

**Deriving grows with the project in proportion.** Every status, report and check derives
the whole status, so its work over a project twice as large (twice the spec units, code,
tests and relations) is about twice, not four times. What derive looks up per item is
indexed once per derive: each declaration resolves against one resolver (§11), and each
spec unit finds the tests verifying it or a unit inside it from an index built by walking
each verified unit's `within` chain once, for the triangle and for the baseline (§18.1)
alike. It doesn't scan every record per declaration, nor every verifies relation per spec
unit.

```test:derive-linear
plain derive's work over a project twice as large, in spec units, code, tests and
relations, is about twice the work, counted in reductions
```

### 13.1 States
<!-- surfex: status-states -->

The judgement in force for a relation is its **tip**: an entry of the relation that no
other entry of it names as a parent (`Surfex.Status.tips/2`).

| State | When |
|---|---|
| **conflicted** | tips that **disagree**: entries recorded without seeing each other (sharing a parent, or both with none) that record different judgements. A tip's judgement is its operation, type, ends at their versions, and basis; who recorded it, when, on which commit and with which note are context. Tips that agree are one judgement, represented by the one with the smallest id (`Surfex.Status.representative/1`): two branches confirming the same change identically make no conflict. A different basis at the same versions still disagrees: CI checks an `evidence` claim and not a review, so a person picks which record stands |
| **retired** | the tip retires the relation |
| **orphaned** | an end recorded at a hash is no longer scanned (removed or renamed) |
| **planned** | an end was recorded without a hash, before it existed, and still isn't scanned. Once it is scanned, a planned `implements` relation is proposed until evidence or a review validates it (§18); a planned relation recorded without a basis is dangling on that end until it is confirmed. |
| **dangling** | both ids are scanned, but at least one is at a different hash than the tip recorded; the report names which ends changed |
| **current** | both ends are at the hashes the tip recorded |

```test:agreeing-tips
tips that record the same judgement (operation, ends at their versions, basis) are one
judgement: judged, validated, checked and reported as one tip, and re-recorded with every
one as a parent; a different operation, version or basis is a conflict, and resolve refuses
tips that agree
```

- **Impacted** is a flag, not a state. A relation is impacted when one of its ends has a
  `depends_on` relation to something that is not current. It doesn't fail the check:
  re-confirming everything downstream of every change would train people to confirm
  without reading.
- **New:** a scanned id in no relation at all.
- **Unimplemented:** a spec id whose every non-retired `implements` relation is planned:
  meant to be built, and not built yet.
- **Unmet:** a scanned id that the `require:` policy says must take part in a
  non-retired relation of one of those types, and doesn't. A planned relation meets
  it: the intent is on record. The policy is `[key: [type, …]]`, where a key is a
  kind (every scan of that kind) or a spec role (`section`, `block`, `test_hint`: every
  spec unit in that role). For example, `require: [code: [:implements], test_hint:
  [:verifies]]` means every code item implements something and every test hint is
  verified. An id that fails several rules is unmet once per rule. Code is one item per
  definition to the policy, as it is to the triangle (§11): a function's default-argument
  arities (`f/1`, `f/2` from one `def f(x, y \\ 1)`) are met by a relation to either.
- **Broken:** a test's declaration (§11) that names no spec unit, or a bare id more
  than one file has.
- **Broken citation:** a name the spec cites (§6) that resolves to nothing the code has,
  or to more than one item. The spec can't name what doesn't exist. Names the profile
  declares external, and documented absences, are not broken.
- **Failed:** a current `verifies` or `tests` relation whose test, at the version the
  relation holds, failed in the evidence given as `evidence:` (§17), **whatever validated
  it**: evidence, a review, a judgement or a baseline. A review judged that the test
  covers the claim, and relied on the run to say whether it passes. The relation isn't
  invalidated: once its test passes again it holds, with no new review.
- **Unproven:** a current relation confirmed by evidence (§17) that the evidence given to
  `Surfex.Status.derive/4` as `evidence:` doesn't bear out: its test, at the version the
  relation holds, didn't run or ran against another version of the code (one that failed
  is failed, above, and not also unproven). For an `implements` relation, some test with
  a current `verifies` relation to the unit (or inside it) and a current `tests` relation
  to the code must pass. Without `evidence:` nothing is checked, and a relation confirmed
  by hand is never required to have run.
- **Undeclared:** a live `verifies` relation whose test no longer declares its spec unit
  (§11): the tag was removed or changed. A tag isn't part of a test's version, so without
  this the relation would stay current, asserting a claim the source no longer makes. A
  `verifies` relation recorded by hand, with no declaration, is undeclared too: a test must
  say what it verifies.
- **Unrecorded:** a test's declaration (§11) that resolves to a spec unit, where the log
  holds no `verifies` relation between the test and that unit, in any state: the tag was
  written and the relation never recorded. It is undeclared's other direction, a claim in
  the source that the log doesn't hold, and it fails the same way; a `require:` policy
  can't say it, since the policy names what every id of a kind must relate to, not what a
  tag asks for. `mix surfex.suggest` offers the relation (§15), to record and validate. A
  declaration whose relation was **retired** is listed under the same heading as its own
  line, with the retire's note: "declared, but the relation was retired (note): remove the
  tag, or reinstate it". It fails too: the source asserts what the log decided against, and
  the fix is one of those two explicit acts. Suggest never proposes it, so a retirement is
  never undone without someone choosing to.

```test:unrecorded-declaration
a test's declaration resolving to a spec unit with no verifies relation in the log is
unrecorded and fails; one whose relation was retired is listed with the retire's note and
fails too; a relation in any other state, proposed or dangling included, clears it
```
- **Moved:** a live `refines` relation, both ends scanned, whose unit no longer sits in
  (`within`, §11) the unit it refines. A `refines` relation records a fact read from
  source, not a judgement: the block or hint sits in that section or block. Moving a marked
  block unchanged into another section changes neither end's version (a section's version
  leaves its blocks out), so the relation would stay current while stating what the source
  no longer says. Status re-reads `within` for every live `refines` relation and lists one
  that no longer holds as moved, naming where the unit sits now. It fails: the log states
  a structure the spec no longer has. `mix surfex.suggest` proposes its retirement beside
  the new `refines` relation (§15), so `mix surfex.suggest --accept` settles both. Only a
  unit that sits in another (a block, hint, table or row) is checked: a section sits in
  nothing, so a `refines` relation from one, related by hand, states no fact the source
  could contradict.

```test:moved-refines
a refines relation whose block now sits in another section is moved, naming the section
it sits in now, and fails, though neither end's version changed; one whose block still
sits in its unit, a retired one, and one from a section, which sits in nothing, is not
moved
```
- **Unselectable:** a test that `mix test --only verifies:id` can't select by some of its
  declared ids (§11), with those ids. A list of several ids is legitimate, the only honest
  tag for a test that verifies several requirements, so this is information for choosing
  between a list and one id per test, never a failure.

```test:unselectable-status
status reports each test some declared id can't select, with its ids, without failing:
the text counts them in one line and the JSON lists each test
```

- **Deviating:** a spec unit a test declares it verifies, where the test also declares a
  deviation (`@tag deviation: "<ref>"`, §11). The spec is right and the code doesn't meet
  it yet; the test, written from the spec, fails, and is skipped with the reference to
  what will fix the code. That is not `needs_update` (§14), where the spec is wrong and the
  code agrees with it. Status lists each deviating unit with its test and reference, and,
  given evidence (§17), the test's last result at its version. A **proposed** relation
  that deviates doesn't fail: one whose spec end is a deviating unit or a unit one sits in
  (the requirement whose code deviates), such as the unit's `implements`, the deviation
  test's own `verifies`, or another test's `verifies` that a review would overstate. It stays
  proposed, marked `deviates` with the references, and is reported as a planned relation
  is. The check is then a gate a project can join while it fixes its deviations, failing
  on anything else. A status derived with `deviations: :fail` (`mix surfex.status
  --no-deviations`) fails on any deviation, for a release. A deviation tag on a test that
  declares no spec unit deviates from nothing it names, and is **broken**.
- **Stale deviation:** a deviating unit whose deviation test **passed**, at its version,
  in the evidence given (§17): the code meets the unit now, so the tag comes off and the
  relations are validated by the process. It fails, or tags would outlive their fixes. It
  is judged only from a run where the test ran: a skipped test records `skipped`, never
  `passed`, so without such a run a deviation is listed, not judged stale.

```test:deviating-status
a unit a deviation-tagged test declares is listed as deviating with its reference and
test; its proposed relations and those of a unit it sits in don't fail and are marked
deviates, while other states still fail; deviations: :fail fails on any; a deviation tag
on a test declaring no unit is broken
```

- **Proposed:** the tip is recorded with basis `proposed`: a claim (a citation, a tag, a
  pair named by hand) that nothing has validated yet (§18). It fails the check, like a
  dangling relation, until evidence or a review validates it.
- **Unvalidated:** a current `implements` or `verifies` relation whose tip has no
  validating basis (`evidence`, `review` or `judgement`, §18), such as one recorded before
  validation existed. It is reported, and fails the check only when the status is derived
  with `validated: true` (`mix surfex.status --validated`).
- **Marks** (§14): a `needs_update` mark says the spec unit itself needs to change: the
  tests reflect it and the code passes them, but the result is wrong or clearly
  sub-optimal. Each mark is **open** while the unit is at the version it recorded,
  **resolved** once the unit's version moves (the spec changed, and the process starts
  over from it), **withdrawn** when a retire names it, and **orphaned** when the unit is
  no longer scanned. Open and orphaned marks are reported (`Surfex.Status.derive/4`'s
  `marks`); resolved and withdrawn ones live on in the history.
- **Stale:** a live `excuses` relation whose code item its class no longer covers,
  judged by `Surfex.Coverage.verdict/3` as the suggestion was (§15). The item may now be
  implemented, no rule of any class may match it, or another class's rule may match it
  first. An excuse must stay true to its class's rules, not only to the versions it was
  confirmed at. Judging it needs the rules, which `Surfex.Status.derive/4` takes as
  `coverage:`.

`Surfex.Status.failing?/1` is true when any relation is dangling, orphaned, conflicted or
proposed (unless it deviates), any id is unmet, any `verifies` relation is undeclared, any declaration is
unrecorded, any `refines` relation is moved, any declaration or citation is broken, any excuse or deviation is stale, any relation's
test failed, or any confirmation by evidence is unproven. New,
retired, impacted, planned, unimplemented, unselectable, deviating and unvalidated don't fail on
their own. A status derived with `deviations: :fail` fails on any deviating unit. A
status derived with `planned: :fail` (`Surfex.Status.derive/4`) fails on any planned
relation too: the check that everything planned has been built, such as a release's. One
derived with `validated: true` fails on any unvalidated relation: the check that every
relation was validated by the process (§18). Marks don't fail on their own either: a known
spec problem is backlog, not a broken build. One derived with `marks: :fail` fails on any
open or orphaned mark: the check that nothing ships with a known spec problem.

**The triangle.** A spec unit, its tests and its code relate three ways: the code
`implements` the unit, a test `verifies` the unit, and the test `tests` the code. When
tests are scanned, every spec unit that something implements is checked for each side
missing:
- `:no_test`: no test verifies it, or any block or hint inside it;
- `:test_misses_code`: a test verifies it but exercises none of the code that
  implements it, naming the test;
- `:code_untested`: code implements it that no verifying test exercises, naming the
  code.

A test exercises code when a live `tests` relation says so, or when the test **reaches**
it. A call is a fact the source states, so the triangle doesn't wait for it to be
recorded, and a project that doesn't record `tests` relations sees no false gaps. Only
code that is scanned counts: a test calling a function not written yet still shows the
code missing.

**What a test reaches** (`Surfex.Scan.reach/1`) is the scanned code its `calls` (§11)
name, and the scanned code that code calls (§4 `calls`), transitively: a test calling a
public entry point reaches the functions it hands the work to. A call naming code a
record covers but that isn't a record (a module's hidden definition, §4 `hidden`), such
as a Mix task's undocumented `@impl` `run/1`, reaches that record and goes on through the
hidden code's calls; the hidden code itself is never a record. It is a pure function of
the scans. A spec unit can then cite the function that does the work while its test
drives the entry point that users call. Reach credits only relations someone recorded.
It never makes one, and a `tests` relation still names what the test calls itself (§15).

Code is compared by its definition (§11), so a test calling `f/1` exercises the `f/2` a
unit names when both are one function with a default argument. The same holds when
evidence confirms `implements` (§17) and when CI checks the claim. The gaps are the
status's `triangle`. They are reported, and fail the check only when
derived with `triangle: :fail`. Without scanned tests there is no triangle, since
every unit would otherwise report a missing test for a project that doesn't scan them.
`Surfex.Status.summary/1` counts relations per type and state. `Surfex.Status.units/1`
counts the spec units scanned, by role: sections, blocks, test hints, tables and rows. The
reports show tables and rows only when a spec marks a table.

```test:dangling-side
a relation whose end changed since it was confirmed is dangling,
and names which end changed
```

```test:planned-state
a relation to an id that doesn't exist yet is planned and passes the check; when the id
appears a planned implements is proposed, and one recorded without a basis dangles; once
validated or confirmed it is current
```

```test:triangle-gaps
given a spec unit something implements:
no verifying test, a verifying test that reaches none of its code, and implementing
code no verifying test reaches are each reported, naming the test or the code
```

```test:test-reach
a test reaches the scanned code it calls and, transitively, the scanned code that code
calls;
a cycle ends, unscanned code is passed over, and code reached only through an entry point
closes the triangle without suggesting a tests relation to it
```

```test:hidden-reach
a call naming a module's hidden definition reaches the module and what that definition
calls, never the definition as a record; a public function dispatching through its own
behaviour reaches what the hidden callbacks call
```

```test:mark-states
a mark is open while its unit is at the marked version, resolved when the unit changes,
withdrawn by a retire naming it, and orphaned when the unit is gone; open and orphaned
marks are reported and fail only under marks: :fail
```

```test:status-failing
the check fails on a dangling, orphaned, conflicted or proposed relation (one that
deviates excepted), an unmet id, an undeclared verifies relation, an unrecorded
declaration, a moved refines relation, a broken declaration or citation, a stale excuse or
deviation, a failed test's relation, or an unproven confirmation, and on nothing else by
default; planned: :fail adds planned relations, validated: true unvalidated ones, and
deviations: :fail deviating units
```

```test:units-by-role
the spec units scanned are counted by role: sections, blocks, test hints, tables and rows,
and the reports show tables and rows once a spec marks a table
```

```test:undeclared-verifies
a verifies relation whose test no longer declares its spec unit is undeclared and fails;
suggest --accept retires it
```

### 13.2 Configuration
<!-- surfex: status-config -->

`Surfex.Status.Config` reads `.surfex.exs`. It holds the profile keys (§8) and the
log's own: `heading_ids` (§11), `rule_ids`, `fold_paths` and `unit_refs` (§11.1),
`scanners` (§5.3), `scanner`, `scanner_opts`, `namespace`, `goldens` (§10.3), `require`,
`tests`, `declares` (§11.6), `triangle`,
`require_red` (§17), `router` (§11.3), `claim_implements` (§15.1),
`spec_classes` (§11.7), `issues` (§13.7), `adoption` (§18.1),
`process` (§19) and
`completeness` (§20).
- `Surfex.Status.Config.read!/1` reads and validates it. It raises on an unknown key, and
  on a key of the removed trace, saying so (§9). Every task reads its config through
  it, so a misspelt key is never silently ignored.
- Claim grain is how surfex reads a spec (§13.4, §13.5): there is no switch for it.
  `claim_grain:`, the transitional key it shipped behind, is refused, `true` or `false`:
  the message names the key, says claim grain is now the only reading, and points to the
  upgrade step and `mix surfex.migrate --report` (§14.1). A config that only drops the
  key reads as before.
- `Surfex.Status.Config.items/2` runs the code scanners and combines their items (§5.3):
  the built-in Elixir scanner, the project's own (`Surfex.Scanner`), or several.
- `Surfex.Status.Config.scans/2` scans each markdown unit of `sources`, reading headings
  by `heading_ids:` (§11), folded by `fold_paths:`, and marked lists by `rule_ids:`
  (§11.1), each item the code
  scanners (§5.3) find, and, when `tests:` names globs of test
  files, each test in them (`Surfex.Scan.ExUnit`), its declarations in a `unit_refs:` form
  given as the full ids they name.
  A missing `sources`, or one that matches no section, raises. So does a `tests:` that
  matches no file.
- `Surfex.Status.Config.options!/1` reads `triangle: :report | :fail` (`:report` by
  default) for `Surfex.Status.derive/4`, and raises on any other value. When `classes:`
  is present it also gives the class rules as `coverage:`, for judging stale excuses. It is the
  project maintainer's intent, and a wrong value is loud: the gaps are reported either
  way.
- `Surfex.Status.Config.load/3` gives everything `Surfex.Status.derive/4` needs from the
  config, scanning the code once: the scans, and the options, including the broken
  citations (`Surfex.Status.Config.broken_citations/4`). Those are read with
  `Surfex.Cite` under `Surfex.Status.Config.profile!/2`: the config's profile keys
  (`Surfex.Profile.keys/0`) over the Elixir scanner's defaults for `namespace:`, which
  defaults to the project's app name.
- `Surfex.Status.Config.status/4` derives the whole status as `mix surfex.status` does: the
  scans, the log, the policy and the options (`load/3`), plus the caller's own options.
  Every task that needs the status derives it this way.
- `Surfex.Status.Config.process!/1` validates `process:` (§19): `:print`, the default, or
  `{:command, argv}` with a non-empty list of strings, and raises on anything else.
- `Surfex.Status.Config.require!/1` validates the `require:` policy against the known
  kinds, spec roles and types, and raises naming what's wrong.

A spec rule (a spec role's key such as `section:`, or the kind key `spec:`) judges claims
only (§13.4). A container isn't judged: it has no text of its own, and its claims answer
under their own roles. A claim is judged by its own relations, as any id is, so a
`:contained` claim that a `verifies` rule fails is unmet and says why: `contained`
(`why: :contained`, the JSON's `why`, and the report's `contained: a unit inside it is
verified, its own text isn't`). `require: [spec: [:verifies]]` is then the one-line
policy that every claim is verified.

```test:contained-unmet
under require: [section: [:verifies]] a container isn't unmet, a contained section is
unmet with why contained, and a section with its own tag is met; spec: [:verifies] judges
rules and rows and skips containers
```

```test:status-config-read
.surfex.exs is read and validated: unknown keys and keys of the removed trace are refused,
the code is scanned once, and citations are read under the config's profile
```

```test:claim-grain-refused
a .surfex.exs setting claim_grain: true or claim_grain: false is refused, naming the key,
saying claim grain is now the only reading and pointing to mix surfex.migrate --report;
the same config without the key reads
```

### 13.3 Reports and the check
<!-- surfex: status-reports -->

`Surfex.Status.Report` presents a status:
- `Surfex.Status.Report.text/1`: the verdict, the counts per type and the spec units by
  role. Then each dangling, orphaned, conflicted, proposed (one that deviates with its
  references), impacted and planned relation, the deviating units (each with its location,
  reference, test, and the test's result in the run) and, apart, the stale deviations with
  what settles them, the unimplemented, new and unmet ids, the broken citations, the failed relations (each
  with its test), the unproven confirmations, the undeclared relations, the unrecorded
  declarations (each with its test's location, and what settles it), the moved `refines`
  relations (each with its unit's location, where it sits now, and what settles it), the
  stale excuses, the broken declarations, the triangle's
  gaps and the open and orphaned marks (with their notes, who and when), with locations. A block or hint in a list says what it sits in. Unvalidated
  relations (§18) are counted; they are listed only when the status is derived with
  `validated: true`, where they fail, since a project moving over has many. Unselectable
  tests are counted in one line, `unselectable by --only: N tests`, and listed only in the
  JSON (`unselectable`, each test with its ids): a project that tags with lists has many,
  and the count is the decision's input.
- `Surfex.Status.Report.golden/2` (`Surfex.Status.Report.golden/1` writes `RELATIONS.md`):
  the status as a golden to commit. It has a table per relation type (end, other end,
  state, the ends that changed or are planned), then the unimplemented, new and unmet
  ids, the broken citations (by section, not line), the undeclared relations, the
  unrecorded declarations, the deviating units (unit, reference, test: the ledger of known
  deviations), the moved `refines` relations, the stale excuses, the broken
  declarations, the triangle's gaps and the open and orphaned marks (unit, state, note), with
  the spec units by role among its stats. It holds no hashes and no times, so it changes when a relation's
  state changes, not on every confirmation. It's a pure function of the scans and the
  log, so a merge conflict in it is resolved by regenerating. A proposed relation that
  deviates reads `:proposed` · deviates. It leaves failed relations, stale deviations
  and unproven confirmations out, since they depend on the last test run, not on the
  source or the log,
  and validation too: it follows the relations' states, and a proposed relation shows as
  one.
- `Surfex.Status.Report.json/1`: the work list for tools and agents. For each relation,
  its type, state, impact and the references it deviates by (`deviates`), and per end the recorded hash, the current hash, whether
  it changed, whether it is planned, and its location. Plus the spec units by role, the
  unimplemented, new and unmet ids (each with its role, what it sits in and what it
  declares), the broken citations, the failed relations (`failed`, each with its type,
  ends and test), the unproven confirmations, the undeclared relations, the unrecorded
  declarations (`unrecorded`, each with its test, its spec unit, whether its relation was
  retired, and the retire's note), the moved `refines` relations (`moved`, each with
  `from`, `to`, and `now`, where the unit sits now), the deviating units (`deviating`, each
  with its `spec`, `test`, `ref`, the test's `run` result or null, and whether it is
  `stale`), the stale excuses, the
  broken declarations, the triangle's gaps, the unvalidated relations, the unselectable
  tests (each with its ids) and the open and orphaned marks (unit, state, note, who,
  when, entry id, location). A value that
  doesn't exist (a planned end's recorded hash, a gone end's hash and location, or a
  conflicted relation's recorded hashes, since it has no one tip) is JSON `null`.

`Mix.Tasks.Surfex.Status` is `mix surfex.status`:
- `--format text|json` (text by default)
- `--verify` verifies the log (§12.2) **before** loading it, so a tampered log gets its
  report, not the first decode error
- `--evidence` checks every confirmation by evidence against the evidence the last
  `mix test` recorded (`evidence:`), and fails on any that is unproven, and on any
  relation whose test failed. It only reads.
- `--no-planned` fails on any planned relation as well (`planned: :fail`). Which
  pipeline allows plans is the caller's decision, so it is a flag, not a setting
- `--validated` fails on any unvalidated relation as well (`validated: true`, §18), and
  lists them
- `--no-marks` fails on any open or orphaned mark as well (`marks: :fail`), for a release
  that must not ship with a known spec problem
- `--no-deviations` fails on any deviating unit as well (`deviations: :fail`), for a
  release that must not ship with a known deviation. Which pipeline allows them is the
  caller's decision, as with `--no-planned`
- `--no-baseline` fails on any relation resting on the baseline (`baseline: :fail`,
  §18.1), and the report counts them with the adoption mode
- `--merge PATH`, once per file, checks the confirmations by evidence against several CI
  jobs' evidence files together, in place of `--evidence`'s one file (§17)
- with `completeness: [min: N]` in `.surfex.exs`, it fails when the overall completeness
  score (§20) is below `N`, printing the score
- it fails when the status is failing, or the log doesn't verify
- it compiles the project first only when `.surfex.exs` names a project scanner

```test:status-report-forms
the status reads as text for people, JSON for tools and a golden for review, the golden
with no hashes or times
```

### 13.4 What verified means
<!-- surfex: verified-means -->

Whether a unit's text is verified is a question about the spec, and only evidence aimed
at that text answers it. A claim inside a section says nothing about the section's other
text, and a section's own `verifies` says nothing about the claims inside it.

**Own text, claims and containers.** A unit's **own text** is what its version covers
(§11): its body without the finer units inside it (blocks, hints, rules, rows, contracts)
and without markers, so empty exactly when `Surfex.Scan.Markdown.empty?/1` says so.

- A **claim** is a unit with own text: a section or marked block whose own text isn't
  empty, and every test hint, rule, row and contract.
- A **container** is a unit with none: a heading over subsections, a section whose whole
  body is named claims (a marked list, a marked table, hints), or a marked table, whose
  rows are its claims (its version is its header, which is structure). A container
  states nothing itself.

A unit is **inside** what it sits in (`within`, §11) and, for a section, the section
whose heading it sits under: the nearest heading above it at a higher level, a fact of
the scan (`under`, `Surfex.Scan`, recorded by `Surfex.Scan.Markdown.sections/3`), at any
depth. Fully decomposing a section's prose into named claims makes it a container: the
claims then answer for the section's text, each for its own.

```test:container-units
naming a section's last prose paragraph as a rule turns the section from a claim into a
container: the section's version and the rule's change, and no other unit's
```

**Verdicts.** `Surfex.Status.derive/4` gives every spec unit one **verdict**
(`verdicts`, read one at a time with `Surfex.Status.verdict/2`), from the current
`verifies` relations of tests and checks (§11.4):

| Verdict | When | What it needs |
|---|---|---|
| `:verified` | a claim with a current `verifies` of its own | nothing |
| `:contained` | a claim with no `verifies` of its own, where a claim inside it is `:verified` | its own evidence: its text is not verified |
| `:unverified` | a claim with no `verifies`, own or inside | its own evidence |
| `:through_claims` | a container, reported with how many of the claims inside it are verified | nothing of its own: its claims answer for themselves |

Two more verdicts, `:excused` and `:deferred`, are reserved for classes of spec text, a
section whose own text asserts nothing. A classed section with claims inside reads as a
container.

- **No credit moves upward.** A unit's own `verifies` says nothing about the claims
  inside it, so each claim answers for itself.
- **No credit moves into own text.** A claim inside a unit says nothing about the unit's
  other text. `:contained` names that state, so a unit green only through containment is
  a fact the status states, not something a reader infers.

The verdicts come from one index of the units' `within` chains, built once per derive
(§13), not a walk per unit.

```test:containment-verdicts
a section verified by its own tag is verified; one with prose and a verified hint inside
but no tag of its own is contained, naming the hint; one with prose and nothing is
unverified; a heading over two subsections, one verified, is a container with 1 of 2
claims verified; a section whose body is only a marked rules list is a container; a
marked table whose rows a check verifies is a container, through its rows
```

**On a container.** A `verifies` whose spec end is a container counts for nothing: the
container has no text to verify, and its claims aren't verified by it. Each is listed
**on a container** (`on_container`), with the claims inside it, so the tag can move to
what it checks. The relation stays valid: moving the tag is the project's call, as with
an inert tag (§11.5).

```test:verifies-on-container
a test tagging a heading-only section is listed on a container with the claims inside
it; it doesn't fail the status, and it verifies none of those claims
```

**Reports.** None of this fails a status on its own. `Surfex.Status.Report.text/1`
counts the claims by verdict and the containers in a header line (`claims: N · verified
· contained · unverified · containers`) and lists each contained claim (with its location
and the verified claims inside it) and each `verifies` on a container.
`Surfex.Status.Report.json/1` gives `verdicts` (each unit's), `contained`, `on_container`
and `containers` (each container's claims and how many are verified). The status golden
(`Surfex.Status.Report.golden/2`) counts claims by verdict among its stats and holds a
`contained` table and an `on container` table.

### 13.5 Earning an implements
<!-- surfex: earning -->

Whether code earns its `implements` is a question about the code, and it is answered by
the verified claim whose tests exercise it (§13.4). `Surfex.Status.earning/2` gives the
answer for one `implements` relation, and `Surfex.Status.earned?/2` says only whether it
earns. It is the one definition of "earned": a policy that counts only earned relations
reads it, as do the migration's candidates (§14.1).

An `implements` from code C to a spec unit U **earns** when:

1. U is `:verified`; or
2. U is a container, or `:contained`, and a `:verified` claim inside U has a verifying
   test that exercises C, or a verifying check whose inputs hold C (§11.4). A test
   exercises C as the triangle reads it (§13.1): it reaches C, or holds a live `tests`
   relation to it. A current review of the relation through a named path (`--through`,
   §18.2) counts as a verified claim's test exercising C.

Otherwise it is **unearned**, and the answer says why:

- `unverified`: no evidence at all, neither on U nor on a claim inside it;
- `not_exercised`: a claim inside U is verified, but none of its tests or checks
  exercise C;
- `nothing_inside`: U is a container with no claims inside it.

Code compares by definition (§11), so a function's default-argument arities earn
together.

Rule 2 is containment for code, bounded by evidence. A claim with no verifying test earns
nothing, however many relations hang off it, and a verified claim earns only for the code
its tests exercise: no credit moves upward from a verified section to the claims inside
it, and none is given wholesale to whatever a verified claim's section names. An earned
contained relation is exactly one the migration can re-home to its claim (§14.1). The
answers come from indexes derive builds once (what each test and check exercises, the
claims inside each unit), so asking about every relation stays linear.

```test:earned-through-claims
code implementing a contained section earns when a verified hint inside it has a test
reaching the code, directly, through a function that calls it, or through another arity
of the same definition, or a verifying check whose inputs hold it; it is not_exercised
when the hint's test reaches only other code, and nothing_inside when the section is a
container with no claims
```

```test:no-upward-credit
a rule inside a section the section's own tag verifies is unverified, and code related
only to that rule is unearned as unverified
```

```test:earning-agrees
on one fixture, an earned contained relation is one the migration finds a candidate claim
for among the verified claims inside, and a not_exercised one has none
```

### 13.6 Excused and deferred units
<!-- surfex: classed-units -->

A class (§11.7) says a section's own text asserts nothing a test or check could fail on,
and a deferral says a unit's text isn't built yet. `Surfex.Status.derive/4` lists both
whatever the configuration: `excused` (each classed unit with its class, the class's
reason, the marker's own reason, how it was selected, and the claims inside it) and
`deferred` (each deferred unit with its reference and reason). The status also reads
them, which is what can change what fails.

**A class excuses a section's own text, and nothing else.** Every named claim inside a
classed section (a rule, a row, a hint, a contract, a marked block, a subsection) stays a
claim and is judged as any claim is. A pattern can't hide what an author named.

- A classed section with no claim inside is `:excused`, and a deferred unit with none is
  `:deferred` (C.1's verdicts, §13.4). With claims inside, either reads as a container:
  its claims answer for themselves, and code relating to it earns through them (§13.5).
- `require:` spec rules don't judge an excused or deferred unit, as they don't judge a
  container.
- The triangle skips excused and deferred units.
- An `implements` to an excused or deferred unit earns nothing (`Surfex.Status.earning/2`
  says `unverified`): there is no evidence to earn through.

```test:classed-claims-stay
a section classed by pattern holding a named rule, a marked table, a hint, a contract, a
marked block and a subsection: each stays a claim, unverified until a test verifies it,
and require: [spec: [:verifies]] fails on the claims, not the section; a classed section
with no claims inside is excused and not judged
```

**Contradicted.** A class says the text asserts nothing, so a relation saying something
checks or realises it means one of the two is wrong. Each is listed as `contradicted`, and
the status fails:

- a live `verifies` whose spec end is a classed section, with claims inside or not (it
  should name the claims);
- a live `implements` whose spec end is an excused section (one to a classed section with
  claims inside is a container's);
- a test's or check's declaration naming a classed section, when no live `verifies`
  records it yet.

The fix is to retire the relation (it was narration) or to remove the class.

```test:contradicted-relations
an implements and a verifies to an excused section, and a test declaration naming it,
are contradicted and fail; retiring the relation or removing the class clears it; an
implements to a classed section with claims inside isn't contradicted
```

**Deferred.** A deferred unit isn't judged by `require:` spec rules, is out of the
completeness score (§20.2) and is listed with its reference and reason. A deferral
excuses the spec unit, never code: the MR that builds it adds the test and the
relations, and removes the marker.

```test:deferred-units
a deferred section and a deferred block are deferred, listed with reference and reason,
not judged by require: spec rules; an implements to one is unearned as unverified
```

`Surfex.Status.Report.text/1` counts the excused units (and how many hold claims) and the
deferred units in one header line, and lists each contradicted relation.
`Surfex.Status.Report.json/1` gives `excused`, `deferred` and `contradicted`.

### 13.7 Deferrals go stale
<!-- surfex: stale-deferrals -->

A deferral (§11.7) holds while its issue is open and the unit isn't built. One that no
longer holds is a **stale deferral**, and the status fails, as it does on a stale
deviation (§13.1). `stale_deferrals` lists each with
its reference and why:

- `built`: the unit has a current `verifies` of its own (§13.4). It has its evidence now,
  so the marker comes off. This is judged with or without issue states.
- `closed`: its issue is closed. Remove the marker, or re-defer with the issue that now
  holds the work.
- `unresolved`: the caller asked the tracker and it couldn't say. A reference the tracker
  can't answer could never go stale.

```test:deferral-stale
with issues: %{"#12" => :closed} a deferral by #12 is stale and fails; :open isn't; an
unanswered reference is unresolved; without issues: the deferral is listed with no issue
state and not judged closed; a deferred unit with a verifies of its own is stale as built,
with or without issue states
```

**How surfex learns an issue's state.** Surfex knows no tracker (§19), and
`Surfex.Status.derive/4` is pure (§13), so an issue's state is an input, like evidence
(§17): derive takes `issues: %{ref => :open | :closed | :unresolved}`, and lists each
deferral's state beside it (`issue`, `nil` when none was given).

- `issues:` in `.surfex.exs` declares how to ask, in the shape of `process:`'s command:
  `issues: [command: ["glab", "issue", "view", "{n}", "--output", "json"], state: "state",
  project: "group/project"]`. `Surfex.Status.Config.issues!/1` validates it.
- `Surfex.Issues.states/3` runs the command once per distinct reference, with no shell,
  substituting `{ref}` (as written), `{n}` (its number) and `{project}` (the reference's
  project, else `project:`). With `state:` the output is JSON and that key holds the
  state; without it the trimmed output is. `open` or `opened`, in any case, is open;
  `closed` is closed; anything else is unresolved. A command that exits non-zero stops the
  asking with its output: a tooling failure is loud, never a state.
- `mix surfex.status --issues` asks once per run, through
  `Surfex.Status.Config.status/4`, for the references the scanned spec's deferrals name,
  and passes the answer to derive. Without `--issues` nothing runs, and only `built` is
  judged.
- **Nothing is stored.** An issue's state is for the run that asked. A committed snapshot
  would be stale the day after.

Staleness is judged only where the command can run: a CI job whose token can't read the
tracker leaves the judgement to a developer's check or a scheduled job that can.

```test:issue-resolver
a declared command printing opened, JSON {"state":"closed"} read with state:, and garbage
give open, closed and unresolved; {n}, {ref} and {project} are substituted with no shell;
a non-zero exit stops mix surfex.status --issues with its output; an unresolved reference
fails only under --issues; issues: is validated
```

### 13.8 Excusals in the record
<!-- surfex: classed-record -->

A class or a deferral is reviewed where it is written, in the spec and in `.surfex.exs`,
and recorded nowhere else: no log entry is written for one. So that a new excusal is
still seen, the status golden (`Surfex.Status.Report.golden/2`, `RELATIONS.md`) holds an
`excused` table (each classed unit, its class and how it was selected: `marker`,
`pattern` or `alias`) and a `deferred` table (each deferred unit and its reference), keyed
by unit id and ordered by it, plus `contradicted` and `stale deferrals` tables when there
are any. Its header counts `excused`, `deferred`, `contradicted` and `stale deferrals`
where a spec has any class or deferral, so a pattern that suddenly classes twenty units
shows as twenty rows and a moved count. The completeness golden (`COMPLETENESS.md`)
counts the units excused by class and deferred in its header (§20.2). Whether a growing
excusal is right is the project's call: a decision log should grow.

```test:classed-goldens
the status golden's excused and deferred tables are keyed by unit, in id order whatever
the scan order, counted in the header, and change only when a class or deferral does;
a spec with neither renders as before; the completeness golden's header counts both
```

### 13.9 Unearned implements
<!-- surfex: unearned -->

An `implements` counts for a `require: code:` rule only when it earns (§13.5). A claim written only to own code, with nothing that could fail
behind it, satisfies nothing: the code it names is judged by the relations that do earn.

- `Surfex.Status.derive/4` lists every `implements` that doesn't earn as `unearned`: its
  code, its spec unit and why (`unverified`, `not_exercised` or `nothing_inside`). A
  retired, orphaned or planned relation is judged by its state instead.
- An unearned `implements` meets no `require:` rule. Code whose only `implements` are
  unearned fails `require: [code: [:implements]]` as unmet, and its entry says
  `why: :unearned`.
- Where the policy asks code for an `implements` (`require: [code: [:implements]]`),
  every unearned relation fails the status, including one from code another relation
  already meets: the spare claim is what the rule guards against. Without that rule the
  list is reported and doesn't fail.
- Credit never flows up from an enclosing unit: a claim inside a verified section earns
  nothing through the section's tag, and code earns through a verified unit, or through a
  verified claim inside the unit whose test exercises it (§13.5).

The fix for each depends on its why. `unverified` needs a test or check that verifies the
unit, or the relation retired; `not_exercised` usually means the code belongs to another
claim, and `mix surfex.migrate --report` shows which (§14.1); `nothing_inside` names a
container with no claims to earn through.

```test:unearned-fails
with require: [code: [:implements]], code related only to an unverified hint is unearned and unmet with why unearned, and the status fails; verifying
the hint clears both; code related to a contained section earns through a verified hint
whose test reaches it, and is unearned as not_exercised when the test reaches other code;
a spare unearned relation fails even when another relation meets the code; without the
rule the list is reported and passes
```

`Surfex.Status.Report.text/1` counts the unearned relations in a header line (marking it
failing under `require: code:`) and lists each with its why; `Surfex.Status.Report.json/1` gives `unearned` (`code`, `unit`, `why`); and the status
golden (`Surfex.Status.Report.golden/2`, `RELATIONS.md`) holds an `unearned` table keyed by code and counts it in its
header.

```test:unearned-report
the text counts unearned relations in its header and lists each with its why, the JSON
gives code, unit and why, and the golden has an unearned table and header count
```

### 13.10 A contract stated differently in several files
<!-- surfex: restated-contracts -->

A spec often states one function's contract in more than one file: in the chapter that
defines it and again in a reference chapter. Within a file that is an error (§11.9);
across files it is legitimate, but the statements drift apart unseen. A contract stated in
several spec files whose statements differ is **restated**, listed and never failing.

- Two contract units state the same contract when their ids after the file are equal:
  the same `Module.name/arity` and the same kind, so a `@callback` never pairs with a
  `@spec` of the same name (§11.9).
- Their statements differ when their versions do (§11.1): a signature's text with its
  whitespace collapsed, so reflowing one changes nothing.
- `Surfex.Status.derive/4` gives `restated`, one entry per such contract, `%{contract,
  units}` with the units' ids sorted, whatever its other units' versions; the same
  statement in several files is not listed. It never makes the status fail.
- `Surfex.Status.Report.text/1` lists each with the units stating it, the JSON gives
  `restated` (`contract`, `units`), and the status golden holds a `restated` table and
  counts it in its header, each only where there is one.

```test:restated-listed
one contract stated in two files with different signatures is listed with both units, and
the status doesn't fail; the same statement in two files is not listed, nor are two
functions of one name in two modules, nor a callback and a spec of one name; a statement
differing only in whitespace is the same; text, JSON and golden show the list only where
it has an entry
```

### 13.11 A deviation holds back what it sits in
<!-- surfex: deviation-holds -->

A deviation (§13.1) says the code doesn't meet a unit yet. Code related to a section that
holds that unit doesn't meet the section either, however green its other tests are. The
deviation **holds back** the units it says that of, and nothing validates or earns an
`implements` to one of them while the tag is on.

- `Surfex.Status.derive/4` gives `held`: each spec unit a deviation holds back, with the
  references of its deviations, sorted. That is every deviating unit and each unit it sits
  in (`within`), at any depth, and each section it sits under (`under`). A proposed
  relation is marked `deviates` when its spec end is held, so a section above the
  deviating one is marked too.
- `mix surfex.confirm --evidence` (`Surfex.Record.confirm_by_evidence/4`) doesn't record
  an `implements`, dangling or proposed, whose spec unit is held, whatever the evidence
  says. It lists each one the evidence would have validated as `held back by a
  deviation`, with the references (`Surfex.Record.evidence_outcome/4`). Other relations
  are confirmed as
  before: a deviation test's failing run is still its `verifies` relation's evidence.
- Once the tag goes and the test passes, nothing is held, and the next evidence run
  validates them as it would have.

```test:deviation-holds-back
with a deviation tag on a hint, confirm by evidence records neither the hint's nor its
section's implements and lists both as held back with the reference, while it still
records the verifies; a section the deviating one sits under is held too; with the tag
off and the test passing, the next run validates them
```

An `implements` whose spec unit is held is unearned (§13.9),
asked before anything else: `Surfex.Status.earning/2` answers `{:unearned, :deviates}`.
So `unearned` lists it with why `deviates` and it meets no `require: code:` rule; code
with no other `implements` that earns misses `unearned` in completeness (§20.3). Status
and confirm agree: what confirm holds back doesn't earn.

```test:deviation-unearned
an implements to a deviating hint, to its section, or to a section above it is unearned
as deviates, even when a verified claim's test exercises the code; the text names the why
```

### 13.12 Lines that are not claims
<!-- surfex: not-claims -->

Everything in a section that isn't a finer unit is its own text (§13.4). A spec that names
every behavioural statement as a claim can still keep lines that state nothing a test could
fail on: a provenance line (`Legacy reference: …`), or a sentence naming the code a section
is realised by. Such a line keeps the section a claim with no evidence of its own, contained
for good. Classing the section (§11.7) would excuse it, and with it any behavioural sentence
added later, which no one would then judge. The spec's author knows which lines these are,
so the project declares them, where config review sees it:

- `not_claims: [~r/^Legacy reference: \S+$/]` in `.surfex.exs`
  (`Surfex.Status.Config.not_claims!/1`) is a list of regexes. Anything else is refused.
  `Surfex.Status.Config.scans/2` passes it to `Surfex.Scan.Markdown.sections/3` as
  `not_claims:`.
- Each non-blank line of a section's or marked block's own text is matched **as written**,
  one line at a time, against the patterns in order; the first that matches takes it. A
  statement wrapped over several lines needs each line matched, and a pattern on a prefix
  alone takes whatever the line says after it, so anchor it to the whole line. Hints, rules, rows and
  contracts are always claims and are never matched.
- A matched line is left out of the unit's own text for verification (§13.4) and
  completeness (§20) only, read with `Surfex.Scan.Markdown.claimless?/1`: a unit is
  claimless when its own text is empty or every line of it was matched. A section left with
  no own text is a **container** and answers through its claims. A sentence added later that
  no pattern matches is own text again, so the section is a claim, contained when a claim
  inside it is verified.
- **Versions are unchanged.** A unit's version still covers every line, matched or not, so
  editing a provenance line still dangles the unit's relations.
- A paragraph that only names code is **not** left out by rule. A project that wants that
  writes a pattern for it, so the choice is visible in config, and a behavioural sentence
  that happens to name code stays a claim.
- The scan records what was matched on the unit (`not_claims`, `Surfex.Scan`):
  `%{lines, own_text}`, the lines each pattern took (by the pattern's `inspect/1` form) and
  whether any own text is left. `Surfex.Status.derive/4` gives `not_claims`, one entry per
  pattern, `%{pattern, lines, units}`, the configured ones in order and a pattern matching
  nothing with `0`, so a pattern that starts swallowing prose, or matches nothing any more,
  shows. `Surfex.Status.Report.text/1` lists each pattern with its lines and units, the
  JSON gives `not_claims`, and the status golden holds a `not claims` table, each only
  where `not_claims:` is set. Information, never a failure.

```test:not-claims-container
a section whose own text is only a provenance line, with a verified hint inside, is
contained without not_claims: and a container with a pattern matching that line; its
version and the hint's are the same either way
```

```test:not-claims-added-sentence
adding a sentence no pattern matches beside the matched line makes the section a claim
again, contained by its verified hint, and completeness counts it
```

```test:not-claims-reported
derive lists each pattern with the lines it matched and the units holding them, and a
pattern matching nothing with 0; text, JSON and golden list them only where not_claims: is
set; a not_claims: that isn't a list of regexes is refused
```

### 13.13 A deferral may cite a spec unit
<!-- surfex: unit-deferrals -->

Some deferrals are spec decisions, not tracked work: a feature "deferred past v1", with the
reason recorded in a decision table. No issue exists for it, and none should, so a deferral
(§11.7) may cite the **spec unit** that records the decision instead of an issue:

```markdown
## 6. View abstraction
<!-- surfex: deferred spec/06-architecture.md#deferred@1a2b3c4d: past v1 -->
```

- `<!-- surfex: deferred UNIT[@VERSION][: REASON] -->`: a reference that isn't an issue
  (`#123`, `group/project#123`) cites a spec unit, in any form a test's declaration may
  name one (§11, the project's `unit_refs:` forms included). A reference starting with `#`
  is always an issue. The reference holds no `:` or `@`.
- **It must resolve.** `Surfex.Scan.Markdown.records/4` resolves each citation against the
  whole spec once every file is scanned (`Surfex.Status.Config.scans/2` passes it the
  project's `unit_refs:`), and one that names no unit, or several, raises naming the file
  and line, as a malformed marker does. The cited unit must be in `sources:`.
- `@VERSION` **pins** the cited unit's version (8 lowercase hex characters, §11) as the
  author last judged it. Anything else after `@` raises.
- The scan records the citation on the deferred unit as `%{ref, reason, cites, pin, line}`
  (`Surfex.Scan`): `cites` the cited unit's full id. An issue deferral's record is
  unchanged, `--issues` asks only issue references, and a citation's `issue` is `nil`.

```test:unit-deferral-resolves
a deferral citing a unit by full id, by bare anchor or by a unit_refs: form records the
cited unit's id, its pin and its reason; one citing no unit or several raises naming the
file and line; a malformed pin raises; an issue deferral's record is as before
```

**It goes stale when the cited unit changes.** A citation is a stale deferral (§13.7)
when:

- `changed`: the cited unit's version isn't the pinned one. The decision text was edited,
  so the deferral is judged again: write the new version in the marker, or remove it.
- `unpinned`: the marker pins no version. The status names the version to write.
- `gone`: no unit has the cited id any more (a scan with `records/4` raises first).
- `built`, as for every deferral.

The text report says what to write and on which line; the JSON's `stale_deferrals` carry
`pin`, `version` and `line`, and its `deferred` carry `cites` and `pin`, for citations only.

```test:unit-deferral-stale
a citation pinning the cited unit's version holds; editing the
cited unit makes it stale as changed, naming the new version; without a pin it is
unpinned; with the unit gone it is gone; a built unit is built; issue deferrals are judged
as before and only issue references are asked of the tracker
```

**Why a pin, and not only disappearance.** Nothing is stored in the log for a deferral
(§13.8), so what the author judged has to live in the marker. Going stale only when the
cited unit disappears would need no pin, but a deferral would never be judged again when
its decision text is edited, which is the point of citing it. The pin costs one hash in the
marker, rewritten by hand from the status report each time the deferral is judged again.
Nothing needs a tracker, so it is judged offline and in every CI job.

## 14. Recording
<!-- surfex: recording -->

`Surfex.Record` turns a decision into the entries to append. It's pure: the tasks read
the scans and the log, and append what it returns. Every function returns the entries
or the reason there are none. **Nothing edits or removes an entry**: a new entry
supersedes the relation's tips by naming them as parents.

**Ids** are scan ids (`MyApp.Cart.add/2`, `spec.md#Carts/Adding items`). A `spec:`,
`code:`, `test:` or `class:` prefix picks between kinds when an id is scanned as more than
one. An id that isn't scanned is an error, since the relation would be orphaned from the
moment it was recorded. Every entry carries who recorded it (git's user), HEAD at the
time (context only), and when.

| Function | Records |
|---|---|
| `Surfex.Record.relate/6`, `Surfex.Record.relate/7` | a relation of a type between two ids at their current hashes, from → to for a directed type, superseding the relation's tips. `implements` is recorded as proposed; `verifies` too, unless the test's current version has failed in the evidence given (`evidence:`), when it is recorded on that evidence (§18) |
| `Surfex.Record.plan/7` | a **planned** relation: one end is scanned and recorded at its hash, the other doesn't exist yet and is recorded without one. Its kind is its `spec:`/`code:` prefix, else `spec` when it has a `#`, else `code`. A plausibility check refuses an id the project couldn't have, so a typo doesn't become a permanent plan. `mix surfex.relate --planned` asks whether a spec id is in a file the scanner reads, and whether a code id has the scanner's shape (for Elixir, a name under one of the project's namespaces). Both ends scanned, or neither, is an error. |
| `Surfex.Record.confirm/6`, `Surfex.Record.confirm/7` | one **dangling** or **proposed** relation, named by its ends and type, again at the current hashes, with a note (required) saying what was judged: basis `judgement`. `implements` is refused, since code is validated by evidence or a review (§18), never asserted, unless its code end is a shape, which no run exercises. Orphaned and conflicted relations aren't confirmed: they need re-pointing or resolving. Under `require_red:` (`opts`, §17) a `tests` relation isn't confirmed until its test's current version has discriminated |
| `Surfex.Record.validate/6` | a review (§18): the `verifies` relation from a test to a spec unit, and each `implements` relation of that unit whose code the test calls (§11), or reaches (§13.1) and the review names (`meta[:code]`), and whose version the test's green run recorded, by definition, so one arity covers a function's others (§11), each only if it isn't validated already, validated at the current versions, basis `review`; with nothing left to record it is refused. The unit may be a section whose block or hint the test verifies: that relation must already be current, and the review records the section's `implements` relations alone |
| `Surfex.Record.confirm_by_evidence/4` | every confirmation the test evidence justifies (§17, §18), basis `evidence`, with the evidence in the note: each dangling or proposed `verifies` relation whose test's current version has failed (unless only the spec changed, which is a judgement), then each dangling `tests` relation and each dangling or proposed `implements` relation whose test went red and then green. Nothing justified is `{:ok, []}`, not an error |
| `Surfex.Record.annotate/6` | a new note on one **current** relation: a re-recording at the tip's own versions, with its basis, parented on every tip, so a re-review that changes nothing still lands in the log rather than a commit message. A dangling or proposed relation is refused (confirm settles it), and the note is required |
| `Surfex.Record.retire/6` | that a relation no longer applies, naming every tip as a parent, with the ends as the tip recorded them. An end need not still be scanned: this is how an orphaned relation is put to rest. A pair **never related** is declined: a retire at both ends' current versions with no parent, recording the decision not to relate them, so `mix surfex.suggest` never proposes it. Both must be scanned, and the note is required, as the only record of why. A later relate revives it. |
| `Surfex.Record.resolve/7` | the chosen tip of a **conflicted** relation, recorded again **with its basis** and every tip as a parent: picking a side judges nothing new. Tips that agree are no conflict, and it refuses them. If the scans have moved since, the relation is then dangling, and confirming it is next. |

`Surfex.Record.move/5` moves every live relation of an id onto a new one: a renamed
heading, an anchor added, a section moved. For each relation whose tip names the old id
it appends a `retire` of that relation, and a `relate` of the same type with the new id
in the old one's place, both noting the move. The moved end keeps the hash **recorded
for the old id**, and the other end its recorded hash, so a move carries a judgement
across and never makes one. If the text changed as it moved, or the other end changed,
the moved relation dangles. The new id must be scanned. A conflicted relation must be
resolved first. An old id with nothing to move is an error.

**A retirement moves too.** A retired relation is a decision with a reason, and a rename
is no reason to forget it: for each retired relation of the old id, the move records a
`retire` of the same relation on the new id, at the recorded versions, its note naming
the original retire and its reason. It is skipped where the new id already has that
relation, so it never retires a decision taken since. `mix surfex.move` reports how many
live and retired relations came across, and `mix surfex.suggest` finds a move whose old
id only retired relations name (§15), so a declined pair stays declined.

**Moving a test carries its records.** A test's version is its content (§11): renaming
its module, its `describe` or its file leaves the version as it was, under a new id. Each
`red_green` and `baseline` observation (§12.1) of the old id at the version the new id is
scanned at is recorded again on the new id, with its type and basis and a note naming the
original entry, so the test stays discriminated and, under trust, baselined. A test with
such records and no live relation still moves. An observation at another version stays
behind (`Surfex.Record.left_behind/4`, which `mix surfex.move` lists): the test changed, so
it earns it again. Renaming the test itself changes its version, since its name is part of
the test node that is hashed, so its records stay behind too.

`Surfex.Record.mark/5` records a `needs_update` mark on one spec unit, at its current
version, with a note (required) saying what is wrong in the result. The id must be a
scanned spec unit. `Surfex.Record.withdraw/5` withdraws its open mark, with a note saying
why it proved unfounded: a retire naming the mark as its parent. With no open mark it is
an error; with several, the one to withdraw is named by an id prefix (`pick:`).

`Surfex.Record.history/2` lists every entry touching an id, oldest first, from the log
alone.

The tasks:
- `Mix.Tasks.Surfex.Relate`: `mix surfex.relate FROM TO --type T [--planned] [--note N]`
- `Mix.Tasks.Surfex.Confirm`: `mix surfex.confirm FROM TO --type T --note N`, or
  `mix surfex.confirm --evidence`
- `Mix.Tasks.Surfex.Validate`: `mix surfex.validate TEST SPEC_UNIT --note N`
- `Mix.Tasks.Surfex.Annotate`: `mix surfex.annotate FROM TO --type T --note N`
- `Mix.Tasks.Surfex.Retire`: `mix surfex.retire FROM TO --type T [--note N]`
- `Mix.Tasks.Surfex.Resolve`: `mix surfex.resolve FROM TO --type T --pick ID_PREFIX`
- `Mix.Tasks.Surfex.Move`: `mix surfex.move OLD NEW [--note N]`
- `Mix.Tasks.Surfex.History`: `mix surfex.history ID`
- `Mix.Tasks.Surfex.Mark`: `mix surfex.mark SPEC_UNIT --needs-update --note N`, or
  `mix surfex.mark SPEC_UNIT --withdraw --note N [--pick ID_PREFIX]`

Each reads `.surfex.exs` as `mix surfex.status` does. Each needs the log to exist
(`mix surfex.log --init`), and prints every entry it recorded.

```test:confirm-named
confirming a relation with nothing to confirm is an error, never a silent no-op
```

```test:move-carries
a move records the recorded versions under the new id: if the text changed as it
moved, or the other end changed, the moved relation dangles
```

```test:move-carries-observations
moving a test whose version is unchanged carries its red_green and baseline records to
the new id, which is then discriminated and baselined; a carried baseline is no new one;
a changed version carries nothing and says what stayed behind
```

```test:move-carries-retirements
a move carries a retired relation onto the new id as retired, with its reason, and never
over a relation the new id already has; suggest still finds the move and goes on
skipping the declined pair
```

```test:decline-recorded
retiring a pair never related records the decision not to relate it, at both ends'
current versions, and needs a note; suggest never proposes it again, and a later relate
revives it with the retire as its parent
```

```test:mark-recorded
mark records one needs_update mark at the unit's current version with a required note;
withdraw retires the open mark, naming it when there are several; both refuse what
isn't a scanned spec unit, and history shows them
```

```test:recording-by-name
relate, confirm, validate, retire, resolve, move and history each record or read entries by
name, and never edit or remove one
```

```test:annotate-current
annotate gives a current relation a new note at its own versions and basis, keeping it
current; a relation that isn't current, or a missing note, is refused
```

### 14.1 Migrating to claim grain
<!-- surfex: migration -->

A project whose `implements` relations are on sections moves them to the claims inside
(§11.1) **section by section, as work touches a section, never wholesale**. A section's
existing relations are the input of its migration and of nothing else (§15.1).
`Surfex.Migrate` is the aid, and `Mix.Tasks.Surfex.Migrate` is `mix surfex.migrate`.

`mix surfex.migrate SECTION` lists, for each live (current, dangling or proposed)
`implements` between code C and the section, or a table in it, the claims inside that
unit that could own C (`Surfex.Migrate.proposals/3`). `--within PREFIX` takes every section
whose id starts with PREFIX, such as one spec file (`Surfex.Migrate.sections/3`). A claim is
a candidate when:
- it **names** C: a row whose key is C's id, or C's name with no arity, which names every
  arity (`Mod.fun` is `Mod.fun/*`, since one row may honestly state one claim over both
  arities); a contract unit whose function is C;
- one of its verifying tests **calls** C directly: a test with a live `verifies` to the
  claim or a unit inside it, or a declaration resolving there, whose `calls` (§11) name C,
  or name a hidden definition of C's module;
- failing both, for any claim, one of its verifying tests **reaches** C (§13.1) through
  an item it calls, named in the listing. A test reaching a helper through a public entry
  point is a weaker sign than a call, so reach counts only where nothing names or calls C.

A check (§11.4) verifies a claim but calls nothing, so it proposes nothing.

**Arities of one definition are one proposal.** A function's default-argument arities
share a definition (§11), and one review credits both (§18.4), so they are proposed, and
judged, together, said to be one definition.

Each proposal is one of:
- **single**: one candidate claim;
- **several**: the candidates are listed, never decided: which claim code realises is a
  judgement;
- **none**: no claim inside the section owns C. It is realised by a claim not written yet,
  or it doesn't belong to the section. These are the spec work the migration surfaces, so
  they are listed **first**.

The listing writes nothing.

```test:migrate-candidates
a section's section-grain implements are each proposed to the claims inside it that name
the code (an arity-less row key every arity), whose tests call it, or failing those reach
it, naming through what; arities of one definition are one proposal; none is listed first,
then several, then single; --within takes every section under a prefix
```

**Recording a migration.** `mix surfex.migrate SECTION --accept [--note N]`
(`Surfex.Migrate.accept/4`) records each single proposal: an `implements` between the code
and its claim, **proposed** like any pair named by hand (§18), with the entry's `migrates`
naming the section, or table, it moves from (§12.1). `--code ID --to CLAIM` (`--to` once
per claim, `Surfex.Migrate.choose/6`) records a reviewer's choice the same way: one or more
of several candidates, or the claim written for code that had none. It takes every arity
of ID's definition that the section relates, and refuses a claim that isn't inside the
section. Where a claim already holds a validated relation to the code, nothing is left to
wait for, and the section's relation is retired at once.
- **Migrating.** A proposed relation recorded with `migrates` doesn't fail the status (an
  exception to proposed, §13.1) while the relation between the same code and the section
  it names is live. So a section can move across several commits with the check green,
  rather than in one commit that relates, validates and retires everything at once. The
  status lists each code item moving (`migrating`: code, section, claims, and whether it
  is settled) in its text, its JSON (`migrating`, and each relation's `migrates`) and
  `RELATIONS.md` (a `migrating` group, and the flag on the relation), where there are any.
- **Settled.** The claim relation is validated as any `implements` is: by evidence, a
  review, or a review naming a path (§18). Once every claim relation of a code item from a
  section is validated, the pair is settled: `mix surfex.suggest` proposes retiring the
  section's relation and `--accept` retires it, as `migrate SECTION --accept` does
  (`Surfex.Migrate.settle/4`), with a note naming the claims it moved to
  (`Surfex.Migrate.settled_note/2`). **A section never loses a relation before its claim
  holds one.** Retired another way first, the section no longer holds it, and the claim's
  proposed relation fails as any proposed relation does.
- A pair once declined, a retired relation between the code and a claim, is no candidate:
  undoing a retirement takes someone choosing to, with `--code --to`.
- **A baseline is carried.** A section relation resting on the baseline (§18.1, basis
  `baseline`, while it holds) moves to a claim whose verifying test is the same baselined
  test, at the version the baseline trusted, exercising the same code: a live `verifies`
  from it to the claim or a unit inside it, and a current `tests` relation to the code. The
  claim relation is then recorded **keeping basis `baseline`**: the trust already given
  covers the same test and the same code. It holds at once, so the section relation is
  retired in the same `--accept` (or `--code --to`). Any other claim relation is proposed,
  and needs evidence or a review. A project that took its baseline at section grain starts
  its migration from it rather than redoing it. The task reads `adoption:` for this, as
  status does.

```test:migrate-baseline
a baselined section relation migrates keeping basis baseline, and the section's retires at
once, only when the same baselined test at its trusted version verifies the claim and
exercises the code; a changed test version, or another test, makes the claim relation
proposed
```

```test:migrate-accept
--accept records each single proposal proposed with migrates, which doesn't fail while the
section holds its relation and is listed as migrating in the reports; once the claim's
relation is validated the pair is settled, and accept or suggest retires the section's,
noting the claim; retired early, the claim's relation fails; --code --to records a chosen
claim and refuses one outside the section; a claim already holding the code retires the
section's at once; a declined claim is no candidate
```

**What migration buys.** `mix surfex.migrate --report [SECTION…]` is read-only
(`Surfex.Migrate.report/3`). For each section:
- its **relations**, of every type, now and **after** migration: what one edit of its text
  dangles. After is now less each section-grain `implements` with a candidate or a claim
  it is migrating to;
- its section-grain `implements` by outcome (single, several, none, or migrating already);
- the **marking**: a **prose** section, one with no claim inside it that is a unit yet,
  needs its claims named before anything can move (§11.1), and the first marker or id
  comment re-versions the section, dangling all its relations at once. It is counted,
  since it is paid even for relations that don't move. A section whose claims are already
  units needs none;
- the **dangle width** of each proposal: the relations a change to the text stating its
  claim dangles, now (the section's, or the table's) and after (the claim's own and the
  moved ones; for several, the widest candidate's; for none, the section's after).

Each section's totals come first, then its proposals, widest after first. With no section
it reports every section holding a section-grain `implements`, worst first (most
relations), after the **population**: how many of those sections, and their `implements`,
have claims that are already units (tables, hints, blocks, rules or contracts), which
migrate today, and how many are prose, which need their claims named first.

```test:migrate-report
the report gives a section's relations now and after, its implements by outcome, the
marking a prose section needs and none for one with units, and each code item's dangle
width now and after, widest first; with no section, every section holding section-grain
implements, worst first, and the population split into units and prose
```

### 14.2 The migration work list
<!-- surfex: migration-work-list -->

The report (§14.1) is the migration's **one work list**: it lists, too, the sections whose
own text has no evidence while a claim inside them is verified, the `:contained` sections
of §13.4, read as the status reads them (the project's classes, §13.6, decide what is
contained). `Surfex.Migrate.report/4` takes `Surfex.Status.derive/4`'s
options, and the task passes the project's.
- Each contained section's report names the **verified claims inside it**, from the
  status' `contained` list, sorted: a `contained:` line in the text, `contained` in each
  section of the JSON. A section that isn't contained names none.
- With no section named, the report covers every contained section as well as every section
  holding a section-grain `implements`, **including contained sections that hold no
  `implements`**: they are settled by a test of their own, by naming their claims, or by a
  class (§20.1), never by moving code. The population counts the contained sections.
- `--format json` (`Surfex.Migrate.json/1`) gives the same report, the population and each
  section with its counts, `contained` and dangle widths, for tools. It is the report's
  form: without `--report` it is refused, as is any format but text or json.

```test:migrate-report-contained
the report names the verified claims inside each contained section, none for one that isn't
contained; with no section named it includes a contained section holding no implements and
counts the contained sections in the population; --format json gives the same report and is
refused without --report
```

## 15. Suggesting relations
<!-- surfex: suggesting -->

`Surfex.Suggest` proposes relations from what the spec already says. Its candidate
`implements` relations come from citations. `Surfex.Suggest.candidates/5` reads the spec's
citations (§6) under the project's profile (`Surfex.Status.Config.profile!/2`),
and for each **resolved** citation proposes the pair *(the innermost spec unit
containing the citation's line, each code item it names)*. A citation in a marked block
relates the block, not its section. A family names every member. Left out:
- a pair already related, in any state, including retired: suggesting it again would
  overrule a decision already on record
- a citation inside a fenced code block, which has no line to place it in a section
- a named item that is not a code scan

Each candidate records where it was first cited. `Surfex.Suggest.accept/4` records one
`relate` per candidate, at the current hashes. It only ever creates relations that don't
exist. **It never confirms a dangling judgement**: a dangling `implements`, `verifies` or
`excuses` relation is settled by evidence, a review or a named `confirm` (§14, §18).

`Surfex.Suggest.all/5` gives every suggestion at once, computed together so that none
repeats another:
- **moves**: a spec or test id the log knows that is no longer scanned, and an id of the
  same kind with no records of its own at the **same version**: a renamed heading or an
  added anchor; a test whose module, `describe` or file was renamed, as when a test file
  is split into sub-modules. A test is known by its relations, or by its records alone
  (§14). Accepting carries the relations and a test's records (`Surfex.Record.move/5`).
  Only one-to-one matches are suggested. A version found under several gone or new ids is
  **ambiguous**: reported, never suggested, since which is which is a judgement (`mix
  surfex.move` does it by hand). A section renamed and reworded at once is not
  recognised, and neither is a renamed test, whose name is part of its version: their old
  relations stay orphaned for review.
- **code moves**: a code id with relations that is no longer scanned, and a new code item
  of the **same module and name with another arity**, with no records of its own: a
  function whose arity changed (`render/2` became `render/3`). Its version changed with
  it, so the version rule above can't see it. One to one only; several candidates are
  ambiguous. A code move carries the old end's recorded version like any move, so the
  moved relations aren't current until judged again (`mix surfex.move` works for any id,
  code included).
- **refines**: each marked block and test hint `refines` the unit it sits in
  (`within`, §11), unless that relation exists in any state.
- **implements**: the candidates above.
- **verifies**: each test's declaration (§11) that resolves to a spec unit.
- **tests**: each test paired with each scanned code item in its `calls` (§11).
- **refresh**: each dangling structural relation whose fact the source still states: a
  `tests` relation whose test still has the code in its `calls`, a `refines` relation
  whose block or hint is still `within` the unit. These relations record what the source
  says, not a judgement, so reading the source again re-establishes them. Judgement
  relations are never refreshed.
- **undeclared**: each undeclared `verifies` relation (§13.1), which accepting retires:
  the test's own source says the claim is gone.
- **moved**: each moved `refines` relation (§13.1), which accepting retires: the unit no
  longer sits in what it refines. The relation to where it sits now is suggested under
  refines, so one `--accept` records the new relation and retires the old. A dangling
  `refines` relation whose unit moved is never refreshed (its fact is gone) and is
  retired here too.
- **excuses**: each code item that nothing implements, and that no `implements`
  candidate in the same run will, paired with the class of the first rule it matches.
  Matching is `Surfex.Coverage`'s (§7), where a parent counts as cited when something
  implements it. A `never_excused` kind is never proposed. With `require: [code:
  [:implements, :excuses]]`, every public item is then either described or deliberately
  excused, and both are on record. An excuse dangles when its item or its class changes.

**A judgement suggestion shows how to decline it.** Under each `implements` and `excuses`
candidate, `mix surfex.suggest` prints the exact command that declines it
(`Surfex.Suggest.decline_command/3`): a `retire` of the never-related pair, ids
kind-prefixed and quoted, with a note to replace. That records the decision, so the pair
is never proposed again (§14). Declining in a commit message or an MR leaves the log
without it, and the suggestion comes back. Structural suggestions (`tests`, `refines`, a
refresh) state what the source says, so they have no decline; a wrong `verifies` is fixed
in the test's tag.

**Suggesting grows with the log in proportion.** An agent runs it on every work item, so
its work over a log twice as large is about twice, not four times: each relation's
judgement in force is read from that relation's own entries.

```test:suggest-linear
suggest's work over twice the relations is about twice the work, counted in reductions
```

```test:decline-shown
a judgement suggestion's decline command retires the pair with ids kind-prefixed and
quoted, and a quote in an id escaped; a structural suggestion has none
```

The others are computed as if the moves were already recorded, so a moved
section's relations are not suggested again under its new id.
`Surfex.Suggest.accept_all/5` records them (its options carry the test evidence): each
move (`Surfex.Record.move/5`), then each other suggestion as a `relate` at the current
hashes, except an undeclared or moved relation, which is retired. **Accepting validates nothing:**
an `implements`, `verifies` or `excuses` relation is recorded as proposed, since a
citation, a tag or a matching rule is a claim, not a judgement (§18). A `verifies`
relation whose test has already failed at its current version is recorded on that
evidence.

`Mix.Tasks.Surfex.Suggest` is `mix surfex.suggest [--accept] [--note N]`: without
`--accept` it lists every suggestion and writes nothing. Adopting the relation log is
`mix surfex.log --init` followed by `mix surfex.suggest --accept`.

```test:suggest-never-confirms
a pair already related, in any state, is never suggested again,
so accepting suggestions never confirms a dangling relation
```

```test:suggest-code-moves
a function whose arity changed is suggested as a move, and the moved relation isn't
current until judged again; two candidates of one name are ambiguous
```

```test:suggest-test-moves
a test id gone from the scan whose version one new test id has, with no records of its
own, is suggested as a move, and accepting carries its relations and records; a version
under several new ids is reported as ambiguous and not suggested
```

```test:suggest-moves
an anchor added to a related section is suggested as a move, and its relations
aren't suggested again; two new ids at one version are not suggested
```

```test:suggest-moved
after a marked block moves unchanged into another section, suggest proposes the refines
to the new section and the old one's retirement, and accepting both leaves the status
with nothing moved
```

```test:suggest-refresh
a dangling tests relation whose test still calls the code is refreshed on --accept; one
whose test no longer calls it is not, and a dangling implements relation never is
```

```test:suggest-proposes
suggest proposes relations the spec and tests already imply and records them only on
--accept
```

### 15.1 Claim relations from a claim's tests
<!-- surfex: claim-implements -->

A citation is how a spec names code, not why code belongs to a claim. Code realises a
claim (§11.1) when the claim's tests exercise it, so `suggest` also proposes `implements`
from what a claim's tests call. It is **opt-in until the release that completes claim
grain**: `claim_implements: true` in `.surfex.exs` (`Surfex.Status.Config.claim_implements?/1`,
`false` by default; anything but a boolean is refused), passed as `claims: true` to
`Surfex.Suggest.all/6`. Off, nothing below is proposed and `reached` is empty.

- **A claim** is a spec unit of role block, test hint, row, rule or contract. A section is
  not one.
- **Its verifying tests** are the tests with a live `verifies` relation to it or to a unit
  inside it, and the tests whose declaration (§11) resolves there.
- **Proposed**: each scanned code item a verifying test **calls directly**, its own
  `calls` (§11): its body's and its private helpers' calls, and the requests, events and
  messages §11.3 resolves. A call naming a hidden definition (a LiveView's `mount/3`, a
  server's `handle_call/3`) proposes its module, the item that versions it (§3.3). A
  module the test names by itself is not proposed: a test names every module it calls
  into. Each candidate lists its tests (`tests`), and the task prints how many and its
  decline command (§15).
- **Reached code isn't proposed item by item.** For each code a claim's tests call that
  reaches further code (§13.1) the test doesn't call itself, `reached` lists one line,
  `%{spec, through, count}`: `reached spec.md#x through MyApp.Protocol.handle/2: 41 more
  items; name the ones that realise this claim with mix surfex.relate`. It is information
  for the person relating the claim, never recorded and not counted as a suggestion. A
  single-dispatch core is one line, not every module behind it. What a call reaches is
  `Surfex.Scan.reacher/1`'s answer, the closure `Surfex.Scan.reach/1` follows for a test,
  asked once per called item.
- **A claim with no verifying test gets nothing**: a claim without evidence earns no
  relation.
- A pair related in any state, a declined (retired) one included, is left out, as are
  the pairs a citation already proposes. Accepting records each as proposed, as for any
  `implements` (§18).

**The filter is the direct-call rule, not the section.** Decided at #206's evaluation: a
claim's proposals are bounded by what its test author wrote, plus the one-line reached
summary. A section keeps no permanent coarse `implements` as the pool its claims draw
from: that would keep both grains alive, and with them the section-wide dangling waves
a section-grained relation causes. A migration aid may read a section's existing
relations as its input while the section is migrated; that ends with the migration.

```test:claim-suggestions
a claim's verifying tests' direct calls are proposed, a hidden callback as its module and
a module named alone not at all; code reached only through a called item is one reached
line with its count; a claim with no verifying test, or a section, gets nothing; a
declined pair isn't proposed again; nothing without claim_implements: true
```

### 15.2 Classed text suggests nothing
<!-- surfex: classed-suggestions -->

A classed section's citations are narration: an architecture map names modules to orient a
reader, not to say what they realise. `mix surfex.suggest` (`Surfex.Suggest.all/6`)
proposes no
`implements` from a citation in a classed section's own text, and no `verifies` to a
classed unit from a declaration, which is contradicted instead (§13.6). The citation is
still read and checked: one that resolves to nothing is broken (§6) whatever its section's
class. A citation inside a named claim within the section, a block or a hint, is the
claim's and is suggested as before.

```test:classed-citations
a citation in a classed section's text yields no implements suggestion and a broken one
is still reported; a declaration naming the classed section yields no verifies
suggestion; the same citation unclassed does
```

## 16. The specification guide
<!-- surfex: spec-guide -->

`guides/writing-specs.md` is a guide to writing a specification that Surfex can relate
precisely. It covers:

- what the scanner sees
- sizing sections to what changes together, and that a section's prose is a claim (§13.4):
  name its claims, class it, or verify it
- keeping headings stable
- naming the code in the section that describes it, and relating code to the claim it
  realises rather than to every section that mentions it, by hand where no citation
  proposes the pair (`relate`, then `validate`, with `--through` for a path no call shows)
- stating requirements as checkable claims
- naming a claim smaller than a section (§11.1): which form for which claim, and
  why bold is never one
- examples and invariants
- test-first work with Surfex
- excusing code the spec shouldn't describe
- working with an LLM
- a worked example
- a checklist

It ships in the package and the docs.

Its worked example is a claim about Surfex's behaviour, so it is tested. The guide's two
versions of a spec are scanned, related as `mix surfex.suggest --accept` would relate
them, and then edited. The test asserts exactly the dangling, current and orphaned
relations the guide describes.

### 16.1 Text the spec doesn't verify
<!-- surfex: guide-unverified-text -->

Beside "Code the spec doesn't describe", the guide has "Text the spec doesn't verify":
when to class a section (§11.7) rather than verify it, when to name a claim instead, that
a class never hides a claim named inside its section (§13.6), that a rule a tool checks is
verified by a check (§11.4) rather than classed, and when to defer a unit by its issue.

```test:guide-classes
the guide has a "Text the spec doesn't verify" section beside "Code the spec doesn't
describe", saying a class never hides a named claim, pointing at checks for tool-verified
rules and at deferrals by issue
```

## 17. Test evidence
<!-- surfex: evidence -->

**People or agents judge meaning, and evidence judges behaviour.** A test run shows
whether a test passes against the code as it is. Recorded at the exact versions of the
test and the code, that is evidence a confirmation can rest on.

`Surfex.Evidence` is one record per test per run: the test's id and version (§11), whether
it passed, when, which run, and the versions of the code the test reaches at that moment
(§13.1: what it calls, and what that code calls).
Evidence is **scratch data**. It lives under `_build` (`Surfex.Evidence.path/1`) and is
never committed; what it justifies is written into the relation log (§14), and the raw
runs aren't needed afterwards.
- `Surfex.Evidence.append/2` appends records, and `Surfex.Evidence.load/1` reads them back,
  oldest first.
- **A test version discriminates** once it has failed against one version of its code
  and later passed against a different one, the test itself unchanged:
  `Surfex.Evidence.discriminating?/3`, with `Surfex.Evidence.red_then_green/3` giving the
  two runs that show it. A test changed in any way (body, helpers, setups, cases) is a new
  version, which must fail and pass again.
- **Discrimination is kept in the log.** The evidence file is scratch: `mix clean`, a
  fresh checkout, another machine or another worktree loses it. So `mix surfex.confirm
  --evidence` records each test version that has discriminated as a `red_green`
  observation (§12.1), once. From then on that version counts as discriminated wherever
  the log is, and a green run against the current code is all a re-confirmation needs
  (`tests`, `implements`, and `require_red`). The guards stay: the red and green runs
  were against different code, and the observation is for one test version only.
  `Surfex.Status.derive/4` lists the discriminated test versions (`discriminated`).
- **A red→green that can't be credited is reported.** A test version that failed and
  later passed against the **same** code versions, and never discriminated, showed a
  change its reach doesn't hold: a call the scanner doesn't see, a change outside the
  scanned code, or a flaky test (`Surfex.Evidence.uncredited/1`). Given evidence,
  `Surfex.Status.derive/4` lists each scanned test version so (`uncredited`, with the
  code its red run recorded) unless the log records it as discriminated, and `mix
  surfex.status --evidence` reports it. It is information, never a failure: without it
  the red→green would be lost in silence.

```test:evidence-uncredited
a test version that failed and then passed against the same code versions, never
discriminating, is listed as uncredited with the code it reached, without failing; one
that discriminated, in the evidence or the log, or another version, is not
```
- `Surfex.Evidence.latest/3` is a test version's most recent record.

`Surfex.ExUnitFormatter` records it. A project adds it beside the usual formatter
(`ExUnit.start(formatters: [ExUnit.CLIFormatter, Surfex.ExUnitFormatter])`):
- At the start of the suite it scans the tests `tests:` names and the code, as
  `mix surfex.status` does. Without `tests:` it raises.
- A finished test is matched to its scanned record by file and line, so a test a
  comprehension generates counts for the record that defines it. A record whose test ran
  once per case failed if any case failed.
- An excluded test (a tag the run's `exclude:` or `--only` leaves out) is recorded as
  `excluded`, and a skipped one (`@tag :skip`) as `skipped`, at its version with no code
  versions. They are neither red nor green: `Surfex.Evidence.latest/3` and
  `Surfex.Evidence.red_then_green/3` pass over them. Invalid tests, and tests outside
  `tests:`, record nothing.
- When the suite finishes, the run's records are appended to the evidence file. The
  formatter never touches the relation log.

```test:evidence-discriminates
a test that failed against one version of the code and then passed against another,
unchanged itself, discriminates; never red, red with the code unchanged, green before
red, or a changed test, does not
```

**Who confirms what.** People or agents judge meaning, and evidence judges behaviour:

| Relation | Confirmed by |
|---|---|
| `verifies` (test → spec) | recorded on the red step: the test's current version has failed (§18). After a spec rewording that changes no behaviour, a named judgement (`Surfex.Record.confirm/6`) |
| `tests` (test → code) | evidence: the test's current version has discriminated, and its latest run passed against the code's current version |
| `implements` (spec ↔ code) | derived: a test with a current `verifies` relation to the spec unit, or to a block or hint inside it, has a current `tests` relation to the code or reaches it (§13.1), and such evidence. When the **spec** side is what changed, the `verifies` must be to that exact unit: someone has judged that the test expresses the reworded requirement |

`Surfex.Record.confirm_by_evidence/4` records these, and `mix surfex.confirm --evidence`
runs it after a test run: `mix test && mix surfex.confirm --evidence`. Each entry's note
records the evidence (the red run and the green one, with versions), so the conclusion is
kept in the log and the raw runs aren't needed afterwards. Once a test version has
discriminated, later green runs re-confirm its relations as the code changes: a refactor
never goes red, and doesn't need to. A test that has never been red leaves its relations
for a confirmation by hand, unless `require_red: true` in `.surfex.exs` refuses that too
(`Surfex.Status.Config.require_red!/1`), which is strict test-first work.

```test:evidence-confirms
a code change whose test went red then green confirms the tests relation and then the
implements relation; a test never red, failing now, or green against other code confirms
nothing; a changed test's failing run confirms its verifies relation, but after a spec-only
rewording that is a judgement; after a spec change only a test verifying that exact unit
carries implements
```

**Code reached through an entry point.** A spec unit cites the function that does the
work; its test drives the public entry point that hands the work on. The run records the
versions of everything the test reaches, so evidence carries `implements` to the cited
function as it does to code the test calls: a change to it dangles the relation, and a
green run of a discriminated test re-confirms it. Reach credits a relation someone
recorded and never makes one. Code reached only at run time (a message to another
process, a dispatch by name) isn't reached by reading the source, and a test reaching it
that way is credited for what it calls.

A review (§18) is narrower than evidence. Its note is a judgement about what was read, so
it credits the code its test calls, and reached code only where the reviewer names it
(`mix surfex.validate … --code ID`). Otherwise one note would stand for every function
behind an entry point, including code nobody read. A named id the last green run didn't
reach is an error, unless the review says how the test exercises it (§18.2).

```test:evidence-reach
a test calling an entry point that calls the cited code carries the implements relation
by evidence, and the check bears it out against the code's version or reports it run
against other code; with the path cut, nothing carries it; the run records the versions
of what the test reaches
```

```test:review-reach
a review credits implements to the code the test calls; code the test only reaches is
credited when the review names it, and not otherwise; naming code the green run didn't
reach is an error
```

**CI validates; it records nothing.** A CI job runs the tests with the formatter, from an
empty evidence file (a cached `_build` would otherwise carry evidence from other runs),
then `mix surfex.status --verify --evidence`. That checks that every relation holds and
that every claim a run can bear out (`Surfex.Evidence.claimed?/1`: its basis is
`evidence` or `baseline` (§18.1), or, recorded before bases existed, its note begins
`Surfex.Evidence.note/0`) is borne out by this run. The basis decides, not the note: a
move keeps a relation's basis and replaces its note, and the relation stays checked. A claim the run contradicts fails the
pipeline. A project whose backlog of unvalidated relations is done adds `--validated`, so
no relation is current without validation. CI never appends to the log and never saves
evidence: the log's claims are made where the work is done, and CI only checks them.

```test:evidence-claims-by-basis
a relation is checked against a run by its basis, evidence or baseline, so a moved one
stays checked whatever its note; a note marks only an entry written before bases; a
review or judgement is never required to have run
```

```test:evidence-checked
a run where the test passes against the code as confirmed bears the claims out; a test
that didn't run, failed, or ran against other code disproves them and fails the check;
without evidence nothing is checked, and relations confirmed by hand are never required
to have run
```

**A failed test fails its relations, whatever validated them.** The basis decides
whether a run must bear a relation out, not whether a red run counts. A current
`verifies` or `tests` relation whose test failed at the version the relation holds is
**failed** (§13.1), whether it was validated by evidence, a review, a judgement or a
baseline: a review judged that the test covers the claim, and relied on the run to say
whether it passes. With several jobs' evidence, a failure in any job's file counts. A
test excluded, skipped or absent from the run fails nothing here. The relation isn't
invalidated, so once the test passes again it holds with no new review. A project
validated entirely by review (`adoption: :reevaluate`) is therefore held to its run as
well: its gate can't pass while the tests behind its requirements fail.

**A deviation test's run is its declared state.** A test tagged `deviation:` (§11) is
expected to fail: the code doesn't meet its requirement yet. When it runs and fails, its
relations aren't failed. The deviating unit is listed with its result instead
(§13.1). Only the tag does this: any other test's failure fails its relations as above.
When it runs and passes, at its version, the deviation is **stale** and fails the check:
the fix landed, and the tag must come off. Evidence of that needs a run that includes the
test (`mix surfex.test --deviations` runs exactly the deviation tests with their skips
included; or `mix test --include skip`, or the skip removed): a skipped test is recorded as
skipped, never as passed. With several jobs' evidence, a pass in any job makes it stale.

```test:deviation-runs
a deviation test that failed is listed with its deviation, not as failed, while an
untagged test's failure still fails; one that passed at its version is a stale deviation
and fails, in any job's evidence; skipped, or without evidence, nothing is judged stale
```

```test:evidence-failed
a test that failed at the version its relations hold fails every current verifies and
tests relation it is an end of, whatever their basis, and the check; a pass afterwards
clears it with no new review; another version's failure, an excluded, skipped or absent
test fails nothing; with several jobs, a failure in any fails it; a claim by evidence
whose test failed is reported as failed, not also unproven
```

**The check grows with the project in proportion.** It runs on every merge request, over
every claim, against a run's whole evidence file, which accumulates across work items.
So its work over a project twice as large (twice the tests, code, claims and evidence
records) is about twice, not four or eight times. Each claim finds its candidate tests,
and each test its latest record, from indexes built once per check. It doesn't scan
every relation, reached pair or record for each claim.

```test:evidence-check-linear
the check's work over a project twice as large, in tests, code, claims and evidence
records, is about twice the work, counted in reductions
```

**A suite split across jobs.** Projects often run some tests in a job of their own
(mutation, integration, slow or external-service tests) and exclude them elsewhere. A
claim whose deciding test this run excluded or skipped is **not checked here**
(`unchecked`, beside `unproven`): it is listed by test and reason in every report, and
doesn't fail the check. A test with no record at all still **didn't run**, and fails: a
job that silently stopped running a test can't hide behind an exclusion. For an
`implements` carried by several tests, a disproof outranks a test that didn't run, which
outranks an exclusion.

**The jobs together check every claim.** Each job passing alone isn't enough: a claim
could then be checked by none of them. A final job reads every job's evidence file (its
CI artifact) with `mix surfex.status --evidence --merge PATH` (once per file;
`Surfex.Status.derive/4` with `evidence: {:merged, [records, …]}`), and judges each claim
against each file:
- a disproof in any file (its test failed, or ran against other code) fails it, whatever
  another file shows;
- otherwise, a file that bears it out passes it;
- otherwise no job's evidence ran it, and it fails (**no job**).

A test absent from one job's file is normal in a split. Only the named files are read,
and a named file that doesn't exist fails, rather than reading as an empty run. CI still
records nothing: the evidence files are artifacts, not log entries.

```test:evidence-merged
merged evidence passes a claim one job bears out, fails one any job disproves, and fails
one no job ran; only the named files are read, and a missing one fails
```

**Recording reads other runs too.** A test that needs an environment the local host lacks
(a directory server, a filesystem watcher) is excluded locally and run in CI. The commands
that record from evidence (`confirm --evidence`, `validate`, `relate`, `suggest --accept`)
take `--merge PATH`, once per file, typically CI jobs' artifacts, and read the local run
and those files as one history ordered by time (`Surfex.Evidence.combined/1`). A test
excluded here and run there reads as run; a later failure in any run is the latest. A
named file that isn't there fails, as for the check.

```test:evidence-merged-recording
a test excluded here and passed in another run reads as passed in the combined history,
and a later failure in any run is the latest; --merge refuses a file that isn't there
```

```test:evidence-excluded
an excluded or skipped test is recorded at its version as such, neither red nor green;
a claim whose test this run excluded or skipped is not checked here, listed and not
failing, while a test with no record still fails
```

```test:red-green-recorded
a test version's red then green is recorded in the log once; with it, a green run
re-confirms after the evidence file is lost; a changed test version, or red and green
against the same code, records nothing
```

```test:evidence-recorded
a test run's results are recorded per scanned test at its versions, under _build, and
read back oldest first
```

### 17.1 Evidence from a check
<!-- surfex: check-evidence -->

A check's run (§11.4) is evidence in a test's form (`Surfex.Check`): its `test` is the
check's id, its `test_hash` its declaration's version, and its `code` its inputs'
versions at the run. So a check discriminates as a test does
(`Surfex.Evidence.red_then_green/3`): its declaration, unchanged, failed against one set
of input versions and later passed against another. Breaking an input on purpose to show
the check can fail (the rule's config, or the code it inspects), then restoring it, is
that run.
- `mix surfex.confirm --evidence` records a check's discrimination as a `red_green`
  observation of the check at its declaration's version (§12.1). It confirms the check's
  `verifies` relations once the check has discriminated, in the evidence or by that
  observation, and its latest run passed against the inputs as they are. A check that has
  never failed is confirmed only by a review (§18.3). After a spec-only change it is a
  judgement, as for a test.
- **CI's check covers checks.** A `verifies` relation confirmed by a check's evidence is a
  claim a run bears out (§17): the run's latest record of the check, at its declaration's
  version, passed against the inputs that make the version the relation holds. A check
  the run didn't run **didn't run**, so CI runs `mix surfex.check` in the job whose
  evidence the check reads; a checks job of its own is merged in with `--merge`, as any
  job is. A pass against other inputs ran against other code.
- **A failed check fails its relations**, whatever validated them, as a failed test does:
  its latest record at the declaration's version failed against the inputs of the version
  the relation holds.

```test:check-discriminates
a check that failed and then passed with its inputs changed is observed as discriminated
and confirms its verifies by evidence, and a green run after an input change re-confirms
it; one that never failed, or failed and passed against the same inputs, confirms nothing
```

```test:check-evidence-checked
a claim a check's evidence confirmed is borne out by a run where the check passed against
its inputs, and is unproven as not run without one, as failed when it failed, and as other
code against other inputs; merged evidence from a checks job bears it out
```

### 17.2 Evidence comes from an asserting block
<!-- surfex: asserting-evidence -->

Evidence is a run of something that asserts: a test or property block, which the formatter
matches by its lines (§17), or a declared check (§17.1). A tag outside such a block makes
no test record (§11.5), so no run carries it. The formatter records nothing for a helper
under `test/support/` or a module with no test, and nothing it records can credit the
requirement such a tag names.

```test:inert-no-evidence
a module with a verifies tag and no test, and a helper under test/support, give no test
record, so no run of them is evidence for the unit the tag names
```

## 18. Validation by process
<!-- surfex: validation -->

A current relation means the spec, the tests and the code were shown to belong together,
not that someone said they do. **Saying code is implemented without following the process
is a guess**, and a guess must never pass for validation. The process:

```
a spec unit changes, or has no relation
  → ensure a failing test: it may exist already; if not, write it
  → update the test relation (verifies), on the failing run
  → build the code until the test is green
  → update the code relation (implements), on the red run and the green one
```

**The balance.** A relation established by the process gives a reasonable understanding
that the code is correct **while its tests pass**, and CI checks that on every run (§17).
Nothing is revalidated unless the spec, the test or the code changes. A refactor that
stays green is re-confirmed by evidence.

**Each entry records its basis**, how it validates the relation (§12.1):

| Basis | How the relation got it |
|---|---|
| `evidence` | test runs: a failing run for `verifies`; a red run and then a green one for `tests` and `implements` (`Surfex.Record.confirm_by_evidence/4`) |
| `review` | a test examined against its spec unit and judged to validate it, then run green against the code (`Surfex.Record.validate/6`) |
| `judgement` | a relation confirmed by name, with a note (`Surfex.Record.confirm/6`): for `verifies`, a spec reworded without a change of behaviour; for `excuses`, the item is what its class says; for `implements`, only to a shape (§4): a reviewer read the type against the spec |
| `proposed` | a claim nothing has validated: a citation, a tag with no failing run, a pair named by hand (`Surfex.Record.relate/7`) |

A move carries its tip's basis across (§14, `Surfex.Record.move/5`): a move changes where a relation points, not
what validates it. A resolve (`Surfex.Record.resolve/7`) carries the chosen tip's: picking between judgements
already made validates nothing new.

- **What makes each relation current:**
  - `implements`: **only** evidence or a review. Never by hand.
  - `verifies`: evidence of the failing run, a review, or the judgement path.
  - `excuses` and the other types: a judgement by name.
  - Structural relations, `refines` and `tests`, record facts read from source and need no
    basis.
- **Unvalidated relations** are current `implements` or `verifies` relations with no
  validating basis, such as those recorded before validation existed. They are reported,
  and `mix surfex.status --validated` fails on them, so a project moves over once its
  backlog is done. `Surfex.Status.derive/4` reports them, and fails on them under
  `validated: true`.
- **Validating an existing relation** means doing the work. Read the spec unit's claims
  and the test. Judge whether the test accurately validates that component of the spec:
  its cases, its boundaries, what must stay unchanged. Fix or extend the test where it
  falls short; a changed test is a new version and follows the process. Then run it green
  against the code, and record it with `Surfex.Record.validate/6`
  (`mix surfex.validate TEST SPEC_UNIT --note N`). The note says which claim each
  assertion checks. It needs the `verifies` relation, and green evidence at the current
  versions of the test and of the code it reaches (§13.1). It credits the code the test
  calls, and reached code only when named (§17). Code usually implements a section
  while its tests verify the hints inside it, so the unit may be that section: the review
  judges that the test validates the section's claims for the code it exercises, and
  needs the test's relation to the hint to be current already (validated first, against
  the hint). A review records only what isn't validated already: a `verifies` on its
  failing run keeps that basis, so CI goes on checking it (§17), and with nothing left to
  record `validate` refuses rather than write the same judgement again. Breaking the code
  to get a red run was rejected: a test that fails against a mutant shows it is sensitive
  to the code, not that it reflects the spec.
- **One relation at a time.** `confirm` (`Mix.Tasks.Surfex.Confirm`) names one relation, and
  `validate` (`Mix.Tasks.Surfex.Validate`) one test and one unit. The note is required, and
  records what was checked. There is no form that confirms every relation touching an id.
- **A batch is many single judgements** (`Surfex.Record.batch/3`). `--file PATH` on
  `confirm` and `validate` reads one relation per line (tab-separated: `FROM`, `TO`,
  `TYPE`, `NOTE` for `confirm`; `TEST`, `UNIT`, `NOTE` for `validate`), and records them
  in one run, which saves the project load per relation that made agents script loops. Each
  line is still one relation, judged as the single form judges it, with its own note.
  **The notes must be distinct**: one note copied across many relations is a template,
  not a judgement of each, and such a batch is refused before anything is recorded. A
  line its own rule refuses fails the whole batch, naming the line; nothing is recorded.

```test:batch-distinct-notes
a batch records each line in order, each seeing the ones before it; notes that aren't
distinct are refused, naming the lines; a line its rule refuses fails the batch, naming
it; the file is tab-separated with numbered lines, and an empty one is refused
```

```test:process-proposed
a citation, a tag with no failing run, or a pair named by hand is proposed and fails the
check until evidence or a review validates it; accepting suggestions validates nothing
```

```test:process-validated
an implements relation becomes current only by evidence or a review, never by hand; a
current relation without a validating basis is unvalidated, and fails only under
--validated; a move, and a resolve, keeps its basis
```

**A shape is judged.** A type has no behaviour for a run to exercise: no test calls it,
so neither evidence nor a review can show it. An `implements` relation to a shape (§4) is
confirmed by judgement (`mix surfex.confirm SPEC TYPE --type implements --note N`): a
reviewer reads the type against the section, and the note says what was compared. It
dangles when the type changes. Code with behaviour is never judged: a judgement on its
`implements`, even one written by hand, is unvalidated.

```test:shapes-by-judgement
an implements to a shape is confirmed by judgement and validates; code with behaviour is
still never confirmed by hand, and a judgement on its implements is unvalidated
```

```test:process-one-at-a-time
confirm takes one relation and a note, refuses implements unless its code is a shape, and
records a judgement; validate needs the verifies relation and green evidence, and records
a review; against a section it needs the test's current relation to a hint inside it, and
records the section's implements alone
```

```test:review-records-once
a review records only what isn't validated already: a verifies on its failing run keeps
that basis, and validate with nothing left to record refuses
```

### 18.1 Adopting an existing suite
<!-- surfex: adoption -->

The process (§18) suits new work. A project with an established suite has another
problem: its passing tests can't honestly go red, so every relation would need a review
up front, and again after every refactor until its test next discriminates. A project
chooses how it adopts, with `adoption:` in `.surfex.exs`, read against the test files under the
project root by `Surfex.Status.Config.adoption!/2` (and asked of one file by
`Surfex.Status.Config.trusted?/2`):

- `:reevaluate`, the default: nothing is taken on trust. Every relation is validated by
  evidence, a review or a judgement, as §18 describes.
- `:trust`: the existing tests may be adopted once, by a **baseline**.
- `[trust: globs, reevaluate: globs]`: per area, for a codebase with both a well-kept
  core and tests nobody has read in a while. The globs lie within `tests:` and don't
  overlap; a test in neither is re-evaluated.

`Surfex.Record.baseline/4` takes the baseline; `Mix.Tasks.Surfex.Baseline` is `mix surfex.baseline
--note N`:

- **Once.** It refuses when the log already holds a baseline, under `:reevaluate`,
  without `tests:`, and when a trusted test's current version hasn't run green.
- **It adopts the tags already written.** The baseline doesn't create `verifies:` tags,
  and a tag added afterwards is an ordinary claim, validated on its own (§18). So tagging
  comes first. With no tags on the trusted tests it refuses, since a one-shot step would
  spend itself on test versions alone, unless `--no-tags` (`meta[:no_tags]`) says that is
  intended. `Surfex.Record.baseline_summary/2` reports what it adopted, and the task
  prints it: the trusted test versions, the `verifies` relations, and the spec units no
  adopted `verifies` reaches.
- **What it records:** a `baseline` observation (§12.1) for each trusted test version,
  basis `baseline`, with the adoption setting and the note; and, for each trusted test's
  `verifies:` declarations at that commit, a `verifies` relation with basis `baseline`.
- **What it means:** a baselined test version counts as if it had discriminated. A green
  run then re-confirms its `tests` and `implements` relations after a code change, with
  basis `baseline`, so they stay counted as trusted.
- **It only shrinks.**
  - A changed test version isn't covered by its record.
  - A test's first real red→green records a `red_green` observation (§17), and its
    relations move to `evidence`.
  - Narrowing `adoption:` (`:trust` to `:reevaluate`, or a smaller trusted glob) stops the
    records it no longer covers from counting. Their relations are reported unvalidated:
    the backlog to review.
  - Moving a test carries its baseline record to the new id when its version is unchanged
    (§14): the same trust under a new name, not a second baseline.
  - Widening trusts nothing new: the baseline is one-shot, and only the test versions it
    recorded are trusted.
- **It is visible.** A `baseline` relation is validated while its test's current version
  is baselined and trusted; for an `implements`, while some such test verifies the unit
  and exercises the code (`Surfex.Status.validated?/2` answers it for any relation).
  `--validated` accepts it. Status, its JSON and `RELATIONS.md`
  count `baseline` relations (`Surfex.Status.baseline_count/1`) and name the adoption mode,
  and `mix surfex.status --no-baseline` (`baseline: :fail`) fails on them, for a project
  finishing that backlog.

```test:adoption-modes
adoption: is :reevaluate by default, :trust, or trusted and re-evaluated globs that lie
within tests: and don't overlap; anything else is refused
```

```test:baseline-one-shot
the baseline refuses under :reevaluate, a second time, without tests:, or before the
trusted tests run green; it records each trusted test version and its declared verifies,
basis baseline
```

```test:baseline-adopts-tags
the baseline adopts the verifies: tags already written: with none it refuses, unless
--no-tags says that is intended, and it reports the trusted test versions, the verifies it
adopted and the spec units left without one
```

```test:baseline-shrinks
a baselined test re-confirms a refactor as baseline; its first red→green moves it to
evidence; a changed test version, or narrowed trust, leaves its relations unvalidated;
baseline relations are counted, and fail under baseline: :fail
```

### 18.2 Reviews through a path no call shows
<!-- surfex: review-through-path -->

Some tests exercise code by a path no call in the source states: a route a router maps to
a controller, a message to a process, a JavaScript hook driven from Elixir, a capture
handed to a helper. Reach (§13.1) can't follow it, so neither evidence nor a review that
credits only reached code can validate the code's `implements`, though the test does
exercise it. **The path is recorded by hand, and named.**

Code relates to the claim it realises, not to every section that mentions it. Where no
citation proposes the pair, `mix surfex.relate CODE CLAIM --type implements --note N`
records it (proposed). Then
`mix surfex.validate TEST CLAIM --code ID --through HOW --note N` reviews it: `HOW` says
how the test exercises the named code (`route GET /settings`, `message {:feed, _}`,
`js hook Canvas`, `capture via growth/3`).
- Each named item the test's green run didn't reach is credited: its `implements` to the
  unit is recorded with basis `review` and the entry's `through` (§12.1). Named code the
  run did reach is credited as any review credits it, with no `through`.
- The review still needs everything a review needs (§18): the `verifies` relation, green
  evidence at the current versions, and a note. The named code must be scanned and
  already related to the unit (in any live state); `--through` without `--code`, or with
  every named item reached, is refused. A batch (`--file`) takes no `--through`, as it
  takes no `--code`. Without `--through`, an unreached `--code` stays an error.
- A move, a resolve and a new note (`annotate`) keep `through` with the basis.
  `mix surfex.history` shows it. `Surfex.Status.through_count/1` counts the current
  relations resting on such a review, and the status text, its JSON (`through`, and each
  relation's) and `RELATIONS.md` (when there are any) report the count, so an audit can
  find them.
- The triangle (§13) takes the review's word: code whose current `implements` rests on a
  `through` review counts as exercised by the unit's verifying tests. It dangles when the
  code changes, like any relation, and is judged again by a review naming the path.

```test:review-through
a review naming how its test exercises code the green run didn't reach credits that code
with through recorded, counted, shown and kept by a new note, and the triangle closes;
without --through it is refused; --through without --code, for reached code, for code
not scanned or not related to the unit, is refused
```

### 18.3 Reviewing a check
<!-- surfex: review-check -->

A check that has never failed hasn't shown it can, so evidence can't confirm it (§17.1).
`mix surfex.validate CHECK UNIT --note N` reviews it instead. CHECK is the check's id (a
`check:` prefix picks it where a test has the same id), and the note says what the check
would catch. The review needs the live `verifies` relation from the check to the unit and
a green run of the check at its current version. It records that relation with basis
`review`, and nothing else, since a check carries no `implements`.

```test:check-review
a review of a check records its verifies to the unit on a green run at its current
version, with the note; without a green run, a relation or a note, or when already
validated, it is refused
```

### 18.4 A review of several arities
<!-- surfex: review-arities -->

A function with default arguments is one definition the scanner reports as several items
(§11): `put/3` and `put/4` from one `def put` whose last argument has a default. A review
credits code by definition, so a review of a test calling one credits every arity the
unit's `implements` names, each under the review's one note. A note written about one
arity ("put/3 is the arity with no options") is then wrong on the other's relation.
`mix surfex.validate` says when it records several arities of one definition, naming them
(`Surfex.Record.shared_definitions/2`), so the reviewer writes the note for the
definition; `mix surfex.annotate` corrects one already written.

```test:review-arities-named
a review crediting a unit's implements to two arities of one definition records both, and
validate names them as one definition; a review crediting one arity names none
```

## 19. Handing off found work
<!-- surfex: hand-off -->

What Surfex finds is work: a spec unit marked as needing an update (§13.1), an id the
policy says is unmet, a gap in the triangle. That work is a change to the spec, the tests
or the code, and **it goes through the change process of the environment Surfex runs
in**: a tracker, a proposal queue, a pull-request template, a person's inbox. Surfex knows
no tracker. It drafts, and the environment's process decides.

`Surfex.Change` makes a **change draft** for one piece of found work:
- a title;
- the problem: the mark's note, who made it and when; the unmet requirement; or the gap;
- what it touches: the spec unit (with its location and current text), the tests that
  verify it and the code that implements it, each with its relation's state;
- the steps the process implies (§18): spec, failing test, test relation, code, code
  relation.

`Surfex.Change.drafts/3` gives a draft for every open mark, unmet id and triangle gap of a
status, or, given ids, for those items: a marked unit's marks, or a draft to change the
item. `Surfex.Change.markdown/1` renders one for a person; `Surfex.Change.json/1` renders
a list for tools and agents.

The **hand-off** is the project's own setting, `process:` in `.surfex.exs`
(`Surfex.Change.hand_off/3`):
- `:print` (the default): the draft is printed, for a person or an agent to carry into
  their process;
- `{:command, argv}`: the command runs once per draft, with `{title}`, `{body}` (the
  markdown) and `{file}` (a file holding it) substituted into `argv`, and never through a
  shell. A command that exits non-zero stops the hand-off with its output. The draft is
  given as arguments, not on standard input: an argument needs no wrapper around the
  command, and `{file}` serves a command that reads a file.

`Mix.Tasks.Surfex.Draft` is `mix surfex.draft [ID…] [--format markdown|json] [--file]`.
It prints the drafts and changes nothing. With `--file` it hands each draft to the hook
instead. Surfex never hands anything off on its own initiative: only `--file` runs the
hook. `mix surfex.mark` prints the new mark's draft.

Surfex's own `.surfex.exs` hands drafts to this environment's proposal system:
`{:command, ["glab", "issue", "create", "--label", "Proposal", "--title", "{title}",
"--description", "{body}"]}`.

```test:change-drafts
a draft is made for each open mark, unmet id and triangle gap, or for a named item, with
its title, problem, the unit, tests and code it touches, and the process's steps; it
renders as markdown and JSON
```

```test:change-hand-off
process: :print prints each draft; {:command, argv} runs once per draft with {title},
{body} and {file} substituted and no shell; a failing command stops, with its output;
mix surfex.draft prints unless --file is given
```

## 20. Completeness
<!-- surfex: completeness -->

Status answers whether anything is broken. Completeness answers **how much of the project
is covered**: which spec units, tests and code are missing a relation they need, and a
score to move. `Surfex.Completeness.report/1` (`Surfex.Completeness`) derives it from a status, and it is a pure
function of the scans and the log.

**What counts.** Every spec unit that is a claim (§20.1: a unit with text of its own,
`Surfex.Scan.Markdown.empty?/1`; a container, such as a heading over subsections, carries
no claims of its own), every
test, and every code item. What's complete is judged on **validated** relations (§18):
complete means validated, not asserted.

| Kind | Complete when | Missing, as listed |
|---|---|---|
| spec unit | a test or check verifies it by a current, validated relation of its own (§20.1); and every `implements` relation to it is current and validated. A unit no code implements (a policy, say) is complete with its test alone: every unit needs a test, and code only where the spec describes code. A deviating unit (§13.1) is never complete, whatever its basis: a `verifies` recorded on its test's red run is validated, but the code doesn't meet the unit | `no_relation` (in none), `no_test`, `contained (<unit>, …)` (§20.1), `unvalidated`, `deviates (<ref>, …)` |
| test | it verifies a spec unit by a current, validated relation, and exercises code (a current `tests` relation). A test verifying only units no code implements is complete with its `verifies` alone | `no_relation`, `verifies_nothing`, `exercises_nothing` |
| code | it implements a unit by a current, validated relation, or is excused by a current `excuses` relation; and, when implemented, a test verifying the unit exercises it (no `code_untested` gap, §13.1). Code is one item per definition (§11): a function's default-argument arities are judged together and counted once, listed under the first of their ids | `no_relation`, `not_described` (neither implemented nor excused), `unvalidated`, `untested` |

**The score** is complete items over all items, per kind and overall (every item weighted
the same), as a percentage to one decimal with the counts. With nothing to count, a kind
scores 100.

- `Surfex.Completeness.text/1`: the scores, then each incomplete item with what it lacks
  and where it is.
- `Surfex.Completeness.json/1`: the work list for an agent: the scores, and each
  incomplete item with its kind, id, what it lacks and its location.
- `Surfex.Completeness.golden/2`: the report as a golden, `COMPLETENESS.md` by default, for
  `goldens:` (§10.3). It holds counts and lists, with no hashes and no times.
- `Surfex.Status.Config.completeness!/1` reads `completeness: [min: N]` (a number from 0
  to 100; no key, no minimum), raising on anything else. It is the maintainer's intent,
  and loud: below it (`Surfex.Completeness.below?/2`), `mix surfex.status` fails with the
  score. Off by default, since the score is information first.

`Mix.Tasks.Surfex.Completeness` is `mix surfex.completeness [--format text|json]`. It prints
the report, and fails below `completeness: [min: N]`. With `--summary` it prints only the
score lines (`Surfex.Completeness.summary/1`), for a gate step whose work list is already a
golden; the floor holds as without it.

```test:completeness-summary
mix surfex.completeness --summary prints the overall and per-kind score lines and nothing
else, and below completeness: [min: N] fails with the same message as without it
```

```test:completeness-rules
a spec unit is complete when a validated test verifies it (its own evidence) and any
implementing code is validated, with a test alone for a unit no code implements; a
heading-only section isn't counted; a test is complete verifying and exercising code, or
verifying only code-less units; code is complete when validated and exercised, or
excused, one item per definition whatever its default-argument arities; each incomplete
item names what it lacks
```

```test:completeness-deviates
a deviating unit is incomplete, missing deviates with its references, whatever validated
its relations and whether or not code implements it; with the tag removed and its
relations validated, it is complete
```

```test:completeness-score
the score is complete over all per kind and overall, to one decimal, 100 with nothing to
count; it reads as text, JSON and a golden with no hashes or times; below completeness:
[min: N] the status check fails
```

### 20.1 Own text in the score
<!-- surfex: own-text-score -->

Completeness (`Surfex.Completeness.report/1`) reads a spec unit's text as verification
does (§13.4): a unit is counted when it is a claim, and
a claim is complete only with **its own** evidence.

- A claim is complete when a test or check verifies **it** by a current, validated
  relation, and every `implements` relation to it is current and validated, as before.
  A `verifies` to a unit inside it doesn't complete it: that unit's evidence is the inner
  unit's, not the claim's own text's.
- A `:contained` claim, one with no `verifies` of its own while a claim inside it is
  verified, misses `contained`, which names the verified claims inside it
  (`contained (<unit>, …)`), so the work list says where evidence already is. A claim
  with nothing, own or inside, misses `no_test`, as before.
- Containers (§13.4) aren't counted: a heading over subsections, a section whose body is
  only named claims, and a marked table, whose rows are its claims.

A contained claim is settled by adding it to the tag of a test that really checks its
prose, by naming the prose's claims (§11.1) and migrating its relations (§14.1), or by
classing the prose as asserting nothing.

```test:completeness-own-text
a contained section is incomplete, missing contained with the verified unit inside it; the same section tagged by a validated test is complete; a
container, a marked table among them, isn't counted
```

### 20.2 Excused and deferred in the score
<!-- surfex: classed-score -->

Completeness reads classes and deferrals as verification does (§13.6): an excused unit and a deferred unit are left out of the spec score's total,
and counted on lines of their own, so a growing excusal moves a number the summary shows:

```text
  spec units: 91.2% (52/57)
  excused by class: 14 spec units, out of the score
  deferred: 2 spec units (#12, #40), out of the score
```

The claims inside a classed section are counted as any claim is.
`Surfex.Completeness.report/1` gives them as `excused` (the units) and `deferred` (each
unit and its reference), and `Surfex.Completeness.json/1` under the same keys.

```test:classed-completeness
an excused section and a deferred section are out of the spec total and counted on their
own lines in text and JSON, while a rule inside a classed section is counted
```

### 20.3 Code described by an earning implements
<!-- surfex: earned-code-score -->

Completeness (`Surfex.Completeness.report/1`) reads code as `require:` does (§13.9): code is described only by an `implements` that earns (§13.5).
Code with `implements` relations of which none earns misses `unearned`, whatever
validated them; one earning relation describes it. Code with no `implements` misses
`not_described`, as before, and an `excuses` still describes code nothing implements.

```test:completeness-earned-code
code whose only implements is to an unverified hint misses unearned, and no longer does
once a test verifying the hint reaches it
```

## 21. Discovering the tool
<!-- surfex: info -->

Surfex's main user is an agent working out the state of a spec, its tests and its code.
The agent shouldn't have to piece the tool together from the README, the guides and each
task's moduledoc. `Mix.Tasks.Surfex.Info` is `mix surfex.info [TOPIC]`, and it tells the
agent how the tool works from the installed version itself:

- **With no topic, a directory** (`Surfex.Info.directory/0`) of fewer than 100 lines: what
  Surfex is in a few lines, every topic with a one-line summary, and every `mix surfex.*`
  command, grouped by job, one line each.
- **With a topic, its page** (`Surfex.Info.page/1`): a focused, dense page with exact
  commands, written for an agent. An unknown topic is refused, and the refusal names the
  topics.
- **The pages live with the code; the usage rules point to them.** The directory is
  `priv/info/index.md` and each topic `priv/info/TOPIC.md`, except the agent topic, which
  is the package's `usage-rules.md`: the one short page the `usage_rules` tool copies into
  a project's `AGENTS.md`. It holds what surfex is and the rules for an agent, and points
  to `mix surfex.info` for the rest, so a project's agent context stays small and the
  detail always matches the installed version. All of them are the docs' "Using surfex"
  section. They are shipped in the package and built into
  `Surfex.Info` at compile time, so they always match the installed version.
  `Surfex.Info.topics/0` lists each topic with its summary.
- **The docs show the tool.** Surfex is used as `mix surfex.<cmd>`, so its published docs
  lead with the README and the usage pages, then the mix tasks, and the three modules a
  project writes code against: the evidence formatter (§17, in `test_helper.exs`), and the
  scanner behaviour with its item (§5, for a project scanner). Every other module keeps its
  documentation in the code, for `h` in iex, without being presented as the package's.
- **Two modules are supported library API** (§23), so they are on hexdocs too.
- **Using isn't adopting.** A project that renders goldens with surfex but keeps no
  relation log (`.surfex/`) hasn't adopted it. The directory says so when run there
  (`Surfex.Info.adoption_note/1`), and points to the adoption topic.

```test:info-directory
the directory is under 100 lines, names every topic and every mix surfex.* command; each
topic prints its page; an unknown topic is refused, naming the topics
```

```test:usage-rules-shipped
usage-rules.md is the short agent page and points to mix surfex.info; the directory and
every other topic are pages under priv/info, listed as items; the package ships them and
the docs carry them; hexdocs show the mix tasks, the formatter, scanner and item modules,
and the golden and source-scan library modules, and no other module
```


```test:info-without-log
the directory notes when the project has no relation log, pointing to adoption, and says
nothing once the log exists
```

## 22. A model of the log
<!-- surfex: model -->

Unit tests sample the relation log's behaviour; a model explores it. The log is checked
by an exhaustive model in extla, the TLA+-style checker for Elixir: branches record,
retire and resolve relations, main takes each branch by git's union merge, and a branch
catches up with main the same way. Every entry is written by the real recording code and
every judgement made by the real status derivation, called from the model, so the model is
an oracle for the code rather than a second copy of it.

In every state the bounded model reaches:
- **The order of lines doesn't matter.** A log's status is the same for its lines in any
  order, as a union merge writes them.
- **A conflict is a disagreement.** A relation is conflicted exactly when the tips in force
  record different judgements; tips that agree are one, so two resolutions that pick the
  same side don't conflict again.
- **Resolving ends a conflict.** Picking a side always leaves one tip.
- **Every entry reads back.** Each entry written decodes under its own id.

Two more models follow the log through versions and validation, on one log: a spec unit,
its code and a test that verifies the unit and calls the code, each at one of two
versions, with test runs that pass or fail against the code as it is. The **evidence
path** changes the code and confirms by evidence; the **review path** changes the spec and
the test and validates by a review or a judgement. In every state each reaches:
- **Current means current.** A current relation's ends are at the versions scanned now.
- **Shown, not asserted.** A current `implements` rests on evidence or a review.
- **Evidence is borne out.** Every entry on evidence is backed by runs that happened: a
  `verifies` by its test version's failing run, anything else by a red then a green.
- **A review rests on a green run** of the test at the versions it records.

Two more follow what a change asks of the log. **Moves:** a spec section and a test are
renamed and moved (`mix surfex.move`, or suggest's move); a move keeps every claim with its
basis, every retirement and every red→green record, under the new ids. **Recovery:** from a
triangle established test-first, the spec, the code and the test change in any order, each
test version runs once against each code version, and recording is done when it can be
(`confirm --evidence`, `validate`, confirm by judgement, suggest's refresh). Under that
fairness, a dangling relation whose test passes is brought back to current, unless the test
stops passing: nothing is current on a red test. It is a liveness property, so nothing in
the model is capped; a stranded relation would be a finding, not the bound.

The models run in a Mix environment of their own (`MIX_ENV=model`), so the rest of the
suite neither fetches nor compiles the checker, and CI runs it as a job of its own. A
violation fails it with the shortest trace to the state that breaks the property.

```test:log-model
in every state branches and merges reach, a log's status is the same for its lines in any
order, a relation is conflicted exactly when the tips in force disagree, resolving ends
the conflict, and every entry reads back under its id
```

```test:log-model-validation
with versions changing and tests run red or green, a current relation's ends are at the
current versions, a current implements rests on evidence or a review, every entry on
evidence is borne out by runs, and every review rests on a green run
```

```test:log-model-moves
renaming and moving a spec section and a test keeps every claim with its basis, every
retirement and every red→green record, under the new ids
```

```test:log-model-recovery
whatever changes, a dangling relation whose test passes is brought back to current by the
work surfex asks for, unless the test stops passing
```

## 23. The library API
<!-- surfex: library-api -->

Some projects use surfex for one thing: a surface golden, a committed document generated
from a source scan, which CI regenerates and byte-compares, keeping no relation log. They
call two modules directly, and their gates rest on them, so both are **supported library
API**: `Surfex.SourceScan`, which reads Elixir source without compiling it, and
`Surfex.Golden`, which renders a golden from a plain data spec. Both are on hexdocs, with a
guide (`guides/using-surfex-as-a-library.md`). They carry a promise:
- **Their documented functions and types keep their names, arities and shapes**, as they
  have since v0.1.0.
- **`Surfex.SourceScan.definition_hash/1` gives the same version for the same code**,
  release to release, so a golden doesn't churn on an upgrade.
- **A change to either never comes in a patch release.** It comes in a minor release,
  listed under "Changed" in the CHANGELOG and marked for library users.

A test pins the documented surface of both, so a change is always deliberate.

```test:library-api-stable
the documented functions and types of Surfex.Golden and Surfex.SourceScan are exactly the
pinned ones, the one-argument project_root stays callable, and definition_hash gives the
same version for the same code
```

### 23.1 The scanner contract
<!-- surfex: scanner-api -->

A project scanner, and above all a DSL plugin (§4.1), is code another project writes
against surfex, often in a package of its own. `Surfex.Scanner` and `Surfex.Item` are
therefore supported API too, under §23's promise:
- **`Surfex.Scanner`'s callbacks keep their names, arities and argument and return
  shapes.** A callback added later is optional, so an existing scanner keeps working.
- **`Surfex.Item`'s fields keep their names and meanings**, a field added later has a
  default, and `Surfex.Item.key/1` keeps its rule, so a plugin's keys, and every relation
  recorded against them, survive an upgrade.
- **A change to either never comes in a patch release.** It comes in a minor release,
  listed under "Changed" in the CHANGELOG and marked for scanner authors.

A test pins both.

```test:scanner-api-stable
Surfex.Scanner's callbacks and Surfex.Item's fields are exactly the pinned ones, only kind,
name, file and hash are enforced, and key/1 keeps its rule
```
