SvEx.Baseline (SvEx v0.4.2)

Provenance and drift checking for the checked-in generator baselines.

Every function is pure over a directory — nothing here shells out to a generator, so the drift check runs offline on every commit and 100% coverage never depends on having phx_new installed.

This module must NOT share a normaliser with SvEx.Hash. They require opposite semantics: the manifest hash ignores an added comment (a # credo:disable-for-next-line must not lock a project out of updates), while a comment added to a baseline IS drift.

Summary

Functions

Names an archive from its inner tar's sha.

A single digest over the whole tree.

Selects one baseline's archive members.

Replaces phx.new's four crypto:strong_rand_bytes values with placeholders.

Reads the provenance record.

Recomputes every entry's derived fields from disk.

Builds one baseline's self-verifying snapshot.

Builds reproducible, uncompressed tar bytes from in-memory members.

Maps every relative path under dir to the sha256 of its normalised bytes.

Functions

archive_name(sha)

@spec archive_name(binary()) :: binary()

Names an archive from its inner tar's sha.

Only the last 8 characters: the full digest is recorded as archive_sha256 in the manifest and again inside the archive itself, so the filename only has to be short and distinguishing.

digest(dir)

@spec digest(Path.t()) :: binary()

A single digest over the whole tree.

members(entry)

@spec members(map()) :: [{charlist(), binary()}]

Selects one baseline's archive members.

Contents come from disk but membership comes from the entry's files map, never from a fresh wildcard — that is what makes it structurally impossible for the archive and the manifest to disagree about the baseline.

Names are project-relative, identical to the files keys, so each archive unpacks as a standalone snapshot of that generated project and a member name can be looked up in files directly.

normalise_secrets(source)

@spec normalise_secrets(binary()) :: binary()

Replaces phx.new's four crypto:strong_rand_bytes values with placeholders.

Anchors on the key name, never on a bare length match: the alphabet is unpadded base64, so values contain + and / and may start with /.

read!(path)

@spec read!(Path.t()) :: %{required(atom()) => map()}

Reads the provenance record.

Not every entry is a generator baseline. derived_from: marks one produced by applying a plugin — mix sv_ex.baseline.compose — and such an entry has no argv:, generator: or generator_version:. A test that drives a generator must select entries that carry them rather than iterating all.

record(manifest)

@spec record(%{required(atom()) => map()}) :: %{required(atom()) => map()}

Recomputes every entry's derived fields from disk.

files and tree_digest are recomputed; every other field is passed through untouched. generator_version above all must never be sourced from the running toolchain — it is the pin that makes the toolchain test fail with one line on a phx_new bump instead of a 45-file hash diff.

:file_count is dropped: files supersedes it and map_size/1 is exact.

archive_sha256 is recorded here rather than in a sidecar file. It is not circular: the archive holds only the project's files, so the manifest is not an input to its own recorded hash and one pass converges.

snapshot(entry)

@spec snapshot(map()) :: {binary(), binary()}

Builds one baseline's self-verifying snapshot.

The .tar.gz is an OUTER tar holding two members: baseline.tar — the project's files — and baseline.sha256, its checksum. The checksum sits beside the inner tar rather than inside it, which is the only placement that terminates: a file recording a tar's hash cannot also be a member of it.

Returns {sha, gzipped} where sha is the hex sha256 of the INNER tar.

tar(members)

@spec tar([{charlist(), binary()}]) :: binary()

Builds reproducible, uncompressed tar bytes from in-memory members.

Members are {name_charlist, contents} and are sorted here rather than by the caller, so member order cannot vary with map iteration.

tree(dir)

@spec tree(Path.t()) :: %{required(Path.t()) => binary()}

Maps every relative path under dir to the sha256 of its normalised bytes.