<feed xmlns='http://www.w3.org/2005/Atom'>
<title>rulesets/.ai/scripts/tests/test_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:40:22+00:00</updated>
<entry>
<title>feat(flashcard): support multi-tag drill headings + --tag-filter / --guid-salt</title>
<updated>2026-07-19T23:40:22+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-19T23:40:22+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=a14e43b566946ba45abf58f75eb0a445b5b67ef3'/>
<id>urn:sha1:a14e43b566946ba45abf58f75eb0a445b5b67ef3</id>
<content type='text'>
Craig wanted a curated "DeepSat Fundamentals" deck: the ~100 most fundamental cards out of the 465-card deepsat.org deck, marked with a second org tag (:fundamental:) so they stay grep-able in the source. Both flashcard-to-anki.py and flashcard-stats.py keyed a card on a heading ending in exactly :drill:, so adding any second tag turned the heading into :fundamental:drill:, which that anchor didn't match. The 100 tagged cards would have silently dropped from the full-deck apkg and been undercounted by stats. That's the passing-gate-skips-your-file failure.

CARD_RE now matches a trailing org tag block, and a heading is a card when drill is among its tags. The other tags ride along as Anki tags next to the section tag. Card bodies are bounded by any L1/L2 heading (HEADING_RE) rather than only a section or a drill heading. flashcard-to-anki.py gains --tag-filter &lt;tag&gt; (emit only cards carrying that org tag, the subset deck) and --guid-salt &lt;s&gt; (a distinct GUID space so the subset imports non-empty instead of colliding with the full deck on GUID and importing empty). No salt is unchanged, so the existing deck's GUIDs and SRS state are untouched. flashcard-stats.py gets the same CARD_RE/HEADING_RE broadening plus a drill-membership guard.

This is the rulesets-canonical reconcile of a stopgap the work project ran locally. I re-derived it against the current canonical rather than copying the preserved files, which predate the #+TITLE deck-name fix (060a938) and would have reverted it.

parse()'s third element is now the Anki-tag list rather than a single tag string. Only build() and the tests consumed it. The existing parse tests move to the list contract, and new tests cover multi-tag parsing, --tag-filter, the guid salt, and the stats count. A bats case guards that a :fundamental:drill: card still counts. The full deck's real 465/100 check needs the work deck and isn't runnable here. Verified end to end instead on a synthetic multi-tag deck (2 cards, --tag-filter fundamental returns 1) through the real genanki path.
</content>
</entry>
<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>
