Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

materialshapekindwhy
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.structuralOne 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.structuralOrder 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: drawstructuralThe 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.judgementThe 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).judgementThe 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: bothjudgementOne 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.judgementThe 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, an at: 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.

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 type H2O for H<sub>2</sub>O. A dropped tag still separates what stood on either side of it, so x<sup>2</sup>_i_ italicizes instead of reading as one word.
  • Entities. The full HTML5 named set (&amp;, &euro;, …) and the numeric forms &#65; / &#x41; decode to their characters on display, per CommonMark. Anything that is not a complete, valid entity stays literal. A decoded character is content, never markup: &#42;x&#42; shows *x* without italics, and &lt;div&gt; shows <div> without the tag-shape error. Markup beside an entity is judged on the spelling in the file, where the neighbouring character is & or ;, so &Aopf;_x_ italicizes x. 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 (![diagram](<a b.png>)) 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: sequential in 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: multiple asks 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 flip or cloze card reveals and you self-grade; a line card reveals line by line, then you self-grade.
  • At Reconstruct, you produce it: a cloze card has you type the gap; a card with a short, single-line answer has you type it; a line-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: draw in 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: a ![](...) image 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:

  • failedAgain, a lapse; the card comes back soon and its interval shrinks
  • partlyHard, a weak success; a shorter next interval than a clean pass
  • passedGood, 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-wide retention: 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 -->
  • both makes two cards: purportedangeblich and the swap angeblichpurported.
  • reverse keeps 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?
![](moon-waxing.png)

---
Waxing gibbous

## Play this chord:
G major

---
The open-position shape.
![](g-major-tab.png)

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: ![the open-position shape](g-major-tab.png), and a destination in angle brackets can carry spaces (![tab](<g major.png>)). A GFM title after the destination (![tab](g-major-tab.png "open position")) 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 ![](moon.png) 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
![four quadrants](quadrants.png)
<!-- 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; a hidden= on it is kept but inert, so switching a region between blank: and cover: 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 (default word): word requires the match to stand alone (punctuation next to it is fine); char matches anywhere, for sub-word blanks like hidden="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.

