Some projects use surfex for one thing: a surface golden, a committed document generated from a source scan, listing what the code declares, which CI regenerates and byte-compares so it can't disagree with the code. They call two modules directly and keep no relation log. Both are supported library API (spec §23).
The two modules
Surfex.SourceScanreads Elixir source without compiling it:lib_sources/1: the project's ownlib/**/*.ex, skippingdeps/,_build/,test/,tmp/and hidden directories;defmodules/1: the modules in a quoted file;defs/1andtypes/1: the public definitions and types of a module, each with its version;module_hash/1: a module's public surface as one version. The relation log's module items also cover the module's hidden definitions, and each item the code of the@moduledoc falsemodules it runs (spec §3.3); this function doesn't, so a surface golden changes only with the surface;definition_hash/1: the version of any node or snippet: a SHA-256 over its AST with metadata removed, so a blank line or a comment above it doesn't change it;project_root/1,2,line_range/1andhidden_module?/1.
Surfex.Goldenrenders a golden from a plain data spec (Surfex.Golden.spec/0):render/1;stat/2andstat_line/2for its stats lines;natural_key/1for ordering rows.
The renderer owns the notation, so every project's goldens read the same way, and it never emits a timestamp, so a golden is a pure function of source. A backticked cell reads back as its text under GFM even when the text holds
|or a backtick; a{:raw, …}or plain-string cell is written verbatim, so escape a|in it as\|.
A surface golden in a few lines
rows =
for file <- Surfex.SourceScan.lib_sources(root),
{:defmodule, _, [name, _]} = mod <-
file |> File.read!() |> Code.string_to_quoted!() |> Surfex.SourceScan.defmodules(),
d <- Surfex.SourceScan.defs(mod),
do: %{
"Function" => {:code, "#{Macro.to_string(name)}.#{d.name}/#{d.arity}"},
"Version" => {:version, d.hash}
}
Surfex.Golden.render(%{
name: "API_SURFACE.md",
purpose: "Every public function, with its version.",
task: "my_app.surface",
gate: "surface",
hardness: :hard,
columns: ["Function", "Version"],
rows: rows
})CI runs the task with --check, regenerating and comparing. A difference fails the build,
and the diff shows the reviewer exactly which definitions changed.
The stability promise
- Signatures: the documented functions and types of both modules keep their names, arities and return shapes. They have since v0.1.0. A test in surfex pins the documented surface, so a change is deliberate.
- Hashes:
definition_hash/1gives the same version for the same code from release to release, so a golden doesn't churn on an upgrade. - Changes: a change to either module never comes in a patch release. It comes in a minor
release, listed in the CHANGELOG under "Changed" and marked for library users. One such
change already:
lib_sources/1started skippingtest/,tmp/and hidden directories.
A project scanner, such as one giving the blocks a DSL declares as items, is written against
Surfex.Scanner and Surfex.Item, and the same promise covers them (spec §23.1): the
callbacks and fields keep their names and shapes, and anything added is optional or has a
default.
This isn't adoption
A surface golden says what the code declares. Surfex's purpose is keeping a spec, its tests
and its code aligned, which needs the relation log. When a project depends on surfex but
has no .surfex/, mix surfex.info says so. mix surfex.info adoption covers starting
the log on an existing project.