## Anki 牌组迁移护栏示例
### Before
一次歌词牌组升级会在生成 `.apkg` 后直接走人工判断:
1. 先导出备份。
2. 看 `deck:"歌词"` 是否等于旧 tag 的 43 张。
3. 删除旧 deck。
4. 导入新 `.apkg`。
5. 再查 deck 和 tag 数量。
这个流程能工作,但每个项目都要重新写一次判断。遇到用户中途手动加卡、note type 被多个 deck 共用、AnkiConnect 不支持新增模板时,风险会靠临场经验兜底。
### After
把迁移动作变成统一 guard:
```bash
python learning-bu/tools/anki_migration_guard.py \
--deck '歌词' \
--mode replace-deck \
--apkg english/04-projects/lyrics-cloze-2026-05-09/lyrics-sequence-v2.apkg \
--old-tag lyrics_cloze_20260509 \
--new-tag lyrics_sequence_v2_20260509 \
--expected-old-count 43 \
--expected-new-count 81 \
--backup-dir english/04-projects/lyrics-cloze-2026-05-09/backups \
--dry-run
```
Dry-run 输出:
| 检查项 | 成功信号 | 失败信号 |
| --- | --- | --- |
| AnkiConnect 可用 | version 返回正常 | Anki 未打开或插件不可用 |
| 旧 deck 范围 | `deck:"歌词" = 43` 且旧 tag = 43 | deck 数大于旧 tag,说明可能有手工新增卡 |
| 新包预期 | `.apkg` 可解析且新 tag = 81 | 包内 tag/card 数与预期不一致 |
| 备份 | backup apkg 已写入 | 备份失败则禁止迁移 |
| 执行路径 | replace-deck 可用 | note type/template 变更则切到 collection API |
确认安全后执行:
```bash
python learning-bu/tools/anki_migration_guard.py ... --execute
```
执行后必须给出验证摘要:
```text
backup: backups/歌词_before_2026-05-09_1146.apkg
removed_old_cards: 43
imported_new_cards: 81
verify deck:"歌词": 81
verify tag:lyrics_sequence_v2_20260509: 81
manual_cards_detected: 0
status: success
```
### 钢琴模板拆分场景
对于 `钢琴 基础` 这种不是替换 deck、而是给既有 note type 增加 card template 的迁移,guard 的模式应换成:
```bash
python learning-bu/tools/anki_migration_guard.py \
--deck '钢琴 基础' \
--mode add-template \
--note-type 'Piano Staff Card' \
--template-name '这张谱用哪个手指?' \
--expected-before-count 20 \
--expected-after-count 30 \
--preserve-review-template '这张谱读什么?' \
--execute
```
这里的核心不是删旧卡,而是确认原复习记录留在旧模板上,新增卡只来自新模板。若 AnkiConnect 只更新已有模板而没有新增模板,guard 应明确失败并提示切换 Anki collection API。