SvEx. Plugin. Record
(SvEx v0.4.2)
Records an applied plugin in the target's plugin.exs (SDD 8.2, R5, R6).
SvEx.Plugin.Apply installs; this records what was installed and with
which content hashes, so a later update can tell an untouched file from an
edited one. Nothing else under lib/ writes a manifest.
Only into a project that declares itself
SvEx.Manifest enforces six top-level keys, four of which describe a
project the scaffolder generated. Apply deliberately works against ANY
project — its round trip installs meta_cache into a bare copy of
baseline_otp that has no target.exs at all — so recording is conditional:
| Root | Apply | This module |
|---|---|---|
has target.exs | installs | writes plugin.exs |
| has none | installs, and says so | does nothing |
Inferring the missing fields was rejected: a manifest that asserts an inferred
base is worse than no manifest, because the update flow trusts it. Creating
a target.exs was rejected too — installing a plugin would silently
convert someone's project into a SvEx project.
What a plugin changed in mix.exs, and what is still not hashed
deps, aliases and project are recorded on the entry. Until they were,
SvEx.Plugin.Apply wrote a plugin's dependencies into the target's
mix.exs and nothing said WHICH plugin had put them there, so an update
flow could neither attribute a dependency nor notice that a user had since
hand-edited it.
They are recorded as DECLARATIONS, not as hashes. Derive extracts mix.exs
out of files and into these three keys, so file_entry/3 never sees the
file and no content hash of mix.exs exists in the manifest. That is the
right shape — mix.exs is a file every plugin and the project itself
writes to, so a whole-file hash would report drift on every unrelated edit —
but it means drift detection for these three is a structural comparison an
update flow has to make against the target's CURRENT mix.exs, not a hash
comparison like every other entry. That comparison is D7's work and is not
written yet.
What is re-read and what is carried across
base and config_digest come from the target's CURRENT target.exs on
every write, so the two always describe the same file. generated_at is
MINTED on the first write — when no plugin.exs exists yet — and carried
unchanged from the existing manifest on every write after that: it is a fact
about the past, and a recorder rewriting it would erase the only record of
when the project was FIRST RECORDED. For a project the archive generated
weeks earlier, that first-write instant is when a plugin was first
applied, not when the project itself was laid down — generated_at means
"first recorded", not "generated", and no better source exists:
target.exs carries no timestamp of its own, while plugin.exs
requires the key. applied_at is per plugin and is always this instant.
sv_ex_version sits in tension with that argument, and deliberately so:
it is RECOMPUTED on every write, from the running scaffolder's own
Application.spec/2, never carried forward the way generated_at is. It
means "last written by version X", not "the version that generated this
project" — a project laid down under 1.0.0 and later touched by a plugin
applied under 1.2.0 shows sv_ex_version: "1.2.0" in its plugin.exs,
same treatment as applied_at and unlike generated_at.
config_digest is COMPUTED here. target.exs carries no such field and
SvEx.Manifest checks it for format only — its moduledoc states that
nothing produces one until the generator exists. This is that producer, and it
goes through SvEx.Hash rather than a raw sha256 deliberately: a comment
added to target.exs is not a change of intent, and must not read as one
when the update flow compares digests. SvEx.Hash.content_hash/2 already
emits exactly the "sha256:" <> 64 lowercase hex the format requires.
Hashes come from SvEx.Hash, never SvEx.Baseline
Three normalisers in this repository, three jobs. SvEx.Hash treats an
added comment as NOT a change, because comments are user-owned;
SvEx.Baseline treats one as drift, which is right for a generator
baseline and wrong here. SDD 4.1 records Fireside reading one added # note
as "diverged, aborting", and a # credo:disable-for-next-line must never lock
a project out of updates.
The one exception is a file SvEx.Plugin.Apply left conflict-marked:
that file is not the language its name promises, the parser refuses it, and
SvEx.Hash.text_hash/1 hashes it as text instead. The marker is read off
DISK rather than reported by apply, because a second apply short-circuits on
the marker already being there and would report nothing.
The check is for the GENERIC marker prefix — Apply.marker_prefix(""), the
same one mix sv_ex.check scans for — not this entry's own key. A marker
another key left behind still makes the whole file unparseable, and checking
only this entry's key would fall through to content_hash/2 and raise after
every file has already been written, with no manifest to show for it.
Whichever key wrote a marker, its presence anywhere in the file is what
matters, never which key.
This is not a defence against every unparseable file. A target file that was
already broken before apply ran — the user's own syntax error, or a conflict
marker left by their own VCS rather than by this module — still raises
SyntaxError from content_hash/2, naming the file. That project was
already uncompilable; recording could not have fixed it, and doing so is out
of scope here.
Summary
Functions
Pre-flights the reads run/4 needs, so a malformed target.exs or
plugin.exs raises BEFORE SvEx.Plugin.Apply writes a single file.
Records plugin in target_dir's plugin.exs, if it has a target.exs.
Reports whether target_dir declares itself a SvEx project.
Functions
@spec preflight!(Path.t()) :: :ok
Pre-flights the reads run/4 needs, so a malformed target.exs or
plugin.exs raises BEFORE SvEx.Plugin.Apply writes a single file.
A no-op for an untracked target: tracked?/1 is the same predicate run/4
gates on, so an untracked target takes on no new read and no new failure
mode. Reused rather than reimplemented, so the two can never drift.
@spec run(Path.t(), Path.t(), map(), SvEx.Template.names()) :: :ok
Records plugin in target_dir's plugin.exs, if it has a target.exs.
component_dir becomes the entry's {:path, _} origin, made relative because
a manifest naming one machine's checkout is not portable to another machine or
to CI (SDD 4.1 defect 13). The base point is the SCAFFOLDER's own working
directory — where the plugin lives — never the target directory that
stores the manifest; that is the same base point a project's own
target.exs plugins: entry already uses for a path: plugin
(SDD 8.1). A plugin directory outside the working directory stays
absolute and is refused by SvEx.Manifest, which is the correct answer:
it could not be recorded portably.
names is the triple the files were rendered with, and is what resolves each
declared path to the one actually on disk.
Reports whether target_dir declares itself a SvEx project.
ONE definition, read by run/4 to decide and by
mix sv_ex.plugin.apply to report — so the behaviour and the message
about it cannot drift apart.
Raises SvEx.Root.InvalidRootError if target_dir has no mix.exs —
it is not a project directory at all, tracked or otherwise.