# Surfex specification {#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 {#scope}

### 1.1 What Surfex is for {#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 {#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 {#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 {#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 {#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 |

### 2.3 Determinism {#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 {#reading-source}

`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 {#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 {#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 {#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
```

## 4. Items {#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 |

`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
```

## 5. Scanners {#scanners}

### 5.1 The contract {#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 {#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 skipped, with its functions. A module nested in it
  is not, since Elixir documents it separately.
- 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.
- 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 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
```

## 6. Citations {#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 {#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 {#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 {#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 {#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 {#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 {#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.

## 7. Coverage {#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 {#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 {#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 {#gating}

### 10.1 The gate {#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 row is keyed
  by its table's `Item` column, or the first column when there is none, and compared by
  its `Version` cell, or the whole row when there is none. 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
```

### 10.2 Project goldens {#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 {#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 {#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 {#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` and its parent as `within`.
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`. Since a tag is ordinary ExUnit, `mix test --only verifies:id`
  runs exactly the tests of one requirement. A `verifies` value that isn't a literal
  string or list of strings raises, naming the file and line.
- **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 is listed under each module it imports. 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 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. It gives `{:error, :unknown}` when nothing has the id,
and `{:error, {:ambiguous, ids}}` when more than one file does.

`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/3`
leaves out those under an `exclude:` prefix, §8); `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 file and the path of headings down to it. Two sections with the
  same path in one file are told apart as `~2`, `~3`, in order. A heading **anchor**,
  `{#cart-add}` at the end of the heading, makes the id `spec.md#cart-add` instead: it
  survives renaming the heading or any heading above it. The anchor is not part of the
  heading text, so subsections' paths don't include it.
- **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.

Two 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`. 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.

Anchors, block ids and hint ids share one namespace per file with the section ids, and
are lowercase 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 block that nests or spans a heading;
- a close marker with no block;
- a malformed id;
- a duplicate id, naming both lines.

```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
```

## 12. The relation log {#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 {#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 verifies a spec unit) 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`) 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 |
| `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 |

- **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, 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 → 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 | `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 mark is one
spec end and an observation one test end; 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 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 {#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 {#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)) 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
```

### 13.1 States {#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.
- **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, failed, or ran against another version of the code. 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 never is.
- **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.
- **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, any id is unmet, any `verifies` relation is undeclared, any declaration or
citation is broken, any excuse is stale, or any confirmation by evidence is unproven. New,
retired, impacted, planned, unimplemented and unvalidated don't fail on their own. 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.

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 and test hints.

```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 calls none of its code, and implementing
code no verifying test calls are each reported, naming the test or the code
```

```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, an unmet id, an
undeclared verifies relation, a broken declaration or citation, a stale excuse, or an
unproven confirmation, and on nothing else by default; planned: :fail adds planned
relations, and validated: true unvalidated ones
```

```test units-by-role
the spec units scanned are counted by role: sections, blocks and test hints
```

```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 {#status-config}

`Surfex.Status.Config` reads `.surfex.exs`. It holds the profile keys (§8) and the
log's own: `scanner`, `scanner_opts`, `namespace`, `goldens` (§10.3), `require`, `tests`,
`triangle`, `require_red` (§17), `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.
- `Surfex.Status.Config.items/2` runs the code scanner: the built-in Elixir scanner, or
  the project's `scanner:` module (`Surfex.Scanner`) with its `scanner_opts:`.
- `Surfex.Status.Config.scans/2` scans each markdown unit of `sources`, each item the
  code scanner (`scanner`, `scanner_opts`) finds, and, when `tests:` names globs of
  test files, each test in them (`Surfex.Scan.ExUnit`). 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.

```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
```

### 13.3 Reports and the check {#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, impacted and planned relation,
  the unimplemented, new and unmet ids, the broken citations, the unproven confirmations,
  the undeclared relations, 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.
- `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 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. It leaves 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 and impact, 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 unproven confirmations, the stale excuses, the
  broken declarations, the triangle's gaps, the unvalidated relations 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. 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-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
```

## 14. Recording {#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 exercises (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
```

## 15. Suggesting relations {#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.
- **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 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-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
```

## 16. The specification guide {#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
- keeping headings stable
- naming the code in the section that describes it
- stating requirements as checkable claims
- 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.

## 17. Test evidence {#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 calls at that moment.
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`).
- `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, 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
```

**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 checked against a 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 never are
```

**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
```

## 18. Validation by process {#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 exercises. 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 {#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
```

## 19. Handing off found work {#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 {#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 with text of its own (a section, marked block or test
hint whose own body isn't empty, `Surfex.Scan.Markdown.empty?/1`; a heading over
subsections carries no claims), 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 verifies it, or a unit inside it, by a current, validated relation; 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 | `no_relation` (in none), `no_test`, `unvalidated` |
| 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) | `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]`.

```test completeness-rules
a spec unit is complete when a validated test verifies it (or a unit inside it) 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; each incomplete item names what it lacks
```

```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
```

## 21. Discovering the tool {#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 {#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 {#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
```
