From ddf48dc7ac780da1aacdff4e03f1d7da255b8f39 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 31 May 2026 12:19:34 -0500 Subject: feat: add rename-ai-artifact tool and rename the drill-deck family to flashcard Renaming an .ai artifact by hand is the kind of mechanical job that gets done incompletely: the canonical copy moves but the mirror doesn't, a reference in the INDEX is missed, a trigger phrase points at the old name. I'd also assumed a rename was costly because references scatter, when the index update is trivial and the drift check already guards it. So I built the discipline into a script instead of re-deriving it each time. scripts/rename-ai-artifact.sh takes old and new basenames, moves the file in both the canonical and mirror trees, and rewrites every reference repo-wide on a token boundary so renaming "foo" can't corrupt "foobar" or "foo-bar". It rewrites the underscore module-name variant too (a hyphenated script imported as foo_bar via importlib), leaves the archived session records under sessions/ alone because they're history, and runs workflow-integrity + sync-check at the end to prove no drift. rename-artifact.org documents it and indexes the triggers. Then I used the tool to do the rename that prompted it: the org-drill deck workflow and its helpers are now flashcard-named, since "flashcard" is the word you'd actually search for. The renamed set is flashcard-review.org plus flashcard-stats.py, flashcard-sync, flashcard-to-anki.py, and flashcard-diff-ids.py, with their tests, every reference, and the INDEX entry updated. The deck is still an org-drill deck under the hood, so the ":drill:" tag handling and the "drill deck" trigger phrases stay. I added "review/update the flashcards" alongside them. Tests: 9 bats for the rename tool (including the prefix-collision and history-preservation edges), and the renamed script suites all pass under make test. --- .ai/workflows/flashcard-review.org | 327 +++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 .ai/workflows/flashcard-review.org (limited to '.ai/workflows/flashcard-review.org') diff --git a/.ai/workflows/flashcard-review.org b/.ai/workflows/flashcard-review.org new file mode 100644 index 0000000..31027b3 --- /dev/null +++ b/.ai/workflows/flashcard-review.org @@ -0,0 +1,327 @@ +#+TITLE: Drill Deck Review Workflow +#+AUTHOR: Craig Jennings & Claude +#+DATE: 2026-05-30 + +* Overview + +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. Three helper scripts (=flashcard-stats.py=, =flashcard-diff-ids.py=, =flashcard-sync=) automate the inventory, the safety check, and the regenerate-and-place. + +*Scheduling lives on the Anki side.* Desired retention and the FSRS scheduling model are per-deck Anki options set on the phone, never controlled by the org source or =flashcard-to-anki.py=. The pipeline's only scheduling job is keeping each card's identity (the =:ID:=-derived GUID) stable so Anki's review history survives a rewrite. Don't try to encode retention, intervals, or org-drill's SM-2 state into the Anki output — the two schedulers are separate, and the import carries only card content plus identity. (Anki's desired-retention default is 90%; see [[https://docs.ankiweb.net/deck-options.html][the deck-options manual]].) + +* When to Use This Workflow + +Trigger phrases: + +- "Review the drill deck" +- "Update the drill deck" +- "Refresh the Anki cards" +- "Let's run the flashcard-review workflow" + +Typical timing: + +- After a wave of personnel changes (titles, roles, employment status) +- After a major milestone (a demo ships, a contract closes, a submission goes in) +- When org-drill review surfaces a card with stale or wrong content +- When the Anki deck on the phone hasn't been regenerated in weeks + +* Inputs + +- *Source file*: the org-drill file. Common locations: + - =deepsat.org= at the work project root (symlinked from =~/sync/org/drill/=) + - =health-drill.org= in the health project + - Any =:drill:= deck under =~/sync/org/drill/= +- *Source-of-truth docs for content accuracy*: project-specific. Typical set: + - 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/.apkg= (the phone-sync target). Both =flashcard-to-anki.py= and the =flashcard-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=. + +=flashcard-stats.py= flags any title containing =org-drill= (case-insensitive, hyphenated or spaced) as a workflow violation. + +*Stable-ID caveat.* =flashcard-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. + +Three card families have different question shapes: + +*** Acronym / concept cards +"What does X stand for and what is it?" or "What is X and why does it matter?" Promote the question that was already in the body up to the heading. + + Before: + : ** AFRL :drill: + : What does AFRL stand for and what is it? + : *** Answer + : Air Force Research Laboratory. ... + + After: + : ** What does AFRL stand for and what is it? :drill: + : Air Force Research Laboratory. ... + +*** Person cards +Format: "Who is X? Tell me about their Y." where X is a role descriptor that doesn't name the person, and Y is whatever the answer body covers (background, role, limitations, scope). The answer body opens by naming the person, then continues. + + Before: + : ** Vrezh Mikayelyan :drill: + : Who is Vrezh? What are his key limitations? + : *** Answer + : Developer (also called "Reg"). Armenia-based ... + + After: + : ** Who is DeepSat's Armenia-based developer? Tell me about his background and limitations. :drill: + : Vrezh Mikayelyan. Armenia-based, full-time as of April 2026. Worked with Hayk at Bazoomq on Armenia's first satellite ... + + 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. + + Splitting: the person card deliberately trades atomicity for narrative recall — one card carries identity plus several attributes. When a body bundles genuinely unrelated attributes (role, employment history, limitations, scope) rather than one coherent topic, split it into multiple cards. One inherits the existing =:ID:= (and its SRS history); each new sibling starts fresh and will correctly show in =flashcard-diff-ids.py= as an appeared ID. The criterion: split when the body reads as a list of separate facts, keep it whole when it reads as one story. (Minimum-information principle — Wozniak rule 4, Matuschak "Focused".) + +*** 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 =flashcard-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) + +- *No =*** Answer= sub-header.* The body /is/ the answer; the heading /is/ the question. The sub-header was a workaround for topic-as-heading cards. +- *Body opens by naming the topic.* "Air Force Research Laboratory. Air Force's R&D arm." or "Vrezh Mikayelyan. Armenia-based, full-time as of ..." The Anki back shows this directly under the front question; restating the topic makes the back read as a complete answer. +- *PROPERTIES drawer stays.* Org-drill needs the =:ID:=, =:DRILL_LAST_INTERVAL:=, =:DRILL_EASE:= etc. for SRS state. The Anki output strips it (see the script change). +- *=SCHEDULED:= / =DEADLINE:= planning lines stay.* Same reason. The Anki output strips them. +- *Source citation goes at the very end, after two blank lines.* When a card cites a source, put a =Source: