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
|
#+TITLE: Build request — apkg → org-drill converter (inverse of flashcard-to-anki.py)
From: home session, 2026-06-21. Craig wants this built (backlogged, not
urgent). Standalone build request — the earlier anki-title-fix-proposal
only mentioned it in passing; this is the real ask.
* Why
The flashcard pipeline is one-directional (org-drill → apkg). Decks
authored or curated on the phone, and orphaned apkgs whose .org source
was never saved, can't get back into the org source-of-truth. Concrete
case: deepsat-fundamentals.apkg — a 100-card DeepSat subset generated
once with --deck "DeepSat Fundamentals" — has no .org source anywhere on
ratio, velox, or in work git history. The converter recovers it and makes
phone → org round-tripping possible.
* What — contract (inverse of flashcard-to-anki.py)
Input: an Anki =.apkg= (a zip containing collection.anki2 / .anki21
sqlite, plus a media blob).
Output: an org-drill =.org= file in the house canonical shape that
flashcard-stats.py / flashcard-to-anki.py already agree on.
Mapping (mirror flashcard-to-anki.py's parse/build):
- Deck name (from the apkg) → =#+TITLE:=.
- Each note → =** <Front> :drill:= with the Back as the body.
- Card tag → top-level =* Section= grouping (inverse of section_to_tag;
cards sharing a tag collect under one section; the slug won't round-trip
to the exact original section title, so this is best-effort — emit the
tag as the section heading and let a human retitle).
- Back HTML → org: convert =<br>= back to newlines; unescape
=&/</>=; strip the =<hr id="answer">= the card template adds
(the Back field itself shouldn't contain it, but guard anyway).
- Generate a fresh =:ID:= UUID per card in a =:PROPERTIES:= drawer so the
output is immediately org-drill-valid and round-trips back through
flashcard-to-anki.py. (Note: GUIDs in flashcard-to-anki.py are derived
from the front text, not the :ID:, so a regenerated apkg still matches
existing phone cards by front — call that out in the docstring.)
Edge cases to cover in tests (Normal/Boundary/Error):
- Multiple decks in one apkg (emit one file per deck, or error asking for
a deck filter — pick one and document it).
- Notes with multiple fields / non-basic note types (the pipeline only
models Front/Back — skip or warn on others, don't silently drop).
- HTML entities, embedded =<br>=, and any =Source:= footer surviving
round-trip.
- Empty back; media references (flag, since org side has no media path).
- collection.anki2 vs .anki21 schema differences.
* Where it lives
Rulesets-owned, beside the flashcard-* family
(=claude-templates/.ai/scripts/=): suggest =anki-to-flashcard.py= (or
=apkg-to-orgdrill.py= — your naming call). Add tests under
=scripts/tests/=. A new file can't be built downstream — home/.ai/scripts/
is wiped to match the template by the startup =--delete= rsync — so this
has to be built in the rulesets canonical. PEP 723 uv-run script like its
sibling; genanki isn't needed for reading (stdlib =zipfile= + =sqlite3=
suffice), so it has no runtime deps.
* Acceptance
Round-trip test: take a known org-drill source, run it through
flashcard-to-anki.py, run the result back through this converter, and
assert the cards (front/back/section) match the original (modulo
regenerated :ID:s and best-effort section titles). Plus: run it on the
real deepsat-fundamentals.apkg and hand the recovered .org back so its
source can be filed (work project).
|