TokenScopeWhat it does
## frontcardStarts a card at column 0; the lines below are the answer. → ch 3
# headingsectionOpens 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 ###### frontcardA 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
> blockcardA 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
<!-- -->anywhereA 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: multiplecardInvocation 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.
cardscardInvocation comment on the line below a pipe table: maps it to a card table. A bare table renders literally.
plaincard · sectionOn 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).
choicesdeckDeck-wide default task-list mapping: single or multiple. A per-card invocation overrides it. → ch 3
format-versiondeckReserved. 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
iddeckThe 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
idcardThe 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
revealdeck · cardHow the answer is uncovered: flip (default) or line. Cloze is triggered by a blank: directive, never by a reveal: value.
reviewdeckThe order cards are served at review: scheduled (default) or sequential. → ch 5
inputdeck · cardtype (default) or draw: answer on a canvas instead of typing. → ch 4
directiondeck · cardReview direction: forward, reverse, both.
samplingdeck · cardon (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.
ignorecardBare <!-- 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
strictnessworkspaceExam grading rigor for the members, in alix.toml’s [defaults] only: a learner setting, so a deck declaring it gets an unknown-key lint.
requiresdeckPrerequisite deck that gates unlocks (repeatable).
titledeckThe 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
descriptiondeckA short summary, shown in the web picker’s deck drawer. → ch 3
authorsdeckWho 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.
licensedeckThe deck’s licence, a single string, by convention an SPDX identifier.
created-atdeckWhen the deck was made, a single string, by convention an ISO 8601 date. Stored verbatim and not validated.
linkdecktutor reference URL, tutor-only (repeatable).
sourcedeckExam 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.
tracedeckWhat a trace walks; its presence makes the deck a trace.
atcardA 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.
givencardA trace checkpoint’s off-screen symbol, as name - meaning (repeatable).
diagramcardMachine-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
blankcardMasks 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
covercardMasks 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
cropcardA 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 ![alt](src) where you want one to appear, and its position decides the side. See Image cards.

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 same direction: or reveal: line at the top of six files.
  • One deck should come before another. requires only 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 deadline and 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.)

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 WebFetch tool, and the deck opens with a link: 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 a source: 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 due deck 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 that requires: 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:

  1. Predict — type a guess before anything reveals (committing is the point).
  2. Revealalix shows the real excerpt from the source, then the key points and note.
  3. 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.
  4. 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 generate does, 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 .tsv or an alix .md file; 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 doctor recovery 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 --backends probe 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:

  1. Open Settings (the ☰ button on the deck list) and tap Connected devices.
  2. 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, a model override, timeout_secs, an effort, source_access (local source grounding; a workspace manifest may override it, see the tutor), and preflight_threshold (warn and confirm before spending a large model call on a local source tree bigger than this many bytes; 0 proceeds silently).
  • [generate]: alix deck generate’s drafting: model, the absolute timeout_secs (3600), and idle_timeout_secs (300, or 0 to disable). The latter is a resetting inactivity limit for structured-event backends and a nonrenewing absolute fallback for unstructured backends. Other controls are max_cards (100, a soft ceiling: exceeding warns, never truncates), default language and audience, card_style (mixed, plain, cloze, or authored-choices), extra, a prompt override, and review. 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; 0 disables the wait before re-sitting a failed trace exam; fact-deck exams never wait).
  • [trace]: the generate deck --trace and workspace generate planning passes: model defaults to unset, which resolves to the backend’s strong model where it defines one (Claude: opus), and effort defaults to "high" (the build is correctness-critical and amortized); also timeout_secs and extra (extra guidance appended to the build prompt).
  • [ai]: alix deck augment’s generation targets: a model override, distractor_count (3), variant_count (4), keypoint_count (5), and timeout_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 shareable augment/ and assets/, 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 reads resting instead.
  • alix reset <target>: clear progress (--card, --all; -y to 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 documents alix doctor reports 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 its id: line, and any of them failing to parse aborts the sweep, since its cards cannot be told apart from orphans. Run alix doctor first 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’s decks/ instead of the decks dir, and the workspace must already exist).
    • with --trace → that deck is a trace authored over the source (-o/--output defaults to explore.md). --trace --plan prints a ranked menu of suggested traces instead.
    • given an existing trace: stub deck → builds its checkpoints in place.
  • 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/--icon name and brand it; --into <dir> is the folder to build, created if absent, defaulting to one named after the source under the decks dir). --plan prints the plan and stops. A source that is not a directory is refused, naming deck generate instead.

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 .md files are ignored by discovery and never stamped merely because they contain ## headings, and a <deck>.local.md is refused outright: it belongs to the deck beside it and never gets an id: 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 when format considers 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 .bak backups. 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 others require: warns and names them; they unlock rather than break.
  • alix deck restore <deck>: swap every available .bak side for that deck (deck file, .alix/progress history, 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 after deck remove, which deletes the backups too.
  • alix workspace init <dir>: scaffold an empty workspace: an alix.toml (--title names it), an alix.local.toml (personal pacing: deadline, retention), and an empty decks/ plus assets/. Grow it with alix deck generate … --into <dir> or alix 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; --apply publishes it without another model call and --discard removes 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), plus icon, 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 order is deliberately absent: a review order is built per deck, so it lives on alix deck augment. icon redraws 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-N to save a note (the tutor).

The agentic generate runs measure the source size before running and prompt for confirmation when it’s large. Pass --yes to skip the prompts in non-interactive scripts. The AI exam runs unattended in the browser instead, so it can’t prompt: it truncates an oversized source and notes it.

Sharing

  • alix share <path>: send a deck file, a plain folder, or a workspace to someone over magic-wormhole (the wormhole binary must be installed, alix doctor checks). A folder is staged first so your personal state stays home: .alix/, *.local.*, temporary files, and conflict or backup files never travel. Matching augment/deck-<token>.json documents do travel, including when sharing one deck. A single frozen deck also carries its complete assets/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 .zip to mail or hand over instead.
  • alix receive <code-or-zip>: fetch what someone shared, by wormhole code or by a .zip path (the --zip fallback’s output, same landing either way). A deck lands in your decks directory (--workspace <dir> puts it inside a workspace; --force overwrites 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 --init writes 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-deck adds exactly the named deck verbatim, including its card text and authored notes, and names it in report.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, the share helper (wormhole), and the diagrams renderer (sekien) are on your PATH. Name a deck file instead to lint it in depth (syntax, named-field at: 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 with alix 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. For requires: it separates a dangling filename edge from a dangling deck-id edge, a card-… id pasted where a deck belongs, and a file that only shares a required id’s name (the id wins, so add the .md extension to mean the file). It nudges a source: that lists more than a few entries toward their common directory, and flags a source: pointing into assets/ (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. --backends additionally probes the configured AI backend end to end (one real, tiny request); --all-backends probes all four. --grading spot-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.toml root_id are an error naming both profiles. A malformed root id names its profile and file, and a nested .alix/sync.toml below 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 the at: 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 diverged position: anchor to where its span binds today (the keep-what-you-authored resolution). To keep an anchor’s old target instead, set occurrence= 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 like id last). 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, id last). 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 .bak backup 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-files lists and deletes them all after one confirmation (--yes skips 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 alix at 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.