The alix book
alix is a plain-text spaced-repetition tool built around understanding, not
just recall. A fast flashcard core does the drilling, and AI fills in where it
helps: a tutor on any card, decks and workspaces generated from your own
material, and an exam that grades your understanding against the source.
This book is the manual and reference: each feature is expanded with worked examples and the reasoning behind it, and the Directives reference is the quick index to every directive.
Start with Why alix exists for the motivation, or jump
straight to Getting started.
1 · Why alix exists
Most study tools are built to help you remember. alix is built to help you
understand, and the gap between those two is the reason it exists.
Spaced repetition is one of the few genuinely proven ideas in learning: review a fact just as you’re about to forget it, and it sticks. Tools like Anki turned that into a daily habit for millions. But they share a blind spot. They optimize the retention of isolated facts, and they quietly accept a failure mode every serious user eventually feels: you can answer every card correctly and still not understand the thing. Recognizing an answer is not the same skill as being able to derive it, explain it, or see why it follows. You can have a deck at 100% and a head full of trivia you can’t actually use.
alix starts from that gap. It keeps the proven core (drilling facts on a spaced
schedule) but treats it as only the first step: the part that loads the raw
material. On top of it sit two things ordinary flashcards can’t do.
Traces teach you to follow a mechanism, not just recall a fact. A trace is a path along a real chain of reasoning through a real source (a data flow through code, the steps of a proof, the clauses of a contract) where at each step you predict what comes next before it’s revealed. It trains the thing experts actually have: not a bag of facts, but the chain of because this, therefore that.
The exam checks that you understood, not that you memorized. Once you’ve
drilled a topic, alix examines you with fresh questions generated from the
source material itself, never from your cards, because grading you on your own
cards is circular and trivially passable. An AI examiner reads your answers
against the source and decides whether you’ve actually got it. Only then does the
topic count as mastered, and only then does it unlock what depends on it.
That last word is load-bearing. alix only calls something mastered when your
understanding has been tested against the ground truth and held up. A green
checkmark you didn’t earn is worse than none (it’s false confidence) so the
examiner is built to be a real examiner, not a flatterer.
Who this is for
alix asks more of you than a flashcard app, and gives more back. It removes the
tedious part (an AI can generate decks, build traces, and lay out a whole
curriculum from sources you point it at) but the work it asks of you is
harder: predicting, explaining, deriving, being examined. It’s a power tool for
people who want to understand something difficult on purpose and want proof that
they do: a new codebase, a field, a hard paper. If you mainly want to cram names
and dates, a simpler tool will serve you better.
The bet
There’s a wager underneath all of this. As AI gets better at holding and
retrieving facts, the scarce thing for a human mind shifts: away from
retention, toward understanding and judgment. The facts you can always look
up. What you can’t outsource is the structure in your own head: reasoning through
a problem, knowing when an answer is wrong, moving fast because you genuinely
grasp the terrain. alix is a tool for deliberately building that structure, and
for using AI not to do your thinking, but to teach and to test it.
The rest of this book is how.
2 · Getting started
Install
alix is a single Rust binary. The shipped paths, no Rust toolchain
required for the first:
curl -sSf https://alix.study/install.sh | sh # prebuilt release binary
cargo install alix # from crates.io (needs rustup.rs)
The installer first resolves one release tag, then fetches that release’s archive and SHA-256 record and verifies the archive before extracting it. To pin the install to a particular release, use:
curl -sSf https://alix.study/install.sh | ALIX_VERSION=v0.8.0 sh
Verification uses sha256sum on Linux or shasum -a 256 on macOS. The
installer stops if neither exists. ALIX_INSTALL_UNVERIFIED=1 is an explicit
emergency override, but it disables the corruption and asset-mix-up check and
is not the recommended install path. Building from a checkout works too (git clone, then make install).
Any of those puts alix on your PATH. Check it:
alix --help
The flashcard core (reviewing, scheduling, every answer mode, browse, and the web app) runs with nothing else installed: no accounts, no network. The AI features (deck generation, the exam, traces, workspace generation, and the in-session tutor) shell out to a supported model CLI, Claude Code by default; the Gemini, Codex, and Copilot CLIs are also supported. Install at least one and authenticate with it. See chapter 16 for how to switch backends. You can use the entire core without ever touching the AI layer.
Your first deck
On a first run (no decks directory yet), alix creates it, empty. The web
app’s Tutorial (in the ☰ menu on the deck picker, and as a button when the
picker finds no decks) opens a short tutorial: nine cards, walked one at a
time, each teaching one thing by having you do it, from grading an answer to
showing a cited source. The tutorial is never listed among your decks and
never copied into your decks directory; it and its progress live in a folder
of their own (see The tutorial), and
opening it again continues where you stopped.
A deck is a plain .md file. A card is a ## line (the question) with its answer
on the plain lines beneath it:
## What does SRS stand for?
Spaced repetition system.
> [!NOTE]
> It schedules each card just before you'd forget it.
## Which scheduler does alix use?
FSRS, which predicts when you're about to forget each card.
Save it as srs.md in your decks directory (~/decks by default). A
blockquote whose first line is an alert badge (> [!NOTE]) is a note,
shown after you answer; any other blockquote is a quotation that belongs to
the answer. Initialize a file you wrote by hand once:
alix deck init ~/decks/srs.md
Initialization assigns the stable deck and card IDs that preserve review
history. It also tells alix that this Markdown file is a deck; other .md
documents in the same folder remain ordinary files.
Review it
alix
alix opens the web app (printing its URL); pick srs.md there and Learn it.
The question shows in the browser; you recall the answer, press a key to reveal
it, then grade yourself: failed (you missed it), partly (got the gist but
stumbled), or passed. Your grade moves the card along its schedule, so cards
you know come back rarely and cards you miss come back soon. That self-graded
reveal is flip mode, the default; later chapters cover the modes that make you
type the answer, pick from choices, or reveal it line by line.
When nothing is due, there’s nothing to review; come back when cards mature.
The deck picker
That page alix opens is the picker, over your decks directory (~/decks by
default; change it with decks_dir in the config). It groups your decks into
Workspaces, Recent, and Folders and is driven by Vim-style keys (j/k to move,
Enter to open, / to filter by name). Every review starts here; there’s no
direct deck launch. This is what the desktop launcher opens. Focus a deck and
press Browse to read through its cards with no grading or scheduling.
The everyday commands
alix stats srs.md # a progress overview
alix list srs.md # every card with its per-depth schedule and due time
alix doctor srs.md # lint the deck (syntax errors, duplicate cards)
alix reset srs.md # clear stored progress (also --card / --all)
A session is one deck; review them one at a time. From here the book goes deep: the next chapter is the deck format in full, then reveal & session depths and scheduling.
3 · The deck format
A deck is a plain-text Markdown file. You can write one in any editor with no tooling, read it back at a glance, and because it’s real Markdown, it renders sensibly anywhere else too: a preview pane, your file host, GitHub.
When you write a deck by hand, initialize it once before it appears in the picker:
alix deck init ~/decks/my-deck.md
The command adds stable deck and card IDs without rewriting the authored
content. A valid id: deck-<token> in the opening frontmatter marks the file as
an initialized deck. The deck- prefix on the value is what carries the
meaning: it is how alix tells its own decks apart, and the same prefixed string
travels everywhere the id appears, in a frontmatter key, a card marker, a
filename, a prerequisite reference, or an error message. Markdown without that
prefixed id is never listed or modified, so ordinary documents with ##
headings can sit in a decks folder untouched. Generated, imported, and received
decks are initialized when they are created; the tutorial ships initialized.
Choosing a card shape
Read the material before choosing its card shape. The shared guide below names the useful choices and distinguishes structural matches from judgement calls. The sections after it show the exact syntax for each shape.
Which card shape suits which material. Read the material first, then pick the shape; do not pick a shape and bend the material into it.
Some rows are structural: the material has a property the shape exploits, and any other shape wastes it. Some are judgement: more than one shape is defensible and the choice is yours. The difference is marked, because a rule that claims uniform authority gets followed badly exactly where thought was needed.
| material | shape | kind | why |
|---|---|---|---|
| Paired items: a word and its meaning, a term and its definition, a symbol and its name. | A card table: a GitHub pipe table with <!-- cards --> on the line directly below it, no blank line between, one row per pair, columns front, back, and an optional note. | structural | One row per pair, and each row’s Recognize options come from its own column, so the wrong answers are real siblings and cost no AI call. Prose wastes both. |
| Ordered steps that must be reproduced in order: a recipe, an algorithm, a procedure, a verse. | reveal: line, with one step per answer line. | structural | Order is graded, and the answer uncovers one line at a time so recall is stepwise rather than all-or-nothing. A flip card cannot test order at all. |
| An answer that cannot be typed: a diagram, a circuit, a glyph, notation. | input: draw | structural | The learner sketches and self-grades against the reveal. Typing a diagram is not a check, it is a workaround. |
| A statement turning on one term, where the sentence around it is the cue. | Cloze: write the whole sentence on the ## line and hide the span with <!-- blank: span hidden="..." --> on its own line directly below it, above any note. A cloze card has no answer lines and no ---. | judgement | The context does the cueing, so recall is anchored where it will be used. If nothing in the sentence is a natural target, this is a plain card wearing a disguise. |
| A fact whose common confusions are known and nameable. | Authored choice: a task list with one - [x] and one or more - [ ], closed by <!-- choices: single --> on the line below it, ideally two or three (or choices: single in frontmatter, once for the deck). | judgement | The distractors are the teaching. Write them only if you can say what mistaken belief each one represents; if you cannot, the shape is doing nothing. |
| Several items that share a property, out of a set where the rest do not: which of these are X, which statements are false. | Select-all: a task list with a - [x] on every correct option and a - [ ] on every other, closed by <!-- choices: multiple --> on the line below it. | judgement | Only the exact set passes, so every option is judged on its own; a single-answer card lets the learner stop at the first match. Use it when several answers are genuinely correct, not to pad a one-answer question. |
| A term that must be recalled from either side: vocabulary, symbols, names. | direction: both | judgement | One authoring act, two cards. Reach for it when both directions are genuinely useful, not by default: it doubles the review load. |
| Anything else: a definition, an explanation, a cause, a comparison. | A plain card: ## front, answer lines below. | judgement | The default, and not a failure. Most material has no structure to exploit, and a plain card drilled well beats a clever shape drilled badly. |
Rules that hold whatever the shape:
- Every card except a cloze card needs at least one answer line.
- Most cards deserve a
> [!NOTE]note: an example, a caveat, a mnemonic, or why it matters. Never a restatement of the answer. The badge line is required; a blockquote without one is a quotation and belongs to the answer. A card table is the exception: its note is the note COLUMN, and a blockquote below a table does not parse at all. - A choice card’s note names the option’s claim or mistaken premise, never its number, letter, or screen position. Options shuffle between appearances.
- Every
<!-- ... -->line trails its card: it goes directly below the card’s last content line, with no blank line between them, and never above an answer line, an option list, or a table. Ablank:,cover:orcrop:line is the exception: it sits directly below the block it marks, above any note. - An invocation binds the block directly above it, so it sits on the line
immediately below the last option or row.
<!-- cards -->takes card directives after it, anat:locator or a reveal mode, but never a blockquote: a card table’s note is its note column.<!-- choices: single -->and<!-- choices: multiple -->take a> [!NOTE]note after them. - One idea per card. Split compound facts rather than nesting them.
- No two cards may test the same fact. Vary what is asked; do not rephrase.
review: and sampling: are not shapes. They modify how an existing
deck is served and are documented with the other directives.
Cards
A card starts with ## at column 0, the front (the question). The lines
beneath it are the answer (the back), written plainly, and may span several
lines:
## What is the capital of France?
Paris.
## Name the three additive primary colors.
Red
Green
Blue
<!-- reveal: line -->
A physical newline inside an ordinary flip answer is a Markdown soft wrap: the
adult and mobile clients display it as a space, so you can wrap long source
lines for editing without creating visual gaps on the card. Add
<!-- reveal: line --> when the lines themselves are the learning sequence;
line reveal and line typing preserve them individually.
Inline formatting
Card fronts, answer lines, and note prose support **bold**, *italic* or
_italic_, ~~strikethrough~~ (the double-tilde pair exactly; a single or
triple tilde run stays ordinary text), and inline `code`. Inline code is
verbatim, so
`**literal**` displays the asterisks instead of bold text. A long
inline code run may wrap after its punctuation, so Ledger::get_public_key
can end one line at Ledger:: and continue with get_public_key; a word
inside the run never splits unless it is wider than the whole line.
Formatting has two projections: styled display and plain content. Grading uses
the plain content, so type Paris, not **Paris**. To keep emphasis markers
literal, escape them with backslashes such as 2\*3\*4, or wrap the text in
inline code such as `2*3*4`. Run alix doctor <deck> to find card text
that will render as emphasis.
Inline escaping follows the CommonMark rule: a backslash before any ASCII punctuation character yields that literal character, and a backslash before anything else (letters, digits, spaces) stays a literal backslash.
Links
A complete [label](destination) displays as just the label, styled as a
link; the brackets, parentheses, and destination never reach the screen,
and the deck file keeps what you wrote. Destinations are inert on study
surfaces: a card is for recalling, not browsing. Every destination form
displays the same way, including relative paths and #anchor fragments
(alix has no heading anchors, deliberately: a card’s identity survives
editing its text, so nothing may address a card by its words).
Emphasis works inside a label and across a whole link. Grading compares
the label text, so type see the docs, not the syntax. An incomplete
pattern such as [brackets] alone stays ordinary prose, an escaped
\[label](d) stays literal, and inline code or a fence keeps the whole
syntax verbatim. Autolinks (<https://...>) are covered under “HTML in
a deck”.
A link-definition line ([label]: destination) in an answer is deck
metadata, not content: it never displays and is never graded, and a card
whose whole answer is definitions fails loud as answerless. Reference
links resolve against the deck’s own definitions and render like inline
links, in all three GFM forms: [text][label], [text][], and bare
[text]. A footnote definition ([^label]: at the start of a line) is
the one bracket-definition spelling a deck rejects outright: footnotes
are not supported, and the line fails loud rather than silently becoming
prose or metadata; inline [^1] (and a regex class like [^abc]) stays
ordinary text. Labels match case-insensitively with interior whitespace
collapsed, a label never resolves across deck boundaries, and a
reference whose label is undefined stays ordinary prose.
LaTeX math
Use $...$ for a formula inside prose:
## Why does $a^2 + b^2 = c^2$ describe a right triangle?
It is the Pythagorean theorem.
Use $$...$$ for display math, either on one whole logical line or as a
block: a line holding only $$ opens the block, the next such line closes
it, and everything between is one formula. A ```math fence carries
its body the same way. All three render identically:
## What is the Gaussian integral?
$$\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}$$
## State the quadratic formula.
$$
x = \frac{-b \pm \sqrt{b^2 - 4ac}}
{2a}
$$
A card that opens a $$ block without closing it fails to load, with an
error naming the opener’s line. A closed pair in section or context prose
renders as the same display formula. An unmatched opener on those surfaces
stays ordinary text and does not consume the lines after it.
An opening dollar must touch the first formula character, and a closing dollar
must touch the last one. A closing inline $ cannot be followed by a digit, so
$5 and $10 stays literal currency. GitHub’s backtick-anchored spelling also
works: $`x^2`$ renders exactly like $x^2$, with the backtick-quoted body
as the verbatim formula. Escape a literal dollar as \$. Unmatched
dollars and $$...$$ surrounded by prose also stay literal. Dollars inside
inline code or fenced code (other than a math fence) are always verbatim.
Graphical clients render recognized math with the shared RaTeX renderer. If the
delimiters are valid but the LaTeX is malformed, the card still loads and shows
the source with a visible “math could not render” message. alix doctor <deck>
reports the card line, formula snippet, and renderer error.
Grading uses the content between the delimiters, so type x^2, not $x^2$.
Adding or removing math delimiters does not change a card token or stale its
cached augmentations. Generated output that otherwise parses as a deck is
checked before placement; malformed math cannot replace an existing deck.
A ## only starts a card at column 0 and outside a code fence. A ## that is
indented, or sits inside a fenced block, is ordinary answer content, so a Markdown
heading in a sample, a shell comment, or a Dockerfile line needs no escaping:
## What does this script print?
```bash
echo hi
## this line is just part of the answer, inside the fence
```
A fence closes only on a delimiter of its own character at least as long as its opener (the CommonMark closing-length rule); that is what lets the four-backtick fence above contain the three-backtick one.
Multi-line fronts
When the question itself spans more than one line, a --- divider marks where it
ends and the answer begins:
## What does `lo` control in this signature?
def bisect_right(a, x, lo=0, hi=None)
---
The lowest index the search considers; entries below `lo` are ignored.
Here the front is two lines (the prose question plus the code it’s asking about),
and without the --- alix couldn’t tell where the question stops and the answer
starts. (A one-line question needs no divider: the answer just follows on the next
line, as in the cards above.)
The divider’s shape is strict, so a stray break fails loudly instead of silently
joining a card: a front divider sits directly above its answer, with a blank line
(or the card’s own heading) above it, once per card. A break is three or more of
one marker, -, *, or _, with spaces or tabs allowed between them, so
---, ----, - - -, ***, and ___ are one construct: the position decides
what a break means, never how you spelled it. Dividing a card’s front from the
answer below it is the only meaning a break has, so a break anywhere else is a
parse error, a break
alone between blank lines included: alix has no standalone horizontal rule, and
the shape is reserved. A <!-- plain --> on the line below a break keeps it
literal whatever its spelling, and so does a backslash on the line itself (see
Escaping): \---, \----, \- - -, \***, and \___ are all
literal text.
Choice cards (task lists)
A bare GitHub task list is a literal checklist: mappings are opt-in. Name the mapping on the line below the list to make it a choice card:
## Which number is prime?
- [ ] 4
- [x] 5
- [ ] 6
<!-- choices: single -->
Under choices: single the one [x] item is the correct answer. Alix shows
only that answer at Recall and expects it at Reconstruct; the [ ] items are
distractors shown with it at Recognize. Every option is used, so the card
needs no AI choices augmentation and is skipped by that augment target. The
Rust core shuffles the options: their order stays fixed while one question is
on screen, then receives a fresh seed when the card reappears or a new study
session starts. As with any shuffle, two appearances can still produce the
same order by chance.
choices: single demands exactly one checked item and at least one unchecked
item; any other shape fails loudly. Use -, *, or + bullets, with [x]
or [X] for the answer. Task lists inside notes or a card’s front before the
--- divider render as static checkboxes rather than interactive choices.
choices: multiple is select-all-that-apply: every [x] is a correct option
and the reviewer picks all of them. A choices: multiple list that checks
exactly one item is legal: a one-answer select-all is a fair question when
the learner must discover how many options are correct.
A deck built of choice cards declares the mapping once in frontmatter instead
of once per card: choices: single (or choices: multiple). A
per-card invocation overrides the deck default, and <!-- plain --> on the
line below one task list keeps that one literal.
Card tables
Flat material at scale (a vocabulary list, countries and capitals, dates) can
be one Markdown pipe table instead of a ## block per fact. A bare pipe
table renders as a real aligned table: the delimiter row’s alignment colons
set each column’s alignment, short rows pad with empty cells, and long rows
truncate to the header width. In an answer it is one block, like a quotation:
under <!-- reveal: line --> it takes a single reveal rather than arriving a
pipe line at a time, and Reconstruct never asks you to type pipe syntax, so a
card whose whole answer is a table asks you to explain it instead. It never
becomes cards on its own;
<!-- cards --> on the line below maps it, and <!-- plain -->
below one table keeps it literal. A table that declares neither is
still rendered plain, and alix doctor flags it until you write
<!-- cards --> or <!-- plain --> below it: the two meanings are too
far apart to leave to accident, so the file says which one you meant.
Each row is a card: first column front,
second column back, optional third column note. The header row names the
columns for whoever reads the file; it is never displayed and never tested:
| word | meaning | note |
|-----------|-----------|----------------------|
| purported | angeblich | often in legal prose |
| feasible | machbar | |
<!-- cards -->
The table must start at column 0 with a header row and a delimiter row
(alignment colons are fine), exactly like GitHub renders it; every line starts
and ends with |. Inside cells, inline formatting and math work as in any
card text. A table inside a fenced code block stays literal text.
Give a table a title by putting a ## heading directly above it, with
nothing between them but blank lines:
## Verbs of arguing
| English | German | usage |
|-----------|-------------|----------------------|
| to refute | widerlegen | eine These widerlegen |
The heading is the question every row asks: the review shows it, with the
row’s first cell beneath it as a bullet (• to refute), so a lowercase cell
still reads naturally. Under direction: both or direction: reverse the
reversed half asks the same heading with the second column’s cell as its
bullet, so write a heading that works in every direction you study. The
heading is a title only when its body is empty, so a heading with an answer
under it is an ordinary card that happens to be followed by a table. The table’s directives, including its own ID, trail the
table on their own lines below it; a directive on the heading line is a parse
error.
At Recognize, a table card’s wrong options are drawn from its own column: the
other rows’ answers are the distractors, so a table needs no AI choices
augmentation and no authored options (though both take precedence if present).
A row only gets a pick when its column offers at least three other distinct
values; smaller tables stay reviewable at the other depths.
That column sampling is on by default. Turn it off for a table whose rows are
not interchangeable (a mixed list, a table of one-off facts) with
<!-- sampling: off --> among its directive comments, or set sampling: off
in the frontmatter to make that the deck’s default and re-enable single tables
with <!-- sampling: on -->. A table with sampling off and no other option
source is simply not offered at Recognize, and alix doctor reports a
sampling: key that can affect nothing.
Identity works like card IDs, per row. alix deck init (or opening the deck
for review) mints one container ID line after the table, and a short stamp at
the end of each row, after the closing pipe:
| purported | angeblich | often in legal prose | <!-- r:4k2x9w -->
Renderers drop cells beyond the header count, so the stamps stay invisible in a rendered view while keeping the source columns aligned. Both kinds of marker are machine-maintained, never hand-authored, and they travel with their row, so sorting, inserting, and editing rows preserves review history.
Directive comments between the table and its ID line (direction, reveal,
input, sampling) apply to every row. direction: both doubles each row
into a reversed card, which samples its options from the front column.
The format is deliberately narrow: two or three columns only, no cloze
blanks or images inside cells, and nothing but directive comments between a
table and the next card. Anything outside that shape is a parse error rather
than a guess. And a table earns its place at dozens of rows; under roughly
ten cards, plain ## cards read better and can carry everything a card can.
Notes and quotes
A blockquote whose first line is one of GitHub’s five alert badges is a
note: shown after you answer, never part of what’s tested. The badge sits
alone on that line, and the > lines under it are the note’s body:
## Why does TCP open with a three-way handshake?
To agree on initial sequence numbers in both directions.
> [!NOTE]
> SYN, SYN-ACK, ACK: each side learns the other's starting sequence.
The five are [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], and
[!CAUTION], spelled exactly as GitHub spells them. Each renders as its own
block under the answer: the badge word above the text and a bar down its left
edge, both in GitHub’s colour for that badge (one set on light themes, one on
dark, the same on every palette), the text itself dimmer than the answer so
the note never competes with it. Several notes on one card stack in the order
you wrote them. Spelling them GitHub’s way is the point: a deck pasted from a
repository keeps its meaning without editing. A note alix adds itself (a
table’s note column, an AI augmentation, a note the tutor condensed) is a
[!NOTE] like any other.
Every other blockquote is a quote, and a quote is content. It belongs to the answer and reveals with it, so you can finally put someone’s actual words on a card:
## What did Dijkstra say about testing?
That it shows the presence of bugs, never their absence.
> Program testing can be used to show the presence of bugs, but never
> to show their absence.
A quotation is one block however many > lines you wrapped it across, and it
reads as one: a rule down its left edge marks it off, and the > markers are
gone. Under
<!-- reveal: line --> it takes a single reveal rather than arriving a marker
line at a time, and at Reconstruct you are never asked to type it: typing
someone else’s words back tests transcription, not understanding, so you type
the answer’s own prose while the quotation stands beside it. A card whose whole
answer is a quotation has nothing to type, so Reconstruct asks you to explain
it instead.
A badge alix does not recognize, a badge in the wrong case, or a badge with
text after it on the same line is a quote, exactly as on GitHub. That is a
quiet change of meaning, so alix doctor names it rather than leaving you to
find it. A badge whose body is empty, or only blank > lines, draws a warning
too and shows nothing.
A note trails its card the way a directive comment does, so a <!-- reveal: line --> may stand above one, and answer content may not come after one. Prose
or a quotation below a note fails loud pointing at the badge line: move the
content above the note.
Keep the answer to the thing you want to recall, and put the why, the example, or the mnemonic in a note.
A cloze card can give one blank a note of its own, so a note that names an answer doesn’t give it away on the sibling cards. See a note for one blank.
Hints
A [!TIP] block on the question side, between the front and the ---
divider, is a hint: offered while you answer, shown only when you ask for
it. It sits where a front image sits, so a one-line card gains a divider (with
a blank line above it) to carry one:
## What does `?` do on a `Result`?
> [!TIP]
> Think about where an error goes next.
> [!TIP]
> Which function does it leave?
---
It returns the `Err` from the enclosing function.
Several hints on one card are offered one at a time, in the order you wrote
them, so write them to move closer to the answer. A hint is neither question
text nor answer: it is not typed, not a choice, not a step of
<!-- reveal: line -->, and editing it never touches the card’s identity or
review history. Opening any hint turns a pass on that card into Partial for
that review.
Only [!TIP] may stand above the divider; any other badge there fails loud
at its badge line, and a [!TIP] below the answer is an ordinary note. A card
with a blank:, cover: or crop: region takes no hint. A hint that spells
out one of the answer’s lines (four characters or more, any case) draws a
warning from alix doctor, since it would give the answer away.
Sections and sub-cards
Heading depth decides a line’s role. ## is a card front, # opens a section,
and ### through ###### are sub-cards.
# — section context
A single-# heading opens a section. Its text, plus any prose that follows
it outside a card, is the shared context for every card below it until the next
#.
A deck body starts with a heading. Before the first one there is no section to
belong to and no card to be part of, so a line there is an error rather than
text that lands nowhere. A deck of plain cards needs no section at all: opening
with ## is fine, and those cards simply have no context. Whatever a deck is
about goes in title: and description:.
# Ocean depths
Pressure rises by about one atmosphere per ten metres.
## What is the pressure at 30 m?
About 4 atmospheres.
A card’s section is never on the card face. The first card you ever meet from
a section opens the whole section in a sheet that slides up from the bottom, once; after
that, in this sitting and every later one, a Context chip in the bottom
row opens it until the answer shows, as do the c key and the review menu’s
Context entry; the Android app does the same with its Context chip,
and opens the sheet by itself under the same rule (see The mobile app). The kids
client does not expose section context.
A section heading is a heading, nothing more. It takes no directives and no card ID, because a section owns no card to bind either to.
A section runs to the next # heading, and nothing else ends it: every card
carries the context of the nearest titled # above it, and a sub-card chain
does not cross a #. Cards above the first # carry no context. A # with no
title is an error.
alix doctor warns about a section that has prose but no card before the next
#, since a section introduces the cards under it; a # typed where ## was
meant often looks like that.
It also warns when two sections share a heading, since the exam merges them
into one section, and, on a deck with an exam, when a section holds more than
15 cards: the exam asks 3 questions per section and a pass credits every card
in it, so a long section is passed on a small share of what it teaches.
### to ###### — sub-cards
A card written one level deeper than the card above it is a sub-card of it: the same card syntax, gated on the parent. A sub-card stays out of review until its parent has graduated (reached FSRS’s review phase, see Scheduling), so a deck can teach the general case first and release the specialisations once it has stuck.
## What does a TCP handshake establish?
An agreed starting sequence number in each direction.
### Why does the client resend SYN if no SYN-ACK arrives?
The SYN may have been lost; nothing else can distinguish that from a slow peer.
Depth stacks: #### hangs off the ### above it, down to ###### at the
deepest, matching ATX’s own ceiling. One rule is enforced when the deck is
read, so a mis-indented heading fails loudly instead of silently becoming a
top-level card: a sub-card needs its parent one level shallower actually
open, so a ### with no ## above it, or a #### directly under a ##,
is an error. Seven or more hashes are not a heading at all and stay ordinary
answer text, as CommonMark reads them.
A ## closes every open sub-card chain, and a # clears the chain entirely.
Ignoring a card
A bare <!-- ignore --> among a card’s trailing comments takes the card out
of learning without taking it out of the file: it is never served for review,
the exam does not ask about it, and it counts in no total. Its id line stays,
so removing the word later brings the card back with its history. An ignored
card is the one card that may have no answer yet.
## Why does the borrow checker reject this loop?
<!-- ignore -->
<!-- id: card-3g12jfjv4pypppsrx5wvtx65y5 -->
The word trails its card like every other directive: above the card’s content
it is an error, and outside any card it is one too. After a card table it
applies to every row. alix stats shows the count and alix list marks the
cards [ignored].
Reserved Markdown shapes
Four Markdown spellings have no alix meaning and fail loudly with the line
number and a suggested rewrite, instead of showing their markers to the
learner: a setext === underline directly beneath a prose line (write an
ATX #-prefixed heading), four-space/tab indented code opening after a
blank line or a heading (wrap the code in a ``` fence; ordinary paragraph
and task-list continuation lines are unaffected), a nested > > quote
(notes are flat, one > deep; put literal > text in a fence), and a
blank-surrounded thematic break, whatever its spelling and wherever it sits
(delete the line, or keep it literal with <!-- plain --> below it). Inside
fenced code every
shape is literal, as always. A trailing-two-space hard break is not a spelling
at all: content lines shed trailing whitespace when the deck is read, so the
break changes nothing about what a card shows. The file keeps it anyway,
because GitHub renders it; see the next section.
Normalization: the bytes alix writes back
Editors add bytes you never typed, and no deck needs them, so alix settles on
one form instead of carrying whatever arrived. Whenever alix writes a deck
file, it normalizes it. That covers initializing a deck, stamping a new card
id, augmenting, receiving a shared deck, and every alix doctor repair:
- a leading byte-order mark is dropped;
- a line’s trailing carriage returns are dropped, so CRLF endings become LF, everywhere in the file;
- trailing spaces and tabs are removed;
- invisible bytes with no rendered role are dropped from prose: form feed, DEL, a byte-order mark anywhere past the file start, a carriage return in the middle of a line, and the two bidi override characters LRO and RLO. Every invisible character that does render something keeps its place: zero-width joiners and non-joiners (Persian and Indic text), the zero-width space and word joiner (wrap control), the soft hyphen, variation selectors (emoji presentation), and the bidi isolates and embeddings that make mixed-direction text read correctly.
Two things survive. A hard line break, meaning a line ending in two or more spaces, is kept and written as exactly two spaces, so a deck that renders as several lines on GitHub still does. And a code fence keeps the trailing blanks on every line it owns, its opening and closing lines included; only its line endings change. Those bytes are code rather than layout, and alix identifies a card partly by them, so trimming a fence would quietly detach the card from its cached notes and distractors.
alix never rewrites a file it had no other reason to touch, so a deck you only
read is left exactly as it is. If an editor puts those bytes back after the
deck was initialized, alix doctor <deck> --normalize rewrites it.
Because the kept invisibles are, by definition, impossible to see, alix doctor tells you about them: one calm per-deck note with counts by class
(bytes inside a well-formed emoji stay out of the count, since you can see
the emoji). The note is informational; a Persian, Thai, or emoji-rich deck is
supposed to look like that. Doctor warns in exactly two cases, because these
two have no legitimate reading: tag characters outside a flag emoji, which
encode invisible text, and bidi override characters inside a code fence,
where the rendered order of the code can differ from the stored order.
Typed answers are graded on what the screen shows: a character that paints no ink (a zero-width joiner your emoji picker added, a soft hyphen, a variation selector) never decides pass or fail, on either side of the comparison.
HTML in a deck
alix renders Markdown, never HTML, so a tag shape is reserved rather than
silently shown as text: a < directly followed by a letter, or </, on any
deck surface fails to load, naming the line, the column, and the two outs
(wrap literal markup in backticks, or escape a lone bracket as \<).
Ordinary brackets are unaffected: a < b, a<3, and <1> are plain text.
Three HTML spellings do render, each with a fixed meaning:
- Autolinks.
<https://alix.study>and<user@host>display as the bracket-free URL styled as a link, with no navigation attached: the deck is a study surface, not a browser. - The styled subset.
<sub>…</sub>,<sup>…</sup>, and<ins>…</ins>display as subscript, superscript, and underline on every client, tags dropped. One element opens at a time on a line, and each pair must close in order; a mismatched, doubled, or unclosed pair is a tag-shape error. Grading compares the inner text, so typeH2OforH<sub>2</sub>O. A dropped tag still separates what stood on either side of it, sox<sup>2</sup>_i_italicizes instead of reading as one word. - Entities. The full HTML5 named set (
&,€, …) and the numeric formsA/Adecode to their characters on display, per CommonMark. Anything that is not a complete, valid entity stays literal. A decoded character is content, never markup:*x*shows*x*without italics, and<div>shows<div>without the tag-shape error. Markup beside an entity is judged on the spelling in the file, where the neighbouring character is∨, so𝔸_x_italicizesx. Grading compares the decoded text, and the deck file keeps the entity exactly as you wrote it.
The usual protections apply on top: inline code, fenced blocks, and math
bodies keep every one of these spellings literal, a whole-line <!-- -->
comment protects its interior, and a complete image destination in angle
brackets () is an address, not a tag.
Title, and deck-wide settings
A deck’s name, its deck-wide settings, and its machine-maintained deck ID all
live in frontmatter: a ----fenced YAML block at the very top of the file.
Both fences are the same shape: a line starting with exactly ---, with spaces
or tabs allowed after it and no indentation before it.
The name comes from title:. A deck without one falls back to a condensed form
of its trace: sentence, and a deck with neither is named by its filename stem.
A # heading is never the name: it is section context
for the cards below it.
---
id: "deck-9w2c7x4k1m8q3z5t0v6b2n4d8f"
title: French vocabulary, chapter 4
description: The verbs from the chapter's dialogue, plus their prepositions.
authors: [Alex, "Claude (Opus 5)"]
license: CC-BY-4.0
created-at: 2026-07-31
reveal: line
review: sequential
---
description is a short summary. The web picker shows it when you open a
deck’s drawer; nothing else reads it.
format-version is a reserved key: the version of the deck format, not of
the deck itself. A deck that does not declare it is format version 1, so alix
never writes the key. Declaring format-version: 1 by hand is accepted; alix
refuses a deck declaring any other number rather than guessing at a format it
does not know.
authors takes one value or a list; title, description, license, and
created-at are single strings, by convention an SPDX identifier and an ISO
8601 date for the last two. Put both people and any AI that helped in authors.
These five are yours to fill in and alix never changes them.
Apart from id, frontmatter carries only what differs from the defaults, and a command-line flag always overrides it. The full set of
frontmatter and per-card keys gets its own Directives reference chapter.
Key order never matters and yours is never diagnosed. Frontmatter alix itself
writes follows one canonical order: authored keys first (title and
description up front), machine lines like id last. To rewrite an existing
deck into that order, opt in with alix doctor <deck> --repair-frontmatter-order. The same applies to a card’s trailing comment
machinery (any order parses, the id last is canonical):
--repair-comment-order rewrites each machinery run into the canonical
order without touching content. A run holds recognized machinery only, so an
invocation reads down through the card’s own directive comments to the block
above them, and an editorial comment or an unknown key ends the run: the
invocation below one maps nothing and fails loud. One invocation consumes the
block it maps, so a second below the first fails the same way.
Escaping
Because ##, >, a break line of any spelling, and the fence markers are
structural, an answer line that must start with one literally is escaped
with a leading backslash: \##, \>, \---, \***. The backslash is consumed; the line displays without it.
For a break line of any spelling, <!-- plain --> on the line below keeps it as
content too.
## How do you write a second-level heading in Markdown?
\## Section title
Why editing a deck is safe
Every initialized deck and card carries a stable identity. alix deck init
writes the deck ID as id: deck-<token> in frontmatter and each card ID as a
<!-- id: card-<token> --> line. If you later add a card to that initialized deck,
opening review or augmentation assigns the missing card ID. Those tokens, not
the text, are what your review history hangs on. You don’t type or manage them;
alix adds and maintains them after you explicitly initialize the file.
Because identity is the token and not the words, you can edit anything (reword
the question, fix a typo in the answer, rewrite a note, reorder cards) and its
history follows. The only thing that starts a card’s
history over is deliberately replacing it. (alix doctor warns if an id line goes
missing, for instance if an external tool stripped the HTML comments.)
So a deck is safe to refactor freely: your progress rides on the token, not on the words.
Your personal file
A deck you didn’t write is still yours to annotate. Anything alix or you add to
someone else’s deck goes into a personal file beside it, never into the deck
itself: spanish.md gets spanish.local.md. The deck file stays
byte-identical, so you can pull an updated copy of it without losing your work,
and your notes never leak back when you share the deck.
The personal file is the deck’s .local.md twin, following the same .local
rule as alix.local.toml.
It is an ordinary Markdown file with one extra frontmatter key naming the deck it belongs to:
---
for: deck-9w2c7x4k1m8q3z5t0v6b2n4d8f
---
<!-- note: card-3f7k2m9q1x8w5z0t6v4b2n8d7c -->
> [!NOTE]
> the "cuenta" is the tally you finally add up
## a gap the exam found
the answer
<!-- id: card-5k1m8q3z5t0v6b2n4d8f7c2x9w -->
Two kinds of block live there, in any order:
- A note: a
<!-- note: <card-id> -->marker followed by a note in the deck’s own grammar, a>block opening with a badge line. It stands beside that card’s own notes when you review it; a block with no badge line is not a note, andalix doctornames it. The tutor writes> [!NOTE]; you may write any of the five. If you want a label above your note, write a##heading; it is yours, alix never writes or rewrites one. - A card, written exactly like a deck card and closed by its own
<!-- id: -->line. It joins the session after the deck’s own cards and is drilled and scheduled like any other, but it does not count toward the deck’s card count.
The two machine lines sit at opposite ends of their block on purpose. A card’s id closes it, because the id names the card itself. A note’s marker opens it, because a note is an attachment: it has to say which card it belongs to before anything below it means anything.
Nothing in this file is a copy of anything in the deck, so nothing here can go stale. The card id is the only link, and it never changes.
alix writes this file for you: the tutor’s Make this a card, its
Make a note, and the exam’s remediation cards all land here. You can also
edit it by hand. Personal files are never listed as decks of their own, and
alix share leaves them at home in both directions: a bundle you send never
carries one, and a bundle you receive can’t overwrite yours.
A note addressed to a card that no longer exists is left alone rather than
dropped, and alix doctor reports it.
4 · Reveal & session depths
How a card is checked isn’t one setting you pick per card. It falls out of two independent things:
- Reveal-method (how the answer is uncovered): authored per card (or
deck-wide) with
reveal:, because only the author knows the answer’s shape. - Session depth (how deeply you’re asked to retrieve it): chosen per session (Recognize, Recall, or Reconstruct), because only you know how well you want to know this material right now. It isn’t a deck directive, and not personal config either: it’s a property of the session you start.
alix derives the concrete check from the pair, so you never hand-write “type this one” or “explain that one.” Keeping them separate keeps presentation (the author’s job) apart from how deep you’re drilling (your call, per session).
The reveal-method axis: reveal:
Two ways to uncover an answer. Set it deck-wide with a reveal: line in the
frontmatter, or per card with a <!-- reveal: ... --> directive (default flip):
- flip (default): the whole answer is revealed at once.
- line: the answer is revealed one line at a time, for ordered material
(lyrics, a sequence of steps). Pair it with
review: sequentialin the frontmatter to walk the deck top to bottom.
A card becomes cloze (a gap to fill) when a blank: directive hides a
span of the text directly above it; the directive is the trigger, never a reveal: value.
See cloze cards.
## Stage every change in git, including deletions?
git add -A
## Recite the opening.
Now is the winter of our discontent
Made glorious summer by this sun of York
<!-- reveal: line -->
A per-card <!-- reveal: --> overrides the deck’s; the deck’s overrides the
default. It’s a review property, not content, so it’s not part of a card’s
identity: adding or changing it never resets progress.
Session depths: Recognize, Recall, Reconstruct
Every review session runs at one of three independent depths, picked when you
start it with the web picker’s split Depth… button, whose small ▾ opens a menu
of the three (on the keyboard: v, then 1/2/3; Esc cancels; rebindable in
[keys.picker]). The menu also carries the cram
tick-box (c); see Cramming, the confidence tick-box
(f; Confidence tracking), a skip intro
tick-box (s) that grades never-seen cards at once instead of introducing
them first (for this launch only), Walk (w), an
unscheduled read-through (Walking a deck).
A deck with an exam is sat from the drawer’s Take exam chip (x), not
from this menu (The AI exam). The main button repeats the
deck’s last launch, remembered per deck: Learn at its last depth, or
Walk after a walk. The first time you ever open a
deck, that default is Recognize only when every card has a genuine
multiple-choice pick ready to go: authored choice options (an invoked task
list), AI-generated distractors (alix deck augment --target choices, or the
web Augment screen), or a card table’s own column. If
even one card has no buildable pick, the default is Recall.
- Recognize: pick-only, and scheduled on its own FSRS state at a laxer
desired retention (
recognize_retention, default 0.85), because recognition holds far longer than production. It’s a genuine multiple-choice pick, built from a card’s authored choice options (an invoked task list;choices: multipleasks for every correct option, graded all-or-nothing), the deck’s cached AI distractors (alix deck augment --target choices), or, for a card table’s rows, the other rows of the same column: a cloze card asks you to pick its gap, a line card to pick the whole sequence in the right order. Only recognizable cards (the ones with a buildable pick) are scheduled, so a Recognize session never falls back to a plain reveal, which would just be a Recall in disguise. Options are never sampled across unrelated cards; a table column is the one sanctioned pool, because its rows answer the same question by construction. A deck with no authored options, no cached distractors, and no table has nothing to recognize: the picker greys the Recognize depth out until a card carries options or you run the augment. A correct pick grades the card at Recognize; a quiet “I guessed” link right after lets you undo that, re-queuing it. A wrong pick shows which option was right, then Continue re-queues it too. - Recall (the default): the classic flashcard. Bring the answer to mind, reveal it, and self-grade. Its own FSRS schedule.
- Reconstruct: produce the answer in full, on its own independent FSRS schedule per card. Recall and Reconstruct are two separate practices, so a card can be due for one and not the other; the pass-only downward credit that flows to every shallower schedule is covered in Scheduling.
Nothing climbs or descends between depths on its own: a card’s Recall and Reconstruct schedules just sit there side by side, and which one you exercise is entirely your call each time you start a session.
What you actually get: reveal + depth combined
The check derives from the reveal-method and the depth:
- At Recall, a
fliporclozecard reveals and you self-grade; alinecard reveals line by line, then you self-grade. - At Reconstruct, you produce it: a
clozecard has you type the gap in place; a card with a short, single-line answer has you type it; aline-reveal card has you type each line in turn; a card with a richer, multi-line answer becomes an explain prompt whose back lines are the key points you self-grade against.
A typed check normalizes both sides (case, whitespace, trailing punctuation) and compares exactly, with no edit-distance tolerance, then shows the diff. The automated comparison is evidence, not the verdict: grading is still yours, so a mismatch you recognize as a typo (not a wrong answer) can still be graded Got it.
Grading is always the same three (missed it / partly / got it), feeding FSRS Again / Hard / Good. See the scheduling chapter for how Recall and Reconstruct’s independent schedules work, and how badges summarize a deck’s progress at each depth.
Math during review
The adult web app, kids web app, and mobile app all display the same Rust-rendered SVG for authored LaTeX math. Inline formulas follow the text baseline and display formulas are centered and scaled to the card width. They inherit the current text color and add no background rectangle.
A cloze marker may sit inside math:
## Complete the identity.
$$a^2 - b^2 = (a-b)(a+b)$$
<!-- blank: span hidden="(a-b)" -->
<!-- blank: span hidden="(a+b)" -->
During review, the active hole renders as a boxed question mark and another hidden hole as a boxed ellipsis inside the formula. The substitution is display only and never reveals either answer. If RaTeX rejects a recognized formula, review shows its source plus “math could not render” rather than a blank or a plausible substitute.
explain: the self-graded Reconstruct check
The Reconstruct check for a rich (multi-line) answer is an open prompt: the back lines are the key points a good answer should cover, not a string to reproduce. You optionally type an explanation (never checked, just there to make you commit before you peek), reveal the points, and grade whether you hit them. It’s for cards aimed at understanding rather than exact recall, and it’s the everyday, self-graded tier beneath the AI exam (a later chapter).
## Explain why spaced repetition beats massed review.
Retrieval just before forgetting strengthens memory the most.
Spacing forces effortful recall; cramming lets you coast on short-term memory.
The reveal is a checklist by default: every multi-line explain card ticks
against its own answer lines as the rubric, and the grade is derived from the
coverage (all covered → got it, some → partly, none → missed it), a per-claim
check rather than a gut call. alix deck augment <deck> --target keypoints
replaces that rubric with model-written claims distilled from the card, which
usually tick more cleanly than prose lines. Atomic-answer cards get no key
points and keep the plain reveal.
A different augment target, alix deck augment <deck> --target format, instead
reshapes a badly-shaped card (a list crammed into one prose answer, say) into
clean display lines, non-destructively: it changes how the card is shown, not the
deck file or how it’s graded.
The check badge
In the web frontend a small badge above the answer names the check you’re doing
right now (flip, line, typing, typing · line, choice, or
explain), so how you’ll
interact is clear before you commit. It badges the present interaction, not
the depth: a Recognize pick shows choice whatever the card’s own mode is.
A brand-new (introduction) card is prefixed new · and names its on-ramp rather
than a check, because no check is happening yet on a card you’re only meeting:
new · choice when it offers options, new · draw on a sketch card, and
new · reveal otherwise.
Draw instead of type: input: draw
input: is a third, separate axis: it changes how you produce an answer, not
how it’s graded. draw swaps the usual typed/reveal input for a canvas: instead
of typing (or just reading) the answer, you draw or handwrite it, then self-grade
against the card’s normal reveal.
Two ways to reach it:
- Draw-only cards. Set it deck-wide with
input: drawin the frontmatter, or per card with<!-- input: draw -->, when the answer can’t be typed (a diagram, a circuit, a piece of notation). The reveal is whatever the card already uses: aimage on the answer side, or an explain card’s key points. An authored draw card uses the canvas, and the per-device toggle below can’t turn it off (you can’t type a diagram). The one exception is a cloze card in the desktop web app, which ignores both the directive and the toggle and serves the typed check. - The per-device toggle. For a card that can be typed, the web ☰ menu’s Draw answers switch lets you answer on the canvas anyway, for the retention of writing by hand, without changing the deck file. It’s remembered per browser.
Grading a draw card is entirely self-reported: there’s no OCR or vision model
reading the canvas, so it works like a self-graded flip/explain card. You judge
your own drawing against the reveal. In this version input: is honored on
self-graded checks only (a flip reveal or an explain); it’s ignored
elsewhere, and the desktop web app also ignores input: draw on any cloze
card, authored or defaulted. One case needs no directive on mobile: a cloze
hole cut out of a formula defaults to the sketch canvas there.
To drop a card mid-session, choose Remove card in the review menu (or a
remove key, if you bind one) instead of grading it: it leaves the session and
is deleted from the deck file right away.
5 · Scheduling, retirement & completion
Spaced repetition is really just bookkeeping: each card remembers how well you know it and when to show it next. This chapter is that bookkeeping: the scheduler, retirement, and how a whole deck reaches “done.”
Each initialized deck keeps this bookkeeping in
.alix/progress/deck-<token>.json at its content root: the workspace root for a
workspace member, or the containing folder for a loose deck.
FSRS
alix schedules with FSRS, the Free Spaced Repetition Scheduler (FSRS-5, via
the rs-fsrs crate). There’s one scheduler and nothing to choose: FSRS keeps a
small memory model per card (its stability and difficulty) and, from your
grade, works out when the card is next due.
Grading feeds FSRS a rating:
- failed → Again, a lapse; the card comes back soon and its interval shrinks
- partly → Hard, a weak success; a shorter next interval than a clean pass
- passed → Good, the interval grows
So a card you keep getting right stretches to longer and longer intervals, a miss pulls it back in, and a partly (you got the gist but stumbled) lands in between. Early on the first successful reviews are minutes-to-hours apart (FSRS’s short-term learning steps); a card graduates into the review phase (where intervals grow to days, then weeks) only after two spaced correct recalls, and missing it resets that progress, so a slip doesn’t shortcut it.
Set short_term = false in [review] to turn the learning steps off: a
card’s first correct recall graduates it straight onto a day-scale interval, so
loading a whole deck takes one pass per card instead of several same-day
returns. A miss before that first pass schedules it a day or so out without
graduating it.
A session shows each due card once. Miss one and it returns spaced (after its short step, interleaved behind other cards) not drilled again the instant you saw the answer (which would test your working memory, not your recall). That gap is floored at the moment you actually move off the card, not when you first saw the answer, so time spent on the feedback screen or working the next card still counts against a short retry interval. When nothing is due right now the session ends; a card still cooling is picked up the next session, or slots back in on its own if you leave the window open.
One knob shapes the whole schedule: retention, the recall probability FSRS
aims for (0.70–0.99, default 0.9). Raise it to see cards more often, lower it to
stretch the gaps. Set it in the [review] config section, or per workspace in an
alix.local.toml (see Configuration).
A workspace deadline
Set a personal deadline on a workspace (Workspaces) and
scheduling leans toward it while the date hasn’t passed: FSRS intervals cap at
the days left (floored at one day), the target retention ramps up linearly to a
fixed 0.95 over the last deadline_ramp days (never lowering a higher personal
retention), and a due-date ceiling keeps anything from being scheduled past
the deadline day. Once the date passes, both the cap and the ramp lift and
scheduling releases back to your base pacing automatically.
New cards: an attempt before they’re tested
A card you’ve never seen isn’t quizzed cold (you can’t reconstruct what you’ve
never read) but it isn’t simply handed to you either, whichever depth you’re
reviewing at. The first encounter is a low-stakes attempt, then the answer,
then one key (Seen) records it without a grade. Usually: the front shows
first, you try, then reveal. You can then hide and show the answer again
(h or a click on it on the web, a tap on it on the phone) to self-test the
fresh encoding before you press Seen, as on any revealed card (see
The web app). If the card has authored choice options, or the deck has AI
distractors (alix deck augment --target choices) and the card is atomic
(single-line answer), it instead
greets you as a multiple-choice question: pick one, see which was right.
Either way a guess records no grade and never punishes it, and the first
graded quiz then comes back later in the same session (once a settle
gap passes it resurfaces, interleaved behind the other cards you’re seeing),
so seeing a deck flows straight into drilling it. That gap is
introduction_cooldown in the [review] config (default "5m"); it also
sets the floor before any just-seen card (a miss, a wrong pick) may return,
so nothing you moved off comes straight back. "0" disables both gaps. If the
gap passes while you are sitting on the session summary, the summary says so
and arms Continue; it never starts the next card for you. Each sitting serves up to max_session cards
(default 10); its new-card share is new_cards_percent (default 30%, so three
of ten) and the rest are due cards, with whichever pool runs short letting the
other fill the cap (a fresh deck fills entirely with new, a deck with nothing
new fills entirely with due). A larger backlog therefore slows introductions
proportionally, so chain another sitting when a deadline is near. Both keys live
in [review] (see Configuration; a workspace can
override them in its alix.local.toml), and --session N overrides
max_session for one launch. This holds at every depth, Recognize included: a
Recognize sitting splits the same cap between new cards and cards due at
Recognize.
Due means scheduled: a card is due only once a grade has given it a schedule and that date has come. A card you met but never graded (introduced, then left before its graded quiz came back) has no schedule, so in a later sitting it counts as new and waits for a slot in the new-card share, once its settle gap has passed. When it comes up it is graded at first sight, without a second introduction. This is the first step of a card’s life: introduce, then let its depth(s) schedule it.
A launch can skip the introduction: every fresh card is then graded on
first sight, as if it had been introduced before, with no Seen press and no
settle gap in between. The card’s first grade is its first learning event,
and the sitting’s introduced count stays at zero. This is for a pass over
a deck you already know (a cram, a re-read), where the introduction step is
friction; a deck you are meeting for the first time loses the guess-first
encounter and its section sheets. On the phone it is the Skip introduction
switch in the sheet a tap on a deck row opens; over the API it is
skip_introduction on /api/select.
Session depths: Recognize, Recall, Reconstruct
FSRS decides when a card is due; the session depth decides how deeply it’s asked when it comes up. A session runs at one of three independent depths, picked when you start it (the web picker’s Depth… menu). See Reveal & session depths for the full check matrix. In short:
- Recognize, Recall, and Reconstruct each keep their own FSRS schedule per card, so a card can be due for one and not the other; nothing cross-credits between them, with one downward exception below.
- Recognize schedules at its own desired retention (
recognize_retention, default 0.85) rather than the deck-wideretention: one mechanism, two tunings.
Nothing climbs or descends between depths on its own. A card doesn’t get harder over time just by surviving reviews. Which depth you exercise, and when, is entirely your call each session.
The exception flows downward, and only on a full pass: get a card fully right at a deeper depth (in cram: only when it was due) and that also counts for every shallower schedule the card already has. Reconstruct credits Recall and Recognize, Recall credits Recognize: if you can produce the answer, you can certainly recall and recognize it, so alix won’t re-ask an easier form days later. If the shallower depth was due at that moment, the pass stands in for that review: full schedule credit, recorded in the card’s history and marked as propagated. If its schedule existed but wasn’t due yet, only its due date is pushed out from now (memory untouched, nothing recorded, the same refresh a cram pass gets). A partly or a miss never propagates, and a missing schedule is never created: a card drilled only at Reconstruct never gains a recall or recognize schedule from this.
Badges
A deck can earn a badge at each depth, shown in the picker: a quick read on how solid it is, never a gate on anything (only passing the AI exam unlocks a dependent deck). A deck earns a depth’s badge once every one of its cards is currently solid at that depth: at or past 21 days of FSRS stability (in practice a few weeks of regular drilling), the same bar at all three depths, Recognize included. Only the highest badged depth shows: solid while the deck still clears the bar, dotted once a card has since lapsed below it (a badge, once earned, keeps its date, a high-water mark, not a live pass/fail).
A deck shows a small “new” chip while any of its cards has never been presented, whether or not the deck is badged. It clears only once every card has been seen at least once, so a large deck you are working through keeps the chip until you reach the end of it: the chip answers “is there anything in here I have never met?”, not “have I started this deck?”.
Retiring cards
A card doesn’t stay in rotation forever. Once a graduated card’s Recall
interval reaches retire_after (a positive duration, default one year),
the card retires: it rests and is no longer scheduled, not even under
cram, until you alix reset it. Set
retire_after = "never" to keep drilling a deck forever (facts you never want to
risk forgetting); a workspace can override it in its alix.local.toml.
Completion states
A deck’s state is derived from how far its cards have progressed, and shown in
the picker and alix stats:
- not started: you haven’t reviewed any card yet
- started: somewhere in between
- finished (
done ✓): every card has graduated (reached FSRS’s review phase, past the initial learning steps)
A deck that declares an exam: adds a state between started and finished:
exam due (every card answered right at least once, at any depth or in a
walk, and the deck not yet mastered). For those decks, drilling the cards
never finishes them: passing the AI exam unlocks dependents (passed),
and a perfect full sitting finishes the deck (mastered). That’s the subject
of a later chapter.
On the web, Learn and Enter keep drilling an exam-due deck. The separate
Take exam action is filled when the exam is due and unlocked; it does not
appear again in the Depth menu or on a session summary.
Unlocks, in one line
Passing also drives dependencies, with no extra syntax: a deck’s exam is locked while any of its sourced prerequisites hasn’t passed its own exam. The deck itself stays drillable throughout. Passing a foundation’s exam unlocks the exams that build on it. The lock applies to the exam only, never to drilling, and is recomputed live. The dependencies chapter covers it in full.
Cramming
Need to review everything now, schedule be damned, the night before an exam?
Cram ignores due times and shows every card that isn’t retired, at every
depth (the sitting stays bounded by max_session; at Recognize the pick-only
filter still applies, so only cards with something to pick from appear).
It’s a per-launch tick-box in the picker’s Depth… menu (key c while the
menu is open); plain Learn never crams. On the phone it is the Cram
switch in the sheet a tap on a deck row opens.
Cram changes which cards are queued, never how a due card is graded: a card that was genuinely due grades exactly like a normal review: full schedule credit, recorded (a due Reconstruct pass even propagates to Recall as usual). Only a pass on a card that wasn’t due yet is treated as the low-information event it is: its due date re-anchors by the current interval, memory untouched, nothing recorded, so a heavy grind can’t inflate your long-term spacing. A card you miss under cram always lapses normally. Retired cards stay out (that’s what retirement is for).
Review order
By default your due cards come up in scheduler order: soonest-due first. That’s right for retention, but it can feel random: a card about parsing, then one about persistence, with no thread between them.
A review order gives the session a thread. alix deck augment <deck> --target order asks the model to read the deck and lay out a graph of how the cards
relate: a suggested walk through them, plus a few coarse named regions
(stages or themes). It’s cached in the deck’s shareable augmentation document
alongside distractors and notes;
a deck can hold several, one per --with principle:
alix deck augment internals.md --target order
alix deck augment capitals.md --target order --with "north to south"
alix deck augment capitals.md --target order --with "by continent"
Then review along it: select the deck in the web picker and an inline focus drawer opens beneath it: choose which order runs the session (“Whole deck” is the default), then start.
The key thing: this changes only the order, never the schedule. SRS still decides which cards are due and how they advance. The order just serves that due set in walk order instead of shuffled, so each card is a natural follow-up to the last. Not-due cards are skipped, so the session stays as short as your due pile.
As you go, a thin region breadcrumb sits above each card (e.g.
Ingestion · Review Engine · Persistence · Frontends, the one you’re in
emphasized) so you see where you are in the material, not just what’s in front
of you. The names are deliberately coarse: they orient without giving away any
card’s answer. Under each region is a card-tier heatmap: one small cell
per card. Neutral means untouched; grey means seen (you met the card:
acknowledged it, or answered and got it wrong); white means learning (at
least one full or partly pass, not yet graduated); a learned (graduated) card is green, yellow, or red by how
well you’d recall it right now; purple means retired. An outlined cell,
unfilled, is a sub-card still
gated on its parent’s graduation, which is why it is usually untouched as well.
A region visibly greens up as you master it, and the breadcrumb doubles as a
progress map.
To drill one weak region on its own, tap its heatmap in the focus drawer to scope the launch to it.
SRS still chooses what’s due within that region. You’ve just narrowed the session to it.
The choice is made before the session; the in-card breadcrumb itself stays read-only. The breadcrumb, the heatmap, and the ordering are all in place; richer map views are still to come.
6 · Cloze, dual-direction, image & diagram cards
Three extensions to the basic card, each a small addition on top of the format from chapter 3.
Cloze cards: fill in the blank
A cloze card hides part of its own text; you create one by pointing a
blank: directive at a span of the block above it.
Write the sentence as ordinary prose, then name the hidden text in a
<!-- blank: span hidden="..." --> comment directly below it. A directive
binds to the block directly above it (a run of text lines, an image line, a
code fence or a $$ block), and the hidden text must be in that block.
Comment lines and link definitions between the two do not count; a blank
line, a note or the --- divider in between is an error. Each directive is
a blank, and the card expands into one sub-card per blank. A cloze card has
no answer section: it needs no answer lines, a --- divider on it is an
error, and explanations go in a > [!NOTE] note. No marker sits in
the prose, so the deck reads normally in any other Markdown tool.
## Complete the Rust declaration
let mut x: u64 = 0;
<!-- blank: span hidden="mut" -->
<!-- blank: span hidden="u64" -->
The ## line may itself be the sentence, which needs no separate heading:
## A value is dropped when its owner goes out of scope.
<!-- blank: span hidden="dropped" -->
This makes two cards. One blanks mut and shows the rest; the other blanks
u64. Each card’s question is the whole card from its ## line down, with
the asked span shown as ⍰ and the other spans hidden as ⬚, so no card
gives away its siblings’ answers. You only produce the hidden text. On reveal
the question fills the asked span in place; the siblings stay hidden, and no
separate answer repeats it. The web and mobile apps draw every unrevealed
blank as an empty outlined pill of one fixed width, so the gap never hints at
the answer’s length. The asked pill is faintly filled with the accent colour; a
sibling’s is muted and dashed. Filled text is highlighted in the accent
colour. A card asking several spans at once (a named group) fills one span per
press under reveal: line, all at once under flip.
When you type a cloze card (at Reconstruct), the web and phone apps make each
asked blank the typing field itself: type each answer in its gap and press
Enter (on the phone the keyboard’s action moves to the next gap, and on the
last one checks).
Each gap is checked against its own answer, so swapping two answers is two
misses. The result shows in the gaps: a right answer turns green, and a miss
shows what you typed, struck through, before the answer. A blank inside a code
fence or a formula keeps its own field below the question instead, as does a
group that walks reveal: line. A cloze card has no dividing line under its
question: the question and its field or choices sit together in the middle of
the card.
The hidden text must appear in the bound block exactly as written. When it appears
more than once, occurrence=2 picks the second match; matches snap to word
boundaries unless the directive says boundary=char.
alix keeps a card’s cloze siblings apart in the queue when other cards are
available, so you don’t see mut right after u64. Editing is safe: each
span’s identity is the stamp alix mints into its directive (b:...), so
rewording the question, or moving the prose, keeps your history.
Reach for cloze when the context is the cue: a definition with its key term removed, a line of code with the operative token blanked.
A span inside $...$ or $$...$$ is a piece of the formula, and must be a
complete structural unit of it: a lone term such as 4ac, a base with its
whole script such as b^2, a standalone symbol such as \pm or \alpha,
or the whole formula. A script cut from its base (^2 alone) or a command
cut from its arguments (\frac{a} without its {b}) is a loud error
naming the cut. At review a formula span defaults to the sketch canvas on
mobile, since a formula’s piece has no keyboard spelling (the desktop web
app does not honor the default yet and types it); write input: type on the
card or the deck to keep the keyboard. An authored input: wins over the
default on mobile; the desktop web app currently ignores input: draw on a
cloze card and types it regardless.
alix doctor warns when a typed span’s answer would need LaTeX to type.
It also warns when the question still shows a span’s hidden text outside
its blank: a ## line repeating the hidden words above the blanked
sentence, another block of the card, a second occurrence the blank does not
cover, or the card’s section heading and prose. The card would show its
answer before the reveal. The check ignores case and runs of whitespace and
matches whole words only, so a hidden ox does not fire on box. An image
blank’s label is not displayed text and is not checked.
A note for one blank
A > note belongs to the card you wrote, so every blank of it shows the same
note, and a note that spells out one blank’s answer gives it away on all the
others. Give the span a name in square brackets and write the note to that
name:
## The test pyramid, bottom to top
Unit, integration, end-to-end
<!-- blank: span [base] hidden="Unit" -->
<!-- blank: span hidden="integration" -->
<!-- blank: span hidden="end-to-end" -->
> [!NOTE]
> base: Fastest and most numerous, which is why they sit at the bottom.
Only the Unit card shows that line; the other two show nothing. Written as
> base+: ... it is added below the shared note instead of replacing it. A
name is one or more of a-z, A-Z, 0-9, _ or -.
Everything else stays prose. A note line is only an address when the card
names a span and the name before the : is one of them, so a note that opens
2: the second one is still a note. alix doctor reports an address that
names no span of its card, and shows the line as an ordinary note.
Blanks that belong together
Give two spans the same name and they become one card asking both, instead of two cards each asking half a fact:
## The TCP three-way handshake, in order
SYN, SYN-ACK, ACK
<!-- blank: span [open] hidden="SYN" -->
<!-- blank: span [open] hidden="SYN-ACK" -->
<!-- blank: span hidden="ACK" occurrence=2 -->
That is two cards, not three: one asking SYN and SYN-ACK together, one
asking ACK (occurrence=2, because the first ACK in the stream sits
inside SYN-ACK). Both spans show as ⍰ on the merged card, and you answer
them as a list, one line per span. They don’t have to sit next to each other
or even on the same line.
Grouping starts that card’s history over. Two spans recalled together are a harder question than either alone, so the merged card takes no schedule from the blanks it replaces: it comes back as if it were new. The spans you did not group keep their history. Group early if you are going to.
A name addresses a span; it is not its id. Identity is the minted stamp,
so renaming a span, or adding a name to one you have already drilled, keeps
your history exactly as rewording the question does. A name alone does not
make a group: the group, and its history reset, begins when a second span
takes the same name. The name means nothing outside the card it is written
in: two cards may each have a base.
Dual-direction cards: direction:
Reviewing a card both ways is what you want for vocabulary and other reversible
facts. Set it per card with <!-- direction: both -->, or deck-wide with a
direction: line in the frontmatter:
## purported
angeblich
<!-- direction: both -->
bothmakes two cards:purported→angeblichand the swapangeblich→purported.reversekeeps only the swapped one.forward(the default) is the card as written.
The two directions get distinct progress, are kept apart in the queue, and are
removed together; the reversed card keeps the note. It’s best for single-line
cards. A cloze card is never reversed: a deck-wide direction: skips it, and
a direction: written on the card itself is an error. When a reversed card’s question side
comes from several answer lines, they render as separate centred lines rather than
running together.
Image cards
Write a standard Markdown image on its own line, and its position decides the side: an image in the question is a front image, one in the answer is a back image, and a card can carry more than one per side. An image sharing a line with prose is rejected: alix displays images as media beside the text, not inline within a sentence, so a mixed line would silently lose its shape.
A front image sits between the heading and the divider, so the front has more than the heading line and the divider needs a blank line above it. Attached directly under a content line the break is in no valid position and fails loudly, naming the line:
## What phase is the moon in?

