aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/flashcard-diff-ids.py
blob: 152bb70a9fe831f40d02bc995f18ee2caa01a957 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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())