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

6 · Cloze, dual-direction, image & diagram cards

Three extensions to the basic card, each a small addition on top of the format from chapter 3.

Cloze cards: fill in the blank

A cloze card hides part of its own text; you create one by pointing a blank: directive at a span of the block above it.

Write the sentence as ordinary prose, then name the hidden text in a <!-- blank: span hidden="..." --> comment directly below it. A directive binds to the block directly above it (a run of text lines, an image line, a code fence or a $$ block), and the hidden text must be in that block. Comment lines and link definitions between the two do not count; a blank line, a note or the --- divider in between is an error. Each directive is a blank, and the card expands into one sub-card per blank. A cloze card has no answer section: it needs no answer lines, a --- divider on it is an error, and explanations go in a > [!NOTE] note. No marker sits in the prose, so the deck reads normally in any other Markdown tool.

## Complete the Rust declaration
let mut x: u64 = 0;
<!-- blank: span hidden="mut" -->
<!-- blank: span hidden="u64" -->

The ## line may itself be the sentence, which needs no separate heading:

## A value is dropped when its owner goes out of scope.
<!-- blank: span hidden="dropped" -->

This makes two cards. One blanks mut and shows the rest; the other blanks u64. Each card’s question is the whole card from its ## line down, with the asked span shown as ⍰ and the other spans hidden as ⬚, so no card gives away its siblings’ answers. You only produce the hidden text. On reveal the question fills the asked span in place; the siblings stay hidden, and no separate answer repeats it. The web and mobile apps draw every unrevealed blank as an empty outlined pill of one fixed width, so the gap never hints at the answer’s length. The asked pill is faintly filled with the accent colour; a sibling’s is muted and dashed. Filled text is highlighted in the accent colour. A card asking several spans at once (a named group) fills one span per press under reveal: line, all at once under flip.

When you type a cloze card (at Reconstruct), the web and phone apps make each asked blank the typing field itself: type each answer in its gap and press Enter (on the phone the keyboard’s action moves to the next gap, and on the last one checks). Each gap is checked against its own answer, so swapping two answers is two misses. The result shows in the gaps: a right answer turns green, and a miss shows what you typed, struck through, before the answer. A blank inside a code fence or a formula keeps its own field below the question instead, as does a group that walks reveal: line. A cloze card has no dividing line under its question: the question and its field or choices sit together in the middle of the card.

The hidden text must appear in the bound block exactly as written. When it appears more than once, occurrence=2 picks the second match; matches snap to word boundaries unless the directive says boundary=char.

alix keeps a card’s cloze siblings apart in the queue when other cards are available, so you don’t see mut right after u64. Editing is safe: each span’s identity is the stamp alix mints into its directive (b:...), so rewording the question, or moving the prose, keeps your history.

Reach for cloze when the context is the cue: a definition with its key term removed, a line of code with the operative token blanked.

A span inside $...$ or $$...$$ is a piece of the formula, and must be a complete structural unit of it: a lone term such as 4ac, a base with its whole script such as b^2, a standalone symbol such as \pm or \alpha, or the whole formula. A script cut from its base (^2 alone) or a command cut from its arguments (\frac{a} without its {b}) is a loud error naming the cut. At review a formula span defaults to the sketch canvas on mobile, since a formula’s piece has no keyboard spelling (the desktop web app does not honor the default yet and types it); write input: type on the card or the deck to keep the keyboard. An authored input: wins over the default on mobile; the desktop web app currently ignores input: draw on a cloze card and types it regardless. alix doctor warns when a typed span’s answer would need LaTeX to type. It also warns when the question still shows a span’s hidden text outside its blank: a ## line repeating the hidden words above the blanked sentence, another block of the card, a second occurrence the blank does not cover, or the card’s section heading and prose. The card would show its answer before the reveal. The check ignores case and runs of whitespace and matches whole words only, so a hidden ox does not fire on box. An image blank’s label is not displayed text and is not checked.

A note for one blank

A > note belongs to the card you wrote, so every blank of it shows the same note, and a note that spells out one blank’s answer gives it away on all the others. Give the span a name in square brackets and write the note to that name:

## The test pyramid, bottom to top
Unit, integration, end-to-end
<!-- blank: span [base] hidden="Unit" -->
<!-- blank: span hidden="integration" -->
<!-- blank: span hidden="end-to-end" -->
> [!NOTE]
> base: Fastest and most numerous, which is why they sit at the bottom.

Only the Unit card shows that line; the other two show nothing. Written as > base+: ... it is added below the shared note instead of replacing it. A name is one or more of a-z, A-Z, 0-9, _ or -.

