# 配置迁移规则 配置的形状会随版本变(0.3 把九个扁平键搬进了 `files.*` / `content.*` / `scopes.*` / `limits.*`)。用户可能从任意旧版本直接跳到任意新版本——**中间版本没有存在过**,所以迁移不能按版本号分步执行。 ## 硬规则 1. 一次形状变更 = `lib/config.js` 的 `RETIRED_PATHS` 里**追加**一条记录:`{ from: 旧路径, to: 新路径 }`。 2. **永不删除、永不改号、永不改 `from`**。`from` 写当时的字面路径,不引用 `PATHS`——否则将来改 `PATHS` 会把这行历史一起改掉。 3. 记录只搬位置,**不改语义**;值的解释(编码别名、默认值、类型)归 schema 与 `readConfig`。 4. 三个消费方都只读这张表,加一条就三处同时生效:读取兜底(`readConfig`)、schema 里那几个隐藏的旧键(`lib/schema.js`)、启动迁移(`lib/bootstrap.js`)。 5. 迁移只写**用户层还没有值**的字段,写完清掉旧键;一次 `mutate` 完成,并且在同一个批次里**先写新位置、再清旧键**。 6. **只搬用户层里真的存在的旧路径**。schema 给退役路径也声明了默认值,所以「配置里有值」不等于「用户写过」——按配置判断会把默认值写进 profile,还会让首用自动配置以为地址已经设过(0.3.0 发版前就是这么漏掉 `:9921` 的)。`cordis.patch.yml` 里手写的旧键属于 patch 层,不搬,读取兜底继续认它。 7. 写不进去(profile 被 home patch 覆盖、settings 拒绝)**不算失败**:旧键留在原地,读取兜底让它继续生效。 ## 版本跳跃怎么保证 - 不按版本号分支,按「用户层里还有没有旧路径」判定。`migrationOps()` 按表顺序把值逐段搬(上一段的 `to` 是下一段的 `from`),所以 `0.1 → 当前`、`0.1 → 1.0` 与逐版升级走的是同一条链,中间版本不需要存在过;`readConfig()` 的兜底用自己的 `foldRetired()` 走同一条链。 - 不用 `configVersion` 之类的标记:多一个状态就多一处可能与 profile 不同步;路径探测是幂等的,天然可重跑。 - 一条记录同时覆盖多代旧版本时(比如 0.1 与 0.2 的扁平键完全相同),只写一条,别为每个版本复制一份。 ## 加一条记录时要做什么 1. 在 `RETIRED_PATHS` 末尾追加 `{ from: ['旧', '路径'], to: PATHS.新路径 }`(新路径还没进 `PATHS` 时写死字面量)。 2. 什么都不用改 `lib/schema.js`:它会自动把旧位置加回一个隐藏字段(父级已被普通值占用时跳过,`canInsert` 挡这一种)。 3. 加一条测试:**从最老形状直接迁到当前形状**(`test/bootstrap.test.js` 里「0.1 直装」那条就是这么写的)。 4. 在 CHANGELOG 的迁移段写清「从哪个版本起生效」。 ## 不适用条件 - 单纯的**加字段**不是形状变更:schema 给个默认值即可,不要加记录。 - 改字段**语义**(比如编码标签从 `utf8` 收成 `utf-8`)走 schema 与 `readConfig` 的兜底,不走迁移。 - 旧路径的父级已经变成普通值(对象塌成字符串)时,schema 不会为它建隐藏字段,读侧也不参与——那种情况等于放弃旧值,必须在 CHANGELOG 里明说。