Svelixir
Copy MarkdownScaffolder for Elixir/Phoenix projects with a Svelte 5 UI layer.
Svelixir generates a project and then keeps it upgradable. svelixir.exs records
what the project asked for; fireside.exs records what was actually written, and
with which content hashes, so a later regeneration can tell an untouched file
from one the developer has edited. Every reader here works against an explicit
project root rather than the current working directory — Svelixir.Target
exists for exactly that reason, and Svelixir.BoundaryGuard enforces it
mechanically over lib/.
The two perimeters
The work is split across two repositories, and the split is structural rather than organisational.
| perimeter | repository | what it is |
|---|---|---|
| 1 — the package | wimwian-org/svelixir (this one) | the library a generated project depends on: Svelixir.Target, Svelixir.Config, Svelixir.Manifest, Svelixir.Exs, Svelixir.Hash, Svelixir.Baseline |
| 2 — the archive | wimwian-org/svelixir_new | the mix svelixir.new Mix archive, checked out at priv/svelixir_new |
Perimeter 2 is its own project because a Mix archive has to ship with zero hex
requirements: it is unpacked straight onto the developer's code path, where a
dependency of its own would be unresolvable. A separate Mix project is what lets
mix archive.build produce that, and it is also what keeps the two code paths
disjoint — neither perimeter's ebin is ever on the other's, so a change to one
cannot quietly compile against the other.
The two meet exactly once: the archive writes a svelixir.exs and this package
reads it back with Svelixir.Config.read!/1. There is no shared encoder and no
digest pinned in two files.
priv/svelixir_new is gitignored here. Clone it alongside before running the
cross-perimeter tests:
git clone git@github.com:wimwian-org/svelixir_new.git priv/svelixir_new
Everything degrades visibly without it rather than silently: test/test_helper.exs
excludes the :archive tag when priv/svelixir_new/mix.exs is absent,
bin/check skips that perimeter's gates, and CI annotates the run with a
::warning naming what did not run.
Running the checks
bin/check is the commit gate — format, credo --strict, coveralls at
minimum_coverage: 100, doctor and dialyzer, in both perimeters:
bin/check
It does not run the tagged tests, and a green bin/check alone is therefore
not a green suite. Three tags are excluded by default, and they are disjoint by
dependency: no test carries more than one, because ExUnit's include filter
beats its exclude filter per tag, so a doubly-tagged test would run in an
environment that cannot support it.
| tag | what it needs | how it runs |
|---|---|---|
:toolchain | the external toolchain only (mix new, phx_new) — safe anywhere | mix test --include toolchain |
:determinism | a subprocess probe re-run under a reversed atom-interning order | mix test --include determinism |
:archive | priv/svelixir_new, to build and install the archive | runs in a plain mix test when the perimeter is present; excluded when it is not |
The full local sequence:
bin/check
mix test --include toolchain --include determinism
(cd priv/svelixir_new && mix test --include toolchain)
Svelixir.TagCoverageTest asserts mechanically that every tag the suite excludes
is named by an --include on an unguarded CI step, so a newly excluded
tag cannot go unnamed by every workflow step. It reads ci.yml as text, so what
it proves is that a step exists which would run the tag — never that the step
ran. :archive is exempted from the unguarded half by name: its CI step is
guarded on the perimeter 2 checkout, which reads a private repository through the
SVELIXIR_NEW_TOKEN secret and 404s whenever that secret is absent, expired or
withheld — so :archive coverage in CI is contingent on a credential, and the
::warning in the run summary is what makes its absence visible. That failure
mode is the one this project calls F4: tests that silently did not run.
Known gap: the hex dependency source
mix svelixir.new accepts --svelixir-path, which splices a path dependency
into the generated project. Without it the generated mix.exs asks for
{:svelixir, "~> 0.1", only: [:dev], runtime: false} — and svelixir is not
published to Hex yet, so that mode fails inside mix deps.get, after the
project directory and its patched mix.exs already exist. The result is a
half-built project, reported through a generic deps.get failure that names
neither the unpublished package nor the partial directory.
Both end-to-end tests therefore pass --svelixir-path. SDD step 2's done-when
("generates a project that resolves the hex package") is met in mechanism but not
against Hex itself, and the {:hex, requirement} branch of the generator's
dep_line/1 stays uncovered end to end. Publishing svelixir 0.1.0 closes it;
a local hex mirror would too.
This is recorded rather than tested, deliberately. The honest test needs the
network — hex has to be consulted in order to fail — so as an untagged test it
would turn bin/check red for any offline developer, for reasons unrelated to
their change. Turning the generic failure into a message that names the package
is production work in perimeter 2.
Installation
Not published to Hex yet. Until then, depend on it by path:
def deps do
[
{:svelixir, path: "../svelixir", only: [:dev], runtime: false}
]
endonly: [:dev] and runtime: false are deliberate rather than incidental:
Svelixir is a build-time scaffolder, and nothing it defines is needed once the
generated application is running.
Documentation is generated with ExDoc and, once published, will be available at https://hexdocs.pm/svelixir.