Surfex.Golden (Surfex v0.6.2)

Copy Markdown View Source

The ONE renderer for a project's surface goldens.

A surface golden is a document generated from a source scan, listing what the code declares, gated so that it cannot disagree with the code: CI regenerates it and byte-compares, and a difference fails the build. The document therefore cannot lie — which is the only property that makes a generated document worth reading three weeks later.

Each scanner builds a plain data spec and hands it here. The renderer IS the convention: notation lives in one place, so per-file dialects cannot form — and, now that more than one project renders goldens, per-project dialects cannot either.

The shape a golden always has

  1. Header block — line 1 # <NAME>; line 2 the one-line :purpose; line 3 the machine-parseable attribution Generated bymix <task> --write· gate<gate>(HARD|ADVISORY) — do not edit; a drift FAILS the gate. Domain :prose (semantics, class ladders) FOLLOWS the block, unconstrained.
  2. Stats line(s) — each **<lead>** · <label> <n> · … (see stat_line/2); every golden carries at least a row count.
  3. Tables — first column = the catalogued item (backticked); last column conventionally Locus. Cells are TYPED (cell/0) so the renderer — not each caller — owns the backtick/atom/locus/— notation. Optional grouping: ## <heading> with per-group tables.

No timestamps or VCS data are ever emitted. A golden is a stable function of source; a date makes it drift against itself on a quiet day, and a gate that cries wolf gets disabled. Dates belong in a report, never here. This is a lesson paid for once already in the project this came from, and it is the one rule most likely to be re-broken.

What this does NOT know

The evidence ladder — whether a thing is model_checked, static_gate, property, unit_test or NOTHING — is the calling project's vocabulary, and differs between them. So does what a surface is. This renders typed cells; it never interprets them.

Determinism

render/1 is a pure function of its spec and is invariant under the input order of a row map's keys (cells are pulled in :columns order) and under the input order of the row list (rows are sorted by :sort, default the first column's natural key). So two specs that differ only in such orderings render identical bytes — the property every drift gate rests on, and the reason it is asserted in this package's tests rather than assumed.

Extracted from agentronic's Maatronic.SourceScan.Golden, where ten goldens have used it in production. Behaviour is unchanged; only the prose was rewritten for a home that serves more than one project.

Summary

Types

A typed table cell — the renderer formats it, so notation is central, not per-caller

A ## <heading> grouped table. :columns overrides the spec's top-level columns for this group — for a golden whose sub-tables have different shapes (e.g. AUTHZ's boundary ops vs runtime seams).

One table row: column-name => cell. Order-independent (cells pulled in :columns order).

A surface-golden spec. :groups and :rows are mutually exclusive — supply :groups for a grouped golden (SPEC_COVERAGE), :rows for a flat one. :sort is a row → sortable key (rows are sorted within each group); it defaults to the first column's natural key.

One stats line as DATA: the bold :lead, its :dims ({label, value} pairs / bare flags), and the rendered :text. Built by stat/2 so the renderer AND any cross-surface report read the same counts — the report never parses the markdown.

Functions

A natural sort key for an identifier-ish string: alternating text / integer chunks, so Foo.9 sorts before Foo.10. The renderer's default row order when a spec supplies no :sort.

Render spec to golden markdown. Pure; order-invariant per the moduledoc.

A stats line as DATA (stat/0): lead + dims + the rendered text. Each golden passes these in spec.stats; a cross-surface report reads lead/dims from the same value, so the report's counts round-trip the golden's stats line by construction.

Build the stats line STRING: **<lead>** · <label> <value> · …. lead is the pre-bold summary ("963 sections", or a label like "Freshness (of tagged validations)"); dims is a list of {label, value} (a bare label is a flag). Prefer stat/2 in a spec so the counts stay data.

Types

cell()

@type cell() ::
  {:code, iodata()}
  | {:atom, atom()}
  | {:locus, String.t() | nil, pos_integer() | nil}
  | {:version, String.t() | nil}
  | {:raw, iodata()}
  | :absent
  | nil
  | binary()

A typed table cell — the renderer formats it, so notation is central, not per-caller:

  • {:code, io} → `io` (the backticked-item rule)
  • {:atom, a} → `:a` (atoms always backticked, e.g. an effect class :local)
  • {:locus, file, line} → `file:line` (a nil file ⇒ —; a nil line drops the :line, so a hashed-locus surface passes {:locus, file, nil} for a bare-path Locus)
  • {:version, hash} → `hash` (a content version — Surfex.SourceScan.definition_hash/1's 8-hex output; a nil hash ⇒ —). Named rather than a bare {:code, …} so the content-version notation lives in one place across every golden's Version column.
  • {:raw, io} → verbatim (escape hatch for a composite cell like `unit_test` · tagged)
  • :absent / nil → — (the absent-value glyph)
  • a plain binary → verbatim

group()

@type group() :: %{
  :heading => iodata(),
  :rows => [row()],
  optional(:columns) => [String.t()]
}

A ## <heading> grouped table. :columns overrides the spec's top-level columns for this group — for a golden whose sub-tables have different shapes (e.g. AUTHZ's boundary ops vs runtime seams).

row()

@type row() :: %{optional(String.t()) => cell()}

One table row: column-name => cell. Order-independent (cells pulled in :columns order).

spec()

@type spec() :: %{
  :name => String.t(),
  :purpose => iodata(),
  :task => String.t(),
  :gate => String.t(),
  :hardness => :hard | :advisory,
  :columns => [String.t()],
  optional(:prose) => iodata(),
  optional(:stats) => [stat()],
  optional(:notes) => iodata(),
  optional(:groups) => [group()],
  optional(:rows) => [row()],
  optional(:sort) => (row() -> term())
}

A surface-golden spec. :groups and :rows are mutually exclusive — supply :groups for a grouped golden (SPEC_COVERAGE), :rows for a flat one. :sort is a row → sortable key (rows are sorted within each group); it defaults to the first column's natural key.

stat()

@type stat() :: %{
  lead: iodata(),
  dims: [{iodata(), term()} | iodata()],
  text: iodata()
}

One stats line as DATA: the bold :lead, its :dims ({label, value} pairs / bare flags), and the rendered :text. Built by stat/2 so the renderer AND any cross-surface report read the same counts — the report never parses the markdown.

Functions

natural_key(text)

@spec natural_key(String.t()) :: [String.t() | {0, integer()}]

A natural sort key for an identifier-ish string: alternating text / integer chunks, so Foo.9 sorts before Foo.10. The renderer's default row order when a spec supplies no :sort.

render(spec)

@spec render(spec()) :: String.t()

Render spec to golden markdown. Pure; order-invariant per the moduledoc.

stat(lead, dims)

@spec stat(iodata(), [{iodata(), term()} | iodata()]) :: stat()

A stats line as DATA (stat/0): lead + dims + the rendered text. Each golden passes these in spec.stats; a cross-surface report reads lead/dims from the same value, so the report's counts round-trip the golden's stats line by construction.

stat_line(lead, dims)

@spec stat_line(iodata(), [{iodata(), term()} | iodata()]) :: iodata()

Build the stats line STRING: **<lead>** · <label> <value> · …. lead is the pre-bold summary ("963 sections", or a label like "Freshness (of tagged validations)"); dims is a list of {label, value} (a bare label is a flag). Prefer stat/2 in a spec so the counts stay data.