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 walk 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 and seeds The alix
tutorial, a small deck that teaches alix while you review it, from grading
honestly to the deck format below. Its last card tells you to delete it; alix
seeds it only into a brand-new decks directory, so once deleted it never returns.
If you already have decks, nothing is seeded.
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, received, and
tutorial decks are initialized when they are created.
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: hide the span with <!-- blank: span hidden="..." --> on its own line below the answer. | 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. |
| 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 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. - 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 -->takes a> [!NOTE]note after it. - 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.
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 names the group and, when present, is the row card’s only context line; it 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 opens its own
callout with a chip naming it, coloured from whatever theme you are using
rather than from GitHub’s palette. 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.
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.
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.
The web app shows a card’s section heading as a dim one-line title above the
question. The first card you ever meet from a section opens the whole section
in a sheet under the question, once; after that, in this sitting and every
later one, the title line, the c key, or the review menu’s Context entry
opens it. The Android app shows the heading as a tappable pill
that opens the section in a sheet, and opens that 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. To end one without opening another,
write a bare #: a # with no title opens an empty context, so the cards
after it carry none until the next titled # (a sub-card chain does not cross it
either). Prose under a bare # belongs to the new, empty section, exactly as it
would under a titled one.
A bare # needs a blank line above it, like any other block. Attached directly
under a card’s own lines it is an error, since a reset there would silently
truncate the card.
### 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 -->
> 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>lines. Those lines are appended to that card’s own note when you review it. 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 its answer; 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. Plain Learn reuses the
deck’s own last-used depth, remembered per deck. 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; 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, press the remove key (Ctrl-X by default) instead
of grading it: it leaves the session and is deleted from the deck file when you
finish.
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.
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. On the web you can then hide and show the answer
again (h, or a tap on it) to self-test the fresh encoding before you press
Seen; it flips only the answer’s visibility, so the note, the buttons, and the
layout stay put. 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 never-met cards and cards due
at Recognize.
This is the first step of a card’s life: introduce, then let its depth(s)
schedule it.
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 its interval grows past
retire_after (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 a source: adds one state in between: exam due. For
those decks, drilling the cards no longer finishes them: passing the AI exam
does, which marks the deck mastered. That’s the subject of a later chapter.
Unlocks, in one line
Completion 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 is advisory and 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 long-press 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 the answer; you create one by pointing a
blank: directive at a span of the answer text.
Write the answer as ordinary prose, then name the hidden text in a
<!-- blank: span hidden="..." --> comment after it. Each directive is a
blank, and the card expands into one sub-card per blank. 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" -->
This makes two cards. One blanks mut and shows the rest; the other blanks
u64. The asked span is marked apart from the other spans, which are hidden
as ⬚, so no card gives away its siblings’ answers. You only produce the
hidden text. The web app draws both as chips rather than showing the glyphs
themselves.
The hidden text must appear in the answer 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 card’s front contains a blank’s answer: the card
would show its answer before the reveal.
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
> [!NOTE]
> base: Fastest and most numerous, which is why they sit at the bottom.
<!-- blank: span [base] hidden="Unit" -->
<!-- blank: span hidden="integration" -->
<!-- blank: span hidden="end-to-end" -->
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, and it doesn’t apply to cloze cards. 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 beneath the image it marks, 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.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:
<!-- 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 source: 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. Declare the deck’s source
with a source: line in the frontmatter, give the card an <!-- at: ... -->
locator into it, and on reveal the card offers to swap the worded answer for the
exact source lines:
---
source: src/string.rs
---
## What does the `String` struct hold?
A `Vec<u8>` (its bytes).
<!-- at: src/string.rs:1-3 fingerprint: xxh64-0123456789abcdef -->
The locator is the same shape a trace checkpoint uses. Its
fields are named and ordered: at: is the source path and line range (e.g.
src/string.rs:1-3, just lines when source: is a single file, or a
range-less path or URL to cite the whole source, the form a frozen URL
source uses), and
fingerprint: is an xxh64-<hex> digest of the displayed source text. 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: src/state.rs:64-74 fingerprint: xxh64-0123456789abcdef -->
<!-- at: src/state.rs:114-118 fingerprint: xxh64-123456789abcdef0 -->
<!-- at: src/state.rs:152-158 fingerprint: xxh64-23456789abcdef01 -->
Each locator remains one contiguous range; separate directives never imply
that disjoint code is adjacent. On reveal a </> marker appears on the answer:
click the answer (or press s) to swap it for the same editor-style source
panels used by trace walks, and back. Multiple excerpts are stacked in authored
order inside the one scrollable answer 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 trace walks 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 source: pointed at the real material: each frozen
citation keeps its real path in at: and gains an asset: field naming the
content-addressed object, so a drift check can still compare against the live
source when it is reachable.
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 source:. |
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). |
source | deck | Exam ground truth: a YAML list of URLs, files, or directories (one entry is the norm), also a trace’s cited path 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 | What a trace walks; its presence makes the deck a trace. |
at | card | A repeatable named-field locator into the source (at: file:lines fingerprint: xxh64-..., plus asset: once frozen; a range-less path or URL cites the whole source): 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: a region of the preceding image (rect x= y= width= height= hidden="...", optional [group]) or a text span in the answer block (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 preceding image or span in the answer block. No group, no stamp, no card. → ch 6 |
crop | card | A viewport onto the preceding image (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 source
Two that look similar but aren’t. Both point at material a deck is about, but
source 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: a source 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, and a shared source, 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.
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
source, 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. An inherited or relative source is written
explicitly into the transferred deck so the destination cannot reinterpret its
live provenance through unrelated workspace defaults.
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/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 walks 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 to a walk) 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
A workspace is a self-contained folder, so sharing one is sending the folder
with its decks/ structure intact.
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 a source:, 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 a source:
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 source: 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 a source: 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 masters by passing
its exam (retracing the path from memory) so it gates and unlocks like any
exam-grounded deck.)
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 mastery (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 ? 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 asks for an explicit click on Leave
anyway; Escape chooses Stay so it cannot also abandon the 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: Ctrl-N
When an exchange clears something up, press Ctrl-N: the tutor condenses the
conversation into at most three short note lines and appends them, addressed
to the card, to the deck’s personal sidecar (<deck>.local.md); the deck
file itself is untouched. Notes aren’t part of the card’s identity, so its
progress is untouched: you just keep the insight. (In the web panel, Make
this a note does the same.)
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 source: (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: source:
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 source: (and a workspace’s source) records where that evidence
came from and can give the tutor broader current context. A URL source is
fetched when the selected backend can use WebFetch. A local source is readable
only when [ask] source_access = true. 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 grounding 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. An
explicit deck or workspace source defines the readable root. Without one,
alix does not grant the tutor filesystem access: a source: identifies the
cited evidence, but it never implicitly authorizes the surrounding project.
This keeps decks portable across profile-managed deck directories and keeps
every wider live-source grant reviewable: globally in your config, or per
workspace in a manifest you can read.
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.
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 and is drilled and scheduled
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 asource: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.
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 a source: source:
Name one or more sources in the deck’s frontmatter, each a URL, a file, or a directory:
---
source:
- https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
- notes/ownership.md
---
A URL source: 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.
Once every card in an exam-grounded deck has graduated (reached FSRS’s
review phase, past the initial learning steps) the deck is exam due rather
than finished: drilled, but not yet counted, so it doesn’t unlock its
dependents yet. A deck with no source: at all (and no workspace source)
simply becomes finished when all its cards graduate, unlocking its
dependents directly.
Sitting the exam
The exam is a guided, one-question-at-a-time flow (answer, move Back/Next, then a per-question breakdown) in the browser. You reach it two ways:
- From the picker: choosing an
exam duedeck starts the exam instead of an empty review. - From the summary: when you drill a deck’s last cards and it turns exam due, the session-end summary offers it.
alix asks the model to read the source (URLs via WebFetch, local files embedded)
and write fresh understanding questions (application and connections, not
the card facts) each with the key points a correct answer must hit. 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.
- Pass (every question by default, tune with
pass_threshold) marks the deck mastered (mastered ✓). Mastery, not mere drilling, is what unlocks decks thatrequires:this one. Source-less decks are unaffected: finishing them just means drilled (done ✓). - Fail lists the gaps and offers to turn them 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 drill like any other card (a first pass one
introduction cooldown later, then FSRS schedules them) and 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.
A trace deck is examined differently: instead of generated questions, its exam asks you to retrace the whole path from memory in a sentence or two (the compression) graded holistically against the checkpoints (no question generation, no source read). Passing masters the trace; a fail sends you back to re-walk it. See trace decks for the full flow.
Resetting a whole deck (alix reset <deck>) also clears its mastered state, so a
re-drilled deck must pass again; resetting only an individual card (--card)
leaves mastery 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 (how
many answers must pass). Both, plus model, timeout_secs (default 300),
num_questions (default 5), an extra guidance field, and
retry_cooldown_secs (default 3600; 0 disables the wait before re-sitting
a failed trace exam; a failed fact-deck exam remediates instead), live in
the [exam] config 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 mastery (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 walking 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 walks, and the
thing that marks the deck a trace) and a source: (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
source: .
---
## 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: src/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: src/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
file:start-end (just line numbers when source: is one file; a range-less
path or URL cites the whole source, the form frozen URL sources use), never
comma-separated, since a stitched excerpt makes disjoint code look adjacent. Its
fingerprint: xxh64-... field fingerprints the displayed lines. A live walk
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
source:, 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.
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 source: 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.
Walking it
Pick the trace in the web picker, or on the mobile app (the walk runs fully offline there too): a trace opens as a walk: 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 walk 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 walk is complete. That’s the drill; the verification (what masters the trace) is its separate exam, below.
The exam — the compression
A trace’s trace: is a question (“how X becomes Y”). The exam is to
answer it: retrace the whole path in a sentence or two, from memory. The model grades
that compression against the path’s checkpoints (AI-graded, exactly like a
fact deck’s exam) and
passing masters the trace, which unlocks its dependents. So the symmetry is:
- walking the checkpoints (predict → verify each edge) is the drill;
- the compression is the exam.
You reach it in the browser: the capstone offered at the end of a walk
(Take the exam?), or the picker’s
“Take exam” button. A paired phone offers the same
capstone from its own walk. Like a fact deck, you can sit it early to test
out — gated only by requires: (a trace’s sourced prerequisites must be
mastered first).
A failed trace exam is re-walked, not turned into remediation cards (a
trace is a path, not a card pile) — the weak checkpoints already resurface sooner
through their own SRS. After a fail the exam cools down for a while before you
can re-sit it, so the graded feedback can’t simply be pasted back into the one
fixed question ([exam] retry_cooldown_secs, default one hour; 0 disables it).
Immediate freezing
Because <!-- at: 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 source: pointed at the real material (a path or a URL): it is
never rewritten to point into assets/. Each <!-- at: --> keeps its real
at: path and lines, keeps its excerpt fingerprint:, and gains an asset:
field naming the content-addressed object:
<!-- at: 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: path
and the deck’s source: retain provenance for drift reporting and a future
deliberate source update. When the live source is available and permitted, the
tutor and exam may consult it for surrounding context and staleness detection;
offline, they report 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 source: —
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 separately reports live-source drift.
A trace deck degrades gracefully — even outside a walk 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 walks 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 source: 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; --title names it) and one file per item under
decks/: a trace: deck per trace and a source: facts deck per deck, wired together
with requires: so they unlock in dependency order, each source: 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 walk — --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. It’s
written to a file (-o, default explore.md; --into places it inside a
workspace), and you walk 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 opens a walk) 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. 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 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.
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. 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 keeps its section heading above the question as a dim,
one-line title. 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 to just under the question,
as wide as the card, so the question stays legible above 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.
Select the title (its key sits beside it), press c, or use Context
beside Ask Tutor in the review menu to open the sheet on demand. While
the sheet is open, the card and its review keys are inert; a click on the
dimmed page, c, or Escape closes it, and focus returns to the title.
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 Tutor and,
when available, Context; a trace walk holds Ask Tutor. On the deck
picker, the library actions above plus keyboard
shortcuts 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.
The AI features come along too: the tutor, the AI exam, and trace walks 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 walk): 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 trace walks) 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 (including a trace’s compression 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. 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, including Make a
card and Make a note (condenses the exchange into up to three lines
and appends them to the deck’s personal sidecar,
<deck>.local.md, on the phone; sync that file alongside the deck, and an empty result says so rather than doing nothing silently), re-sending the whole exchange to the paired desktop on every turn (the server keeps no session of its own for a remote turn). - A Take the exam chip on the session summary, for any deck that
declares a
source:. It opens a full-screen exam: one question at a time, then a Pass/Partial/Fail breakdown per question and, on a fail, a Turn the gaps into cards button. A pass and any remediation cards it creates 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 reaches the exam differently: its walk (predict, reveal, self-grade) runs entirely on-device, no pairing needed. Only once paired does the walk’s done screen offer “Take the exam” for the trace’s compression question, graded on the desktop the same way a fact deck’s exam is; a fail is re-walked rather than turned into remediation cards, since a trace is a path, not a card pile.
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, the Take the exam chip, and the walk’s own exam offer all 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 chip or offer simply is not there.
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’s own SnackBar sits under its own still-open modal and has no
room for one, so there you follow the message’s own instruction instead.
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 walk. 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 (a trace’s compression exam
included), 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 ships
a few sample decks so there is something to review immediately, and pairing
with a desktop server (see Pairing a device) adds the
Generate deck row for bringing in more.
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.
Tapping a deck opens it at the depth it was last reviewed at. Long-press a deck row to choose the depth for this session instead. The same 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, and a plain tap on the deck never crams.
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. A card that sits under a # section in its deck (see
Sections and sub-cards) shows
the section’s heading as a one-line pill above the question; tapping the pill
opens the whole section, heading and prose, in a sheet. Neither appears on a
card without a section, and the card itself never moves to make room. The
first card you ever meet from a section opens that sheet by itself, once;
swipe it away and the pill stays as the way back in. Every later card from
the section, in this sitting or any later one, shows the pill only.
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. 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 take the desktop’s, naming what each choice discards. 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 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, 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"). The
rebindable [keys.review] actions are failed, partly, passed, reveal, submit, skip,
remove (default ctrl-x), ask (default ?), context (default c, open
the card’s section drawer),
continue, restart (default r), quit, 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-n) and make_card (default ctrl-d). 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>.
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 to toggle its
tick-box, defaults v, 1/2/3, and c), 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"]
remove = ["x"]
quit = ["q", "esc", "ctrl-c"]
[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 card rests once its Recall interval reaches this ("2w", "6m", "30d", or "never")
introduction_cooldown = "5m" # settle gap before a new card's first quiz ("90s", "10m", "1h"; "0" = none)
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 when
a 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.
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 mastered, or finished when they have no exam grounding. Mastery 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,timeout_secs(300),num_questions(5),pass_threshold(1.0),strictness(balanced),extra,retry_cooldown_secs(3600;0disables the wait before re-sitting a failed trace exam; fact-deck exams never wait).[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 walking a trace 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.
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.
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 the mastered flags and personal-card schedules in the workspace’s own store, after one confirmation. 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 source:) 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.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
?) in a session,Ctrl-Nto 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. 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 and version 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, named-fieldat:locators, and frozen cards that have drifted from their live source); 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 nudges asource:that lists more than a few entries toward their common directory, and flags asource: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.--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] --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. 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.