Everything else stays prose. A note line is only an address when the card names a span and the name before the : is one of them, so a note that opens 2: the second one is still a note. alix doctor reports an address that names no span of its card, and shows the line as an ordinary note.

Blanks that belong together

Give two spans the same name and they become one card asking both, instead of two cards each asking half a fact:

## The TCP three-way handshake, in order
SYN, SYN-ACK, ACK
<!-- blank: span [open] hidden="SYN" -->
<!-- blank: span [open] hidden="SYN-ACK" -->
<!-- blank: span hidden="ACK" occurrence=2 -->

That is two cards, not three: one asking SYN and SYN-ACK together, one asking ACK (occurrence=2, because the first ACK in the stream sits inside SYN-ACK). Both spans show as ⍰ on the merged card, and you answer them as a list, one line per span. They don’t have to sit next to each other or even on the same line.

Grouping starts that card’s history over. Two spans recalled together are a harder question than either alone, so the merged card takes no schedule from the blanks it replaces: it comes back as if it were new. The spans you did not group keep their history. Group early if you are going to.

A name addresses a span; it is not its id. Identity is the minted stamp, so renaming a span, or adding a name to one you have already drilled, keeps your history exactly as rewording the question does. A name alone does not make a group: the group, and its history reset, begins when a second span takes the same name. The name means nothing outside the card it is written in: two cards may each have a base.

Dual-direction cards: direction:

Reviewing a card both ways is what you want for vocabulary and other reversible facts. Set it per card with <!-- direction: both -->, or deck-wide with a direction: line in the frontmatter:

## purported
angeblich
<!-- direction: both -->
  • both makes two cards: purported → angeblich and the swap angeblich → purported.
  • 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. A cloze card is never reversed: a deck-wide direction: skips it, and a direction: written on the card itself is an error. When a reversed card’s question side comes from several answer lines, they render as separate centred lines rather than running together.

Image cards

Write a standard Markdown image on its own line, and its position decides the side: an image in the question is a front image, one in the answer is a back image, and a card can carry more than one per side. An image sharing a line with prose is rejected: alix displays images as media beside the text, not inline within a sentence, so a mixed line would silently lose its shape.

A front image sits between the heading and the divider, so the front has more than the heading line and the divider needs a blank line above it. Attached directly under a content line the break is in no valid position and fails loudly, naming the line:

