aboutsummaryrefslogtreecommitdiff
path: root/docs/design/2026-06-21-apkg-to-orgdrill-buildreq.org
diff options
context:
space:
mode:
Diffstat (limited to 'docs/design/2026-06-21-apkg-to-orgdrill-buildreq.org')
-rw-r--r--docs/design/2026-06-21-apkg-to-orgdrill-buildreq.org68
1 files changed, 68 insertions, 0 deletions
diff --git a/docs/design/2026-06-21-apkg-to-orgdrill-buildreq.org b/docs/design/2026-06-21-apkg-to-orgdrill-buildreq.org
new file mode 100644
index 0000000..37a866f
--- /dev/null
+++ b/docs/design/2026-06-21-apkg-to-orgdrill-buildreq.org
@@ -0,0 +1,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
+ =&amp;/&lt;/&gt;=; 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).