<feed xmlns='http://www.w3.org/2005/Atom'>
<title>rulesets/.ai/scripts/tests/test_drill_deck_stats.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-05-31T17:19:34+00:00</updated>
<entry>
<title>feat: add rename-ai-artifact tool and rename the drill-deck family to flashcard</title>
<updated>2026-05-31T17:19:34+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-31T17:19:34+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=ddf48dc7ac780da1aacdff4e03f1d7da255b8f39'/>
<id>urn:sha1:ddf48dc7ac780da1aacdff4e03f1d7da255b8f39</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>fix(drill-deck): cut leakage false positives and codify source/date conventions</title>
<updated>2026-05-30T20:46:00+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-30T20:46:00+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=b80a9ceb3fc9cdca9798b48fbc4f9ab9c1592b57'/>
<id>urn:sha1:b80a9ceb3fc9cdca9798b48fbc4f9ab9c1592b57</id>
<content type='text'>
Health ran the new leakage check on a 43-card deck and hit two false-positive classes. The check read the whole card body, so a =Source: &lt;label&gt; — &lt;url&gt;= citation line inflated the front/back overlap whenever the URL slug repeated the question's words. Range/category cards ("What are the HbA1c ranges across normal, prediabetes, and diabetes?") tripped it too, because the question's categories echo in the answer even though the recalled content is the numbers.

drill-deck-stats.py now routes leakage through an is_leaky helper. It strips =Source:= and created-date lines before computing overlap, and exempts a card when the answer carries a numeric range or threshold the question lacks. leakage_ratio itself is unchanged, so the genuine-restatement case still flags.

Two body conventions now hold: a =Source:= citation goes at the end of a card after two blank lines, and no created/added date goes on a card. drill-to-anki.py now strips =Created:= / =:CREATED:= lines from the back as a backstop, and the workflow's Phase C removes them from the source during the rewrite. I added tests for the source-strip, the numeric carve-out, and the created-line strip, and documented all of it in drill-deck-review.org.
</content>
</entry>
<entry>
<title>feat(drill-deck): add authoring-quality checks and a card-authoring section</title>
<updated>2026-05-30T18:55:05+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-30T18:55:05+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=5bd759151d3ccf2d0a90f4b7de71e8c0e6e4a0a1'/>
<id>urn:sha1:5bd759151d3ccf2d0a90f4b7de71e8c0e6e4a0a1</id>
<content type='text'>
I researched spaced-repetition best practices (Wozniak's twenty rules, Matuschak's prompt-writing guide, Nielsen, the Anki and FSRS docs) and folded the findings into the drill-deck pipeline.

drill-deck-stats.py now checks authoring quality on top of structure. Two checks block: answer leakage (a question that echoes &gt;= 80% of its own answer's content words tests recognition, not recall) and duplicate / near-duplicate fronts (confusable cards interfere). Three checks warn without blocking, surfacing rewrite candidates without failing the gate: overloaded backs, list-shaped backs, and binary yes/no prompts. The fuzzy thresholds live in constants at the top of the script, so a real deck that trips false positives can be tuned. I pulled the card-parsing into a parse_cards helper that captures each card's body, and added focused tests for every new helper plus CLI coverage of the leaky, duplicate, and notes-only cases.

drill-deck-review.org gains a Card Authoring Principles section (the why behind the canonical shapes, with sources), a person-card splitting path bounded by the :ID:-preservation rule, a Phase B cost-benefit-removal and leech-reformulation disposition, and a scheduling-is-Anki-side note so a future editor doesn't try to encode FSRS retention in the org source. I left out cloze cards (would need a second note type), per-card tractability targeting and retention encoding (Anki-side telemetry that never reaches the source), and on-face source-stamping (the converter strips those drawers by design). Each is noted with its reason.
</content>
</entry>
<entry>
<title>test(scripts): cover drill-to-anki internals, broadcast, and daily-prep</title>
<updated>2026-05-30T18:27:29+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-30T18:27:29+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=a6313954fc297ee4a6c1c42ba903730a364cd5df'/>
<id>urn:sha1:a6313954fc297ee4a6c1c42ba903730a364cd5df</id>
<content type='text'>
I backfilled the gaps left after the flashcard work landed. drill-to-anki.py had tests only for its two default helpers. I added coverage for the core parser and its pieces: parse (section-to-tag mapping, drawer-only body, blank trimming, multiline join, no-card input), strip_org_metadata (drawer and planning-line stripping, unclosed drawer), section_to_tag, escape_html, and the deterministic stable_id. I also filled the remaining drill-deck-stats / drill-deck-diff-ids branches (missing-title and PROPERTIES-mismatch warnings, the appeared-IDs note path).

I added test_cross_project_broadcast.py for the two scripts that had none here: is_broadcastable / discover (SEARCH_ROOTS pointed at a tmp tree) / sender_project / inbox_send_path, plus an ERT suite for daily-prep-agenda.el (dp-iso-date, dp-bucket with the clock pinned, dp-format-entry, and dp-collect end to end on a temp org file).

daily-prep-agenda.el needed one change to be loadable under ERT: its batch entrypoint fired on any load. I gated it behind dp--cli-invocation-p, the same readable-files check lint-org.el already uses, so requiring the file for tests no longer runs the extractor. A real invocation with a file argument still fires. A no-argument run now no-ops instead of printing an empty header.
</content>
</entry>
<entry>
<title>chore(scripts): add drill-deck stats, diff-ids, and sync wrapper</title>
<updated>2026-05-30T18:17:47+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-30T18:17:47+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=0234e52b727b34ade93961eb05b5638685f4406f'/>
<id>urn:sha1:0234e52b727b34ade93961eb05b5638685f4406f</id>
<content type='text'>
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.
</content>
</entry>
</feed>
