aboutsummaryrefslogtreecommitdiff
path: root/.ai/workflows
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-30 13:17:47 -0500
committerCraig Jennings <c@cjennings.net>2026-05-30 13:17:47 -0500
commit0234e52b727b34ade93961eb05b5638685f4406f (patch)
treeb7ee5f66a9fceb3fd4d9b1d2ba8c44e89dde76c5 /.ai/workflows
parent038d59b7e548d2323f43dcd92ba14cba876d840d (diff)
downloadrulesets-0234e52b727b34ade93961eb05b5638685f4406f.tar.gz
rulesets-0234e52b727b34ade93961eb05b5638685f4406f.zip
chore(scripts): add drill-deck stats, diff-ids, and sync wrapper
I incorporated the flashcard-tooling bundle from the work project's deck-review workflow, validated there against a 93-card deck. Three scripts now live under .ai/scripts/: drill-deck-stats.py (pre-rewrite inventory plus a gate that warns on stray *** Answer headers, missing :ID:, non-prompt headings, and #+TITLE jargon like "org-drill"), drill-deck-diff-ids.py (SRS-state preservation check that flags any :ID: lost across a rewrite), and drill-deck-sync (bash wrapper chaining stats, optional diff-ids, then drill-to-anki, writing to ~/sync/phone/anki/ only when the gates pass). The drill-deck-review.org workflow gains a Helper Scripts section and references the scripts from its phases. I reconciled its output-path prose with the drill-to-anki default that just moved to ~/sync/phone/anki/, so it no longer claims the script still defaults to ~/sync/org/drill/. I added tests for both Python scripts (pure logic plus CLI gate behavior) and a bats suite for the wrapper's guard paths. The clean end-to-end sync path stays uncovered since it needs uv-resolved genanki.
Diffstat (limited to '.ai/workflows')
-rw-r--r--.ai/workflows/drill-deck-review.org124
1 files changed, 100 insertions, 24 deletions
diff --git a/.ai/workflows/drill-deck-review.org b/.ai/workflows/drill-deck-review.org
index a891f62..7e9eed5 100644
--- a/.ai/workflows/drill-deck-review.org
+++ b/.ai/workflows/drill-deck-review.org
@@ -6,7 +6,7 @@
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.
+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 (=drill-deck-stats.py=, =drill-deck-diff-ids.py=, =drill-deck-sync=) automate the inventory, the safety check, and the regenerate-and-place.
* When to Use This Workflow
@@ -34,10 +34,26 @@ Typical timing:
- 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). The script's default is =~/sync/org/drill/=; override with =--output= per this workflow.
+- *Output location*: =~/sync/phone/anki/<basename>.apkg= (the phone-sync target). Both =drill-to-anki.py= and the =drill-deck-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=.
+
+=drill-deck-stats.py= flags any title containing =org-drill= (case-insensitive, hyphenated or spaced) as a workflow violation.
+
+*Stable-ID caveat.* =drill-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.
@@ -72,8 +88,10 @@ Format: "Who is X? Tell me about their Y." where X is a role descriptor that doe
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.
-*** Talking-points cards
-Already in question form ("Introduce Yourself", "What is DeepSat?", "What do you do at DeepSat?"). Leave the heading alone. Still strip the =*** Answer= sub-header and audit the body content for staleness.
+*** 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 =drill-deck-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)
@@ -84,15 +102,17 @@ Already in question form ("Introduce Yourself", "What is DeepSat?", "What do you
* Approach: Phases
-** Phase A: Question-form audit (per card)
+** Phase A: Question-form + title audit (per card and per file)
-Walk every =** ... :drill:= card and flag the ones that don't already match the canonical shape:
+Run =drill-deck-stats.py= on the source first to get the structural inventory:
+
+#+begin_src bash
+.ai/scripts/drill-deck-stats.py <source.org>
+#+end_src
-- Heading is the topic / acronym / person's name → flag for rewrite.
-- Heading is already a question but the body still has a =*** Answer= sub-header → flag for sub-header removal.
-- Heading is a question /and/ the body is clean → no action.
+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. Each surfaced card is a candidate for the rewrite, plus the title itself if flagged.
-Output of Phase A: a list of cards needing rewrite, with the proposed new heading for each. For person cards, this means proposing the role descriptor up front so Phase C is mechanical.
+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)
@@ -145,6 +165,18 @@ For the file as a whole, use a single =Write= rather than per-card =Edit= calls.
** Phase D: Regenerate the Anki deck
+Use the =drill-deck-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/drill-deck-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 =drill-to-anki.py= directly:
+
#+begin_src bash
.ai/scripts/drill-to-anki.py <source.org> --output ~/sync/phone/anki/<basename>.apkg
#+end_src
@@ -153,48 +185,85 @@ The script writes the =.apkg= with stable deck/model IDs derived from the deck n
** Phase E: Verify
-- Spot-check the new =.apkg= size against the prior version. Significant size changes are expected when many cards were rewritten; a wildly smaller file may mean the parser dropped cards (PROPERTIES drawer mishandling, etc.).
-- Open the source in Emacs (or read with =head -100=) and confirm a few cards visually: question heading, no =*** Answer=, PROPERTIES preserved, body opens with topic name.
-- If the user keeps an org-drill session open, mention they'll want to revert the buffer to pick up the rewrite.
+The =drill-deck-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/drill-deck-stats.py <source.org>
+.ai/scripts/drill-deck-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 =drill-deck-review.org= or =drill-to-anki.py= in the rulesets repo, commit those separately with =chore(workflows):= or =chore(scripts):= subjects.
+- *Workflow / script changes* (if any): if this run prompted updates to =drill-deck-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/=):
+
+** =drill-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.
+
+** =drill-deck-stats.py=
+
+Inventory + workflow-violation warnings 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. Exits 0 when clean, 1 when warnings present, so it gates =drill-deck-sync=.
+
+Imperative-verb allowlist: Spell, Describe, Explain, Name, List, Give, Show, Tell, Define, Compare, Identify, Outline, Introduce, Walk, State, Recite, Recall, Summarize.
+
+** =drill-deck-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.
+
+** =drill-deck-sync= (bash wrapper)
+
+Single command for the canonical "rewrote the deck, now ship it" step. Runs =drill-deck-stats=, optionally =drill-deck-diff-ids= (with =--diff-against=), then =drill-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
+drill-deck-sync <source.org>
+drill-deck-sync <source.org> --diff-against <previous-version.org>
+#+end_src
+
* Anki Script Behavior
-The =drill-to-anki.py= script (under =.ai/scripts/=) has these contracts that this workflow depends on:
+The =drill-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* from the card body. Same reason.
-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.
+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. =drill-deck-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 (e.g., not stripping PROPERTIES), update the script before regenerating. Don't work around a script bug in the source rewrite — the next deck will hit the same problem.
+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 the script: =~/sync/org/drill/<basename>.apkg= (matches the convention where org sources live in project repos and symlink into =~/sync/org/drill/=).
-- Default in this workflow: =~/sync/phone/anki/<basename>.apkg= (the phone-syncable Anki target). Override the script default with =--output= every time.
+- Default in =drill-to-anki.py=: =~/sync/phone/anki/<basename>.apkg=.
+- Default in =drill-deck-sync=: =~/sync/phone/anki/<basename>.apkg= (same target; the wrapper passes =--output= explicitly).
-Both paths can coexist. The =~/sync/org/drill/= dir holds Anki exports alongside the org sources (build-artifact convention); =~/sync/phone/anki/= holds the version that syncs to the phone (consumption-target convention). For most decks, only the =/sync/phone/= copy is actually consumed, so this workflow writes there directly and skips the intermediate.
+=~/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.
+2. *Dropping the PROPERTIES drawer in source.* Org-drill stores SRS state there; losing it resets every card's review history. =drill-deck-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. The source rewrite must drop them.
+4. *Forgetting to strip =*** Answer= sub-headers.* The Anki output will show them as visible card content. =drill-deck-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. *Outputting to the script's default path.* The phone won't pick up =~/sync/org/drill/<deck>.apkg=. Always pass =--output ~/sync/phone/anki/<deck>.apkg=.
-7. *Treating subagent output as gospel.* Medium- and low-confidence findings need human review before baking. The subagent surfaces; the main thread decides.
+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 =drill-deck-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).
* Living Document
@@ -203,8 +272,15 @@ 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 =drill-deck-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: =drill-deck-stats.py= (inventory + warnings), =drill-deck-diff-ids.py= (SRS-state preservation between versions), and =drill-deck-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: =drill-deck-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 =drill-deck-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.