# Contributing to `alix`
Thanks for wanting to help. `alix` is small on purpose, so the most useful thing
you can read before writing code is *what it's trying to be* — and what it's
deliberately not. This page is the contributor-facing distillation of the house
rules; [`CLAUDE.md`](CLAUDE.md) is the full version (it's also how the AI
collaborator on this repo is instructed). The testing expectations are in the
[house rules](#house-rules-the-craft-gate) below.
## What `alix` is
It's an AI-augmented spaced-repetition tool: a flashcard core — plain-text deck
files, with image cards too — plus Claude woven in (an ask-Claude tutor, AI deck
generation, and an AI exam that tries to gate progress on understanding, not just
recall). The **library crate (`src/`) is the single source of logic**; the web
server and the CLI are thin consumers.
It's early and changing fast — expect rough edges and breaking changes (the
README says it outright: don't trust it with serious learning yet). What it's
*aiming* at — the "north star" in [`CLAUDE.md`](CLAUDE.md) — is to turn the things
you read into understanding you've actually verified, not just remembered.
The bet is to stay **narrow on purpose** — to do a few things well rather than
many adequately. That only holds if the focus is written down and applied *while
building*, which is what the gate below is for. Contributions are very welcome;
the gate is about whether a change fits, not about you.
## Good first contributions
Bug reports, typo and documentation fixes, a clearer error message, a deck-format
example — these need no proposal and no ceremony. Just open a PR. The fit gate
below is only for *new features*; most contributions never touch it.
## Before you open a PR — the fit gate
The fit gate decides *whether* a change belongs. Apply it **before** writing
code — once something is built, sunk cost quietly biases everyone toward merging
it.
- **Default is no — but I'll always tell you why.** A new feature has to earn
its place; staying small and focused is the whole bet. That's not hostility,
and it's rarely final — "no" usually means "not like this" or "not yet," not
"never."
- **For anything non-trivial, email first.** Get a go/no-go before you build. A
surprise PR for a big feature is the hardest to accept, however good the code:
talking first means your effort lands somewhere it'll actually get merged.
- **Reach is welcome; scope must clear the gate.**
- *Reach* = work that widens access to the **same one job**: install and
onboarding, the web surface, backends, performance, reliability,
docs. Admissible even though it deepens no step of the loop.
- *Scope* = a **new job** — a new card type, a new subsystem, a feature that
sits *beside* the core loop rather than deepening a step of it
(review → understand → verify → retain). Name the step it deepens; if you
can't, it's probably out.
- **Be wary of "it'll bring users."** Almost any feature can claim it, so on its
own it doesn't help us decide — show which step of the loop it deepens instead.
- **Prefer extending an existing concept** (a `%` directive, an answer mode)
over adding a subsystem the user has to learn. Conceptual surface area is the
real cost, not lines of code.
### What `alix` is **not** (the NOT-list)
Negative space defines the focus. `alix` is **not**:
- a migration/compat layer for other SR tools (`import` is cards-only TSV);
- a SaaS with accounts — it's local-first, your plaintext files;
- an open-ended chat wrapper (the tutor, exam, and generator each serve a
card/deck/source; the AI never floats free of the learning loop);
- a notes app / second brain (decks are for drilling, not storing everything);
- a gamified study suite (no streaks/XP/leaderboards for their own sake);
- a content marketplace (you bring or generate decks from *your* sources).
Proposals that move `alix` toward any of these are unlikely to land as-is. If
you think something here actually belongs in `alix`, that's a fair discussion:
email contact@alix.study to raise it.
## Feature requests and PRs
Feature ideas go to contact@alix.study and get triaged onto the maintainer's
roadmap, not into a public backlog. Bug reports use the issue form. PRs come
after a discussed issue or email thread; large or generated PRs without prior
discussion are usually closed unread.
## Development setup
`alix` is a Rust project; everything goes through the **Makefile**.
| `make build` | Compile. |
| `make test` | Run the test suite (the primary gate). |
| `make lint` | `cargo clippy --all-targets`. |
| `make fmt` | Format — **nightly** rustfmt (see below). |
| `make fmt-check` | Verify formatting without writing. |
| `make check` | `lint` + `test` — run before you call work done. |
| `make coverage` | Coverage report (`cargo-llvm-cov`, HTML). |
| `make calibrate` | Real-Claude grader calibration (costed): before touching `grade_*`. |
| `make run ARGS="stats mydeck.txt"` | Run the binary. |
| `make web ARGS="~/decks-test --lan"` | Web frontend (a scoped root). |
| `make book` | Serve the mdBook manual live. |
| `make e2e` | Playwright smoke suite for the alix web clients (Node; needs a browser download the first time). |
CI runs the same gates on every PR: `fmt` (nightly rustfmt), `check` (clippy +
tests, with `-Dwarnings`), `core` (the lean no-default-features build),
`bridge` (the mobile bridge crate's host tests), `mobile-e2e` (the mobile app's
Dart tests plus a full-app integration run in a headless Linux window, against
the real embedded core), `e2e` (Playwright, browser-driven, blocking), and an
informational `coverage` job. Two scheduled drift detectors run off the PR
path: the backend CLI flag-drift smoke test and a weekly mobile toolchain
build (`make frb-check` + an APK build against current stable Flutter).
The library crate is feature-gated behind `full` (on by default), which pulls in
the AI backends and the web server. `make build-core` builds the lean core that a
future mobile client would consume, and CI enforces it with its own `core` job.
A core module must never reach into a gated one: if it must, the thing it reaches
for belongs in core, not behind `full`.
### Formatting is nightly-only
`rustfmt.toml` uses nightly-only options, so **format with `make fmt`**
(`cargo +nightly fmt`). Do **not** run plain `cargo fmt` (stable) — it can't
apply the config and produces a large bogus diff. The tree has some pre-existing
drift, so don't reformat unrelated files; keep your diff to what you touched.
## House rules (the craft gate)
The fit gate decides *whether*; the craft gate decides *how well*. Match the
surrounding code; when in doubt, mirror it. The essentials:
- **Behavior goes in the library**, not a frontend, so the web and CLI surfaces
share it. Frontends only consume.
- **Test-first for library logic** — write the test before (or alongside) new
`src/` behavior, above all the AI plumbing's error paths. New behavior ships
with a test written first; a bug fix ships with a fails-first regression test.
Thin frontend glue is the exception.
- **`make check` and clippy must be green** before a change is done. CI denies
warnings — fix the cause, don't silence it. If a lint is a genuine false
positive, suppress it item-scoped with a one-line reason and prefer
`#[expect(…)]` over `#[allow(…)]`.
- **No `unwrap` / `expect` / `panic!` in library paths** — use `?`, `.or(…)`,
`unwrap_or_default()`, etc. `.expect("…")` only when the line directly above
guarantees the invariant. `.unwrap()` in `#[cfg(test)]` code is fine.
- **Errors come in two layers.** Domain modules expose a `thiserror` enum;
workflow code returns `anyhow::Result` with `bail!` + `.context(…)`. Messages
are **lowercase, no trailing period** — `bail!("the deck declares no \`%
source:\`")`.
- **Don't break card identity.** A card's id is `XxHash64(deck file name + its
back lines)` — or, for cloze cards, its `hash_lines` with the `{{ }}`
delimiters stripped, plus a per-hole index (see `Card::id` in `src/card.rs`).
It ignores the front, notes, and comments on purpose. A careless change to the
parser, `hash_lines`, or deck rewriting silently **wipes users' review
progress** — preserve it.
- **Keep the UI calm.** Both frontends must stay distraction-free: only what the
user needs right now. Default to *less* — cut a readout before adding one,
tuck rare controls behind a menu. A noisy UI diff is treated like a failing
test.
- **No new dependency without a one-line reason** in the PR/commit. Reach for
std or an existing dep first.
- **Keep the two docs in sync.** `docs/book/` is the **reference and manual**
(deck format, every directive and flag, all features), updated on every
user-facing change. `README.md` is the **landing page** for GitHub and
crates.io (pitch, install, quickstart, an inline deck example, a command table,
a capability list), self-contained but not a reference: change it only for a
new/renamed top-level command or a headline capability; finer detail is
book-only. README book-links are relative (`docs/book/src/…md`) so they work on
GitHub and offline; only the top banner uses hosted URLs.
- **User-facing changes get a `CHANGELOG.md` entry** under `## [Unreleased]`
(Added / Changed / Fixed). While we're pre-1.0, **break freely** — change
renamed/removed flags and directives outright and record a **Breaking** note
under Changed; no back-compat shims.
- **Prompt changes** (anything touching `grade_*`) ship with a `make calibrate` run
and the calibration delta noted — that's how "mastered" stays honest.
## Testing — two layers
`alix` is unusual: it's *built by* an LLM, and it *calls* one at runtime (Claude
today). That splits testing into two jobs, and it helps to know which you're in:
1. **Deterministic correctness** — does the code do the right thing given known
inputs? Ordinary QA: unit + integration tests, clippy, fmt. The AI code paths
are covered by a **fake model CLI** — a test helper that stands in for the real
LLM binary and emits canned output — so they run without ever calling a live
model. This is the **blocking gate** (`make check`, enforced by CI).
2. **AI-behaviour quality** — do the *prompts* produce good output: fair grades,
coherent traces, a non-lenient exam? The fake CLI can't see this, so it needs
the **calibration harness** (`make calibrate`), which runs the real prompts against a live
model and scores them. It's non-deterministic and costs money, so it lives
**outside** the blocking gate — run it before any change to a `grade_*` prompt,
and note the calibration delta.
Rule of thumb: a code change must pass layer 1 (`make check`); a *prompt* change
must also clear layer 2 (`make calibrate`).
### Backend flag-drift checks
Each AI backend (`claude`, `gemini`, `codex`, `copilot`) hardcodes the CLI
flags it relies on in `required_help_flags()`. A nightly CI job
(`.github/workflows/backend-drift.yml`) installs each CLI without login and
checks that every flag still appears in `--help`. If a CLI update renames or
removes a flag the workflow fails and surfaces it before any user hits it.
To run the drift check locally (needs each CLI installed, no login required):
```
cargo test --test backend_help -- --ignored --nocapture
```
To do a full end-to-end probe — a real tiny request through each backend — use
`make check-backends` (needs each CLI installed and your own logins configured;
this is a maintainer-only check, not part of the normal contributor flow).
## Commits & pull requests
- **Don't break the build, and run `make check` before pushing.**
- We keep a **linear history** — rebase your branch on `main` and avoid merge
commits; a clean fast-forward is the goal.
- Fill in the PR template: which issue or email thread it follows up on, what
it changes, how you tested it. If you can't answer the first one, stop and
open a discussion before going further.
- Commits made with AI assistance carry a `Co-Authored-By` trailer; keep that if
you use it.
## License
By contributing, you agree your work is dual-licensed under
[MIT](LICENSE-MIT) and [Apache-2.0](LICENSE-APACHE), as described in the
README's Contribution clause. Unless you state otherwise, any contribution you
intentionally submit for inclusion is licensed as above, with no additional
terms.