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;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 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.
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.