## What phase is the moon in?
![](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 directly beneath the image line it marks (comment lines between them are fine, a blank line or text is an error), so any other Markdown viewer still shows the plain picture:

## Name the quadrant colors
![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, and every blank: needs one that is not empty once grading ignores spaces, invisible characters and trailing punctuation.
  • cover: masks a region and never asks: for a legend or label that would give an answer away. It creates no card; 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:

## Name the organelles
![a cell](cell.png)
<!-- 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 exam: freezing needs the workspace’s asset store.

Editing a frozen fence makes its stamp stale, and the fence falls back to its source until the next deck init re-freezes it. Fallback always means the same thing: the fence displays as a code block instead of a picture; on a masking card (below) the hidden text inside it is blanked, so the card stays reviewable either way. When a session opens on a deck with a stamped diagram that cannot be loaded (a stale stamp, a missing frozen file, or a geometry file that does not read back), the app shows a one-line warning so the fallback is never mistaken for a successful freeze. One kind of damage is deliberately outside that check: loading verifies shapes, not bytes, so a frozen file whose content was corrupted in place (bytes that no longer match their content-addressed name, after a faulty copy or restore) still serves. alix doctor is what re-hashes every frozen object and names such a file. alix doctor <deck> --repair-diagrams removes stamps that lost their fence and re-freezes stale or unfrozen ones; the corrupt-bytes case is repaired by deleting the stamp line and re-running alix deck init.

Theming

The diagram’s colors are decided by its own source, at freeze time:

  • an init directive on the fence’s first line: %%{init: {"theme": "dark"}}%%, or

  • mermaid’s YAML frontmatter at the top of the fence:

    ---
    config:
      theme: forest
    ---
    

An in-fence theme wins over anything set outside the fence (sekien’s own --theme flag included), so a shared deck renders the same everywhere. alix reads the rendered theme’s text color and puts the raster on whichever background, light or dark, keeps that text readable. The trade-off to know about: colors are baked in when you freeze. A frozen diagram does not follow the app’s light/dark theme, and re-theming means editing the fence and re-freezing.

Masking diagram labels

The span directives from the previous section work on diagram source, and on a frozen diagram they mask on the rendered image:

## the request path
```mermaid
flowchart LR
  Client --> LB[Load Balancer] --> API --> DB[(Postgres)]
```
<!-- blank: span hidden="Load Balancer" -->

The card shows the diagram with a mask over the Load Balancer node’s label; the mask lifts when the answer shows. Sibling blanks and covers on the same fence stay masked, exactly like image occlusion, and a reader using assistive technology hears only the visible labels, never the hidden ones.

A span must cover one complete visible label. Node and edge labels can be masked; node ids, arrows, and keywords never can, and a span whose hidden text lands on one is reported by alix doctor while the card falls back to masked source (reviewable, just not drawn). Where the same text appears more than once, occurrence= picks which one, counted over the block like any span. A bare node like A, whose label is its id, masks like any other label. Some labels still cannot be masked, and doctor says so when a span targets them: labels whose rendered text is not literally in the source (multi-line <br/> labels, HTML entities), and a bare node the fence references on more than one line, where which occurrence is “the” label is ambiguous (write A[A] once to settle it).

Source citations

A plain fact card can show where its answer comes from. Name the folders a citation may point into with a cite: list in the frontmatter (each entry is one key and one root), give the card an <!-- at: ... --> locator under one of those keys, and on reveal the card offers to swap the worded answer for the exact source lines:

---
cite:
  - code: src
---

## What does the `String` struct hold?
A `Vec<u8>` (its bytes).
<!-- at: code:string.rs:1-3 fingerprint: xxh64-0123456789abcdef -->

A cite: key is lowercase ([a-z][a-z0-9_-]*). Its root is a folder, relative to the workspace folder (the deck’s own folder for a loose deck), or a URL, which is never fetched: a URL root only holds frozen passages. A workspace’s alix.toml declares roots every member sees in a [cite] table (code = "src"), and a deck may not repeat a manifest key or declare a root inside another root it sees. The locator is the same shape a trace checkpoint uses. Its fields are named and ordered: at: is <key>:<path>[:<lines>], the key, the file’s path below that root, and a line range (code:string.rs:1-3; without a range the whole file is cited; a bare <key> cites the frozen passage of a URL root), and fingerprint: is an xxh64-<hex> digest of the displayed source text. A locator without a key, a key no root declares, or a path that leaves its root is an error; nothing searches parent folders or file names on its behalf. alix writes the fingerprint when it creates a cited card or when you explicitly repair a hand-authored citation. A fact card may repeat the whole directive when its answer rests on several disjoint source ranges:

<!-- at: code:state.rs:64-74 fingerprint: xxh64-0123456789abcdef -->
<!-- at: code:state.rs:114-118 fingerprint: xxh64-123456789abcdef0 -->
<!-- at: code:state.rs:152-158 fingerprint: xxh64-23456789abcdef01 -->

Each locator remains one contiguous range; separate directives never imply that disjoint code is adjacent. In the browser, a </> marker appears on the revealed answer: click the marker (or press s) to swap the answer for the same editor-style source panels used by traces, and back; a click on the answer itself hides it for a self-test, as on every card. On the phone, tap the revealed answer and choose Source in the Hide / Source sheet; the sheet reopens on the view last used for that card. Multiple excerpts are stacked in authored order inside the one scrollable region. For a live citation, alix shows the lines only when their fingerprint still matches. A moved, changed, deleted, ambiguous, or unfingerprinted excerpt shows a warning instead of unrelated lines, without hiding the other citations. Short evidence keeps the answer’s centered vertical alignment; long evidence aligns to the top and scrolls.

This is the same machinery traces use to reveal source, brought to ordinary fact cards. Like every directive, <!-- at: --> is not part of a card’s identity: adding a citation never resets its progress.

You rarely write these by hand. Generating a deck from a local source (alix deck generate <path>) cites the lines each fact came from and fingerprints every citation. Plain alix doctor reports missing fingerprints and fingerprint drift without writing. After reviewing the cited text, alix doctor <deck> --repair-source-locators stamps a missing fingerprint or rebases a uniquely relocated exact excerpt; changed or ambiguous excerpts remain untouched for semantic review. Initializing a workspace member goes one further and freezes its source evidence and local images below assets/deck-<token>/, so the deck travels without the original source and the quotes never shift. Freezing stores only the cited excerpt, never the whole file, and leaves the deck’s cite: roots pointed at the real material: each frozen citation keeps its key and path in at: and gains an asset: field naming the content-addressed object, so a drift check can still compare against the live file under that key’s root when it is reachable. That check is opt-in: alix doctor --warn-on-source-locator-drift reports frozen drift as a warning; plain doctor stays quiet about it.