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

## The hex dependency source

`mix svelixir.new` splices `{:svelixir, "~> 0.1", only: [:dev], runtime: false}`
into the generated project by default, and accepts `--svelixir-path` to splice a
**path** dependency instead. The default resolves: the package is published, and
`~> 0.1` admits every 0.x release.

Both end-to-end tests still pass `--svelixir-path`, so the `{:hex, requirement}`
branch of the generator's `dep_line/1` stays uncovered end to end. That is
deliberate rather than an oversight: the honest test needs the network, and as an
untagged test it would turn `bin/check` red for any offline developer, for reasons
unrelated to their change. What publishing changed is the failure mode — the
branch is no longer guaranteed to fail, so it is untested rather than known-broken.

## Installation

```elixir
def deps do
  [
    {:svelixir, "~> 0.1", 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.

To work against an unreleased checkout, depend on it by path instead — this is
what `--svelixir-path` generates:

```elixir
{:svelixir, path: "../svelixir", only: [:dev], runtime: false}
```

Documentation is generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published at <https://hexdocs.pm/svelixir>. The vendored libraries in
`vendor/` compile into this application but are filtered out of the docs by
`filter_modules` — their API is not ours to document.

## Acknowledgements

### Prior art

Svelixir's central idea — that a generated project should stay **upgradable**,
which means telling a file the generator wrote from a file a human has since
edited — is not original to it.

- **[Fireside](https://github.com/ibarakaiev/fireside)** by Ihor Barakaiev (MIT)
  is where `fireside.exs` gets both its name and its shape. Fireside imports code
  components into an _existing_ project together with their dependencies and can
  upgrade them later, which is the same problem this project solves for a whole
  scaffold. The `svelixir.exs` / `fireside.exs` split — intent in one file, what
  was actually written in the other — is Fireside's insight applied to a
  generator. The name is kept deliberately rather than rebranded: it should be
  obvious where the idea came from.
- **[Igniter](https://github.com/ash-project/igniter)** by Zach Daniel and the
  Ash team (MIT) is the code-generation and project-patching framework Fireside
  is built on, and the reference for what patching an existing project well looks
  like — composable tasks over a parsed project rather than string surgery.
  Svelixir does not depend on Igniter; the structural-placement modules
  (`Svelixir.MixExs`, `Svelixir.ConfigExs`, `Svelixir.ApplicationEx`) solve a
  deliberately narrower problem against a vendored Sourceror. Igniter is the
  right tool for the general case.
- **[Sourceror](https://github.com/doorgan/sourceror)** by doorgan (Apache-2.0)
  is what makes structural placement possible at all, and it is vendored here
  (see below). `get_range/1` plus `patch_string/2` edits a range of
  an existing file and leaves every other byte untouched — as opposed to
  reprinting the AST, which reformats code the user never asked to change. Every
  `mix.exs`, `config/*.exs` and `application.ex` edit this project makes goes
  through it.

Fireside depends on Igniter, and Igniter depends on Sourceror. Svelixir sits at
the end of that chain and owes all three.

### Vendored libraries

Four libraries are compiled directly into this application from `vendor/` rather
than resolved as Hex dependencies, so that the generated archive perimeter can
carry zero Hex requirements. Each directory holds the upstream `lib/` tree and
the upstream licence file, unmodified except for two recorded one-line patches.
Full provenance, checksums and patch notes are in
[`vendor/README.md`](vendor/README.md).

| Library                                                     | Version | Licence    | Author                                | Used for                                        |
| ----------------------------------------------------------- | ------- | ---------- | ------------------------------------- | ----------------------------------------------- |
| [`sourceror`](https://github.com/doorgan/sourceror)         | 1.12.2  | Apache-2.0 | doorgan                               | structural placement — parse and patch by range |
| [`vex`](https://github.com/CargoSense/vex)                  | 0.9.2   | MIT        | Bruce Williams                        | validating `svelixir.exs` sections              |
| [`typedstruct`](https://github.com/saleyn/typedstruct)      | 0.5.4   | MIT        | Jean-Philippe Cugnet and contributors | the config and manifest structs                 |
| [`simple_enum`](https://github.com/ImNotAVirus/simple_enum) | 1.0.0   | MIT        | DarkyZ aka NotAVirus                  | enumerated section values                       |

These are other people's work carrying other people's licences. Nothing in
`vendor/` is formatted, linted or documented by this project's gates, and the
vendored modules are filtered out of the published docs — their API is theirs,
not ours.

### Generated projects

Generated projects get their UI components from
[sv5ui](https://sv5ui.vercel.app), a skin over the
[bits-ui](https://bits-ui.com) headless components, with the
[sveltic](https://sv5ui.vercel.app/templates) theme template. The project
skeleton itself comes from [Phoenix](https://www.phoenixframework.org)'s own
`phx.new`, which Svelixir composes on top of rather than replaces.