---
Waxing gibbous
## Play this chord:
G major
---
The open-position shape.

For a deck on its own, an image src is a path relative to the deck file,
exactly the way a standard Markdown viewer resolves it: a bare filename means
the image sits next to the deck, and sub/moon.png means a subdirectory. An
absolute path is used as-is. The brackets can carry alt text:
, and a destination in angle
brackets can carry spaces (). A GFM title after the
destination () parses and is
ignored: pasted Markdown keeps its image, and the title does nothing yet.
Because the paths are ordinary
Markdown, such a deck renders identically in the web app and in any Markdown
viewer that opens the file directly (GitHub, Obsidian, a plain preview pane).
alix doctor warns about an image file it can’t find, but doesn’t fail on it.
Inside a workspace the base is the workspace root, the
folder holding alix.toml, not the decks/ folder the member sits in. So a
bare  in decks/phases.md means moon.png beside alix.toml.
Initializing the member then copies each local image into the deck’s own
assets/deck-<token>/ under its SHA-256 name and rewrites the reference to
point there, which is what makes the member shareable and what keeps it
working on a machine that never had the original.
Image occlusion: blank:, cover: & crop:
A card can hide a region of its picture and ask what is under it. A region is a directive comment on its own line directly beneath the image line it marks (comment lines between them are fine, a blank line or text is an error), so any other Markdown viewer still shows the plain picture:
## Name the quadrant colors

