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

perimeterrepositorywhat it is
1 — the packagewimwian-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 archivewimwian-org/svelixir_newthe 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.

tagwhat it needshow it runs
:toolchainthe external toolchain only (mix new, phx_new) — safe anywheremix test --include toolchain
:determinisma subprocess probe re-run under a reversed atom-interning ordermix test --include determinism
:archivepriv/svelixir_new, to build and install the archiveruns 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}
  ]
end

only: [: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.