package engine // migrate.go 实现 Transcript 格式版本管理与迁移骨架. // // [模块定位] // 这个文件是 INF-6 版本兼容基础设施的核心--不是为了迁移当前数据 // (当前只有 v1,没有历史债),而是为了: // 1. 让未来的 breaking change 有规范的处理路径 // 2. 防止旧版本引擎静默读取并损坏新格式文件(MaxSupportedVersion 保护) // // [版本常量设计] // // transcriptCurrentVersion = 当前写入的版本号(总是最新) // transcriptMaxSupportedVersion = 本版本引擎能读取的最高版本号 // // 正常情况下两者相等.多实例/rolling update 场景: // - 新实例写入 v2,旧实例读到 v2 > maxSupported(=1) → 明确报错,不静默降级 // - 旧实例写入 v1,新实例读到 v1 < current(=2) → 运行迁移函数升级 // // [注册式迁移表] // // 当前 transcriptMigrations 为空(没有历史债).未来添加 breaking change 时, // 在此文件中注册一个迁移函数,`migrateTranscript` 会自动按版本顺序执行. // // 示例(未来 v2 迁移): // // func init() { // transcriptMigrations[1] = func(t *Transcript) error { // // v1 → v2:重命名 Stats.TurnCount → Stats.Turns // return nil // } // } // // [升华改进(ELEVATED)] // 早期实现的 version 字段只是 audit trail(存 MACRO.VERSION // 应用版本号),不用于格式迁移. // 我们明确区分两层: // - FormatVersion int → schema 版本(格式迁移用,单调递增 int) // - EngineVersion string → 应用版本(仅 audit,如 "1.2.3") // // 早期方案做法的问题:无法区分"格式变了"和"应用版本升了"--两者同时用 version 字段. // 替代方案:<只用一个 version 字段存应用版本,格式变更时检查 semver> - // 否决原因:semver 比较需要外部依赖或手写解析,int 比较一行搞定; // 且 semver 的 major/minor/patch 对格式迁移没有额外语义价值. import ( "fmt" ) // transcriptCurrentVersion 是当前引擎写入 Transcript 文件时使用的格式版本号. // // 精妙之处(CLEVER): 用具名常量而非在 SaveTranscript 里硬编码 1-- // 未来 bump 版本时只改这一行,SearchAndReplace 找不到所有调用点的问题消失. // 对应早期实现的 STATS_CACHE_VERSION 常量. const transcriptCurrentVersion = 1 // transcriptMaxSupportedVersion 是本版本引擎能读取的最高格式版本号. // // 正常情况下与 transcriptCurrentVersion 相等. // 当此版本引擎需要读取更新版本写的文件时(降级/多实例场景), // LoadTranscript 会返回明确错误,而非静默读取并在保存时损坏新字段. // // 历史包袱(LEGACY): 目前 maxSupported == current == 1, // 这个常量在单版本时代没有实际效果,是为未来多版本场景预留的防护. // 理想情况:当 maxSupported < current 时应在启动时打警告(TODO 监控). const transcriptMaxSupportedVersion = transcriptCurrentVersion // MigrateFunc 是一个 Transcript 迁移函数的类型. // 输入是版本 N 的 Transcript,函数就地修改为版本 N+1 的格式. // 返回 error 时迁移中止,不会继续执行更高版本的迁移. // // 迁移函数必须满足: // - 幂等:对已经是 N+1 格式的数据调用不产生副作用(防御性检查) // - 无损:迁移后原有字段语义不变,只新增或规范化 // - 可回滚(尽力):能记录变更内容,方便问题排查 type MigrateFunc func(*Transcript) error // transcriptMigrations 是 Transcript 迁移函数注册表. // // key 是"从版本 N 迁移到版本 N+1"的 N(源版本号). // 例如:transcriptMigrations[1] 将 v1 升级到 v2. // // 精妙之处(CLEVER): 用 map[int]MigrateFunc 而非 []MigrateFunc-- // 版本号作为 key 防止注册顺序错误导致的迁移混乱. // 即使开发者在 init() 中乱序注册,migrateTranscript 也会按版本递增顺序执行. // 当前为空:没有历史债,没有需要迁移的旧格式文件. var transcriptMigrations = map[int]MigrateFunc{} // migrateTranscript 将 Transcript 从当前版本迁移到 transcriptCurrentVersion. // // 执行逻辑:从 t.FormatVersion 开始,依次查找迁移函数并执行, // 每成功执行一步就将 FormatVersion 递增,直到达到 transcriptCurrentVersion. // // 如果中间缺少某个版本的迁移函数(如 v2→v3 的函数未注册), // 返回错误而非跳过--跳过会导致数据处于未定义的中间状态. // // 历史包袱(LEGACY): 迁移函数就地修改 *Transcript,中途失败时无法自动回滚-- // 调用方读取的仍是内存里部分修改过的对象.当前影响有限(fn 失败时调用方不会持久化), // 但未来编写真实迁移函数时必须注意: // 1. 迁移前在函数内部做深拷贝备份,失败时手动恢复; // 2. 或确保迁移函数在错误路径上不修改任何字段(先校验后修改模式). // // 理想做法:框架层在调用 fn(t) 前自动深拷贝,失败时还原. // 改进条件:注册第一个真实迁移函数时. func migrateTranscript(t *Transcript) error { for t.FormatVersion < transcriptCurrentVersion { fn, ok := transcriptMigrations[t.FormatVersion] if !ok { return fmt.Errorf( "migrate transcript: no migration registered for v%d → v%d "+ "(current engine supports up to v%d)", t.FormatVersion, t.FormatVersion+1, transcriptCurrentVersion, ) } if err := fn(t); err != nil { return fmt.Errorf("migrate transcript v%d: %w", t.FormatVersion, err) } t.FormatVersion++ } return nil }