# Migration Protocol(schema 演进哲学) 被 `cheat-migrate` skill / `cheat-init` / SessionStart hook / 维护者引用。规定如何安全演进 `.cheat-state.json` schema 而不让老用户被打断。 --- ## 核心原则 1. **每个 release 必须能让老用户的旧 state 工作**——通过 migrate 升级,或通过 `state.get(field, default)` 兼容 2. **MINOR 改动 = 加字段 / 软化 enum**;老 state 不跑 migrate 也能工作(字段缺失用默认值),跑了让 state 完整 3. **MAJOR 改动 = 删字段 / 重命名 / 改语义**;老 state **必须**跑 migrate,否则 skill 读到不一致字段会出错 4. **不允许跳版**;多版升级必须按顺序应用每个 step。每步幂等 5. **失败停在原地**;不回滚,让用户在断点修复后继续 6. **schema_version 是单调递增**;不允许降级(如需降级,cp 历史 git 快照) --- ## 何时算 MINOR vs MAJOR ### MINOR 范围(不需要 migrate 也能跑老 state) - 新增字段(默认值定义良好,老 skill 不读它也不出错) - 软化 enum 取值(如 `"strict" / "lenient"` 加第三个 `"adaptive"`,老值仍合法) - 给字段加新可选取值(如某个 list 字段加新元素) - 改默认值(不改语义) ### MAJOR 范围(必须跑 migrate) - **删除字段**——老 skill 仍写它,新 skill 不读它,会出歧义 - **重命名字段**——老/新 skill 看不到对方写的 - **改字段语义**(如 `mode` 从 enum 改为整数;`baseline_plays` 从 int 改为 list) - **改 enum 取值**(如 `"opinion-video"` 改成 `"opinion_video"`,老值不再合法) - **拆字段 / 合字段** > 模糊地带建议**保守判定为 MAJOR**——多写一份 migration 文件比让用户的 state 出错更好。 --- ## 维护者 checklist:bump schema 时必做的 4 件事 每次准备 release 时如果改了 state schema: ### 1. 改 cheat-init 写新 state 的硬编码 schema_version ```diff - "schema_version": "1.1", + "schema_version": "1.2", ``` 位置:`skills/cheat-init/SKILL.md` Phase 3 的 state 写入段。 ### 2. 改 migrations/registry.md 的 LATEST_SCHEMA 标记位 ```diff - LATEST_SCHEMA = "1.1" + LATEST_SCHEMA = "1.2" ``` 并在"版本链"表追加新行: ``` | 1.1 | 1.2 | NO/YES | [1.1-to-1.2.md](1.1-to-1.2.md) | 一句话描述 | ``` ### 3. 写 migrations/-to-.md 4 段必填(参考 `1.0-to-1.1.md` 模板): - WHAT changed - WHY - HOW (Claude steps for /cheat-migrate) - Manual fallback > 写不出 4 段 = 改动太复杂没想清楚 = 不该 release 这次 schema bump。 ### 4. CHANGELOG.md 标版本号 + 链接 ```markdown ## [0.2.0] — YYYY-MM-DD ### BREAKING / MINOR - schema_version 1.1 → 1.2: <一句话描述>。迁移指南:[migrations/1.1-to-1.2.md](migrations/1.1-to-1.2.md) - ... ``` MINOR 用 `### MINOR`,MAJOR 用 `### BREAKING`,要醒目。 --- ## skill 内部怎么读 state(防御式编程) 每个 skill 读 state 时**必须**用 `state.get(field, default)` 模式: ```python # 好 target_cadence = state.get("target_publish_cadence_days", None) benchmark_status = state.get("benchmark_status", "none") shoots = state.get("shoots", []) # 坏(老 state 没这字段会 KeyError) target_cadence = state["target_publish_cadence_days"] ``` 理由: - MINOR 升级时老 state 缺新字段——`get` 模式让 skill 自动用默认值 - 用户手改 state 删了字段——同上 - 减少 skill 内"必须先迁移才能跑"的强依赖 **例外**:核心标识字段允许直接索引(如 `state["schema_version"]`、`state["rubric_version"]`)——这些缺失意味着 state 文件根本不合法,应该明确报错。 --- ## SessionStart hook 的角色 hook 在每次会话开始时检测: ```bash state_schema=$(jq -r '.schema_version // "unknown"' "$STATE_FILE") if [[ "$state_schema" != "$LATEST_SCHEMA" ]]; then echo "⚠️ schema 版本不一致:state=$state_schema, skill 期望=$LATEST_SCHEMA" echo " 建议跑 /cheat-migrate 升级(不阻塞继续工作)" fi ``` **非阻塞**:用户可以选择"先继续工作,回头再跑 migrate"。MINOR 不一致时大部分功能仍能跑;MAJOR 时部分 skill 可能报错——这时再跑 migrate 也来得及。 --- ## 给开发者:避免 schema 频繁 bump 的实践 不是每个改动都需要 schema bump。下面是哲学: - **优先 MINOR**:能加字段就加字段,少删字段。删字段让老用户不爽 - **批量 bump**:积攒 3-5 个 MINOR 一起 release 比每次小改都 bump 要友好 - **延迟 bump**:MINOR 字段如果 90% 用户用不到,**不**急着 bump schema——可以让该字段 `state.get(field, default)` 默默 work,等下次 release 顺路 bump - **避免 MAJOR**:能用 MINOR 解决的绝不上 MAJOR。例:与其重命名字段,不如保留旧字段 + 加新字段(旧的标 deprecated,下个 MAJOR release 才删) --- ## 备份保留策略 `/cheat-migrate` 写之前会备份到 `.cheat-state.json.backup-`。 备份保留多久: - 用户跑 `/cheat-status` 时,如果有备份 + state 已稳定运行 N 天 → 提示"可以清理 N 个旧备份" - `/cheat-init` 重 init 时清理所有旧备份(既然要重 init,老备份意义不大) - 用户手动 `rm .cheat-state.json.backup-*` 永远 OK 不入版本控制:`.cheat-state.json.backup-*` 应在 `.gitignore` 里(已含 `.cheat-state.json` 通配规则)。