# Svelixir

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.

| 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:

```sh
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:

```sh
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:

```sh
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:

```elixir
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](https://github.com/elixir-lang/ex_doc)
and, once published, will be available at <https://hexdocs.pm/svelixir>.
