aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/flashcard-diff-ids.py
diff options
context:
space:
mode:
Diffstat (limited to '.ai/scripts/flashcard-diff-ids.py')
-rwxr-xr-x.ai/scripts/flashcard-diff-ids.py99
1 files changed, 99 insertions, 0 deletions
diff --git a/.ai/scripts/flashcard-diff-ids.py b/.ai/scripts/flashcard-diff-ids.py
new file mode 100755
index 0000000..152bb70
--- /dev/null
+++ b/.ai/scripts/flashcard-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:
+ flashcard-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"flashcard-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())