<!-- blank: rect x=10% y=10% width=35% height=35% hidden="red" -->
<!-- blank: rect x=55% y=10% width=35% height=35% hidden="green" -->
<!-- cover: rect x=10% y=60% width=35% height=35% -->
Each directive names its shape first: rect is the shape word, and the only
image shape today (span, below, is the text shape). Three keywords, one
concept each:
blank:masks a region and asks about it, the picture form of a text span.hidden="..."is the expected answer, and everyblank:needs one that is not empty once grading ignores spaces, invisible characters and trailing punctuation.cover:masks a region and never asks: for a legend or label that would give an answer away. It creates no card; ahidden=on it is kept but inert, so switching a region betweenblank:andcover:never loses your answer text.crop:shows only a viewport of the source, so one large picture can serve many cards without being cut into files:<!-- crop: rect x=50% y=0% width=50% height=100% -->shows the right half. At most one per image, and region coordinates stay in the full source’s space, never crop space, so adjusting the crop moves no region.
Fields are named, so their order never matters. Bare numbers are pixels in the
source image’s own coordinates (what a paint tool hands you); a % suffix on
a number makes it a percentage of the full source. Every region and the crop
on one image must agree on the unit. A region may reach past the image’s edge
and is clipped there when drawn; only a blank: whose region contains nothing
visible at all is refused, because a question about nothing visible is broken
(percentage geometry is checked when the deck loads, pixel geometry at render
time, since only the app showing the file knows its size).
A block with a blank: is a template, exactly like a text-span block: it
produces one card per blank and nothing else, and each sibling card masks the
others’ regions so no answer leaks. In review the roles look different on
purpose: the region you are asked about shows the ⍰ blank marker, a sibling
card’s masked region shows ⬚, and a cover is a plain fill with no marker,
telling you it is never a question. Masks lift on reveal exactly like text
blanks; a cover keeps hiding on region and cloze cards (its content could give
a sibling’s answer away), and reveals with the answer on an ordinary card that
poses no such sibling questions. Removing the last blank: turns the block
back into its plain card, review history intact. cover: or crop: alone
change only the display; the ordinary card remains.
Blanks that belong together take a bracketed group name, the same idea as cloze groups and with the same warning, a regrouped card starts its history over:
## Name the organelles

