aboutsummaryrefslogtreecommitdiff
path: root/.ai/workflows/flashcard-review.org
blob: 31027b3e4d4a0b7e8f61b8361bb709a244d2560b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
#+TITLE: Drill Deck Review Workflow
#+AUTHOR: Craig Jennings & Claude
#+DATE: 2026-05-30

* Overview

Take an org-drill flashcard file and bring it into the canonical shape — every card a question that doesn't give the answer away, every fact current — then regenerate the Anki =.apkg= and drop it where the phone can sync it.

The workflow has three substantive passes (question-form audit, content-accuracy audit, source rewrite) followed by a mechanical regenerate-and-place step. Content review is dispatched to a subagent because it's bounded research across project source-of-truth files; the structural rewrite stays in the main thread because it touches the SRS state we don't want to lose. Three helper scripts (=flashcard-stats.py=, =flashcard-diff-ids.py=, =flashcard-sync=) automate the inventory, the safety check, and the regenerate-and-place.

*Scheduling lives on the Anki side.* Desired retention and the FSRS scheduling model are per-deck Anki options set on the phone, never controlled by the org source or =flashcard-to-anki.py=. The pipeline's only scheduling job is keeping each card's identity (the =:ID:=-derived GUID) stable so Anki's review history survives a rewrite. Don't try to encode retention, intervals, or org-drill's SM-2 state into the Anki output — the two schedulers are separate, and the import carries only card content plus identity. (Anki's desired-retention default is 90%; see [[https://docs.ankiweb.net/deck-options.html][the deck-options manual]].)

* When to Use This Workflow

Trigger phrases:

- "Review the drill deck"
- "Update the drill deck"
- "Refresh the Anki cards"
- "Let's run the flashcard-review workflow"

Typical timing:

- After a wave of personnel changes (titles, roles, employment status)
- After a major milestone (a demo ships, a contract closes, a submission goes in)
- When org-drill review surfaces a card with stale or wrong content
- When the Anki deck on the phone hasn't been regenerated in weeks

* Inputs

- *Source file*: the org-drill file. Common locations:
  - =deepsat.org= at the work project root (symlinked from =~/sync/org/drill/=)
  - =health-drill.org= in the health project
  - Any =:drill:= deck under =~/sync/org/drill/=
- *Source-of-truth docs for content accuracy*: project-specific. Typical set:
  - Project-root =knowledge.org=, =status.org=, =notes.org=
  - =todo.org= for the freshest signal on people / partnerships / projects
  - =deepsat/assets/= (or equivalent) for meeting transcripts when a specific fact needs confirmation
- *Output location*: =~/sync/phone/anki/<basename>.apkg= (the phone-sync target). Both =flashcard-to-anki.py= and the =flashcard-sync= wrapper default there.

* Canonical Card Shape

** Deck title (=#+TITLE:= line)

The =#+TITLE:= line at the top of the source file drives two surfaces: the org-drill display in Emacs and the Anki deck name on the phone. Pick a title that reads well in Anki — drop tool-name jargon like "Org-Drill" / "Drill" that's meaningful in Emacs but noise on the consumption side.

Good: =DeepSat Flashcards=, =Health Flashcards=, =Philosophy Flashcards=.
Bad: =DeepSat Org-Drill Flashcards=, =DeepSat Drill Deck=.

=flashcard-stats.py= flags any title containing =org-drill= (case-insensitive, hyphenated or spaced) as a workflow violation.

*Stable-ID caveat.* =flashcard-to-anki.py= derives the Anki deck ID from the deck name. Changing =#+TITLE:= changes the deck ID, so the next import lands as a new deck rather than updating the existing one. Two consequences worth flagging:

- Any review history accumulated in Anki under the old deck name stays attached to the old deck — it doesn't migrate.
- On rename, delete the old deck from Anki to avoid having two decks with similar content.

For most decks (especially on first deployment), this is a one-time event. The rename is cheap to do early.

** Heading (the question)

Every card heading is a question that doesn't reveal the answer. Not the topic name, not the acronym, not the person's name — a question that tests recall.

Three card families have different question shapes:

*** Acronym / concept cards
"What does X stand for and what is it?" or "What is X and why does it matter?" Promote the question that was already in the body up to the heading.

  Before:
  : ** AFRL :drill:
  : What does AFRL stand for and what is it?
  : *** Answer
  : Air Force Research Laboratory. ...

  After:
  : ** What does AFRL stand for and what is it? :drill:
  : Air Force Research Laboratory. ...

*** Person cards
Format: "Who is X? Tell me about their Y." where X is a role descriptor that doesn't name the person, and Y is whatever the answer body covers (background, role, limitations, scope). The answer body opens by naming the person, then continues.

  Before:
  : ** Vrezh Mikayelyan :drill:
  : Who is Vrezh? What are his key limitations?
  : *** Answer
  : Developer (also called "Reg"). Armenia-based ...

  After:
  : ** Who is DeepSat's Armenia-based developer? Tell me about his background and limitations. :drill:
  : Vrezh Mikayelyan. Armenia-based, full-time as of April 2026. Worked with Hayk at Bazoomq on Armenia's first satellite ...

  Note: pick a role descriptor that genuinely identifies one person. If multiple people share the role description, add a single distinguishing detail (e.g., "the one who works evenings", "the Vineti alum"). Don't pile on parentheticals.

  Splitting: the person card deliberately trades atomicity for narrative recall — one card carries identity plus several attributes. When a body bundles genuinely unrelated attributes (role, employment history, limitations, scope) rather than one coherent topic, split it into multiple cards. One inherits the existing =:ID:= (and its SRS history); each new sibling starts fresh and will correctly show in =flashcard-diff-ids.py= as an appeared ID. The criterion: split when the body reads as a list of separate facts, keep it whole when it reads as one story. (Minimum-information principle — Wozniak rule 4, Matuschak "Focused".)

*** Talking-points and directive cards
Already in prompt form ("Introduce Yourself", "Spell out these orbital regime acronyms", "What is DeepSat?"). Leave the heading alone. Still strip the =*** Answer= sub-header and audit the body content for staleness.

The =flashcard-stats.py= helper recognizes both =?=-form and imperative-verb form as valid prompts (verbs like Spell, Describe, Explain, Name, List, Give, Show, Tell, Define, Compare, Identify, Outline, Introduce, Walk, State, Recite, Recall, Summarize).

** Body (the answer)

- *No =*** Answer= sub-header.* The body /is/ the answer; the heading /is/ the question. The sub-header was a workaround for topic-as-heading cards.
- *Body opens by naming the topic.* "Air Force Research Laboratory. Air Force's R&D arm." or "Vrezh Mikayelyan. Armenia-based, full-time as of ..." The Anki back shows this directly under the front question; restating the topic makes the back read as a complete answer.
- *PROPERTIES drawer stays.* Org-drill needs the =:ID:=, =:DRILL_LAST_INTERVAL:=, =:DRILL_EASE:= etc. for SRS state. The Anki output strips it (see the script change).
- *=SCHEDULED:= / =DEADLINE:= planning lines stay.* Same reason. The Anki output strips them.
- *Source citation goes at the very end, after two blank lines.* When a card cites a source, put a =Source: <label> — <url>= line at the end of the body, separated from the answer by two blank lines (two empty paragraphs) so it reads as a footer, not part of the answer. =flashcard-stats.py= ignores =Source:= lines when checking for answer leakage, since a URL slug often repeats the question's words.
- *No created/added date on the card.* Don't stamp a card with the date it was written. If a card body carries a =Created:= line (or a =:CREATED:= line outside the drawer), remove it during the rewrite. The Anki output strips =Created:= lines as a backstop, but they shouldn't be in the source either. Volatile facts get dated in the answer prose itself ("full-time as of April 2026"), never via a card-level timestamp.

* Card Authoring Principles

The canonical shapes above are the house style; these are the reasons behind them, drawn from the spaced-repetition literature. =flashcard-stats.py= checks the mechanical ones; the rest guide the rewrite and the content pass.

- *One fact per card (minimum information principle).* A card should test a single retrievable connection. A back that bundles several independent facts gets partially recalled and burns repetitions on the parts you already know. When a body covers unrelated attributes, split it into separate cards. =flashcard-stats.py= flags long backs as a non-blocking NOTE.

- *Demand recall, not recognition (effortful retrieval).* Pulling the answer from memory is what strengthens it, so the question must not let you infer the answer from its own wording. This is why person headings never name the person, and why a question that restates its own answer is a defect. =flashcard-stats.py= flags high front/back word overlap as answer leakage — excluding =Source:= citation lines, and exempting range/category cards whose answer recalls numbers the question doesn't give away.

- *Avoid binary prompts.* "Is X true?" and "A or B?" allow a coin-flip guess and produce shallow understanding. Reformulate open-ended — "How does X affect Y?" beats "Does X affect Y?" Flagged as a non-blocking NOTE.

- *Avoid lists and enumerations.* Unordered sets past about five members, and long lists, recall poorly as a single card. Split the list across cards (overlapping cloze is the textbook alternative, but this pipeline has no cloze shape, so split instead). List-shaped backs are flagged as a non-blocking NOTE.

- *Make cues precise.* A vague question admits several reasonable answers, so you can't tell whether you knew the intended one. Include enough context that only the intended answer fits, without narrowing into provincial trivia.

- *Combat interference.* Confusable cards inhibit each other; two near-identical fronts are the worst case. Disambiguate them with distinguishing context, or merge them. =flashcard-stats.py= flags duplicate / near-duplicate fronts.

- *Understand before you memorize.* Cards are the last step, after the material is understood and structured. A card you can't explain is a leech waiting to happen.

Sources: Wozniak's [[https://www.supermemo.com/en/blog/twenty-rules-of-formulating-knowledge][Twenty rules of formulating knowledge]], Andy Matuschak's [[https://andymatuschak.org/prompts/][How to write good prompts]], Michael Nielsen's [[https://augmentingcognition.com/ltm.html][Augmenting Long-term Memory]], and the [[https://docs.ankiweb.net/][Anki manual]].

* Approach: Phases

** Phase A: Question-form + title audit (per card and per file)

Run =flashcard-stats.py= on the source first to get the structural inventory:

#+begin_src bash
.ai/scripts/flashcard-stats.py <source.org>
#+end_src

The script reports the deck title from =#+TITLE:= (and flags it if it contains source-tool jargon like "Org-Drill"), card count, PROPERTIES-drawer count, =*** Answer= sub-header count, cards missing =:ID:=, and cards whose heading is neither =?=-form nor an imperative-verb prompt. It also flags possible answer leakage and duplicate / near-duplicate fronts (both blocking), and surfaces non-blocking NOTEs for overloaded, list-shaped, or binary cards. Each surfaced card is a candidate for the rewrite, plus the title itself if flagged.

For each candidate, propose the new heading in advance so Phase C is mechanical. For person cards, the proposal is the role descriptor + topical anchor pair. For acronym/concept cards, the proposal is the existing body question promoted to the heading.

** Phase B: Content-accuracy audit (subagent)

Dispatch a subagent with this contract (adapt the source-of-truth list per project):

#+begin_example
Audit the answer bodies of every :drill: card in <SOURCE> against
<SOURCE-OF-TRUTH FILES> and surface every fact that is stale, wrong,
or out-of-date. Don't rewrite the cards; report only items that need
to change.

Output shape per finding:
  CARD: <heading as it appears>
  CURRENT: <quote the stale phrase from the answer body>
  UPDATE: <what it should say instead, with citation>
  CONFIDENCE: high / medium / low

Categories to look for:
- Personnel: title changes, employment-status changes, departures, new joiners
- Partner status: contracts that closed or fell through, partnerships that advanced or stalled, named individuals changing roles
- Project facts: milestone shifts, submission states, exercise / demo dates
- External contacts: title or affiliation changes
- Company facts: head count, funding, customer status
- Removable cards: trivia not worth memorizing, or a fact whose underlying source no longer appears in any source-of-truth doc (flag as a deletion candidate, not a rewrite)

Skip cards where you find no staleness. Cap at 2,000 words.
#+end_example

Include any user-supplied seed fixes in the dispatch (e.g., "Vrezh is now full-time, drop the 'Reg' diarization error"). The subagent folds them into its report so they land in the same disposition table.

Output of Phase B: a structured per-card list of content updates with confidence levels. High-confidence findings get baked in during Phase C. Medium-confidence findings are reviewed inline before baking. Low-confidence findings are surfaced but skipped unless the user calls them in.

*Removal and leeches.* Two dispositions beyond rewrite. (1) Cost-benefit removal: a card flagged as removable is a deletion candidate — weigh whether the fact clears a "worth memorizing" bar before keeping it. (2) Leech feedback: when Anki suspends a card as a leech (8 lapses by default), the card's formulation is the problem, not the review effort; route it back through Phase B/C as a reformulation target, preserving its =:ID:= so Anki keeps the lapse history. The org → Anki flow is one-directional: leech tags, lapse counts, and per-card success rates live in Anki and never flow back to the source, so these signals are carried in by hand. (Anki [[https://docs.ankiweb.net/leeches.html][leech]] guidance is "reformulate, don't grind".)

** Phase C: Source rewrite

Take Phase A's question-rewrite plan and Phase B's content-update list, apply them to the source file. Preserve every card's =:PROPERTIES:= drawer (especially =:ID:=) and =SCHEDULED:= line verbatim — those carry SRS state that must survive the rewrite.

Rewrite shape per card:

#+begin_example
** <question-form heading> :drill:
[SCHEDULED line if present]
:PROPERTIES:
[ID + DRILL_* lines unchanged]
:END:
<answer body, opening with the topic name>
#+end_example

Drop the =*** Answer= sub-header entirely. The body that was under =*** Answer= becomes the body of the card. If the original body had a question above =*** Answer= (the pre-rewrite norm), drop that question — the new heading carries it.

Two body conventions to apply during the rewrite: remove any =Created:= / created-date line (no card-level timestamps), and if the card cites a source, put the =Source:= line at the end of the body after two blank lines.

For the file as a whole, use a single =Write= rather than per-card =Edit= calls. One pass through the source, one write back. Per-card edits multiply tool calls by N and risk drift.

** Phase D: Regenerate the Anki deck

Use the =flashcard-sync= wrapper — it runs the stats check, optionally the ID-preservation check, then regenerates the apkg and places it at =~/sync/phone/anki/=:

#+begin_src bash
.ai/scripts/flashcard-sync <source.org> --diff-against <previous-version.org>
#+end_src

The =--diff-against= flag is recommended on any rewrite where you want to confirm zero card IDs disappeared (zero SRS-state loss). The "previous version" is typically the file as it was before this run; grab it from git with =git show HEAD~1:<path> > /tmp/<name>-prerewrite.org=. Skip =--diff-against= on a first run when there's no previous version to compare against.

If the stats check or ID-preservation check fails, the wrapper exits non-zero and the apkg is not written. Fix the warnings, then re-run.

To bypass the safety gates (rare, only when you know what you're doing), call =flashcard-to-anki.py= directly:

#+begin_src bash
.ai/scripts/flashcard-to-anki.py <source.org> --output ~/sync/phone/anki/<basename>.apkg
#+end_src

The script writes the =.apkg= with stable deck/model IDs derived from the deck name, so re-importing into Anki updates existing cards rather than duplicating them.

** Phase E: Verify

The =flashcard-sync= wrapper covers the structural verify automatically (stats + diff-ids if =--diff-against= was passed). After it succeeds, do a quick visual spot-check:

- Confirm the apkg size matches expectations. Significant changes are expected on a big rewrite; a wildly smaller file may mean the parser dropped cards.
- Open the source in Emacs (or =head -100=) and confirm a few cards visually: question heading, no =*** Answer=, PROPERTIES preserved, body opens with topic name.
- If you keep an org-drill session open, revert the buffer to pick up the rewrite.

For ad-hoc verification on either side of a rewrite, run the individual scripts:

#+begin_src bash
.ai/scripts/flashcard-stats.py <source.org>
.ai/scripts/flashcard-diff-ids.py <before.org> <after.org>
#+end_src

** Phase F: Commit

Two clusters:

- *Source rewrite*: the org file (e.g., =deepsat.org=). Commit subject: =chore(drill): restructure cards to question-form headings + content refresh=. Body lists the content-update categories (Vrezh full-time, DCVC passed, etc.) and notes that =*** Answer= sub-headers were dropped.
- *Workflow / script changes* (if any): if this run prompted updates to =flashcard-review.org= or the helper scripts in the rulesets repo, commit those separately with =chore(workflows):= or =chore(scripts):= subjects.

Push both. The =.apkg= itself lives under =~/sync/phone/= which is outside the repo — no commit needed there; Syncthing (or whatever sync mechanism) handles propagation.

* Helper Scripts

Three scripts under =.ai/scripts/= (canonical lives in =rulesets/claude-templates/.ai/scripts/=):

** =flashcard-to-anki.py=

The core converter. Reads an org-drill source file, emits a stable-ID Anki =.apkg=. Strips =:PROPERTIES:= drawers and =SCHEDULED:= / =DEADLINE:= / =CLOSED:= planning lines from card bodies before rendering. Front = heading text without =:drill:=. Back = cleaned body, HTML-escaped, joined with =<br>=. Deck and model IDs derived from the deck name + a salt, so re-imports update existing cards rather than duplicating.

** =flashcard-stats.py=

Inventory + authoring-quality checks for a single deck source. Counts cards, PROPERTIES drawers, =*** Answer= sub-headers, cards missing =:ID:=, and cards whose heading is neither =?=-form nor an imperative-verb prompt. It also checks authoring quality: answer leakage (front/back content-word overlap) and duplicate / near-duplicate fronts are blocking WARNs; overloaded backs, list-shaped backs, and binary prompts are non-blocking NOTEs. Exits 0 when no blocking warning is present, 1 otherwise, so it gates =flashcard-sync=. The leakage check ignores =Source:= and created-date lines and exempts range/category cards whose answer recalls numbers the question doesn't give away.

Imperative-verb allowlist: Spell, Describe, Explain, Name, List, Give, Show, Tell, Define, Compare, Identify, Outline, Introduce, Walk, State, Recite, Recall, Summarize.

The fuzzy checks (leakage ratio, overloaded word count) are tuned by the =LEAKAGE_*= and =BACK_WORD_LIMIT= constants at the top of the script. Loosen them if a real deck trips false positives.

** =flashcard-diff-ids.py=

SRS-state preservation check between two versions of a deck. Extracts every =:ID:= from each, reports IDs that disappeared (lost SRS state — worst-case bug) or appeared (new cards). Exits 0 when clean, 1 when any disappeared/appeared.

** =flashcard-sync= (bash wrapper)

Single command for the canonical "rewrote the deck, now ship it" step. Runs =flashcard-stats=, optionally =flashcard-diff-ids= (with =--diff-against=), then =flashcard-to-anki= writing to =~/sync/phone/anki/<basename>.apkg=. Exits non-zero if any gate fails; the apkg is not written when a gate fails.

Usage:
#+begin_src bash
flashcard-sync <source.org>
flashcard-sync <source.org> --diff-against <previous-version.org>
#+end_src

* Anki Script Behavior

The =flashcard-to-anki.py= script has these contracts that this workflow depends on:

1. *Strips =:PROPERTIES:= drawers* from the card body before rendering. Org-drill needs them in source; Anki cards shouldn't show them.
2. *Strips =SCHEDULED:= / =DEADLINE:= / =CLOSED:= planning lines and =Created:= / =:CREATED:= date lines* from the card body. Same reason — and a created date never belongs on a card.
3. *Does NOT strip =*** Answer= sub-headers.* If the source still has them, the Anki cards will show them. This workflow's Phase C removes them at the source. =flashcard-stats.py= flags any remaining as a workflow violation.
4. *Front of each Anki card* = the heading text without the =:drill:= tag.
5. *Back of each Anki card* = the cleaned body (after #1 and #2), joined with =<br>= and HTML-escaped.
6. *Stable IDs* derived from the deck name + a salt, so re-importing the same deck name updates cards rather than duplicating.

If you find the script doing something else, update the script before regenerating. Don't work around a script bug in the source rewrite — the next deck will hit the same problem.

* Output Path Convention

- Default in =flashcard-to-anki.py=: =~/sync/phone/anki/<basename>.apkg=.
- Default in =flashcard-sync=: =~/sync/phone/anki/<basename>.apkg= (same target; the wrapper passes =--output= explicitly).

=~/sync/org/drill/= holds the org sources and their symlinks; =~/sync/phone/anki/= holds the =.apkg= the phone consumes. Both tools write the =.apkg= to the phone dir by default, so a deck lands where Anki picks it up without an =--output= override.

* Common Mistakes

1. *Per-card =Edit= calls instead of one =Write=.* Multiplies tool calls and risks drift between cards. Read once, rewrite in memory, write once.
2. *Dropping the PROPERTIES drawer in source.* Org-drill stores SRS state there; losing it resets every card's review history. =flashcard-diff-ids.py= is the safety net.
3. *Rewriting person headings to include the name.* "Who is Vrezh Mikayelyan?" gives away the answer. The whole point is to test name recall from a role description.
4. *Forgetting to strip =*** Answer= sub-headers.* The Anki output will show them as visible card content. =flashcard-stats.py= catches this.
5. *Skipping the content-accuracy pass.* The structural rewrite alone leaves stale facts in place. The drill cards become a memorization tool for the wrong information.
6. *Treating subagent output as gospel.* Medium- and low-confidence findings need human review before baking. The subagent surfaces; the main thread decides.
7. *Running =flashcard-sync= without =--diff-against=.* The stats check still runs, but the SRS-state preservation check doesn't. On a rewrite of any size, pass =--diff-against /tmp/<name>-prerewrite.org= (grab from git first).
8. *Answer leakage.* A question that restates its own answer tests recognition, not recall — the card looks learned when it isn't. =flashcard-stats.py= flags high front/back word overlap.
9. *Encoding scheduling in the source.* Retention, intervals, and FSRS state are Anki-side options; the org files and =flashcard-to-anki.py= carry only card content plus identity. See the scheduling note in the Overview.

* Living Document

Update this workflow as patterns emerge. Specifically:

- New card family beyond acronym / person / talking-point → document the heading shape for it.
- New source-of-truth doc beyond the standard set → add to Phase B's dispatch contract.
- Script behavior changes → mirror them in the "Anki Script Behavior" section.
- New imperative-verb prompt forms → add the verb to =flashcard-stats.py=' s allowlist.

** Updates and Learnings

*** 2026-05-30: First run
Built against =deepsat.org= after Craig flagged that the existing apkg surfaced PROPERTIES drawers + =*** Answer= headers on the back of every card, and that the person-card content (Vrezh in particular) had drifted. The Phase B subagent surfaced 8 high-confidence content updates plus several medium-confidence enrichments. Validated by running the rewrite and regenerating =deepsat.apkg= to =~/sync/phone/anki/=.

*** 2026-05-30: Helper scripts added (same day)
After the first run, scripted the safety-net checks into three helpers: =flashcard-stats.py= (inventory + warnings), =flashcard-diff-ids.py= (SRS-state preservation between versions), and =flashcard-sync= (single-command wrapper). Stats check on the deepsat rewrite flushed a heuristic bug — directive prompts ("Spell out these orbital regime acronyms", "Introduce Yourself") were flagged as non-question. Fix: =flashcard-stats.py= now accepts =?=-form OR imperative-verb-start (Spell, Describe, Explain, ..., Recall) as valid prompt forms.

*** 2026-05-30: Title-audit added (same day)
Craig noticed the Anki deck name still showed as "DeepSat Org-Drill Flashcards" because the source =#+TITLE:= leaks tool-name jargon into Anki. Added a "Deck title" subsection under Canonical Card Shape, expanded Phase A to audit the title, and extended =flashcard-stats.py= to flag any title matching =org[-\s]?drill= (case-insensitive). Stable-ID caveat documented: renaming the deck changes the Anki deck ID, so the next import lands as a new deck and the old one needs deleting from Anki.

*** 2026-05-30: Authoring-quality checks + Card Authoring section (same day)
Researched flashcard / spaced-repetition best practices (Wozniak's twenty rules, Matuschak's prompt-writing guide, Nielsen, the Anki manual, the FSRS docs) and folded the findings in. =flashcard-stats.py= gained answer-leakage and duplicate-front checks (blocking), plus non-blocking NOTEs for overloaded backs, list-shaped backs, and binary prompts. Added a "Card Authoring Principles" section (the why behind the canonical shapes), a person-card splitting path, a Phase B cost-benefit-removal + leech-feedback disposition, and a scheduling-is-Anki-side note in the Overview. Deliberately not adopted, with reasons: cloze cards (would need a second note type and an authoring convention), per-card tractability targeting and FSRS-retention encoding (Anki-side telemetry that never flows back to the source), on-face source-stamping (the converter strips those drawers by design; provenance stays in the org layer).

*** 2026-05-30: Leakage false-positive fixes + source/created-date conventions (same day)
Health ran the leakage check on a 43-card deck and hit two false-positive classes. Fixed both in =flashcard-stats.py=: =Source:= citation lines are stripped before the overlap is computed (a URL slug repeats the question's words), and range/category cards whose answer carries numeric ranges or thresholds the question lacks are exempted (the recalled content is the numbers, which aren't given away). Codified two body conventions: a =Source:= citation sits at the end of the card after two blank lines, and no created/added date goes on a card. =flashcard-to-anki.py= now strips =Created:= / =:CREATED:= lines from the back as a backstop, and Phase C removes them from the source during the rewrite.