<feed xmlns='http://www.w3.org/2005/Atom'>
<title>rulesets/.ai/scripts/apkg-to-orgdrill.py, branch main</title>
<subtitle>Claude Code skills, rules, and language bundles
</subtitle>
<id>https://git.cjennings.net/rulesets/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/rulesets/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/'/>
<updated>2026-07-19T23:30:47+00:00</updated>
<entry>
<title>feat(flashcard): add apkg-to-orgdrill.py, the inverse of flashcard-to-anki</title>
<updated>2026-07-19T23:30:47+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-19T23:30:47+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=a143679a50f5d3a84aea3ed4effcb3ac9e5af69f'/>
<id>urn:sha1:a143679a50f5d3a84aea3ed4effcb3ac9e5af69f</id>
<content type='text'>
The flashcard pipeline was one-directional (org-drill to apkg). A deck curated on the phone, or an orphaned apkg whose org source was never saved, had no way back into the org source of truth. apkg-to-orgdrill.py closes the loop.

An apkg is a zip holding an Anki sqlite collection, so reading it needs no third-party library: stdlib zipfile + sqlite3. genanki is only needed to write apkgs, not read them, so the converter has no runtime deps. It mirrors flashcard-to-anki.py inverted: deck name to #+TITLE, note Front to the ** heading, note Back HTML to the body (&lt;br&gt; to newlines, entities unescaped, the answer &lt;hr&gt; stripped), the note tag to a * section, and a fresh :ID: UUID per card so the output is org-drill-valid.

The entity unescape runs in the order that inverts escape_html, which escaped &amp; first, so &amp; is unescaped last here. Otherwise a literally-escaped &amp;lt; in the source would wrongly collapse to &lt;. Only Front/Back note types convert; a cloze or other model is skipped with a warning rather than silently dropped, and a note referencing media is flagged since the org side has no media path.

17 tests cover the HTML-to-org conversion, the apkg read (single and multiple decks, Default-deck exclusion, non-basic skip, .anki2 vs .anki21, media flag), the org emission, and errors. The round-trip test closes the loop through flashcard-to-anki.py's own parse(): a known org source becomes an apkg fixture, converts back, and the recovered (front, back, tag) tuples match the original. Verified end to end against a real genanki-produced apkg too.
</content>
</entry>
</feed>