<!-- blank: rect x=10 y=40 width=80 height=30 hidden="mitochondrion" [organelles] -->
<!-- blank: rect x=10 y=90 width=80 height=30 hidden="nucleus" [organelles] -->
When you open a deck for review, alix stamps each region with a short
b:<tag> mark, exactly like the card ids from
the deck format: minted once, never hand-written,
and what keeps a region’s review history attached while you nudge its
coordinates or reword its answer.
Span matching, drift, and repair
hidden="..." is both the anchor and the answer: alix finds that text in the
block and masks it. cover: span hidden="..." hides its text the same way
without ever asking. Two optional keys refine the match:
occurrence=N(default 1): mask the Nth occurrence of the hidden text, counted over the block in order. Fewer than N occurrences is an error; nothing silently moves.boundary=word|char(defaultword):wordrequires the match to stand alone (punctuation next to it is fine);charmatches anywhere, for sub-word blanks likehidden="mito".
On first review alix also mints a position:<n> anchor into the directive,
the point where the span bound (counted in characters as you see them, so the
number survives any script). Review never reads it; it is the drift signal.
When you later edit the block and the hidden text moves, alix doctor
reports the divergence with both readings and the exact edit for each: keep
the text you authored (run alix doctor <deck> --repair-positions and the
anchor is rewritten to where the span binds today), accept the new binding by
writing that position: in yourself, or keep the old target by setting
occurrence= (offered only when the region carries a minted occurrence to
name). Doctor never rewrites a diverged span on its own.
A span may sit inside a formula. Its hidden text must then be a complete
structural unit of the math: no half of a \command, no command cut from
an argument group it applies to, no script cut from its base, no split
{...} group, no structural characters inside the match (& and \\,
LaTeX’s column and row separators). A standalone symbol command (\pm, a
Greek letter) and a base with its whole script (b^2) are units, and alix
proves the formula still renders with the span masked; a violation is a
loud error naming the offending piece of the formula when the deck loads. A masked formula draws the blank as a boxed
hole, and when a block pins input: type, alix doctor warns on a span
whose answer needs LaTeX to type, since typing \pm asks for a spelling.
Mermaid diagrams
A fenced ```mermaid block in a card renders as a diagram. alix does
not draw mermaid itself: when a workspace member is initialized
(alix deck init, the same step that freezes local images and cited source
excerpts, both covered below and in Workspaces), each
fence is rendered once through sekien, an optional external CLI that
runs real mermaid.js (cargo install sekien; on Linux it also needs
webkit2gtk, gtk3 and xvfb), rasterized, and stored in the deck’s asset
folder (assets/deck-<token>/ under the workspace) as a
content-addressed image plus a label map: where every node and edge label
sits in the picture, which is what masking uses later. A machine-managed
stamp comment lands on the line after the fence, tying the fence text to
those two frozen files:
<!-- diagram: fingerprint: xxh64-… asset: sha256-….png geometry: sha256-….json -->
From then on every client shows the image, including the mobile app offline; the mermaid source stays the only thing you edit.
## the request path
```mermaid
flowchart LR
Client --> LB[Load Balancer] --> API --> DB[(Postgres)]
```
Diagrams never make deck init fail. Whatever goes wrong, initializing
finishes and tells you: without sekien installed the deck initializes and
warns, and the fence shows as a plain code block until you install the
renderer and re-run alix deck init; a fence sekien cannot render, or a
theme whose colors alix cannot read (below), is reported the same way and
that one diagram stays unfrozen. A standalone deck (no workspace) always
shows exam: freezing needs the workspace’s asset store.
Editing a frozen fence makes its stamp stale, and the fence falls back to
its source until the next deck init re-freezes it. Fallback always means
the same thing: the fence displays as a code block instead of a picture;
on a masking card (below) the hidden text inside it is blanked, so the
card stays reviewable either way. When a session opens on a deck with a
stamped diagram that cannot be loaded (a stale stamp, a missing frozen
file, or a geometry file that does not read back), the app shows a one-line
warning so the fallback is never mistaken for a successful freeze. One
kind of damage is deliberately outside that check: loading verifies
shapes, not bytes, so a frozen file whose content was corrupted in place
(bytes that no longer match their content-addressed name, after a faulty
copy or restore) still serves. alix doctor is what re-hashes every
frozen object and names such a file. alix doctor <deck> --repair-diagrams removes stamps that lost their fence and re-freezes
stale or unfrozen ones; the corrupt-bytes case is repaired by deleting
the stamp line and re-running alix deck init.
Theming
The diagram’s colors are decided by its own source, at freeze time:
-
an init directive on the fence’s first line:
%%{init: {"theme": "dark"}}%%, or -
mermaid’s YAML frontmatter at the top of the fence:
--- config: theme: forest ---
An in-fence theme wins over anything set outside the fence (sekien’s own
--theme flag included), so a shared deck renders the same everywhere.
alix reads the rendered theme’s text color and puts the raster on
whichever background, light or dark, keeps that text readable. The
trade-off to know about: colors are baked in when you freeze. A frozen
diagram does not follow the app’s light/dark theme, and re-theming means
editing the fence and re-freezing.
Masking diagram labels
The span directives from the previous section work on diagram source, and on a frozen diagram they mask on the rendered image:
## the request path
```mermaid
flowchart LR
Client --> LB[Load Balancer] --> API --> DB[(Postgres)]
```
<!-- blank: span hidden="Load Balancer" -->
The card shows the diagram with a mask over the Load Balancer node’s label; the mask lifts when the answer shows. Sibling blanks and covers on the same fence stay masked, exactly like image occlusion, and a reader using assistive technology hears only the visible labels, never the hidden ones.
A span must cover one complete visible label. Node and edge labels can
be masked; node ids, arrows, and keywords never can, and a span whose
hidden text lands on one is reported by alix doctor while the card falls
back to masked source (reviewable, just not drawn). Where the same text
appears more than once, occurrence= picks which one, counted over the
block like any span. A bare node like A, whose label is its id, masks
like any other label. Some labels still cannot be masked, and doctor says
so when a span targets them: labels whose rendered text is not literally
in the source (multi-line <br/> labels, HTML entities), and a bare node
the fence references on more than one line, where which occurrence is
“the” label is ambiguous (write A[A] once to settle it).
Source citations
A plain fact card can show where its answer comes from. Name the folders a
citation may point into with a cite: list in the frontmatter (each entry is
one key and one root), give the card an <!-- at: ... --> locator under one
of those keys, and on reveal the card offers to swap the worded answer for the
exact source lines:
---
cite:
- code: src
---
## What does the `String` struct hold?
A `Vec<u8>` (its bytes).
<!-- at: code:string.rs:1-3 fingerprint: xxh64-0123456789abcdef -->
A cite: key is lowercase ([a-z][a-z0-9_-]*). Its root is a folder, relative
to the workspace folder (the deck’s own folder for a loose deck), or a URL,
which is never fetched: a URL root only holds frozen passages. A workspace’s
alix.toml declares roots every member sees in a [cite] table
(code = "src"), and a deck may not repeat a manifest key or declare a root
inside another root it sees. The locator is the same shape a
trace checkpoint uses. Its fields are named and ordered:
at: is <key>:<path>[:<lines>], the key, the file’s path below that root,
and a line range (code:string.rs:1-3; without a range the whole file is
cited; a bare <key> cites the frozen passage of a URL root), and
fingerprint: is an xxh64-<hex> digest of the displayed source text. A
locator without a key, a key no root declares, or a path that leaves its root
is an error; nothing searches parent folders or file names on its behalf. alix
writes the fingerprint when it creates a cited card or when you explicitly
repair a hand-authored citation. A fact card may repeat the whole directive when
its answer rests on several disjoint source ranges:
<!-- at: code:state.rs:64-74 fingerprint: xxh64-0123456789abcdef -->
<!-- at: code:state.rs:114-118 fingerprint: xxh64-123456789abcdef0 -->
<!-- at: code:state.rs:152-158 fingerprint: xxh64-23456789abcdef01 -->
Each locator remains one contiguous range; separate directives never imply
that disjoint code is adjacent. In the browser, a </> marker appears on the
revealed answer: click the marker (or press s) to swap the answer for the
same editor-style source panels used by traces, and back; a click on the answer
itself hides it for a self-test, as on every card. On the phone, tap the
revealed answer and choose Source in the Hide / Source sheet; the sheet
reopens on the view last used for that card. Multiple excerpts are stacked in
authored order inside the one scrollable region. For a live citation, alix shows
the lines only when their fingerprint still matches. A moved, changed, deleted,
ambiguous, or unfingerprinted excerpt shows a warning instead of unrelated
lines, without hiding the other citations. Short evidence keeps the answer’s
centered vertical alignment; long evidence aligns to the top and scrolls.
This is the same machinery traces use to reveal source, brought to ordinary
fact cards. Like every directive, <!-- at: --> is not part of a card’s identity:
adding a citation never resets its progress.
You rarely write these by hand. Generating a deck from a local source
(alix deck generate <path>) cites the lines each fact came
from and fingerprints every citation. Plain
alix doctor reports missing fingerprints and
fingerprint drift without writing. After reviewing the cited text,
alix doctor <deck> --repair-source-locators stamps a missing fingerprint or
rebases a uniquely relocated exact excerpt; changed or ambiguous excerpts
remain untouched for semantic review. Initializing a workspace member goes one
further and freezes its source evidence and local images below
assets/deck-<token>/, so the deck travels without the original source and the
quotes never shift. Freezing stores only the cited excerpt, never the whole
file, and leaves the deck’s cite: roots pointed at the real material: each
frozen citation keeps its key and path in at: and gains an asset: field
naming the content-addressed object, so a drift check can still compare
against the live file under that key’s root when it is reachable. That check
is opt-in: alix doctor --warn-on-source-locator-drift reports frozen
drift as a warning; plain doctor stays quiet about it.
7 · Directives reference
Every card marker and deck/card key in one place. Scope is where each may
appear: a deck key is a line in the frontmatter (the ----fenced YAML block
at the top of the file), a card key is a <!-- key: value --> comment at the
card’s end (comment machinery trails its card, the id line last), and deck · card keys work either way, with the card one
taking precedence. The heading rows are structure rather than keys: # scopes a
section, the rest scope a card. Each links to the chapter that explains it in
full.
| Token | Scope | What it does |
|---|---|---|
## front | card | Starts a card at column 0; the lines below are the answer. → ch 3 |
# heading | section | Opens a section: its text and the prose under it are the shared context of every card below, shown on demand (c in the web app), never while you answer. Takes no directives and no card ID. → ch 3 |
### to ###### front | card | A sub-card of the card one level shallower, withheld from review until that parent graduates. Six hashes is the deepest, since a seventh is prose. → ch 3 |
> block | card | A blockquote: a note shown after you answer when its first line is an alert badge ([!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION]), otherwise a quotation that belongs to the answer and reveals with it. → ch 3 |
<!-- --> | anywhere | A deck’s comments are alix machinery, not editorial prose. A comment that is not recognized (a directive, a locator, an id, or an invocation) is ignored and draws a doctor finding, whatever its length. Prose about a deck belongs in description:, its origin in exam:. |
choices: single / choices: multiple | card | Invocation comment on the line below a task list: maps it to a choice card, one correct answer or select-all-that-apply. A bare task list stays a literal checklist. |
cards | card | Invocation comment on the line below a pipe table: maps it to a card table. A bare table renders literally. |
plain | card · section | On the line below a task list, a table, or a thematic break of any spelling: keeps that one shape literal (for a break: the divider and thematic-break grammar, in every position, including where the break would otherwise be an error). |
choices | deck | Deck-wide default task-list mapping: single or multiple. A per-card invocation overrides it. → ch 3 |
format-version | deck | Reserved. The deck format’s version, not the deck’s own. Absence means 1, so alix never writes it; any number other than 1 is refused rather than guessed at. → ch 3 |
id | deck | The frontmatter deck ID (deck-<token>) marks an initialized deck and authorizes maintenance of missing card IDs. Its deck- prefix is what tells alix’s decks apart. → ch 3 |
id | card | The HTML-comment card ID (card-<token>) anchors review history. It is minted by alix deck init or a deck-creation workflow and maintained by alix, never hand-authored. After a card table it is the table’s container ID; each row’s card composes it with the row stamp (r:) at the end of the row, after the closing pipe. → ch 3 |
reveal | deck · card | How the answer is uncovered: flip (default) or line. Cloze is triggered by a blank: directive, never by a reveal: value. |
review | deck | The order cards are served at review: scheduled (default) or sequential. → ch 5 |
input | deck · card | type (default) or draw: answer on a canvas instead of typing. → ch 4 |
direction | deck · card | Review direction: forward, reverse, both. |
sampling | deck · card | on (default) or off: whether a card table’s rows may draw Recognize options from their own column. A table’s value overrides the deck’s in either direction. |
ignore | card | Bare <!-- ignore --> in the card’s trailing machinery: the card stays in the file with its id and history but leaves review, the exam, and the counts, and it may lack an answer. For a draft you have not finished, or a card you no longer want to learn. → ch 3 |
strictness | workspace | Exam grading rigor for the members, in alix.toml’s [defaults] only: a learner setting, so a deck declaring it gets an unknown-key lint. |
requires | deck | Prerequisite deck that gates unlocks (repeatable). |
title | deck | The deck’s display name, a single non-empty line. Without it a deck is named by its condensed trace:, else by its filename stem; a # heading is never the name. → ch 3 |
description | deck | A short summary, shown in the web picker’s deck drawer. → ch 3 |
authors | deck | Who made the deck: one value or a list. Holds people and any AI that helped, so there is no separate generated-by key. Yours to fill in; alix never rewrites it. |
license | deck | The deck’s licence, a single string, by convention an SPDX identifier. |
created-at | deck | When the deck was made, a single string, by convention an ISO 8601 date. Stored verbatim and not validated. |
link | deck | tutor reference URL, tutor-only (repeatable). |
exam | deck | Exam ground truth: a YAML list of URLs or files (one entry is the norm; alix doctor reports a file the exam cannot read), also a trace’s cited path (a directory there) and a tutor reference. It identifies evidence but never grants access to a wider local tree. A workspace alix.toml may declare a source too, as supporting context for its members. |
trace | deck | The path a trace follows; its presence makes the deck a trace. |
cite | deck | The keyed roots citations resolve against: a YAML list of one-key entries (- code: src), each a folder relative to the workspace (the deck’s folder for a loose deck) or a URL holding frozen passages. A workspace alix.toml adds roots in a [cite] table. |
at | card | A repeatable named-field locator into a cite: root (at: key:file:lines fingerprint: xxh64-..., plus asset: once frozen; a range-less path cites the whole file, a bare key a frozen URL passage): a trace checkpoint’s reveal target, or a fact card’s source citation shown on reveal. |
given | card | A trace checkpoint’s off-screen symbol, as name - meaning (repeatable). |
diagram | card | Machine-maintained stamp on the line after a ```mermaid fence, tying the fence text to its frozen image and geometry (fingerprint:, asset:, geometry:). Written by deck init, never hand-authored. → ch 6 |
blank | card | Masks and asks, in the block directly above it: a region of an image line (rect x= y= width= height= hidden="...", optional [group]) or a text span (span hidden="...", optional occurrence=/boundary=). Carries a minted b: stamp (and position: anchor on a span), maintained by alix like ids. → ch 6 |
cover | card | Masks without ever asking, for legends and labels that give answers away: rect on the image line or span in the text block directly above it. No group, no stamp, no card. → ch 6 |
crop | card | A viewport onto the image line directly above it (rect x= y= width= height=, at most one per image); region coordinates stay in full-source space. → ch 6 |
Media (images, and later audio/video) isn’t a directive: write a standard
Markdown  where you want one to appear, and its position decides
the side. See Image cards.
link vs exam
Two that look similar but aren’t. Both point at material a deck is about, but
exam is the exam’s ground truth: questions are generated from it and
answers graded against it, and a URL source doubles as a tutor reference.
link is only a tutor reference and never becomes exam material; use it
for supplementary reading the exam should ignore. The implication runs one
way: an exam URL is offered to the tutor, but a link is never promoted to
a source.
Precedence
Where a directive can come from several places, the more specific wins:
card
<!-- -->> deck frontmatter > workspace[defaults]> built-in default
So a card’s reveal directive overrides the deck’s, which overrides a
workspace’s [defaults], which overrides alix’s default (flip).
The session depth (Recognize/Recall/Reconstruct) is not in this chain either: it isn’t config or a deck directive at all. It’s chosen per session (the picker’s Depth… menu), the same way for every deck (see Reveal & session depths).
8 · Workspaces
As your decks multiply, you’ll want to treat a cluster of them as a unit: all your Spanish decks, or every deck about one codebase. A workspace is that unit: a folder of decks reviewed together, sharing settings and a name, with its own progress.
Do you need one?
For a single deck you study yourself, no. A plain .md file in your decks
folder is a complete deck: it reviews, schedules, grades, and takes
the exam the same way a workspace member does. A
workspace adds settings and files around a group of decks; around one deck
there is nothing for it to do.
Reach for a workspace when one of these becomes true:
- Several decks belong together and should share settings. The
[defaults]table is written once and every member inherits it, instead of the samedirection:orreveal:line at the top of six files. - One deck should come before another.
requiresonly means something between members: it decides which decks are unlocked yet, and draws the dependency tree in the picker. - The material has a source you want to keep. Only a workspace member can
freeze its evidence: initializing one copies each cited source excerpt
and each local image into
assets/deck-<token>/, and renders each mermaid fence into a frozen diagram there, so the deck still shows them when the original file has moved on, and still shows them on a machine that never had the source at all. A deck outside a workspace keeps its images beside it and its citations live, so if the cited file moves or changes, the card shows a warning in place of the quoted lines. - You want to hand the whole cluster to someone else. Sharing a workspace carries its members with their frozen assets and generated augmentation, and strips your progress. A loose deck has nothing frozen to carry, so what arrives is the file itself and whatever the recipient can still resolve.
- There is a date you are working toward. A
deadlineand the pacing ramp it drives are read only inside a real workspace; see Personal pacing.
What it costs: a folder with an alix.toml instead of a single file, and
members living in a decks/ subfolder rather than loose.
Changing your mind later is a manual move, not a command. alix deck copy/move transfer between workspaces: both the source and the
destination have to be workspaces already (see Moving decks between
workspaces), so neither promoting a loose
deck nor demoting a member is covered. Promoting one means creating the
workspace and moving the file into its decks/ yourself. The ids inside the
file are untouched by that, but progress now resolves at the new workspace
root, so run alix doctor on the result and check
that the deck still reports the history you expect.
Making a workspace
A workspace has an alix.toml at its root and its initialized .md decks
as direct children of decks/. The manifest is a scoped version of the global
config file. It sets a title and a [defaults] table of directives that every
member deck inherits:
# ~/decks/spanish/alix.toml
title = "Spanish"
[defaults]
direction = "both"
reveal = "line"
Besides title, description, icon, a shared exam, and a [cite]
table of citation roots
every member sees, the manifest may set a top-level source_access, which overrides the global
[ask] source_access for this workspace’s decks in either direction (see
the tutor). The manifest travels with the folder when
shared, so review a received workspace’s alix.toml before an AI call.
Starting from nothing instead? alix workspace init <dir> (--title to name
it) scaffolds an empty workspace: an alix.toml, an alix.local.toml, and an
empty decks/ plus assets/. Both TOML files come fully commented, each key
explained inline, so they document themselves. The fixed layout is:
spanish/
├── alix.toml
├── alix.local.toml
├── decks/
│ └── verbs.md
├── assets/
│ ├── icon.svg
│ └── deck-<token>/
│ └── sha256-<digest>.<ext>
├── .alix/
│ ├── progress/deck-<token>.json
│ └── recent.json
└── augment/
Grow the workspace with
alix deck generate … --into <dir> or
alix deck import … --workspace <dir>, also available from the web UI’s ☰
menu’s Add deck… sheet. Dependencies (requires:) are still edited by
hand in the deck files.
Put hand-authored decks under decks/, then run alix deck init <file> once
for each one. Markdown without a valid opening-frontmatter id: deck-<token> is
ignored by discovery. Root-level Markdown is never a workspace member, so
README-style prose and notes can live beside alix.toml without becoming picker
entries or being stamped.
Initialization also makes the member portable. Cited excerpts are copied from
explicit source files and source directories alike (never a whole file or
repository), and local card images are copied into assets/deck-<token>/. Every managed filename is the
SHA-256 address of its exact bytes. The deck is not initialized successfully if
required evidence or an image cannot be copied. After a successful freeze,
initialization deletes every file in assets/deck-<token>/ that the deck no
longer references, such as the excerpt of a citation whose range changed.
Updating from the live source
Frozen evidence is deliberately stable. It does not follow later source edits in the background. Reconcile every frozen source-backed member explicitly:
alix workspace update ~/decks/spanish
The command gives its AI backend read-only access to each recorded local
exam, then writes one exact proposal into a dot-prefixed sibling workspace.
The original workspace remains untouched. Inspect the proposed decks and
evidence there, then publish those exact bytes without another model call:
alix workspace update ~/decks/spanish --apply
Use --discard instead to remove the proposal. Apply refuses if an original
deck changed after staging.
Every member is checked for frozen evidence before the first model call, so an
unfrozen deck stops the command immediately and costs nothing. After that, a
member whose proposal comes back unusable is reported as that member and left
exactly as the proposal found it, while the other members still stage. The
command exits non-zero and names each one, the reasons are recorded in the
proposal so --apply reprints them, and applying touches only the members that
did update. If every member fails there is no proposal to apply, and the
staging directory is left for inspection.
A card ID belongs to one learning proposition. An unchanged question and answer may keep its ID while its note or source locator improves. If the question, answer, cloze, or learning image changes, the old card and ID retire together and the replacement receives a fresh ID during staging. Obsolete cards are removed rather than rewritten in place under their old learning history.
The first update implementation accepts local file and directory sources. A remote URL source remains review and tutor context, but cannot yet be captured as a new portable snapshot.
Moving decks between workspaces
A workspace deck owns more than its Markdown file. Transfer it with Alix so its frozen evidence and augmentation follow the stable deck ID:
alix deck copy ~/decks/spanish/decks/verbs.md ~/decks/exam
alix deck move ~/decks/spanish/decks/verbs.md ~/decks/exam
Both commands preserve the filename, deck ID, and card IDs. Copy installs the
same public bundle that wormhole sharing sends: the deck,
assets/deck-<token>/, and augment/deck-<token>.json. It never copies progress.
Move requires confirmation, installs that public bundle first, carries
.alix/progress/deck-<token>.json to the destination workspace, then removes
the source.
The destination must be another Alix workspace. Transfer refuses overwrites, stable-ID collisions, missing required decks, and moves that would break a source deck’s dependents.
Transfer never rewrites a deck’s locators, cite: or exam:, and changes
neither workspace’s alix.toml. It is allowed only when every key the deck’s locators
use resolves in the destination, through the deck’s own cite: or the
destination’s [cite] table, to the same root as in the source. Local roots
are compared as absolute paths with symlinks resolved; URL roots must match
exactly. A relative root resolves against its workspace folder, so a key
written as code = "crates" in both manifests names two different folders,
and the transfer refuses. The refusal names each key and both roots. Keys that
no locator uses are not checked.
The same holds for exam:. The deck’s exam material is its own exam: list
plus the workspace’s exam, and every entry must resolve to the same file,
folder or URL in the destination as in the source. In the destination the
deck’s own entries are read against the destination workspace folder, and the
destination’s exam takes the place of the source’s. Paths are compared like cite: roots, URLs exactly, and the
lists are compared as sets. A relative exam: material therefore names a
different folder after the transfer, and the transfer refuses; so does a deck
whose workspace exam the destination does not declare, or whose destination
adds exam material the source did not have. The refusal
names each entry and what it resolves to on both sides, in one message with
any cite: refusals.
Now open the cluster and drill its members one at a time:
alix ~/decks/spanish/
Shared directives
The [defaults] keys are the deck-directive names reveal, input,
review, direction, and sampling from
the deck format,
plus strictness: the learner-side exam rigor, which
a deck itself cannot declare. They fill in only what a deck doesn’t set
for itself, so the precedence is one level deeper than before:
card
<!-- -->> deck frontmatter > workspace[defaults]> built-in default
Set direction = "both" once for the whole folder, and a single irregular deck
can still override it with its own direction: forward in its frontmatter. It’s
the same directive system from chapter 3, just sourced from one more place.
Personal pacing: alix.local.toml
The alix.toml is shared: it travels with the workspace when you hand it to
someone. Your personal review pacing doesn’t belong there. Drop an
alix.local.toml beside it to override the global [review] config (FSRS
retention, retire_after, introduction_cooldown, and the pacing keys
max_session / new_cards_percent) for this workspace’s decks only:
# ~/decks/spanish/alix.local.toml
[review]
retention = 0.95 # see these cards more often
retire_after = "never" # never let them retire
max_session = 20 # bigger sittings for this deck
new_cards_percent = 40 # lean harder on introducing new cards
deadline = "2026-09-01" # a personal "ready by" date, the day itself inclusive
deadline_ramp = "14d" # how early the pre-deadline retention ramp starts
It uses the same [review] keys as the config file, and
it’s kept separate from alix.toml on purpose, so it stays yours and never
travels when you share the workspace. A missing or malformed one is simply
ignored. Its .local. name follows the same private naming rule as a deck’s
.local.md personal file.
deadline and deadline_ramp only take effect inside a real workspace
(a directory with an alix.toml). Set them on a plain decks folder, or on a
loose deck’s alix.local.toml, and they parse but do nothing: no scheduling
ramp, no picker readout, no doctor warning. See
Configuration for the full reference and
Scheduling for what the ramp does to review.
The session depth (Recognize/Recall/Reconstruct) isn’t a workspace setting. It’s picked per session, the same as for a loose deck (see Reveal & session depths).
Warming the whole workspace
alix workspace augment <dir> --target <...> precomputes AI augmentations for
every member deck at once:
alix workspace augment ~/decks/rust --target choices
alix workspace augment ~/decks/rust --target icon
The card targets (choices, notes, questions, keypoints, format) run
as one batched call over every member’s cards, not one call per deck, and
each card’s result is filed back under the deck that owns it in augment/.
icon draws the workspace emblem and is the target only a workspace has; it
redraws unconditionally, replacing the old one.
There is no --target order here. A review order is built per deck, so it
stays on alix deck augment.
Its own files
A workspace keeps shareable material and private learning state at the workspace root:
augment/deck-<token>.json # shareable generated choices, notes, and topologies
assets/deck-<token>/ # shareable frozen excerpts and local images
.alix/progress/deck-<token>.json # private schedules, history, exam state
.alix/exam/deck-<token>.json # private exam idea lists and what was asked
.alix/exam/deck-<token>.prepared.json # a written exam waiting to be taken
.alix/recent.json # private recent-deck history
Renaming a deck file leaves these paths unchanged because the name comes from
its deck id (deck-<token>), not its display name. Private files are colocated
with the workspace, so folder synchronization carries progress too.
That makes a workspace a self-contained, portable unit for moving, backup,
and folder synchronization: authored decks in decks/, frozen excerpts and
images in deck-owned assets/deck-<token>/ directories, workspace icons directly
in assets/, and shareable augmentation all live under one boundary. Sharing
strips progress and local configuration while carrying the matching
augmentation and assets. Decks outside any workspace keep shareable material
beside the deck and private files in that deck’s containing folder. The CLI
commands (alix stats/list/reset) take a deck file, a plain folder, or a
workspace: a folder or workspace expands to its member decks, and every
command resolves the same colocated .alix/ root the launcher uses.
In the picker
Folders show up in the picker in two flavors: a folder with alix.toml and
initialized decks/*.md members appears under Workspaces; one without a
manifest is a plain Folder whose initialized decks are direct *.md
children. Opening either drills in to its decks, drawn as a dependency tree:
each deck nests under the prerequisite that gates it, foundations at the roots
(the next chapter). A trace member carries a trace
badge (facts decks are unbadged), and the drill-in is a single-launch list:
Enter on a facts deck
reviews it, Enter on a trace traces it. Typing a filter flattens the tree
to a plain search.
In the web picker, a workspace can show a small emblem in place of the
chevron, so a long list of similar-named workspaces is quicker to scan. Drop an
image in the workspace’s assets/ and point icon = "assets/<file>" at it in the
alix.toml (or just name it assets/icon.{svg,png,jpg} and skip the key); an SVG
is tinted to the active theme, a raster shows as-is. When you build a workspace
with alix workspace generate <dir>, the model draws an abstract SVG emblem from
the topic automatically, unless you pass --icon <file>.
alix <dir> serves a workspace directly: the picker opens drilled into that
view, scoped to the folder and its own store, routing each
member to the right experience (a facts deck to a review, a trace deck to a trace) and
returning you to the picker when you finish one. (A session is one deck file, so
a whole workspace is never reviewed at once; open it and pick a member.)
A folder without a manifest serves the same way with alix <folder>; it
just applies no shared directives.
Sharing a workspace
Sharing a workspace sends its alix files, not the whole folder: the
alix.toml manifest, decks/, the assets/ its decks own, and their
augmentation. Everything else at the workspace root, such as the source trees
your cite: roots point at, notes, or a README, stays home: the frozen
excerpts in assets/ already carry what the cards show. The phone’s paired
sync sends the same set.
alix share <dir> does that over magic-wormhole with the personal files
(.alix/, *.local.*, backups, and conflict copies) left home; the
other side runs alix receive <code> and gets it beside their own decks, ready
to serve with alix <dir>. Precomputed augmentation documents matching the
shared decks travel: the AI content comes along, unrelated augmentation and
progress do not. A single-deck share carries the .md member, its complete
assets/deck-<token>/ directory, and its matching augmentation. Also available
from the web UI’s ☰ menu
(Share… / Add deck… → Receive), with a .zip download/upload fallback
when neither side has wormhole installed.
Titles
A single deck’s display name comes from its frontmatter title: (failing that,
its condensed trace:, then its filename stem); a workspace’s name comes from a
title in its alix.toml. Either replaces the file
name in the picker, the session header, alix list, and alix stats. It’s
display-only: you still refer to decks by file path on the command line, and a
title never affects a card’s identity.
9 · Dependencies & unlocks
Real subjects have an order: you can’t grasp borrowing before ownership, or a
derived rule before its axioms. alix lets a deck declare what it builds on, and
uses that both to sequence your study and to gate decks until you’re ready.
Declaring prerequisites: requires:
A deck names its prerequisites with a requires: list in its frontmatter
(repeatable):
---
requires:
- rust-ownership
- deck-9w2c7x4k1m8q3z5t0v6b2n4d8f
---
## What does the borrow checker prevent?
Aliasing a value while it's mutably borrowed.
Each entry is a filename or a deck id. A filename (rust-ownership,
with or without the .md) resolves next to the requiring deck or in your decks
directory. A deck id (deck- followed by a 26-character token) resolves to the
deck that carries that id wherever it lives, so the edge survives the
prerequisite being renamed. An entry counts as an id only when it is exactly
deck- plus a 26-character canonical token and nothing else; any other value,
including a deck-… name written with the .md extension or a ./-prefixed
path, is read as a filename. Prefer the id form for a rename-proof edge, the
filename form for readability. Directives aren’t card content, so adding or
changing them never touches card progress. A missing prerequisite or a
dependency cycle is treated as non-blocking. A broken edge never hides a deck.
Dependencies don’t change what you review
requires: is about order and gating, not session contents. When you review
(or browse) a deck, the session holds exactly that deck’s cards; prerequisites
are never pulled in, so the reveal/review you study under is always
the deck’s own. What dependencies shape is the picker’s dependency tree
(foundations shown first) and, for a deck with an exam:, the exam gate
below.
Unlocks
The same requires: graph drives unlocks, with no extra syntax, and the
gate is the exam, not drilling. You can review any deck at any time, in any
order; what requires: controls is exam order: a deck with an exam:
can’t sit its exam until each of its sourced prerequisites has passed its own
exam, and passing a foundation’s exam unlocks the exams that build on it. A
prerequisite with no exam: at all has no exam to pass, so it never gates:
its edge is just a suggested order in the tree. (alix doctor warns when an
exam-grounded deck requires one without exam grounding, since that edge can’t
gate an exam; add an exam: to the prerequisite to make it real. It also flags
a dangling requires:, one naming a deck that does not exist, so a renamed
or deleted prerequisite is caught rather than silently dropping the edge, and
distinguishes an id-mode value that points nowhere from a card id pasted by
mistake or a file that merely shares a required id’s name; see
the doctor reference.) A trace deck has no exam,
so a required trace never gates.
A prerequisite counts as passed once every one of its sections has passed at
[exam] pass_threshold, across one sitting or several. That moment is kept: a
later weaker sitting never locks a dependent again. A deck that has sat its own
exam at least once, pass or fail, stays unlocked from then on, even if a
prerequisite’s exam is reset later; the reset can only lock dependents that
never sat theirs.
In the picker a deck whose exam is locked shows a 🔒, but it stays drillable: only the exam waits on the prerequisites.
This is what turns a folder of decks into a curriculum: order the material by
requires:, and alix gates each step’s exam on passing the last. It’s the
backbone of the AI exam’s notion of passing (a later chapter) and of how
alix workspace generate lays out a generated learning plan.
10 · The Tutor
This is where the AI layer begins. Everything so far (drilling, scheduling,
workspaces) runs entirely offline. From here on alix shells out to the
configured model CLI, and the first place it does is the most useful: a tutor
on any card.
(One reminder: every AI feature shells out to the configured model CLI, so it needs the CLI installed and logged in. See chapter 2. The flashcard core never calls it.)
Asking about a card
On any post-answer screen (a revealed flip card, the feedback after a typed
answer, an answered choice) an Ask button (or the a key) opens a chat
panel without leaving the session: type a question, Send, Make this a note,
Close. alix hands the tutor the card (its front, answer, note, and deck
name) as context, and you can ask “why is that the answer?”, “what’s a
simpler way to see this?”, or anything else, and follow up. The server runs
the model CLI on a background thread and the page polls for the reply, so
the single-threaded server never blocks and the session stays responsive
while it works.
In the web panel, Enter inserts a newline and Shift-Enter sends. Closing a
tutor that contains a conversation moves the caret out of the chat box and
asks for confirmation: Enter chooses Leave anyway, while Escape
chooses Stay and returns the caret to the chat box. Moving on to the next
card drops the conversation and a question still being answered, but a note
still being made lands on the card you asked it on: the panel says the tutor
is still working on the previous card until it does, a note saved after you
closed the panel is announced on the page, and the next card’s tutor waits
for that slot.
Tutor replies use the same content grammar as a card. Ordinary explanation stays prose, backticks mark inline code, and multi-line code arrives in a fenced block that the panel renders as code rather than showing the fence markers. The raw reply is still kept for follow-up questions and for making a note or card.
One conversation spans the whole review run. For Claude, alix uses
--session-id for the first question and --resume for each follow-up, so the
model remembers earlier cards and questions efficiently. Other backends re-inline
the accumulated Q&A transcript into each prompt, so the context carries over, at
the cost of a growing prompt rather than a resumed session. Either way you can ask
how the current card relates to one from ten minutes ago, and the tutor knows.
Ask is available wherever you serve, including over --lan, but the request
runs the model CLI on the host machine, so, like --lan in general, only
enable it on a network you trust.
Saving what you learn: Make this a note
When an exchange clears something up, press Make this a note: the tutor extracts up
to three key points from the exchanges since the last note, one short line
each, and appends them, addressed to the card, to the deck’s personal sidecar
(<deck>.local.md); the deck file itself is untouched. It does not repeat
what the card’s existing notes already say, and saves nothing when those
notes cover every new point. Each point has to stand on its own with only the
card in front of you: an example that exists only in the conversation is
either carried into the note as a short code block or left out, so a saved
note never points at a chat that is gone. Notes aren’t part of the card’s
identity, so its progress is untouched: you just keep the insight. The action
has no default key; bind one with make_note in [keys.review] (see
Configuration).
Make this a note and Make this a card stay disabled until the tutor has answered something newer than the last note or card made from the conversation. They disable again while any tutor call is running and after a note is saved or a card draft lands, then re-enable on the next answer.
Reference links: link:
A deck can point the tutor at background reading with a link: list in its
frontmatter:
---
link:
- https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
- https://tokio.rs/tokio/tutorial
---
These are handed to the tutor with your first question as material to consult when
useful: fetched once and remembered for the rest of the run. They’re tutor-only:
unlike exam: (the exam’s ground truth, covered next chapter), a link:
never becomes exam material. And like every directive, they don’t affect a
card’s identity.
Grounding a frozen card: exam: and cite:
A frozen workspace card is grounded in its deck-owned assets/deck-<token>/
evidence. The tutor receives the exact excerpt shown during review, so deleting
or editing the live source cannot silently change its ground truth.
The deck’s exam: (and a workspace’s exam) is listed to the tutor as
context. A URL entry is fetched when the selected backend can use WebFetch.
The tutor always receives the frozen excerpt first; current source context can
explain the surrounding material or detect drift, but never silently replaces
the captured evidence.
Local file browsing is opt-in with [ask] source_access = true, and a
workspace’s alix.toml may carry its own top-level source_access key,
which overrides the global setting in either direction for that workspace’s
decks. The manifest travels when a workspace is shared, so inspect a
received workspace’s alix.toml before making an AI call over it. With
access on, the tutor browses one local cite: root, chosen per card:
- the root of the first locator (in card order) whose key names a local root;
- for a card without such a locator, the deck’s only local root, when the deck and its manifest declare exactly one;
- otherwise none. A URL root is never browsed, and an
exam:entry never grants browsing.
This keeps every live-source grant reviewable: the grant is on in your config or a manifest you can read, and the folder is a root the deck declares.
With the Claude backend, the tutor reads only inside that root: its file tools
are allowed under the root’s resolved path alone, and the call loads none of
your Claude settings files, whose own allow rules would otherwise widen it. A
root whose path contains (, ), *, ?, [, ], { or }, or any
root on Windows, gets no file tools. Gemini, Codex and Copilot do not confine
reads to the root this way.
When no usable source is available, the tutor still works from the frozen excerpt and card context. The Ask status warns that it lacks the full current source, so the learner can distinguish an evidence-grounded explanation from a freshness check against the live source.
How it’s sandboxed
Because the CLI runs headless, it can’t show interactive permission prompts: an
unanswerable prompt would just hang the call. So alix runs it locked down with a
locked permission mode plus an exclusive tool allowlist (WebFetch, WebSearch
by default). On the Claude backend that list is exclusive in both directions: the
listed tools work without prompting, and no other tool exists for that call, so a
malicious page behind a deck link can’t make the tutor run shell commands or touch
your files. The other backends get the same list, but there alix can only
pre-approve it, not bound it, so that backend’s own defaults and your provider
configuration still decide what else it could reach. Both the permission mode and
the allowlist live in the [ask] section of the config, along with the command, a
--model override, and the timeout.
alix also runs the CLI without your instructions for it. Claude Code reads
CLAUDE.md, Codex reads AGENTS.md, Gemini reads GEMINI.md, and those
instructions would otherwise shape a reply alix parses strictly: a rule as
ordinary as “end every answer with a timestamp” is enough to break exam grading.
So alix asks each CLI to skip them, along with your hooks, skills and MCP
servers. Gemini offers no way to do that, so a GEMINI.md in scope still
applies on that backend.
Make this a card
During an Ask exchange, if the tutor’s reply answers a question about a concept you’d like to drill, click Make this a card. The tutor distills the conversation into a draft front/back for you to edit. Once you’re satisfied, click Add to land it as a new card on the current deck. It works the same during a walk; the new card joins the deck’s next walk.
The card goes into the deck’s personal file
(<deck>.local.md), not the deck itself, so the authored .md is left
byte-identical. It joins your sessions from then on as a new card: the drill
introduces it, then grades and schedules it like any other card. It is a plain
Markdown block in a file you can open and edit.
This is an adult-review feature only; it’s not available in the kids interface.
If the tutor’s draft can’t be parsed as a valid front/back pair, alix reports the
error plainly rather than inventing a card, so you can ask for a clearer format.
11 · Generating decks: alix deck generate
Authoring cards by hand is the slow part of any flashcard habit. alix deck generate removes it: point it at a source and the model drafts a deck of fact
cards for you.
alix deck generate https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
alix deck generate src/scheduler.rs # a local file
The source is a web page URL, a local file, or a directory taken
whole. Generation lives under the noun it produces, and the noun names what you
get: alix deck generate always writes one deck, alix workspace generate
(chapter 14) always explores a directory for a learning plan
and builds a workspace of decks and traces. Adding --trace to alix deck generate makes that one deck a trace (chapter 13).
While the model works, alix prints short progress updates to stderr, such as
source fetching, source reading, and drafting. Partial generated cards stay
hidden until the complete result has passed validation. Deck drafting has a
one-hour absolute limit. With a structured-event backend, every generation
path also has a five-minute inactivity limit that resets on each real agent
event. For a backend without structured events, that five-minute value becomes
a nonrenewing absolute fallback because Alix cannot distinguish silence from
work. Set idle_timeout_secs = 0 to disable either use and leave only the
one-hour limit. Configure the limits under [generate]. Trace and workspace
planning calls keep their absolute limit under [trace].
Also available from the web UI’s ☰ menu (Add deck…), URL sources only. See the web app.
What you get
The model reads the source and returns a deck spread across four layers of
understanding (facts → concepts → application → connections) using
cloze cards for terminology. The prompt has it draft, then re-read the whole set
and merge or drop cards that test the same fact, so the deck doesn’t repeat
itself. alix validates the text it gets back (it only ever accepts cards, never a
write or shell command) and writes it to ~/decks/<slug>.md.
How the source is recorded depends on its kind, and it matters later:
- A web page is read with the
WebFetchtool, and the deck opens with alink:line back to it, so the tutor can consult the page on your cards. - A local source is explored read-only with
Read/Glob/Grep, and the deck opens with aexam:line, so the AI exam can later grade your understanding against that same source (next chapter). Each fact that maps to specific lines also gets a fingerprinted<!-- at: -->citation, so you can flip the card to its source on reveal without trusting a shifted numeric range. The deck declares the source’s folder (a folder source itself) undercite:as the keysrc, and its locators readsrc:<file>:<lines>. With--into <workspace>, a manifest[cite]root that already holds the source is reused instead; when none does and the manifest already namessrc, generation stops before calling the model.
Useful flags
alix deck generate <source> -o ownership # choose the output file name
alix deck generate <source> --cards 15 # aim for at most 15 cards (a soft ceiling)
alix deck generate <source> --review # a 2nd pass that dedups and tightens
alix deck generate <source> --print # print to stdout instead of writing a file
alix deck generate <source> --into ~/decks/rust/ # into that existing workspace's decks/
alix deck generate <source> --goal "pass the citizenship test"
alix deck generate <source> --language German --audience "new voters"
alix deck generate <source> --card-style authored-choices
--goal controls what the learner should understand for every new deck or
workspace, including a single deck generated from a URL or file. --language
sets the language of fronts, answers, choices, and notes. --audience steers
vocabulary, assumed knowledge, examples, and difficulty.
--card-style accepts mixed (the default), plain, cloze, or
authored-choices. Authored choices use the deck’s GitHub task-list format,
with one checked correct answer and unchecked distractors. Alix parses the
result and refuses a generated facts deck containing a card of the wrong shape,
so a model cannot silently turn an authored-choice request into ordinary
recall cards. In a generated workspace the style applies to every [deck]
item; [trace] items keep their predict-and-verify checkpoint shape. Goal,
language, and audience apply to both.
--review runs a second model call that takes the draft and returns a
deduplicated, tightened version while preserving the requested language,
audience, and card style. It costs an extra call, but it’s worth it when the
source is repetitive. The prompt and defaults (model, timeout_secs
(default 3600), idle_timeout_secs (default 300; structured inactivity or an
unstructured absolute fallback, and 0 disables),
max_cards (default 100, a soft ceiling: an overshoot is kept and warned about), language, audience, card_style, and an extra
instruction field) live in the [generate] section of the config.
Generate, then own it
A generated deck is just a plain-text deck like any other: read it, edit it, cut
the weak cards, add your own. Treat the output as a strong first draft, not
gospel. The point is to skip the blank page, not to outsource judgment. That’s
the same division the whole tool runs on (see how alix was made).
12 · The AI exam
This is the feature the whole tool is built around. Drilling cards loads a deck’s material into memory; the AI exam checks that you actually understood it, and passing the exam, not merely finishing the cards, is what marks a deck done and unlocks what depends on it.
The reasoning: recall isn’t understanding. You can drill every card and still not see how the ideas connect. So a deck can name a ground-truth source and require you to pass an exam against that source before it counts.
Declaring the exam material: exam:
Name one or more sources in the deck’s frontmatter, each a URL or a file. A
directory is not exam material: the exam refuses to start on one, and
alix doctor reports any exam: file it cannot read:
---
exam:
- https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
- notes/ownership.md
---
A URL exam: doubles as a tutor reference, so you
needn’t repeat it as a link:. The reverse doesn’t hold: a link: stays
tutor-only and never becomes exam ground truth: keep supplementary reading (a
blog post, an SO answer) as link: so the exam ignores it.
The exam reads only exam: entries (the deck’s own and its workspace
manifest’s). It never reads cite: roots or the text your cards cite: to
examine a cited file, list it under exam: as well.
Once every card in an exam-grounded deck has been answered right at least
once (a passing review at any depth, Recognize included, or a walk graded
Got it or Partly) the deck is exam due. It stays exam due, also
after passing, until a full sitting masters it, so retaking stays one tap
away; passing alone already unlocks its dependents. A deck with no exam: at all (in the deck or its
workspace manifest) simply becomes finished when all its cards graduate,
unlocking its dependents directly, even when it declares cite: roots.
Sitting the exam
An exam is written ahead, then taken. Writing the questions is a long model call (several minutes at a high effort), so it runs in the background while you do something else, and the finished exam waits until you sit it. The sitting itself is a guided, one-question-at-a-time flow (answer, move Back/Next, then a per-question breakdown). It is one of a deck’s launches, next to the review depths and Walk, and you can sit it at any time as practice, before drilling too:
- In the browser: the deck drawer’s Generate exam chip (key
x) starts writing the exam on the server. While it runs, a Writing exam chip shows the minutes so far and a rough wait, beside a Stop chip; a short notice says when the exam is ready, and the chip then reads Take exam (keyx). The chip is filled when the deck is exam due and its prerequisites are passed, and shows a lock while a prerequisite is not passed. Choosing an exam-due deck itself drills; it never starts the exam. - On the phone: the deck’s launch sheet has an exam row below Walk that reads Generate exam, then Preparing with the rough wait, then Take exam; the picker says when the exam is ready. The phone sits the exam through a paired desktop, so the row is disabled while no desktop is reachable.
Generate exam offers only what needs work, on by default: the exam then asks only about the sections that have not passed (those that need work, have new cards, or were never examined). The control is always shown and disabled when every section has passed, since the exam then covers the whole deck either way.
A prepared exam is kept until it is graded. In the browser it lives in the
deck’s store (.alix/exam/<deck-id>.prepared.json), so it survives a closed
page or a restarted server, and your answers are saved into it as you give
them. On the phone, the desktop holds the prepared exam in memory and the
phone saves each answer as you move on from it; a restarted desktop loses
the phone’s prepared exam, and End exam discards it. Either way, an
answer locks once you move on from it, and leaving returns you to the first
unanswered question next time. Grading uses the exam up. In the browser,
editing cards or section prose leaves a prepared exam valid, a section whose
heading was renamed or removed since is dropped from it, and changing the
exam material discards it, so the next Generate exam writes a new one.
Writing the exam gives ids to cards added since the deck was last opened, the way opening a review does, so their results can be recorded.
alix asks the model to read the source (URLs via WebFetch, local files
embedded) and write fresh understanding questions for each of the deck’s
# sections (cards before the first heading form one section, named after
the deck). Each section keeps a list of its ideas, the distinct mental
models its topic holds in the source, whether or not a card states them. A
first call writes that list for every section that has none yet, or for all
of them when the exam material changed. Each exam then picks three ideas
per section: ideas never asked first, then ideas last failed, then ideas
asked longest ago, so repeated exams work through the whole list. A second
call writes exactly one question per picked idea and asks the student to
apply an idea a card states rather than recite the card. A reply that skips
a picked idea, asks one twice, or names an idea or section that was not
listed is rejected and generated once more. Grading records each asked idea
as passed or failed, which steers the next pick. The browser keeps the idea
lists in .alix/exam/<deck-id>.json; the phone keeps its own and sends them
with each exam.
Within a section the questions cycle through three shapes: a concrete instance no card shows, a boundary (when does the idea stop holding), and a fix (a statement with one seeded mistake to find and correct). The section’s heading, prose and cards go into the prompt only as scope, so the model knows what the deck already asks and avoids it. A question stands alone: it shows any program or claim it asks about instead of pointing at the source, and a program comes as a code block with numbered lines, so an answer can name “line 3”. Questions, feedback and key points render inline code and math. Each question comes with the key points a correct answer must hit, shown only with the results. You type a prose answer per question, and an examiner grades each Pass / Partial / Fail against the source’s rubric, never against your cards (grading the cards would be circular). The model calls run on a background thread, so the UI stays responsive while it thinks.
Results: a count, per section
A sitting reports points, not a verdict: each Pass is worth 1, each Partial
0.5, each Fail 0, shown as 17 / 18 or 17.5 / 18, with the same count for
each examined section and the shape of each question. Every examined card
records its section’s latest result:
- A section passes when its share of Pass answers reaches
pass_threshold(a fraction; 1.0, every answer, by default; a Partial counts as no Pass here). Each examined card then stores its section’s verdict, so a section reads as needs work when any of its cards carries a failed verdict, has new cards when some cards were added since its last sitting, passed when every card carries a passing one, and never examined when none carries one. - The deck is passed the first time every section has passed, whether in
one sitting or across several filtered ones. Passing, not mere drilling, is
what unlocks decks that
requires:this one, and a deck that has sat its exam once is never locked again by its own prerequisites. Source-less decks are unaffected: finishing them just means drilled (done ✓). - The deck is mastered the first time a full sitting (every section) scores every answer Pass; a filtered sitting never sets it.
Neither passed nor mastered is taken back by a later, weaker sitting. The
results screen and the deck row both name the highest of them with the day
it was reached, and the row adds the last full sitting’s day and points:
passed (2 Nov) · last (4 Nov) 12 / 18, mastered (4 Nov) · last (4 Nov) 18 / 18, or last (4 Nov) 15 / 18 when examined but not passed. A date
outside the current year names the year (31 Dec 2025).
Whenever an answer missed key points, the results offer to turn the gaps into remediation cards: a cloze card or a plain card for a missed fact, an open understanding card (a prompt plus key points) for a missed concept, with overlapping gaps merged. Re-drill those and re-sit. Once created, the screen reports how many remediation cards it added.
Those remediation cards land in the deck’s
personal file (<deck>.local.md),
never in the authored deck. They start as new cards: the drill introduces them,
then FSRS schedules them like any other card. Once scheduled they count toward
the deck’s due total but not toward its card count. Regenerating the same gap won’t
duplicate it; once a card’s interval reaches the retirement cap it’s archived,
and re-failing the gap brings it back. Because they are plain Markdown blocks
in a file you own, you can read, edit, or delete them yourself.
Resetting a whole deck (alix reset <deck>) also clears its exam state, so a
re-drilled deck must pass again; alix reset <deck> --exam clears only the
exam state (each card’s result, passed, mastered and the last count) and
names the decks whose exams it will lock. Resetting only an individual card
(--card) leaves the deck’s passed and mastered state intact.
Strictness: match the rigor to the material
How hard each answer is judged fits the material: a checklist topic (a procedure,
exact syntax, a security drill) should fail you for omitting a step; a conceptual topic
shouldn’t. It’s a learner setting, not per deck: the [exam] strictness config default,
optionally overridden per workspace in alix.toml’s [defaults]. The levels:
- strict: completeness required: every rubric point must be present, so omitting one is a gap.
- balanced (default): judges understanding, not phrasing: a point counts if your answer shows you grasp it, even briefly; only a wrong or genuinely-absent idea is a gap.
- lenient: benefit of the doubt: only clearly wrong or unanswered points are gaps.
This dial (how hard each answer is judged) is independent of pass_threshold (the
share of a section’s answers that must pass). Both, plus model, effort,
timeout_secs (default 3600) and an extra guidance field, live in the
[exam] config section. effort sets how hard the backend thinks while
writing and grading the exam, over [ask] effort; a workspace may set its
own in alix.local.toml’s [exam] section.
Why this is the centerpiece
Everything else serves this. The drilling loads the facts; the exam is the gate
that turns “I reviewed it” into “I understood it, and here’s the check.” It’s also
why passing (not completion) drives unlocks: a curriculum
should open the next door only when you’ve genuinely passed through the last. The
everyday, self-graded rehearsal for it is explain mode
(chapter 4); the exam is the real thing.
13 · Trace decks
Experimental. Traces are new and still evolving — the deck format and the flow may still change.
Cards drill facts — the nodes of what you know. A trace drills the connections between them — the edges — by tracing a path through a real source and making you predict each hop before it’s revealed. Where the AI exam verifies a set of independent answers, a trace verifies you can follow one chain of reasoning, and the gap between your prediction and the truth is where the understanding forms.
This is the most direct expression of the book’s opening bet: understanding is the chain of because-this-therefore-that, and a trace makes you build that chain yourself.
What a trace looks like
A trace is a deck with a trace: (a path description — what it traces, and the
thing that marks the deck a trace) and a exam: (the path’s origin), then a
sequence of checkpoint cards. Each checkpoint is an explain-style card — an
open predict prompt and the key points a good prediction should hit — plus a
<!-- at: --> locator pointing at the real lines in the source:
---
trace: how `let s2 = s1` moves a String and avoids a double free
exam: .
cite:
- book: src
---
## You write `let s2 = s1`. What gets copied onto the stack, and what stays shared?
Only the stack data (pointer, length, capacity) is copied.
So s1 and s2 point at the *same* heap allocation.
<!-- at: book:ch04-01-what-is-ownership.md:290-297 fingerprint: xxh64-0123456789abcdef -->
> [!NOTE]
> The heap contents themselves are never copied here.
## So s1 and s2 point at one heap allocation. What breaks when both go out of scope, and how does Rust stop it?
Both would call drop on that memory (a double free).
Rust treats the assignment as a move: s1 is invalidated, so only s2 frees it.
<!-- at: book:ch04-01-what-is-ownership.md:322-343 fingerprint: xxh64-123456789abcdef0 -->
> [!NOTE]
> Using s1 after the move is a compile-time error.
The trace description, checkpoint prompt, given values, key points, and note
all support the same inline Markdown and LaTeX rendering as ordinary cards.
Inline-code terms in a checkpoint’s key points are also highlighted wherever
they occur in its revealed source excerpt. Matching is exact and case-sensitive,
so the author controls the emphasis by choosing which terms to put in backticks.
The <!-- at: --> locator’s at: field is a single contiguous range
key:file:start-end under one of the deck’s cite: roots (the keyed form is
described with source citations;
a range-less path cites the whole file, a bare key the frozen passage of a URL
root), never comma-separated, since a stitched excerpt makes disjoint code look
adjacent. Its
fingerprint: xxh64-... field fingerprints the displayed lines. A live trace
reveals the source only while that fingerprint matches, so a shifted numeric
range cannot silently show unrelated lines. When a
tight excerpt leans on a
symbol defined off-screen, name it with a <!-- given: --> line (<!-- given: state — the parser's position so far -->, repeatable); these show as a list under the question,
so the excerpt stays focused without orphaning the names it needs.
Building it with the model
You don’t have to hand-write checkpoints. Declare just the trace: and
exam:, then name the stub deck as
alix deck generate’s source:
alix deck generate mytrace.md
The model explores the source — read-only Read/Glob/Grep, source root as
its working directory, no write or shell access — finds the single load-bearing
path, and writes the checkpoints (with their <!-- at: --> locators) back into the deck.
The locators use the cite: key src, which the build declares for its input:
the exam: directory, or the folder holding an exam: file (so a file is cited
as src:<file name>:<lines>). When a cite: root the deck already sees holds
the input, the build reuses that key instead; when none does and src is
already taken, it stops before calling the model. A URL trace build works only
in a workspace member deck: the URL becomes the cite: root src, and the
model copies each checkpoint’s passage verbatim between a PASSAGE <token>
line and an END PASSAGE <token> line, with a token alix mints for that build.
Alix freezes each passage as a sha256-<hex>.txt object in the deck’s asset
folder and cites it as <!-- at: src fingerprint: ... asset: ... -->. A
missing or altered marker, or a checkpoint without a passage, fails the build
naming the checkpoint, and nothing is written. Alix fingerprints those locators
before placing the result. The result is
cached and version-controlled there, so review it (especially the locators) and
edit freely; re-run it to regenerate.
Building is one-shot, correctness-critical, and fails silently when the model
is weak — you still get parseable checkpoints, just a loose chain you then drill.
So the [trace] config defaults the build to the backend’s strong model
(for Claude, opus; set model to override)
and high reasoning effort (effort = "high"): slower than the other AI features,
but it runs once and is amortized over many reviews. The suggestions pass
(--trace --plan, below) shares those settings.
Don’t know what to trace? — --trace --plan
alix deck generate . --trace --plan
does a single read-only recon pass over a source (a repo ., a directory, a file,
or a URL) and prints a ranked menu of candidate traces — each a path-question,
a one-line spine sketch, and a suggested exam: scope. The list is sized by
coverage (the central spine plus one main path per major subsystem), so it’s
as long as the source needs. It also names the node-shaped subsystems it skips —
a config table, a store’s on-disk format — as facts-deck material, because
facts are a deck’s job and edges are a trace’s. It writes nothing: pick one, paste
its header into a new deck, and alix deck generate it. Knowing what is worth tracing (and
how deep) is the genuinely hard part — it needs you to already understand the
source — so this hands that judgment to the model.
Write it as a chain, not a quiz
A trace’s whole value is that it’s a path: each checkpoint picks up where the last reveal left off (notice how hop 2 above opens with hop 1’s conclusion, “s1 and s2 point at one heap allocation”), so you follow one thread — a data flow, a control flow, a derivation — to an outcome. If the checkpoints are independent facts hanging off one thing, you’ve written a set, which is what cards and the exam already do; choose a subject with a real sequence instead.
Tracing it
Pick the trace in the web picker, or on the mobile app (the trace runs fully offline there too): a trace deck opens as a trace: a checkpoint-by-checkpoint descent (the hop list rides the wire but is not yet rendered as a rail) with each checkpoint’s source shown in a line-numbered excerpt. It goes hop by hop:
- Predict — type a guess before anything reveals (committing is the point).
- Reveal —
alixshows the real excerpt from the source, then the key points and note. - Gap — you judge yourself Missed it / Partly / Got it (the same three grades review uses). The trace is self-judged and offline on every client. A failed or partly hop is a weak edge that resurfaces sooner: a failed one resets, a partly shortens its next interval (FSRS Hard), while a passed hop advances and fades. Each checkpoint is an ordinary card underneath, so this is the normal per-card SRS.
- Done - after the last hop the trace drill is complete. There is no exam
or separate capstone. The checkpoints keep their ordinary SRS state, and the
trace reaches Finished when they graduate. A dependent deck that names the
trace in
requires:waits for that state.
Immediate freezing
Because <!-- at: key:file:lines --> reads the live source, editing a traced
file could shift every excerpt to unrelated lines. Initializing any workspace
member therefore freezes its evidence immediately. Every source, an explicitly
named file and a directory alike, is reduced to the excerpts cited by its
cards, so Alix never exports a whole file or an entire repository.
Every copied excerpt lives below assets/deck-<token>/ and is named
sha256-<digest>.<ext>, where the digest covers its exact stored bytes. Freezing
leaves the deck’s exam: and cite: roots pointed at the real material (a
path or a URL): they are never rewritten to point into assets/. Each
<!-- at: --> keeps its key, path, and lines, keeps its excerpt
fingerprint:, and gains an asset: field naming the content-addressed
object:
<!-- at: code:scheduler.rs:90-98 fingerprint: xxh64-0123456789abcdef asset: sha256-<digest>.rs -->
Review reveals the frozen excerpt (display evidence, numbered from the at:
start line), and the fingerprint: verifies those stored bytes. The at: key
and path retain provenance for drift reporting, judged against that key’s own
root, and a future deliberate source update. When that root is available and
source_access permits it, the tutor browses it for surrounding context and
staleness detection; offline, it reports the missing live source rather than
silently degrading. A loose trace over a live source is left as-is.
Checking the locators
For a trace that isn’t frozen — a loose .md over a live exam: —
alix doctor <deck> validates that every
<!-- at: --> still resolves and matches its fingerprint. A missing fingerprint,
missing file, changed excerpt, or ambiguous exact match is reported without
writing. If the exact text moved to one other range, doctor reports that safe
rebase. After reviewing it, run
alix doctor <deck> --repair-source-locators to stamp missing fingerprints and
apply only unique exact rebases. Changed and ambiguous excerpts remain
untouched. Frozen assets do not move, but doctor still verifies their
captured text, and with --warn-on-source-locator-drift it also warns about
live-source drift.
A trace deck degrades gracefully — even when it is not being traced it’s a valid deck of
explain cards. See docs/examples/workspace-showcase/decks/ownership-move.md
for a complete trace, frozen evidence from The Rust Book’s ownership
chapter, so it traces offline.
14 · Generate a workspace — goals & curricula
alix deck generate --trace --plan lists central traces.
alix workspace generate goes a layer up: give it a directory and a goal
and it explores the source first — one AI planning pass — and prints an ordered
learning plan: the facts decks and traces worth authoring to reach that
goal, dependency-ordered.
alix workspace generate . --plan # a plan to understand the whole source
alix workspace generate . --plan --goal "how review scheduling works" # a narrow goal → a focused subset
Each item is tagged [trace] or [deck], chosen by the shape of the
knowledge: a path you predict hop by hop becomes a trace; a table of facts —
a config’s knobs, a store’s on-disk format — becomes a facts deck. Each carries its
requires: prerequisites (the list is a valid dependency order, foundations
first) and a exam: scope. The --goal scopes coverage: a broad goal spans
every subsystem; a narrow one collapses to its slice and traces it in more detail.
--plan is read-only — it prints the plan and stops, so you can author the items
yourself (alix deck generate a trace or
a facts deck per item).
Building the workspace
alix workspace generate . --goal "how review scheduling works" --into ~/decks/scheduling/
alix workspace generate . --source-url https://example.com/project --into ~/decks/project/
Without --plan, you always get a workspace, whatever the plan’s size: a
one-item plan still writes the manifest, the assets, and that one deck, ready to
grow. (For one deck from a directory taken whole, with no planning pass at all,
run alix deck generate <dir>.) A workspace build
goes like this: the plan prints, alix confirms
(Build N items into <dir>? [y/N] — -y skips it), then goes all the way — it
explores the source once and reuses that single session to fill every item —
predict-verify checkpoints for the traces, fact cards for the decks — so the
workspace comes out review-ready in one command: an
alix.toml (carrying the goal and the source as the [cite] root src, which
every generated locator names; --title names it) and one file per item under
decks/: a trace: deck per trace and a exam: facts deck per deck, wired together
with requires: so they unlock in dependency order, each exam: pointing
back at the real source. Writing the whole set from one understanding keeps the
items coherent (each builds on its prerequisites instead of repeating them).
Before the hidden staging workspace becomes visible, Alix initializes every
complete member and freezes its evidence. Cited excerpts
(from files and directories alike) and local card images land below each
member’s assets/deck-<token>/ directory. A missing or changed required source
aborts publication instead of leaving a live or partially frozen workspace.
--source-url <URL> records a public source in the workspace defaults.
Tutor and exam calls can use it for wider context and staleness checks after
the local generation source is gone, while review continues to use the frozen
assets.
The destination is --into <dir>, defaulting to a folder named after the
source under your decks directory.
Populated destinations
Pointing a build at a destination that already has files never blocks the run
or loses anything: alix builds into a scratch staging folder next to it
first, then moves the new member files into decks/ one by one. A name that’s already there
keeps your existing file untouched — the new version stays behind in the
staging folder, reported at the end so you can compare and move it in by
hand — while everything else lands normally. Pass --force to overwrite
collisions instead.
This is the tool’s high-water mark: name what you want to understand, and alix
assembles a dependency-ordered curriculum of facts and traces — gated by
mastery — that you climb.
The explore trace — --trace
Before you even know what to trace, alix deck generate <source> --trace builds a
short tour of the source’s shape, written as a trace deck: you predict what
kind of program it is (from the manifest), its domain nouns (from the module
list), how it’s driven (the entry point), its spine (the central file), and
finally the first paths worth tracing, each hop revealing the real lines
cited under the cite: root src it declares for the source, as
a trace build does. It’s
written to a file (-o, default explore.md; --into places it inside a
workspace), and you trace it from the web picker: run alix
and pick it.
15 · The web app
alix is a web app: review, browse, and the exam all run here. alix opens a
small local web server and shows you its URL, writing to the same progress
store that alix stats/alix list read: what you grade here is exactly
what they show. It’s especially handy on a tablet or
phone, where touch (and images) work naturally.
alix # the deck picker, at http://127.0.0.1:7777
alix --port 8080 # a different port
alix --lan # reachable from other devices on your network
alix ~/decks-maria --lan --port 7781 # serve one folder as its own scoped root
Choosing decks in the browser
Run alix and the page opens the deck-selection
screen. Up / down move between
decks; a search box in the header filters the list (focus it with /).
Focus a deck and Learn it with Enter (a facts deck opens a
review, a trace deck opens a trace) one deck per session. Browse
on b opens a read-only, in-page read-through instead: step the cards with
Prev/Next, Esc to leave. A browse leaves no trace: it records no progress and
does not add the deck to your recent decks. Focusing any deck opens an inline focus drawer
beneath it: it shows the deck’s frontmatter description:, if any, and
a per-card tier heatmap: neutral for an untouched card, grey for one merely
seen (met: acknowledged, or answered and got wrong), white once learning
(at least one full or partly pass, not yet graduated), green/yellow/red for a learned card by how well you’d recall it right
now, purple once retired, and an unfilled outline for a
sub-card still gated on its
parent’s graduation. When the deck has a review order that
heatmap splits into named regions you can pick to drill (click one or step
through with ← / →); otherwise it is a single whole-deck bar. On a workspace row instead,
→ enters it, and Esc or Backspace backs out. After a session, Leave (on the summary)
or Esc (also the footer’s Back chip while inside a drill-in) returns
here, so you can switch decks without restarting. Every review starts from
this screen; there’s no direct deck launch. A focused deck’s split
Depth… button opens the depth menu (Scheduling)
without starting it.
A fact deck with an exam has one separate Take exam action (x).
Learn and Enter still start a drill when the exam is due; they never
start the exam. Take exam fills with the accent color only when the exam
is due and unlocked. A locked exam stays unfilled and disabled with a lock
glyph. The Depth menu does not repeat the exam action, and a session summary
does not offer an exam. An only what needs work tick always follows
Take exam, ticked by default; untick it to take a full sitting. It is
disabled while the exam is locked or every section has passed, since the
sitting then covers every section either way. Results show the point
total, each section’s count and status, and the shape of every question.
mastered appears only after a perfect full sitting.
A workspace row that has a personal deadline set shows a
small chip: a date, days left, and ready percent, colored to flag urgency
inside the last week or past due; the same readout sits inline behind the
title once you drill in. Press d (or the row’s Ready by… action) to
set, move, or clear it from an inline date prompt.
A deck whose own progress document cannot be read shows a red error line
and refuses to start: reviewing it would write fresh progress over the
document alix could not read. Its siblings are unaffected. alix doctor
names the damaged file; fix or remove it (alix reset <deck> removes an
unparseable document after confirming) and the row heals on the next
listing.
Walking a deck
A walk reads a deck once, card by card, with an attempt before each
answer. You grade each answer, but nothing is scheduled: a walk only
remembers what you missed so the next walk can bring it back. It is meant for
material you want to go through rather than drill: preparing a talk or a code review, or re-reading a deck now and
then. Start it from the focused deck’s Depth… menu with Walk (key
w while the menu is open). After that, the deck’s main button is Walk
until you start a session at a depth again. It is not offered for a trace
deck.
Each item shows its front first. A card with authored choices is answered by
picking (the pick shows right or wrong); every other item is a flip: try to
recall it, then Reveal (space). A reveal: line card shows one answer
line per Reveal next instead. A flip ends with Missed it, Partly
or Got it (1/2/3). A correct pick ends with Next (space) or
the quiet I guessed (1); a wrong pick ends with Continue (space).
A card with an authored hint offers Hint (i) on its front; opening it
turns a Got it on that card into Partly, as in a session. Each direction of
a two-way card and each blank of a cloze is its own item. The section’s
context sheet opens by itself the first time a walk reaches that section, and
the tutor is available once an answer shows.
A walk keeps only the time each item was walked and its last walk grade; it
never touches the schedule or the review history. An item is waiting until
its last walk grade is Got it (or Next on a pick). While any item waits, a
walk serves only the waiting items, in deck order, so a missed card comes
back in the next walk, leaving early and coming back continues where you
stopped, and the counter shows what is left. Adding a card to a deck makes it
waiting. Once nothing waits, a walk is a full pass, the items walked longest
ago first. At the end, Next walk (r) starts the next walk. Walking
counts for the drill too: the drill grades a walked card at first sight
instead of introducing it.
The tutorial
Tutorial in the deck picker’s ☰ menu, or the Tutorial button the picker shows when it finds no decks, opens a short tutorial deck as a walk. Its nine cards each teach one thing by having you do it: grade an answer, open the section’s text and a hint, pick a choice, reveal line by line, fill a blank, hide an answer, and show a cited source. Opening it again continues where you stopped, as any walk does.
The tutorial ships with alix and is kept apart from your library: it is
never listed in the picker or the recent list, and neither it nor its
progress is written into your decks folder. Each open writes the tutorial
and the small sample file its cited card points into a folder of its own,
under the same platform directory as the server log: tutorial/default/,
or tutorial/profiles/<name>/ for a launch of profile <name>. A scoped
alix <dir> launch and a --config launch that names no profile share
tutorial/default/. A file there that differs from the shipped one is
rewritten on the next open, so an update brings the updated tutorial and an
edit you make there does not last. In the tutorial the tutor offers Ask
but not Make this a note or Make this a card, so nothing of yours is
saved there. A kids-profile server refuses /api/tutorial with HTTP 403.
The tutorial is in English only.
Library actions
The picker’s ☰ menu carries six actions that used to be terminal-only:
everything below is an /api/* endpoint, so it’s also on the wire for other
clients (see docs/API.md):
- Add deck…: one sheet, three ways in, all landing in a chosen
destination (the library root or a workspace): generate a deck from a
URL (with optional guidance) the same way
alix deck generatedoes, but URL sources only, a local-file source stays CLI-only, since a LAN token holder must not be able to point the server’s AI at the server’s own filesystem; import an Anki.tsvor an alix.mdfile; or receive: paste a wormhole code, or upload a.zip. - Share…: sends the focused row (deck, folder, or workspace; the served root if nothing’s focused) device-to-device over a wormhole code, or download as .zip as the offline fallback. Personal state (progress, recent list, local pacing) stays home either way.
- Remove from library…: permanently removes a focused loose deck,
workspace member, or whole workspace and its Alix-owned progress, frozen
assets, augmentations, and backup siblings. The sheet first lists the
stakes, then requires the exact row name. Removing a workspace preserves
ordinary source files and uninitialized Markdown, so its folder remains if
either is present. A partial failure stays visible with completed and failed
artifact labels plus the
alix doctorrecovery step. There is no undo. - Reset…: wipes a row’s progress. Gated on typing the row’s name back exactly, since this can’t be undone; needs a focused row.
- Doctor: the free environment checks (config, log, store, decks,
backend, share) as ✓/!/✗ rows, screenshot-able for handing to whoever set up the
instance. The costed
--backendsprobe stays CLI-only. - Pair a device: a QR of the pairing URL plus the URL itself, to scan
from a phone or tablet. Needs
--lan; a localhost-only instance shows a hint instead (nothing reachable to scan).
Augmenting a deck from the picker
Focus a deck and press a (or its Augment button) to open the Augment
screen: the browser face of alix deck augment. Each of six targets,
choices, notes, questions, key points,
format, and order, gets its own card: a short, plain description of what
that augmentation does, a small neutral before/after preview, its coverage
count, and its action. Generate fills only the cards a target is still
missing, run as a background model call while the page polls (a spinner shows
it working); Remove clears a target, and the order card adds or drops
named topologies. Each card has its own compact guidance input, feeding the
same --with steer as the command line, with a kind-specific example as its
placeholder so you can see what a steer is good for; a batch carries each
ticked card’s own guidance. It writes the same
augment/deck-<token>.json document review reads, so this only saves you the
trip to the terminal.
Cached per-card augmentations are tied to the question and answer they were generated from. Editing either makes that card reappear as a gap, so its augmentations regenerate on the next augment run.
The action also works on a workspace or folder row: the same screen opens over all its decks at once, so a Generate fills a target’s gaps across every member, Remove clears it across every member, and an Order generated here is one workspace-wide pedagogical path. A workspace additionally gets an Icon card: Generate draws (or redraws) the small emblem shown on its picker row, steered by the card’s guidance.
Tick several targets and press Generate selected to run them in one batch (a Select all button at the top ticks everything that can run): it shows a rough estimate of how many generations that will take, then walks each ticked card through its own status, queued, generating, done, or failed, as the batch runs. A target failing doesn’t stop the others; a single per-target Generate still works the same way it always did.
On the Claude backend a batch shares one conversation: the first target sends the cards once and every later target refers back to them by index, which is cheaper and a little faster than re-sending the deck per target. Other backends, and single-target runs, keep making one self-contained call per target. A failed target starts a fresh conversation for the rest of the batch.
The format target is a non-destructive reshaping pass: for each plain card whose answer is poorly shaped (a list crammed into prose, a run-on sentence that wants to be lines) it caches a tidier front, split answer lines, an optional note, and a suggested reveal-method: applied at display time without touching the deck file or card identity. Both review and browse show the reshape, so the two views match. It’s an AI heuristic, so it can miss or produce an unhelpful reshape; Remove clears it with no lasting effect.
Every check, at every depth, plus the AI features
Every check works in the browser, at whichever session
depth you picked: a flip or cloze reveal, a line reveal (it auto-scrolls to
the newest line), a typing Reconstruct check (each line marked ✓/✗ with the
correct answer shown, then you grade), an explain Reconstruct check, and the
multiple-choice pick: a new card’s attempt-first on-ramp, or a genuine
Recognize-session question. Pick-one cards submit when you tap an option;
select-all cards let you toggle each answer independently, then submit the
whole set at once. A correct pick offers the quiet “I guessed” undo. A
revealed note uses the same content-column width and text size as the answer
or choices above it. Question and answer prose is left-aligned inside a block
that shrinks to its content and sits centred in the card: a one-word question
or a one-glyph answer lands in the middle, a wrapped paragraph reads as a
left-aligned block, and a block holding code, a table, a checklist or a
quotation spans the whole column. A note is always left-aligned at the full
column width. The note sits at the bottom of the card, directly above
the buttons, and takes only the height its text needs; a long note takes the
room the answer is not using, and room the answer is using only down to a
third of the card, past which the note scrolls inside its own region. When an answer or a note is taller than
its region, a “more below” or “more above” marker appears at that edge;
clicking it scrolls that region by most of its height, so you can walk a long
answer without reaching for the wheel. Controls are big tap targets and
follow your configured key bindings (the page reads them from the server).
A card under a section names the section on its face only through a quiet
Context chip in the bottom row, offered until the answer shows. On the first
card you ever meet from that section, the section sheet opens by itself
once: the page dims at once and the sheet slides up from the bottom of the
page, as on the phone, taking only the room its prose needs; a long section
scrolls inside it. Dismiss it and it stays closed through reveal or answer
input; later cards from the same section, in this sitting or any later one,
do not open it automatically. The full section never appears inline. Click
the Context chip, press c, or use Context beside Ask in the
review menu to open the sheet on demand; a click on the question opens
nothing, and once the answer shows none of these is offered, since the
section is there to help you answer.
While the sheet is open, the card and its review keys are inert; a click on
the dimmed page, the × in the sheet’s top-right corner, c, or Escape
closes it.
A card with hints offers a quiet Hint
chip to the right of the main button (Context sits to its left) for as long
as you are still answering (on a
reveal: line card, until the last line shows). The chip, i, or Hint in
the review menu opens the same sheet with the first hint; Next hint (or
Enter) adds the next one under it until none are left, and reopening shows the
ones you already opened. c and i swap the sheet between the section and
the hints. Opening a hint turns a pass on that card into Partial.
Confidence tracking
The depth menu’s confidence tick-box (f) switches tracking on or off for the
deck; the choice is saved in the deck’s progress (so it syncs with it) and
kept for every later launch until you switch it again. It is off by default.
With tracking on, a graded card asks Sure or Unsure in place of its
first action, as one button split down the centre: Unsure (Space) and
Sure (Enter). Where Space already types or ticks an option (a typed
answer, an explain box, a select-all card), they move to Shift+Space and
Shift+Enter, and plain Enter marks nothing.
Either one performs that action and records your mark with the review
in the card’s history. On a single-answer multiple-choice card a click (or the
number keys) only selects an option, which you can change; Sure or Unsure then
submits it. Introductions, walks, and line-by-line typing (Check) ask
nothing. The mark does not change scheduling.
To set an unanswered card aside for later in the session, use Skip card in
the review menu (skip has no default key; bind one in [keys.review]).
A dim “N left” count in the header shows how many cards the session still
holds; it can tick up when a card you missed cools back in for its retry. The
☰ menu is context-aware: during review it holds Ask and,
when available, Context; a trace holds Ask. On the deck
picker, the library actions above plus keyboard
shortcuts, Tutorial and about, with Theme… and Draw
answers (a per-device toggle, see below) in both. The ⟳ button (also key
r) re-reads your config, so a changed decks_dir takes effect without
restarting (scoped alix <dir> instances stay pinned to their folder), and
re-fetches workspace icon images, so a regenerated emblem shows without a
reload.
Once an answer is on screen you can hide it to self-test before you grade:
press h or click the answer. A cover fills the answer’s space, an eye-off
glyph with “click to reveal” beneath it. The note disappears while the answer
is hidden and comes back when you show it. Nothing on the card moves. h or a
click shows the answer again, and the corner marker names what the next press does; on a
cited card it sits beside the </> source marker. A choice pick, a typed
check and a sketch have nothing to hide. The next card always starts shown.
The AI features come along too: the tutor, the AI exam, and traces all have a web surface, each running its model call on a background thread while the page polls, so the single-threaded server never blocks.
Draw input
A input: draw card, or a flip/explain card with
the ☰ menu’s Draw answers toggle switched on, swaps the usual typed/reveal
input for a small canvas: Pen · Eraser · Undo · Clear, then
Reveal. The drawing stays on screen (frozen, not editable) while you
self-grade against the card’s normal reveal, then it’s discarded; nothing you
draw is saved or sent anywhere beyond rendering it in the browser. It’s
honored on flip/explain cards only, never on a cloze card (the desktop web
app types those regardless of the directive or the toggle), and there’s no OCR
or vision model reading it back: grading is on you, same as any other
self-graded card.
Themes
The web UI ships a gallery of colour themes: the alix Dark/Light
originals and a Kids group (Sunrise, Ocean, and Berry, the
same three themes the kids app offers, so a kid moving up to
the grown-up app can keep the look they grew
attached to), plus crowd-favourite editor/slide palettes
(GitHub, Dracula, Nord, Solarized, Gruvbox, Catppuccin, Tokyo Night, Monokai, One
Dark, Ayu, Rosé Pine, Everforest). Open the Theme… popover from the ☰ menu (a
small bar button on the trace): a grid grouped Light / Dark / Kids that previews
on a sample card as you hover and re-themes the whole app when you click one,
remembering your choice in the browser (kept in localStorage, not the config).
The palette lives in a shared theme.css the
server hosts, so every screen (review, browse, and traces) themes together.
Kids mode
alix can also serve a second, touch-first frontend aimed at kids (roughly
age 10). Set audience = "kids" in [serve] (see
Configuration) and point it at a folder an adult has
already set up:
alix --config kids.toml ~/decks-family --lan --port 7781
A box is a workspace: the home screen shows the boxes as a grid, tap one to see its decks with a ⭐ mastery indicator per deck, tap a deck, then pick that deck’s depth: 👆 Tap the answer (Recognize) or 🗣️ Say it yourself (Recall); a caught-up choice disables itself instead of starting an empty session. On a select-all card, taps mark options instead of answering, and one Done button submits the whole set. Review works the same way underneath as the regular app (reveal, then the mascot says a short “why” instead of a bare note, then self-rate) with a 💬 Ask Alix button that opens a kid-safe tutor overlay scoped to the current card.
v1 is consumption only: it covers reviewing pre-made boxes at Recognize and
Recall depth, plus the tutor. Augmenting a deck, the AI exam, and traces stay
adult-only for now. An adult prepares a box in the regular web app, then
hands the kid a kids.toml and the box to open. It’s the same engine and the
same /api/* contract underneath, just a different page: self-hosted Baloo 2
type, warmer colours, and no keyboard required.
Building a client?
The JSON API the web app itself speaks is a documented, client-agnostic
contract: docs/API.md in the repository (endpoints, DTO field tables, the
flows, and the stability rules) with every response shape pinned by snapshot
tests. Native or alternative clients build against that file.
Local by design
The server is deliberately local-only: no accounts, no database. By default it
binds to 127.0.0.1 (this machine only). --lan binds all interfaces so another
device on your network can reach it: at startup it prints the pairing URL with
the machine’s real IP, plus a scannable QR code, right in the terminal. Serving
with --lan auto-generates a pairing token and requires it on
/api/*, so the network endpoint isn’t wide open; pin your own with --token or
[serve] token. Open the printed …/?token=… URL (or scan the QR) and the page
attaches the token for you. AI requests still run the model CLI on the host, so
only use --lan on a network you trust. The default port lives in the [serve]
config section; --port overrides it.
alix <dir> serves that folder as a self-contained scoped root: its own
catalog, shareable augmentation and assets, plus private per-deck progress and
recent history under its colocated .alix/ directory. Several instances run
happily side by side, one per family
member, say:
alix ~/decks-maria --lan --port 7781.
If a launch misbehaves, alix doctor checks the setup (config, progress
store, decks directory, backend CLI) and prints a one-line remedy per problem.
The Doctor sheet also names this instance’s local log file.
Preparing a bug report
Every running server keeps a small local diagnostic history without requiring
a flag. From the adult web app, open About and choose Prepare a bug
report. From a terminal, run alix bug-report; --out <dir> chooses where
the archive lands. Both paths use the same local ZIP format. Nothing is
uploaded or sent, so open it and review its plain-text files before attaching
it yourself.
The web archive contains the current instance log and its one rollover; the
CLI collects every instance log so it does not have to guess which server had
the bug. The archive also contains version and platform details, a copy of the
active config with every token and AI prompt override removed, and per-deck
counts keyed only by a SHA-256 hash of the stable deck ID. Home-directory and
user names are redacted. By default, no deck text is included. The CLI’s explicit
--include-deck <path> option adds that one deck verbatim, including card text
and authored notes, and names it in report.md. Personal sidecars, AI prompts,
and AI responses are always excluded. The diagnostic log records minted card
IDs plus content-free panic, AI, parser, and HTTP failure classes; verbose
logging can add operational timings. Only known-safe diagnostic fields enter
the archive. Deleting the archive or either log file does not change decks or
progress.
19 · Pairing a device
alix’s web server can lend a paired phone its AI backend for the tutor, the
exam, deck generation, and
note-taking, over /api/remote/*: the phone keeps its own decks and
progress, the desktop only computes answers.
The pairing token changes on every restart
alix --lan prints a fresh, random pairing token each time the server
starts. This is the single biggest papercut in pairing a device: if an app
that paired fine yesterday suddenly can’t reach the server, the token most
likely changed on the last restart. Re-pair with the freshly printed URL, or
pin one that never changes:
[serve]
token = "pick-your-own-fixed-token"
With token set, --lan reuses it instead of generating a new one, so a
saved pairing survives restarts. A pinned token (or a --token value) must
be at least 16 characters; --lan refuses a shorter one before it binds.
What the remote surface does
Nothing under /api/remote/* writes the server’s own progress store,
session, decks, or recent list; it only computes an answer and hands it
back. A tutor question re-sends the whole conversation with every call,
since the server keeps no session for a remote client. When the stable deck
and card ids match what the desktop serves, the tutor uses the desktop’s
assembled card, notes, links, frozen evidence, and allowed live source root,
just like the desktop tutor. When either id cannot be matched, the reply is
marked card_only and uses only the card text the phone sent; the phone names
that limit as “Answered from the card alone: the paired desktop could not
match this card.” An AI exam sitting is
graded on the server, but the result, any remediation cards, and what counts
as mastered stay the phone’s to keep. A deck generation call hands back the
full deck text and a suggested file name; a note condense hands back up to
three lines. Either way the phone decides where they land: the decks folder
or the deck file, never the server.
The server side of this ships from 0.6.0; see docs/API.md, section 4.11,
for the wire contract if you’re building against it.
Pairing the mobile app
On your computer, run alix --lan and note the URL it prints (the same one
[serve] token can pin, above). On the mobile app:
- Open Settings (the ☰ button on the deck list) and tap Connected devices.
- Paste the printed URL into the sheet and tap Pair.
The app checks the server before saving anything, so a bad paste or an unreachable desktop never gets stored silently. It shows one inline line naming what went wrong:
- an unparseable paste:
that does not look like an alix pairing URL - a desktop it can’t reach:
no alix answered at <host>:<port> - a desktop too old for this app’s remote surface:
alix <version> found, this app needs 0.6.0 or newer - a desktop that answers but rejects the token (most often a server that
restarted, and minted a fresh token, since the URL was printed):
alix answered but refused this token. Copy a fresh pairing URL from the server.
On success the sheet closes with a note of which host you paired with. The
same Connected devices row reopens the sheet later, now showing the
current host:port and an Unpair button; unpairing only clears the
saved config, nothing else on the phone changes.
What’s borrowed once paired
Once paired, review gains things it doesn’t have offline:
- An Ask chip, shown once you’ve attempted the current card (revealed
it, picked a choice, submitted a typed answer, or walked all its lines)
but not before: the same attempt-first rule the web tutor follows. It opens
the same question/answer flow as the desktop tutor: replies render with the
card’s own content grammar (code blocks, checklists, tables, quotations),
and Make this a card and Make this a note enable once an answer
exists that nothing was made from yet. A note condenses the exchanges
since the last note into key points and appends them to the deck’s
personal sidecar,
<deck>.local.md, on the phone and, when the desktop serves that deck, on the desktop too, so the next sync pull carries the same note back rather than a sidecar without it (an empty result says so rather than doing nothing silently). A card you add from a draft is minted on the desktop the same way when it serves the deck, and the phone records it under the id the desktop hands back, so the next pull keeps it. Pressing Add locks the draft’s fields. A refusal (a duplicate, or a malformed card) adds nothing on the phone, says so, and unlocks the draft for editing. When no answer arrives, the phone says the card may already be on the desktop and keeps the fields locked: tapping Add again sends the same card, and if the desktop already kept it, it hands that card back and the phone records it. For a deck the desktop does not serve, the card is minted on the phone alone and stays there. Every question re-sends the whole exchange to the paired desktop (the server keeps no session of its own for a remote turn). The conversation belongs to the card, not the sheet: closing the sheet while the desktop is still working loses nothing, the answer or note lands anyway, a note saved with the sheet closed is announced on the review screen, and reopening the sheet shows the whole transcript and a question you had typed but not yet sent. Moving to another card starts a fresh conversation; while the previous card’s note or card draft is still being made, the new card’s tutor says so and waits, since the desktop makes one thing at a time. A desktop restarted mid-call forgets the call; the phone reports it as failed (the question goes back into the chat box) rather than waiting for a result that will never come. - An Exam row on a deck’s launch sheet, for a fact deck that declares exam material, with an “Only what needs work” switch (on by default, disabled once every section has passed). It opens a full-screen exam: one question at a time, then the points count, each section’s count, a Pass/Partial/Fail breakdown per question and, when an answer missed key points, a Turn the gaps into cards button. Every sitting’s results and any remediation cards land in the phone’s own progress store, exactly like an offline grade, matching the rule above: the server computes, the phone keeps.
A trace deck runs entirely on-device: predict, reveal, and self-grade each checkpoint. It has no AI exam or paired-only capstone.
The Settings page also gains a Generate deck row: give it a URL and
optional guidance, the desktop generates the deck text the same way
alix deck generate does, then the phone asks where to save it (an in-app
folder browser) and writes it under a collision-free file name. It follows
the same liveness rule as the two chips: the row
appears only while the phone has confirmed the paired desktop is reachable
and new enough, and is simply absent otherwise.
The Ask chip and the Exam row both depend on the phone having confirmed the paired desktop is reachable and running at least version 0.6.0; there is no retry chrome for a dead or too-old server: the Ask chip simply is not there, and the Exam row is disabled with a short hint.
If the desktop answers but rejects the token partway through a review, an
exam, a note, or a generation (the restart case above, caught mid-session
instead of at pairing time), the phone shows one SnackBar: “Pairing expired.
Pair again from Settings → Connected devices.” On the review and exam screens it
carries a Re-pair action that reopens the pairing sheet directly; the
tutor sheet shows the same line inside the sheet instead, so there you
follow the message’s own instruction. Pinning [serve] token is what stops
this from happening in the first place.
Security posture
This is plain HTTP on your local network. The bearer token guards against someone stumbling onto the server by accident, not against a hostile network: anyone already on your LAN who gets hold of the token can use it. For anything beyond your own LAN, put alix behind a VPN or a reverse proxy; alix itself will not grow TLS or accounts.
18 · The mobile app
There is a native Android app: the same review loop as the web app, running
the same core (parser, scheduler, progress store) compiled into the app, so
it works entirely offline, including a trace deck’s
predict/reveal/self-grade trace. It is early software with a deliberately
small surface: reviewing decks. Pairing it with a running alix server on
your network lends it the tutor, the AI exam, deck generation, and note-taking: see
Pairing a device.
Settings (the ☰ button) → Theme picks from the web gallery’s 18 non-kids themes (the three Kids palettes are web-only; see Themes); the app re-themes live, no restart.
Install
Grab alix-arm64-v8a.apk from the project’s GitHub Releases (the
alix mobile vX.Y.Z releases) and install it. Android will warn about
installing outside a store; that is expected for now. The app works on
Android 7+ and keeps its decks in private app storage. A fresh install’s deck
list is empty and offers a Tutorial button (see
The tutorial); pairing with a desktop server (see
Pairing a device) adds the Generate deck row for
bringing in decks.
Settings → About shows two versions: the app’s own and the embedded core’s. The app has its own release stream; it does not track the CLI’s version.
The deck list
The first screen lists the phone’s own decks and, once paired, the pulled copy of the desktop’s below them. The two are listed separately and the phone’s own appear first; the screen stays responsive while a large workspace is still being read, and shows nothing in place of the list until the first answer arrives. A deck read once stays parsed in memory for as long as the app runs, so coming back to the list after a review does not read every deck again; a deck whose file changed on disk is read fresh.
Each deck row, at the top level and inside a workspace, carries a thin strip under its title with one cell per card in file order, colored by that card’s learning tier: the same signal as the web picker’s deck drawer heatmap. The number at the strip’s end is the deck’s card count. A workspace row has no strip. Rows appear as soon as the list is read; the strips fill in a moment later, so a large deck never holds the list back. A deck has no icon unless it declares one; a workspace shows its own icon or its initial.
The search button in the top bar opens a search field over everything on the phone: its own decks and workspaces, the pulled desktop copy, and every deck inside a workspace. A row matches when its name contains the query, ignoring case. While a query is active the matching rows are listed flat, without the workspace tree, and “No decks match.” shows when nothing does. Tapping a result opens it exactly as tapping it in the list would.
Tapping a deck opens the launch sheet: pick the depth for this session, and the review starts there. A long-press on a deck does nothing. The sheet opens with a Cram switch above the depths. Switch it on, then pick a depth: that one session also serves cards that are not due yet, under exactly the rules the web app applies (see Cramming). Cram belongs to the launch and is never saved, so the switch is off again the next time the sheet opens.
Below Cram sits Skip introduction. Switched on, that one session grades every new card on first sight instead of showing it first, waiting for the settle gap and only then asking (see New cards). The two switches are independent: a cram over a deck you already know usually wants both, a first pass over new material wants neither. Like Cram, it belongs to the launch and is off again the next time the sheet opens.
The last row of the sheet, below a short centered hairline, is Walk: Cram and Skip
introduction do not apply to it. A walk is an unscheduled read-through of the deck,
the same walk the web app runs (see
Walking a deck). Tapping a trace deck
opens the trace itself, never this sheet. Each item shows its front; pick an
authored choice or Reveal (one line at a time on a reveal: line card),
then grade it as in a session: Missed it, Partly or Got it after a
reveal, Next or I guessed after a correct pick, Continue after a
wrong one. Hint opens a card’s authored hint on its front. Missed and
partly cards come back first in the next walk. The section sheet opens by itself the first time each walk
reaches a section, Ask reaches the tutor once an answer shows (when paired
with a desktop that can run it), and the done screen’s Next walk starts
the next pass.
The tutorial
Settings → Tutorial, or the Tutorial button on an empty deck list,
opens the same tutorial as the web app (see
The tutorial) as a walk; leaving it returns
to the deck list. It teaches the phone’s own gestures too: tapping a revealed
answer to hide it, and switching that sheet to Source on a cited card.
The tutorial and its progress live in a tutorial folder in the app’s
private storage, beside the decks folder, and are never listed among your
decks. Its tutor offers Ask (when paired) but not notes or cards.
Reviewing
The review screen is the web app’s loop on a phone: reveal, then grade, or
pick a choice, with the legend row at the bottom holding the primary action
and the quiet extras. The card is laid out as on the web: the question at
the top, the answer below it, and a revealed note at the bottom, directly
above the legend row, taking only the height its text needs; a long note
takes the room the answer is not using, and room the answer is using only
down to a third of the card, and each of the three scrolls on its own past
its space, with a “more” marker at the edge that hides something. Question and answer prose follows the web’s alignment
rule: left-aligned inside a block that shrinks to its content and is centred
in the card, so a short question or answer sits in the middle and a wrapped
paragraph reads as a left-aligned block; a note is always left-aligned at the
full width. A card that sits under a # section in its deck (see
Sections and sub-cards) shows
a quiet Context chip left of Reveal until you reveal the answer; it opens
the whole section, heading and prose, in a sheet. The section helps you answer,
so the chip is gone once the answer shows. The card face names the section
nowhere else, and the card never moves to make room. The first card you ever
meet from a section opens that sheet by itself, once; swipe it away or tap the
× in its top-right corner, and the chip brings it back. Every later card from
the section, in this sitting or any later one, waits for the chip. A tap on the
question does nothing.
A card with hints shows a quiet Hint chip to the right of the main button (Context sits to its left) while you are still answering. It opens the first hint in the same sheet; Next hint adds each later hint below the earlier ones, and reopening restores the hints already shown. When both context and hints exist, the sheet’s selector swaps between them without losing that stack. Opening a hint turns a pass on the card into Partial. Introductions offer no hints, and the reverse half of a two-direction card carries none.
To set a card aside for later in the session, open the ⋮ menu in the review header and pick Skip card; it is offered until the answer shows.
Tapping a revealed answer opens its answer sheet. An uncited card keeps the
single Hide view: the sheet rises from the answer’s top edge to the bottom
of the screen, covering the answer, the note and the grade buttons while the
question stays in view. A cited card adds a Hide / Source switch in the
sheet header. Source stacks every cited path and numbered excerpt, or its
error when the excerpt is unavailable, using the same editor-style presentation
as a trace. The Source sheet reaches the bottom of the session header so long
lines have more room; only the code turns sideways when its longest line would
overflow the portrait width, while the header and paths stay upright, and a
line too long even sideways wraps. Reopening the sheet on the same card shows
the view last used; the next card starts on Hide. The × in the top-right
corner, or a swipe down, closes either view. A choice pick, a typed check and a
sketch have nothing to hide.
The bar at the top names the deck, cut to one line, and counts the cards left. New session on the summary starts the next sitting with the same launch settings; after a cram launch it carries a small “cram” caption.
Syncing with the desktop
Once paired (see Pairing a device), a pulled entry’s row
gains a Sync action in its ⋮ menu: it pushes every local deck whose
progress changed, then pulls that entry when the desktop’s copy differs
from the phone’s. The app also runs one cycle in the background each time
it opens, for every entry already on the phone, once the paired desktop
answers and still serves the same root; an entry whose files did not
change on the desktop is left as it is. A
review session’s summary silently pushes that deck’s progress too, with
no visible step on a normal pass.
A pull replaces the files the desktop owns for that entry (its decks, their augment and asset files) with the desktop’s current copies; anything the phone added on its own inside the entry is left alone and reported as phone-only. Every card arrives with its id: a card you added on the desktop gets one there before it travels, so reviews and exam results the phone records for it stay attached after the next pull. A member the desktop deleted stays on the phone while its progress is not yet pushed, listed as kept, so nothing you reviewed is lost before it reached the desktop. A conflicting deck, one whose progress changed on both sides since the last sync, stops review of that deck and asks: keep the phone’s progress or keep the desktop’s. Each choice names what it keeps (the phone side’s number of cards with progress, or which desktop saved it, and when), marks the side that saved last as newer, and the side you don’t keep is lost. A conflict that a background sync records while a review or a trace is running does not interrupt it: the choice opens when that session ends, whether you finish it or leave early. An entry the desktop no longer serves stays on the phone, reviewable, listed as orphaned, with a Remove action once you’re done with it. A deck whose progress file on the phone cannot be read is not pushed, and the next pull puts the desktop’s copy in its place, listed as replaced.
A desktop entry the phone has never pulled shows below the phone’s own
entries: its name and size only, in a subdued row with no ⋮ menu and no
review action. Tapping it pulls it for the first time; once it lands it
becomes an ordinary entry, reviewable like any other.
Tapping the status line above the deck list (shown while a cycle runs, or its last report is unread) opens the full report: landed, pushed, kept, phone-only, removed, replaced, renamed, and refused categories, plus left out on the desktop (members inside an entry the desktop could not load) and not on this phone (entries the desktop serves that this phone has never pulled, the same ones shown as rows below the deck list).
Settings gains a Paired desktop row once a pairing is saved, opening a sheet to choose which paired desktop’s entries the picker shows below the phone’s own; the phone’s own decks stay listed whichever desktop is active.
Workspace deadlines
A workspace’s personal “ready by” date shows on its row (date, days left,
and ready percent, colored to flag urgency inside the last week or past
due) and again once you drill in, the same readout as the web picker.
Long-press the workspace row to set, move, or clear it. The date lives
in the workspace’s own alix.local.toml (see Workspaces);
the phone’s own offline sessions bend their scheduling toward the date
exactly as the desktop does.
16 · Configuration
alix works out of the box; the config file is for when you want to change key
bindings, point at a different decks directory, or tune the AI features. It
lives in the platform’s config directory (on Linux
~/.config/alix/config.toml); create it with alix config --init, and
inspect the active key bindings with alix config.
Key bindings
All keybindings live under [keys], one subtable per surface: [keys.review]
(the review screen), [keys.picker] (the deck picker), and [keys.browse]
(the browse overlay). Every action takes a list of keys (the first is shown in the
footer). To grade self-graded cards with j/k/l:
[keys.review]
failed = ["j"]
partly = ["k"]
passed = ["l"]
Keys are a single character ("j"), a special name ("space", "enter", "tab",
"esc", "backspace"), or either with a ctrl- prefix ("ctrl-s"). An
empty list (make_note = []) leaves an action without a key; its button still
works. The rebindable [keys.review] actions are failed, partly, passed,
reveal, submit (default enter, submits a typed answer, a line, or a multiple-choice selection; an answer typed into several fields submits on Shift+Enter instead, and Enter moves to the next field), skip
(no default key), remove (no default key, deletes the card from the deck file),
ask (default a), context (default c, open the card’s section drawer), hint
(default i, show the card’s next hint while you answer), continue
(default space/enter, moves on from an answered card or after a pick in a walk),
restart (default r), quit (default esc, leaves the session, the walk, or
the trace), up/down (defaults k/j) to move within a multiple-choice or
key-point list (the arrow keys always work too), and the tutor’s distill
actions make_note (default ctrl-s) and make_card (default ctrl-d). Each
button shows the first key bound to its action, and no key hint when the action
has none. While you’re typing an answer
(a reconstruct check), plain-character bindings are ignored so they can’t
shadow your input: use ctrl-/special keys for skip and quit there. Pass a
different file with --config <path>.
Two bindings fail at load with an error naming the key. submit may not be a
plain character (a letter, digit, symbol, or space), since an answer field
would type it as text. And two actions may not share a key when both are live
at the same moment of a review: skip and reveal both act on an unanswered
question, so binding both to s is rejected. Actions that are never live
together may share a key, which is how reveal (before the answer) and
continue (after it) both default to space/enter. The same rule covers the
three [keys.browse] actions, which are all live together.
No default claims a shortcut the browser or the system reserves (ctrl-n,
ctrl-t, ctrl-w, ctrl-c, ctrl-v); binding one yourself may not reach
alix.
The picker’s navigation is [keys.picker] (up, down, open, back,
filter, mastered, plus depth to open the depth menu,
recognize/recall/reconstruct to pick within it, and cram,
confidence and skip_introduction to toggle its tick-boxes, defaults v,
1/2/3, c, f, and s), the browse overlay has its own [keys.browse] bindings, and
the web server reads its default port from [serve]:
[keys.browse]
next = ["l", "n", "space"]
prev = ["h", "p"]
quit = ["q", "esc"] # Backspace always leaves too
[serve]
port = 7777
# token = "..." # pairing token required on /api/*; --lan auto-generates one (printed, with a QR); a pinned one needs 16+ characters
audience = "adult" # or "kids", which frontend `/` serves, and the tutor's voice (see 15 · The web app)
[log]
max_bytes = 5242880 # cap for each of the current and rolled files
verbose = false # also record verbose targets such as HTTP timings
(Jump-to-first/last stays fixed at g/G, and the arrow keys always move.)
The server log is always on. [log] max_bytes bounds each of its two files and
must be positive. Card selection and content-free panic, AI, parser, and HTTP
failure records are written by default. [log] verbose = true adds HTTP
timings. For live debugging, --log http,select,error also
mirrors the named targets to stderr and enables verbose file records for that
run. It does not add card content, deck names, or request paths.
Review pacing
The [review] section tunes the FSRS scheduler shared by all three review
depths:
[review]
retention = 0.9 # FSRS target recall probability (0.70–0.99); higher = shorter intervals
recognize_retention = 0.85 # same, for the Recognize depth alone; recognition decays slower
retire_after = "1y" # a graduated card rests at this positive Recall interval ("2w", "6m", "30d", or "never")
introduction_cooldown = "5m" # settle gap before a new card's first quiz ("90s", "10m", "1h"; "0" = none)
short_term = true # FSRS's same-day learning steps; false graduates a card on its first pass
max_session = 10 # cards a single sitting serves (default 10)
new_cards_percent = 30 # new-card share of max_session; the rest are due cards (default 30)
retention is the recall probability FSRS schedules for; recognize_retention
is the same knob for the Recognize depth alone, laxer by default (0.85) because
recognition holds far longer than production. retire_after is the positive
Recall interval at which a graduated card retires (rests until alix reset);
"never" keeps it in rotation forever.
introduction_cooldown is the settle gap between seeing a new card and its first
graded quiz, and the same floor keeps any just-seen card (a miss, a wrong
pick) from returning immediately, so one knob paces both. A bare number is
minutes; "0" disables the gap. short_term switches FSRS’s same-day learning
steps; off, one correct recall graduates a card (see
Scheduling).
max_session is how many cards one sitting serves; new_cards_percent is the
new-card slice of that cap (so at the defaults, three new and seven due out of
ten). Whichever pool comes up short, the other fills the cap, so a fresh deck
serves ten new and a deck with nothing new serves ten due; a big backlog just
slows introductions proportionally. A workspace can override any of these keys
for its own decks in an alix.local.toml (see Workspaces).
The precedence for the cap is --session N on the launch > max_session > the
built-in 10; new_cards_percent has no launch flag.
Ready by a deadline
Two more [review] keys exist only in a workspace’s alix.local.toml, never
in the global config (which rejects both outright):
[review]
deadline = "2026-09-01" # a personal "ready by" date; the day itself counts
deadline_ramp = "14d" # how early the pre-deadline ramp starts ("2w"; "0" = cap only)
deadline is an ISO date (YYYY-MM-DD). deadline_ramp takes a bare number
of days, "<n>d", or "<n>w"; "0" caps intervals at the days left without
ramping retention early. Inside the window the target retention climbs
linearly toward a fixed 0.95 by the deadline day (deliberately not a
config key); see Scheduling for the full mechanics.
These keys are workspace-only: they take effect only in a directory with
an alix.toml. In a plain decks folder, or on a loose deck, they parse but do
nothing (no ramp, no picker readout, no doctor warning). alix workspace deadline refuses a non-workspace directory and points at alix workspace init.
The picker’s ready percent counts a deadline’s member decks as ready once their exam has passed, or finished when they have no exam grounding. Passing itself rests on the AI exam’s sampled questions, not a check of every card. Treat ready% as evidence toward readiness, not proof of it.
How deeply you drill is never configuration: it’s the session depth you
pick per review (the picker’s Depth… menu). See
Reveal & session depths. The old [review] depth config
key (and the per-deck [review.deck."<file>"] override), a dial that fixed
the drilling depth from config, is gone, not renamed; a config that still
sets either now fails to load.
Backends
By default all AI calls go through the Claude Code
CLI. You can switch to one of the other supported CLIs with backend in [ask]:
[ask]
backend = "claude" # default, Claude Code CLI
# backend = "gemini" # Google Gemini CLI
# backend = "codex" # OpenAI Codex CLI
# backend = "copilot" # GitHub Copilot CLI
Auth is each CLI’s own login: alix stores no API keys. Install whichever CLI you want to use and run its login command once.
Each backend is granted read-only tools only (file reading; web fetch where
the backend supports it). Codex runs under a network-blocking sandbox rather
than a tool allowlist, so it can read local source files but can’t fetch URLs:
a URL-based exam or a generate subcommand will refuse and tell you to use a local
file instead, or switch backends.
Run alix doctor --backends to send a quick test request to the configured
backend and confirm it’s installed, signed in, and responding. --all-backends
probes all four.
The multi-turn tutor works on every backend: Claude uses its native session
flags (--session-id / --resume) for efficient continuation; other backends
re-inline the accumulated Q&A transcript into each follow-up so the context
carries over (the prompt grows with the conversation rather than being resumed
efficiently).
The AI sections
Each AI feature has its own section, all reusing the [ask] command and permission
settings:
[ask]: the tutor:command(how to invoke the CLI),backend,permission_mode, the tool allowlist, amodeloverride,timeout_secs, aneffort,source_access(local source grounding; a workspace manifest may override it, see the tutor), andpreflight_threshold(warn and confirm before spending a large model call on a local source tree bigger than this many bytes;0proceeds silently).[generate]:alix deck generate’s drafting:model, the absolutetimeout_secs(3600), andidle_timeout_secs(300, or0to disable). The latter is a resetting inactivity limit for structured-event backends and a nonrenewing absolute fallback for unstructured backends. Other controls aremax_cards(100, a soft ceiling: exceeding warns, never truncates), defaultlanguageandaudience,card_style(mixed,plain,cloze, orauthored-choices),extra, apromptoverride, andreview. Per-run flags override the language, audience, and style defaults. The terminal shows calm live progress, but partial generated cards remain hidden until the result passes validation.[exam]: the AI exam:model,effort(unset:[ask] effort; a workspace’salix.local.tomlmay set its own),timeout_secs(3600),pass_threshold(1.0),strictness(balanced), andextra.[trace]: thegenerate deck --traceandworkspace generateplanning passes:modeldefaults to unset, which resolves to the backend’s strong model where it defines one (Claude:opus), andeffortdefaults to"high"(the build is correctness-critical and amortized); alsotimeout_secsandextra(extra guidance appended to the build prompt).[ai]:alix deck augment’s generation targets: amodeloverride,distractor_count(3),variant_count(4),keypoint_count(5), andtimeout_secs(300, sized for a whole-deck batch).
Decks directory and storage
By default alix looks for decks in ~/decks; set decks_dir to change it.
Shareable material and private user files default to that folder, one document
per initialized deck:
<decks_dir>/
├── augment/deck-<token>.json
└── .alix/
├── progress/deck-<token>.json
├── recent.json
└── sync.toml
.alix/progress/ is private, indispensable learning state: schedules, review
history, exam state, and the last writer. augment/ is
regenerable, shareable material: generated choices, notes, key points,
variants, and topologies. It stays beside the deck so sharing the deck can
carry its generated study material without carrying personal history. The
stable deck id (deck-<token>), not the Markdown filename, selects both
documents, so renaming a deck keeps their ownership stable.
.alix/sync.toml holds the served folder’s paired-sync identity. Alix mints
its root_id the first time that folder is served and preserves it on later
starts and moves. Copying the whole folder deliberately copies that identity.
If the copy is meant to become an independent library, delete only the copy’s
.alix/sync.toml before serving it; the first serve then mints a new identity.
Do not delete the file from an existing paired library merely to fix a phone:
the root check exists to stop progress from landing in a different library.
decks_dir chooses what bare alix serves; it does not relocate state for a
deck named elsewhere. Every deck uses its content root: the workspace root for
a workspace member, or the containing folder for a loose deck. Bare alix,
alix <dir>, stats, list, and reset therefore resolve the same colocated
.alix/ documents. Shareable augment/ and assets/ stay beside the decks.
Each document carries its owner ID, format version, and revision. Saves write a
sibling .json.tmp and atomically rename it into place. A process that can see
that its loaded revision is stale refuses to overwrite the newer document.
If the replacement commits but the final directory flush fails, Alix reports
the failure while retaining the committed revision in memory, so a later save
can retry instead of remaining stale forever.
This protects local overlapping writers; it cannot turn disconnected folder
synchronization into a transaction.
Alix is pre-1.0 and reads only the current version-1 per-deck documents. A
persisted-state format break is handled before installing that build: back up
the affected files, perform any one-time conversion outside production Alix, and
verify the result with alix doctor <folder>. Production does not contain
runtime compatibility branches or converters for superseded pre-1.0 layouts.
Backing up
Everything Alix stores is plain files in one folder, so a backup is a copy of
that folder. Use whatever you already use for folders: a cloud drive, git,
rsync, Time Machine, or cp -r. Alix manages no backup archives or
generations of its own: a general backup could only reproduce what those tools
already do, losslessly, over the same plain files. What it does keep is one
.bak safety net per overwrite: alix deck restore swaps a deck (file, review history,
augmentations) with the backups the last overwrite left behind. Keep an
independent copy of any study history you care about.
What Alix does guarantee is that its own writes cannot corrupt your files. Every state, deck, and manifest write goes to a sibling temporary file that is flushed to disk, atomically renamed over the target, and (on Linux and macOS) has its directory entry flushed. An interrupted save leaves the previous file intact, never a half-written one; a kill-point fault-injection suite checks that at every filesystem operation, including partial multi-document saves and the deck/progress promotion boundary. Surviving a hard power loss additionally relies on flushing before the rename, which the code does but a test cannot simulate. That protects against Alix; your own folder backup protects against disk failure and accidental deletion.
Multi-device via your cloud drive
With the defaults, your decks, augmentation, assets, and progress live in one folder. Put that folder in a cloud drive you already use (Dropbox, iCloud, OneDrive, Syncthing) and it follows you across devices. Alix stays unaware that the folder is synced and uploads nothing itself.
To share only the workspace’s authored and regenerable material while keeping
private files on each person’s device, add this .stignore to the shared
folder:
.alix/
*.local.*
For a free, no-account option that fits alix’s local-first grain,
Syncthing works well: install it on each machine, pair
the devices, and share your decks folder between them. It syncs the folder
peer-to-peer over your own network, with no cloud company in the middle. Add
*.json.tmp to the folder’s .stignore too, since alix writes documents
through a temporary file before the atomic rename above.
The writer boundary is now one deck, not the whole workspace. Different devices may review different decks in the same synchronized folder: their progress documents do not compete. For the same deck, use one active writer and let synchronization settle before switching devices. Alix does not merge concurrent same-deck reviews or decide which schedule is semantically correct. A disconnected collision therefore remains a Syncthing conflict copy. If a running web session’s own deck is replaced under it, that session can no longer save; the review screen shows a persistent banner and the fix is to reopen the deck (grades made after the collision are not kept).
Run alix doctor <folder> before recovery. For a progress conflict, stop both
writers and synchronization, back up the folder, compare the canonical
.alix/progress/deck-<token>.json with its
deck-<token>.sync-conflict-….json copies, and deliberately keep the complete
history you trust at the canonical path. Do not combine schedules by hand.
Augmentation conflicts are regenerable: keep one complete
augment/deck-<token>.json or move all conflicting copies aside and regenerate
that deck’s augmentation. Resume synchronization and rerun doctor only after
the canonical files are settled.
A card’s identity is a minted token alix writes into the file as an
<!-- id: ... --> line, not a hash over its content. Editing any text,
including the answer, preserves a card’s history; only deliberately replacing
a card starts it over. (That’s the “editing is safe” rule from
chapter 3, stated precisely.)
alix reset <target> clears progress so cards go “new” again: a whole deck, a
folder or workspace (every member deck, plus a workspace’s mastered flags and
personal-card schedules), a single card (--card <id-or-front>), or the entire store
(--all); it confirms once unless you pass -y.
17 · Command reference
A quick index of the alix commands. Each links to the chapter that covers it in
depth, where there is one. Run any command with --help for its full flags.
Reviewing
alix: serve the web app: the deck picker over your decks directory (~/decks), printing its URL.alix <dir>: serve that folder as a self-contained scoped root: its own catalog and shareableaugment/andassets/, with private per-deck progress plus recent history under its colocated.alix/directory. A workspace dir opens the picker drilled into it.
Every review starts from the picker. There’s no direct deck launch. Browsing a deck read-only, sitting the AI exam, and tracing a trace deck are all reached from the web picker rather than as their own commands (see the web app).
The single-instance launcher’s flags: --lan / --port / --token
(the web app), --session N (cards per sitting, overriding
the [review] max_session config; unrelated to the AI backend’s own
--session-id), --config <path>, and --log http,select (enable verbose
file records and mirror the named targets to stderr). The session depth is
picked in the picker’s split Depth… menu, an order or region in its focus
drawer (scheduling), and the card order is the deck’s
review: directive.
How each card is checked comes from its reveal: combined with the
session’s depth (reveal & session depths), not a flag.
Launch profiles
Launch profiles make it easy to run one named alix instance per person in a
household, with its own decks folder, port, and adult or kids frontend. Each
profile is a normal config file under the platform config directory’s
profiles/ folder.
A profile name cannot be empty or contain /, \, ., or :.
alix profile add timmy --decks ~/decks-timmy --port 7002 --kids
alix profile list
alix profile timmy
alix profile default timmy
alix --launch-all
alix profile remove timmy
alix profile <name> launches that profile on the LAN and reuses the stable
token generated when it was added, so a phone can bookmark the printed URL.
alix profile default shows the current default, names one when given a
profile, and clears it with --clear; bare alix launches that default.
alix --launch-all starts every profile in the foreground on its configured
port. Ctrl-C or closing the terminal stops them together.
Each profile’s decks folder must be separate: it cannot equal, contain, or sit
inside another profile’s folder. alix profile add refuses an overlap, and
bare alix doctor reports every existing pair without rewriting either
profile. Paths are resolved before comparison, so symlink aliases count as the
same folder.
Each profile keeps its own tutorial
progress, and alix profile remove also deletes that profile’s tutorial
folder.
Progress
alix stats, alix list, and alix reset each take a deck file, a plain
folder, or a workspace: a folder or workspace expands to
its member decks, and each deck resolves exactly as the launcher does: a
workspace member uses the workspace root and a loose deck uses its containing
folder. Inside that boundary, progress is loaded from
.alix/progress/deck-<token>.json; folder-wide commands aggregate the
relevant documents in memory without creating an authoritative combined file.
alix stats <target>: progress overview, completion state, and a per-depth due count.alix list <target>: every card with three per-depth cells, shallow to deep (Recognize | Recall | Reconstruct), each carrying that depth’s state and due time; a retired card readsrestinginstead.alix reset <target>: clear progress (--card,--all;-yto skip the prompt). On a workspace it also clears each deck’s exam state and personal-card schedules in the workspace’s own store, after one confirmation.alix reset <target> --exam(or--exam --all): clear only exam state, per deck: each card’s exam result, the deck’s passed and mastered instants, and its last full count. Drilling progress stays. Before asking, it names the decks whose exams this will lock: dependents in the same folder or workspace that have never sat their own exam. Not combinable with--card. A target progress document that does not parse is removed with the rest, and the prompt says so first; documents outside the target are never opened, and an I/O failure stops the reset before anything is erased.alix reset --orphans [target]clears only orphaned progress: store keys that match no card or deck in the scanned decks (a stripped<!-- id: … -->comment, a hand-deleted deck, a double-mint). Orphans are never removed automatically (they are evidence), so this is the explicit opt-in. It scopes to a named folder/workspace store, else the configured decks folder, and reads every progress document under its.alix/directory (the same documentsalix doctorreports on). A single deck file scopes to that deck’s own document instead. A folder whose last deck was deleted is still a valid target. Every deck-like file in a folder is scanned for live ids, including one still awaiting itsid:line, and any of them failing to parse aborts the sweep, since its cards cannot be told apart from orphans. Runalix doctorfirst to see what it would clear.
Deck dependencies (requires:) are edited by hand in
the deck file. There’s no separate command for it.
The AI features
AI authoring lives under the noun it produces, so the command names what you
get: alix deck generate always writes one deck, alix workspace generate always builds a workspace.
Both take the same steering options. --source-url <URL> records a public
source (added to the deck or workspace exam:) for later tutor and exam
context after local evidence is frozen. --goal <TEXT> scopes what the new
deck or workspace teaches. --language <LANGUAGE> controls learner-facing
output, and --audience <TEXT> controls assumed knowledge and difficulty.
--card-style mixed|plain|cloze|authored-choices selects the facts-card shape;
workspace trace items retain their checkpoint shape. --force overwrites what
is already there. Each subcommand takes --into <dir>, with the meaning its
own result needs: for deck an existing workspace to write into, for
workspace the folder to build.
alix deck generate <source>→ one deck from a web page URL, a local file, or a directory taken whole, with no planning pass (facts decks;-o/--output,--cards,--review,--print;--into <workspace>writes it into that workspace’sdecks/instead of the decks dir, and the workspace must already exist).- with
--trace→ that deck is a trace authored over the source (-o/--outputdefaults toexplore.md).--trace --planprints a ranked menu of suggested traces instead. - given an existing
trace:stub deck → builds its checkpoints in place.
- with
alix workspace generate <dir>→ the directory is explored for an ordered learning plan, which is confirmed and then built as a workspace, whatever its size (--title/--iconname and brand it;--into <dir>is the folder to build, created if absent, defaulting to one named after the source under the decks dir).--planprints the plan and stops. A source that is not a directory is refused, namingdeck generateinstead.
The rest of the AI-and-deck surface:
alix deck init <file>: explicitly initialize a hand-authored Markdown deck with stable deck and card IDs. Uninitialized.mdfiles are ignored by discovery and never stamped merely because they contain##headings, and a<deck>.local.mdis refused outright: it belongs to the deck beside it and never gets anid:of its own.alix deck augment <deck> --target <...>: precompute AI augmentations (choices, notes, questions, keypoints, format, order). The augmentation document stays beside the deck; private progress is read from the deck’s colocated.alix/directory whenformatconsiders personal cards.alix deck copy <deck> <workspace>: copy one initialized workspace member, its owned frozen assets, and its augmentation into another workspace. Stable deck and card IDs are preserved; progress is not copied.alix deck move <deck> <workspace> [--yes]: move the same public bundle, carry progress when the workspaces use different user roots, then remove the source. Refuses missing prerequisites and source dependents. Copy and move both refuse when acite:key the deck’s locators use would resolve to a different root in the destination, or when the deck’sexam:(its own and the workspace’s) would resolve to different files, folders or URLs there. Neither rewrites the deck.alix deck import <file.tsv>: import an Anki TSV export (no model CLI needed;--workspace <dir>imports into a workspace).alix deck remove <deck> [--yes]: remove a deck and everything that is its alone: the file, its review history, its frozen assets, its augmentations, and any.bakbackups. Total by design: nothing is backed up and it cannot be undone, which the confirmation states along with the stakes (cards with progress, reviewed-since date, the exact file list). A deck that othersrequire:warns and names them; they unlock rather than break.alix deck restore <deck>: swap every available.bakside for that deck (deck file,.alix/progresshistory, augmentations). This undoes a forced import, a trace or workspace regeneration, and also the progress backup an accepted paired-phone push leaves when the deck text itself has no backup. The command reports which of the three sides swapped. Nothing is destroyed: the swapped-away state becomes the new backup, so running it again swaps back. There is nothing to restore afterdeck remove, which deletes the backups too.alix workspace init <dir>: scaffold an empty workspace: analix.toml(--titlenames it), analix.local.toml(personal pacing: deadline, retention), and an emptydecks/plusassets/. Grow it withalix deck generate … --into <dir>oralix deck import … --workspace <dir>.alix workspace update <dir>: reconcile frozen source-backed members with their recorded local sources. The first run stages an exact sibling workspace for review;--applypublishes it without another model call and--discardremoves it. Changed or obsolete learning propositions retire their old card IDs; replacements receive fresh IDs.alix workspace augment <dir> --target <...>: precompute AI augmentations for every member deck at once (choices, notes, questions, keypoints, format), plusicon, the workspace’s own emblem. The card targets run as one batched call over every member’s cards, not one call per deck, and each card’s result is filed back under its own deck in the workspace’s augmentation folder.--target orderis deliberately absent: a review order is built per deck, so it lives onalix deck augment.iconredraws unconditionally, replacing the old emblem.alix workspace deadline <dir> [<date>|clear]: show, set, or clear a workspace’s personal “ready by” date (--config <path>); no argument prints the current one. Workspace-only, see Workspaces.- Tutor: the Ask button (or
a) in a session, Make this a note to save a note (the tutor).
The agentic generate runs measure the source size before running and prompt
for confirmation when it’s large. Pass --yes to skip the prompts in
non-interactive scripts. The AI exam runs unattended in
the browser instead, so it can’t prompt: it truncates an oversized source and
notes it.
Sharing
alix share <path>: send a deck file, a plain folder, or a workspace to someone over magic-wormhole (thewormholebinary must be installed,alix doctorchecks). A folder is staged first so your personal state stays home:.alix/,*.local.*, temporary files, and conflict or backup files never travel. From a workspace only its alix files go: the manifest,decks/, ownedassets/, and augmentation; source trees and other files at its root stay home. Matchingaugment/deck-<token>.jsondocuments do travel, including when sharing one deck. A single frozen deck also carries its completeassets/deck-<token>/directory. A symbolic link inside what you share is refused by name rather than followed, since the copy carries files and the folder you picked is the boundary of what leaves the machine: replace the link with what it points to, or remove it. Tell the receiver the code wormhole prints. No wormhole around?--zip [--output <path>]writes the same staged copy as a.zipto mail or hand over instead.alix receive <code-or-zip>: fetch what someone shared, by wormhole code or by a.zippath (the--zipfallback’s output, same landing either way). A deck lands in your decks directory (--workspace <dir>puts it inside a workspace;--forceoverwrites a same-named deck); a folder lands under its own name beside your other decks and is never overwritten. Personal files that leaked from the sender’s side are stripped on arrival, and an archive carrying a symbolic link is refused before anything lands, so the sender cannot decide what appears in your decks folder: ask them for one that carries the file itself.
Config & health
alix config: show the active key bindings;alix config --initwrites the file.alix bug-report [--out <dir>] [--include-deck <path>]: write a local ZIP containing the bounded diagnostic logs, platform, version, and build commit details, a config without tokens or AI prompt guidance, and hashed deck identities with aggregate counts. Every included file is plain text. No deck content is included by default.--include-deckadds exactly the named deck verbatim, including its card text and authored notes, and names it inreport.md. Personal sidecars, prompts, and responses always stay out. The command never uploads or sends the archive; review it before attaching it yourself. It uses the default profile when one is selected, otherwise the default config and decks directory.alix doctor [dir-or-deck]: with no argument or a directory, environment health checks, a one-line remedy per problem: the config parses, the current profile’s local log path is named, the progress store is readable, the decks dir scans, and the backend CLI, thesharehelper (wormhole), and thediagramsrenderer (sekien) are on your PATH. Name a deck file instead to lint it in depth (syntax and named-fieldat:locators); that form runs the deck lint only, none of the environment rows. It withholds stale excerpts and reports a unique exact relocation, changed content, ambiguity, or a missing fingerprint. alix does not recognize or rewrite old deck formats; a deck written in one fails as ordinary invalid input (an unknown key, an id or locator that fails the current grammar). Over a folder or workspace it also reports identity problems across the decks as a set: duplicate deck or card tokens (naming which copy keeps the earned progress), store keys matching no live card or deck (orphans, clear them withalix reset --orphans), a non-canonical token, a frontmatter that can’t be stamped, an id marker away from its card’s closing line (the position stamping mints at), and cards still awaiting a token. Forrequires:it separates a dangling filename edge from a dangling deck-id edge, acard-…id pasted where a deck belongs, and a file that only shares a required id’s name (the id wins, so add the.mdextension to mean the file). It flags aexam:pointing intoassets/(a deck keeps its real source, never its frozen excerpt fragments). It also names deck-like Markdown ignored until explicitly initialized, invalid or orphaned per-deck progress or augmentation documents, and synchronization conflict copies. Workspace checks also reject live source evidence, missing or cross-deck assets, local images outside the owning deck directory, and SHA-256 filenames that do not match their bytes. They warn about a frozen object the deck no longer references and about any non-deck entry indecks/(hidden files, personal.local.mdfiles,README/LICENSE, backups and conflict copies excepted). A liveat:citation that does not resolve is an error. A locator whose key is missing or names nocite:root, that breaks the locator grammar, or that does not fit its root (a file under a URL root, a bare key under a directory root, a URL passage without anasset:) is an error in every mode, repair flags included, frozen or live, trace decks included. Inalix.tomlit warns about each unknown key or invalid value in[defaults], which alix ignores, and reportscite,examinside[defaults]as an error naming where the key belongs.--backendsadditionally probes the configured AI backend end to end (one real, tiny request);--all-backendsprobes all four.--gradingspot-checks the configured model’s exam grading against the hand-labeled calibration probes (a few real, costed calls, batched by strictness): answers that must not pass (wrong, empty, off-topic, incomplete at strict, flawed math derivations) and answers that should (correct ones, including full proofs). A failed must-not-pass probe is the serious direction (exam grades may be too lenient), while a missed should-pass probe only means the grader is harsher than intended. It’s a spot check, not a certification. Without an explicit repair flag, doctor is report-only and fixes nothing. With no argument it also checks every launch profile’s root identity. Two profiles carrying the same.alix/sync.tomlroot_idare an error naming both profiles. A malformed root id names its profile and file, and a nested.alix/sync.tomlbelow a profile folder names the profile and relative path. A missing file or key is clean and doctor never mints one.alix doctor [dir-or-deck] --normalize: rewrite each checked deck into its canonical bytes, dropping a leading byte-order mark, turning CRLF endings into LF, and removing trailing spaces and tabs. A hard line break (two or more trailing spaces) is kept as exactly two, and a code fence keeps the trailing blanks on every line it owns, opener and closer included. alix normalizes every deck it writes anyway, so this is for a deck an editor changed after it was initialized. A rewrite that would stop the deck parsing is refused.alix doctor [dir-or-deck] --warn-on-source-locator-drift: also warn where a frozen excerpt no longer matches its live source: the root or file is gone, the text changed, it occurs more than once, or it moved to other lines. Frozen evidence going stale is normal (a shared frozen deck works without its sources), so without this flag doctor says nothing about it, and with it the finding is a warning, never an error. A live citation that does not resolve is an error either way.alix doctor [dir-or-deck] --repair-source-locators: after you review the reported citations, stamp fingerprints on currently addressed excerpts and rebase any whose lines moved while their content stayed identical, frozen excerpts included (it finds moved frozen excerpts itself, with or without--warn-on-source-locator-drift). A whole-file citation is never narrowed to a range. A rebase corrects theat:line numbers only; the frozen evidence and its fingerprint are never rewritten. Changed or multiply matching excerpts remain untouched and make the command fail, because whether such a card still teaches the truth is a reader’s call. Deck and card IDs are preserved.alix doctor [dir-or-deck] --repair-diagrams: after you review the reported diagram findings, delete stamps attached to no fence and re-freeze every stale or unfrozen fence (workspace members only; needs the renderer on PATH). Orphan removal is whole-line and atomic; a second run has nothing to do.alix doctor [dir-or-deck] --repair-positions: after you review the reported span anchor divergences, rewrite each divergedposition:anchor to where its span binds today (the keep-what-you-authored resolution). To keep an anchor’s old target instead, setoccurrence=yourself; doctor never retargets a span on its own.alix doctor [dir-or-deck] --repair-frontmatter-order: rewrite each checked deck’s frontmatter into the canonical key order (authored keys first, machine lines likeidlast). Opt-in only: doctor never diagnoses your own order, and frontmatter it cannot safely permute (a blank line or comment inside the block) is left as-is with a note. Card and deck IDs are preserved.alix doctor [dir-or-deck] --repair-comment-order: rewrite each checked deck’s trailing comment machinery into the canonical order (invocation, directives, region comments,at:locator,idlast). Opt-in only: any order parses, an editorial comment or content bounds what may move, and IDs are preserved.- Folder and workspace runs also count accumulated
.bakbackup files (overwrite leftovers, including the progress backup an accepted paired-phone push leaves under.alix/progress) with their total size, naming both remedies:alix deck restore <deck>swaps one back,alix doctor <dir> --remove-backup-fileslists and deletes them all after one confirmation (--yesskips it). Backups warn, they never fail the run. --config <path>: use a different config file.
How alix was made
alix is an AI-built project with a human maintainer. That description is more
accurate than either “hand-written by a human” or “made autonomously by AI.”
Models have produced a large share of the implementation, tests, documentation,
and design drafts. The maintainer chooses the problems, sets the constraints,
challenges the design, checks the result, and decides what enters the project.
This chapter follows the same rule: it was drafted with AI assistance for the maintainer to review. It does not pretend to be purely human-authored.
How a change happens
A typical change begins as a conversation, not as a model receiving the whole repository and independently deciding what to build.
- The human defines the job. The maintainer supplies the need, product boundary, and important constraints. For a substantial feature, that becomes a written specification and implementation plan before code changes.
- The coding agent investigates. It reads the repository, finds the relevant contracts and tests, proposes a design, and raises conflicts or missing decisions. It then edits code, tests, documentation, and examples together.
- Deterministic gates check the mechanics. Formatting, linting, unit tests, integration tests, contract snapshots, and end-to-end tests exercise behavior that can be checked repeatably.
- The human reviews the result. The maintainer reviews the product behavior, important design choices, the diff, and any visual result. They may reject the approach, narrow the scope, request another implementation, or approve a commit. The agent is not allowed to commit or push merely because its tests pass.
- Release checks examine the candidate. Every desktop or mobile release receives live-model calibration, prompt changes or not. Public documentation and screenshots receive a read-only semantic audit against the implementation. Release artifacts have their own platform checks.
The loop is deliberately conversational. The model contributes speed, breadth, and persistence; the human supplies intent, taste, risk tolerance, and the go/no-go decision.
What the models produce
During development, a coding model may write almost any repository artifact: Rust and Dart code, HTML and CSS, tests, specifications, plans, runbooks, changelog entries, and first drafts of prose like this chapter. It may also run the project’s tools and explain the evidence it used.
That development-time model is separate from the AI backends that alix calls
as a product. The tutor, deck generator, trace generator, and examiner invoke a
model CLI selected by the user. Replacing that runtime backend does not rewrite
the application, and using alix for ordinary offline review does not require a
model at all.
Generated output is not accepted because it sounds confident. Repository conventions push behavior into the shared Rust library, preserve stable card identities, require tests around important logic, and keep public contracts written down. Those constraints give both a human and another agent something concrete to inspect.
What the human reviews
The maintainer owns the decisions a test cannot make:
- whether a feature belongs in
alixat all; - whether the interaction stays calm and understandable;
- whether a plan protects user data and stable file formats;
- whether a screenshot or manual run actually feels right;
- whether the explanation matches the intended product;
- whether the remaining risk is acceptable.
This is not the same as independent review by a second engineer, and the project should not imply that every generated line has received a deep manual audit. AI-assisted development can produce changes faster than one person can study them. The response is to make review evidence durable: small commits, explicit plans, focused tests, source-linked decision decks, and release audits. These improve traceability; they do not turn a single maintainer into two reviewers.
What tests prove, and what they do not
alix separates deterministic software correctness from model-behavior quality.
The blocking make check gate uses ordinary Rust tests and a fake model CLI. It
can prove that known inputs produce expected state transitions, errors are
handled, contracts remain compatible, and AI plumbing behaves correctly for
canned responses. CI can repeat those claims without network access or model
variance.
Live-model calibration asks a different question: do current prompts produce useful, appropriately strict results? It is costed and non-deterministic, so it is run deliberately before every desktop or mobile release, and again after any grading-prompt change, rather than pretending to be an ordinary unit test. The documentation audit is also a deliberate model call: it compares public text and images with current implementation evidence before a release.
Neither layer proves that every future model response will be good. A green suite also cannot prove that the product decision was wise, the architecture will remain maintainable, a migration is operationally safe, or a human understands every changed line. Manual review, calibration, release practice, and feedback from real use remain necessary.
Attribution and responsibility
The repository history preserves AI assistance with Co-Authored-By trailers.
That is attribution, not a transfer of responsibility. A model cannot own a
release, respond to a data-loss incident, or decide what risk another person
should accept. The human who approves and publishes a change remains responsible
for it.
The useful standard is therefore not “no AI touched this.” It is: the role of AI is disclosed, important decisions are inspectable, repeatable claims have repeatable tests, uncertain model behavior is evaluated as such, and a human makes the final call.