diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-30 13:17:47 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-30 13:17:47 -0500 |
| commit | 0234e52b727b34ade93961eb05b5638685f4406f (patch) | |
| tree | b7ee5f66a9fceb3fd4d9b1d2ba8c44e89dde76c5 /.ai/scripts/drill-deck-diff-ids.py | |
| parent | 038d59b7e548d2323f43dcd92ba14cba876d840d (diff) | |
| download | rulesets-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/scripts/drill-deck-diff-ids.py')
| -rwxr-xr-x | .ai/scripts/drill-deck-diff-ids.py | 99 |
1 files changed, 99 insertions, 0 deletions
diff --git a/.ai/scripts/drill-deck-diff-ids.py b/.ai/scripts/drill-deck-diff-ids.py new file mode 100755 index 0000000..bd2c4cc --- /dev/null +++ b/.ai/scripts/drill-deck-diff-ids.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""SRS-state preservation check between two versions of an org-drill deck. + +Extracts every :ID: from each version and reports IDs that disappeared +or appeared. Disappeared IDs lose org-drill SRS state (review history, +ease, intervals) and are the worst-case bug from a deck rewrite. Appeared +IDs are usually fine (new cards added on purpose) but worth surfacing. + +Exits 0 when clean, 1 when any IDs disappeared or appeared. + +Usage: + drill-deck-diff-ids.py <before.org> <after.org> +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +CARD_RE = re.compile(r"^\*\*\s+(.+?)\s+:drill:\s*$") +ID_RE = re.compile(r"^\s*:ID:\s+(\S+)\s*$") + + +def card_id_map(path: Path) -> dict[str, str]: + """Return {id -> heading} for every :drill: card in path.""" + result: dict[str, str] = {} + lines = path.read_text(encoding="utf-8").splitlines() + i = 0 + while i < len(lines): + m = CARD_RE.match(lines[i]) + if m: + heading = m.group(1).strip() + i += 1 + while i < len(lines): + line = lines[i] + if line.startswith("* ") or CARD_RE.match(line): + break + mid = ID_RE.match(line) + if mid: + result[mid.group(1)] = heading + break + i += 1 + continue + i += 1 + return result + + +def main() -> int: + if len(sys.argv) != 3: + print(f"usage: {sys.argv[0]} <before.org> <after.org>", file=sys.stderr) + return 2 + + before_path = Path(sys.argv[1]).expanduser().resolve() + after_path = Path(sys.argv[2]).expanduser().resolve() + + for p in (before_path, after_path): + if not p.is_file(): + print(f"error: {p} not found", file=sys.stderr) + return 2 + + before = card_id_map(before_path) + after = card_id_map(after_path) + + before_ids = set(before) + after_ids = set(after) + + preserved = before_ids & after_ids + disappeared = before_ids - after_ids + appeared = after_ids - before_ids + + print(f"drill-deck-diff-ids: {before_path.name} → {after_path.name}") + print() + print(f"IDs in BEFORE: {len(before_ids)}") + print(f"IDs in AFTER: {len(after_ids)}") + print(f"Preserved: {len(preserved)}") + print(f"Disappeared: {len(disappeared)}") + print(f"Appeared: {len(appeared)}") + print() + + warnings = 0 + if disappeared: + warnings += 1 + print(f"WARN: {len(disappeared)} card IDs disappeared (SRS state lost)") + for cid in sorted(disappeared): + print(f" - {cid} (was: {before[cid]!r})") + if appeared: + warnings += 1 + print(f"NOTE: {len(appeared)} new card IDs appeared") + for cid in sorted(appeared): + print(f" - {cid} (now: {after[cid]!r})") + + if warnings == 0: + print("clean — SRS state preserved") + return 0 + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) |
