--- date: 2026-07-06 topic: W-SAVE-CAP 存档面定容与 schemaVersion status: 设计+实现同单交付(创始人 2026-07-06 拍"立即独立切单");代码已落、测试见 §6 sot-impact: 不新建 canonical topic;触点 = 产物执行沙箱(storage 四道闸增 save: 键族,两份图说各加一句增补注记、原行号引用标"以现码为准")、contracts/sdk-interface.d.ts 与 game-studio/src/host/contract.ts(additive:SaveEnvelope 类型与 save: 键族注释,semver 只增)、save-progress 插件(v1.0.0→v1.1.0 additive 两方法);O-6 版本语义全案(意图重放/版本卡/兼容检查器)不在本单 上级: docs/mvp/MVP作战清单.md --- # W-SAVE-CAP:存档面定容 + schemaVersion ## 1. 问题与现状(一手核实) 现行 H5 存档面是为「无离线态模板、存档 <200B」设计的四道闸,真实现在 `game-studio/src/host/GamePlayer.vue` 的 `handleStorage`:闸① key 恰等 `idle:{gameId}:{versionId}` 才放行;闸② `value.length > 4096` 丢弃;闸③ 写前 `JSON.parse` 校验、回读只接受对象形态;闸④ 落盘加 `wxgame:` 前缀。游戏侧的 L2 `save-progress` 插件(P9)是通用 KV 面:命名空间 + JSON 序列化 + 单值 32KB(UTF-8 字节)上限 + 坏档容错,alpha 后端 = 本地 adapter,无任何版本字段。 北极星三款的复杂存档(夜市 save_state 多子树/卡牌 run+全局进度/割草 meta 进度)顶破 4KB,且无 schemaVersion 就没有迁移纪律——这是复杂游戏北极星件 §1.1.3 B 段坐实的前置增补需求,三书均以"三款共享基建单"引用本单:卡牌 M2 前硬需要(最早)、割草 M4、夜市 M5。 ## 2. 方案对比与选定 **方案A·单槽版本信封定容(选定)**:白名单新增一个恰等键 `save:{gameId}:{versionId}`(一游戏一版本一槽),该槽上限提到 65536 码元,写入必须是版本信封 `{schemaVersion: 整数≥1, data: 纯对象}`,闸内 fail-closed。**方案B·多 key 结构化分片(不选)**:按子树拆多个 key。A 胜在三点:单键单写是一致快照,B 的多键分写存在撕裂写(部分子树新、部分旧)且宿主闸没有事务;白名单保持恰等匹配纪律,不开前缀通配面;一信封一版本,迁移语义最简。B 只有在单槽超 65536 码元时才有必要——按三书首发定容(卡牌 run+meta、割草 meta、夜市 M5 前影子存档过渡)均在其下;若未来顶破,升级路是"按子树分槽 + 每槽各带信封"(additive,不推翻本单)。 **上限值与两处口径**:宿主闸沿现行 `value.length` 码元口径(localStorage 按 UTF-16 存储,65536 码元 ≈ 实占 128KB/槽;即便 20 个版本各一槽也仅 ~2.5MB,处于常见浏览器 5MB/源配额内——配额数字为公知量级,〔来源缺口:各浏览器精确配额未一手核〕,不作为验收判据);插件侧沿既有 UTF-8 字节口径,缺省 32768 字节不动(它是更紧的缺省内闸),复杂存档游戏经 `maxValueBytes` 上调,受宿主码元闸封顶。换算参考:全中文 JSON 一码元 ≈ 3 UTF-8 字节,插件缺省 32KB 字节 ≈ 1.1 万汉字。 **idle: 键族完全不动**:上限仍 4096、无信封要求,存量行为逐字保持。 ## 3. schemaVersion 与迁移兜底(save-progress v1.1.0,additive) 插件新增两个方法,既有 get/set/remove/clear 签名不动: - `setVersioned(key, schemaVersion, data)`:校验 schemaVersion 为 ≥1 整数、data 为纯对象(非数组),包成信封走既有 set 管道(序列化/字节闸/异常不外抛),返回 boolean。 - `getVersioned(key, currentVersion, opts)`:`opts = { migrators?: {[fromVersion]: (data)=>data}, defaultValue? }`。行为矩阵——缺失 → defaultValue;信封版本 === current → 返 data;版本 < current → 从该版本起逐级跑 migrator(缺任一级或迁移函数抛错 → defaultValue + 告警 + `versionFallbacks` 计数),迁移成功 → `versionMigrations` 计数 + best-effort 回写新信封(回写失败不影响返回);版本 > current(降档读到新档)→ defaultValue + 告警 + 计数,**不回写不毁档**;**旧档无版本**(纯对象但无合法 schemaVersion)→ 视为 `{schemaVersion: 0, data: 原对象}` 进迁移链(提供 0→1 迁移器则迁,否则 defaultValue)。全部异常不外抛,守"存档绝不把游戏带崩"底线。 探针补 `versionMigrations` / `versionFallbacks` 两计数;manifest 1.0.0→1.1.0。 ## 4. 契约先行(additive,semver 只增) `contracts/sdk-interface.d.ts` 与镜像 `game-studio/src/host/contract.ts`(storage 三套形状之二;第三套 inject.ts 应答端只做 requestId 配对、不触 key/value 语义,零改动)各新增 `SaveEnvelope` 接口与 save: 键族语义注释:键族两员(`idle:` 4096 码元无信封 / `save:` 65536 码元强制信封),写入 payload 仍是 JSON 字符串、回读仍是对象|null,与现行读写形状零破坏。 ## 5. 宿主闸实现形态 闸逻辑从 `handleStorage` 抽成纯函数模块 `game-studio/src/host/storageGate.ts`(key 门 + 写门:族判定/长度分层/JSON 校验/save: 信封校验,返回判定与拒因),`GamePlayer.vue` 写路径与读路径 key 判定改调它,idle: 行为与告警语义逐字等价。抽纯函数的唯一动机是可测性:game-studio 无测试跑器,纯函数可由 esbuild 转译后 node 直接断言(测的是真源文件,不是复制品)。 ## 6. 失败模式与验收断言 失败模式:save: 写超 65536 码元 → 丢弃 + warn(游戏侧 fire-and-forget 不崩);写非信封/版本非法 → 丢弃 + warn;读到被篡改的非对象 → null(沿现行);插件读到坏 JSON/坏信封 → defaultValue + 计数;降档读新档 → defaultValue,原档保留。 验收断言(全部真跑,结果见执行记录):①插件单测(node --test):版本信封 roundtrip / 无版本旧档 0→1 迁移+回写 / 缺迁移器兜底 / 新于当前版本兜底不毁档 / 超限拒写计数;②宿主闸负向演示(esbuild 转译真源 + node 断言):idle 4096 边界行为不变 / save 合法信封放行 / 超限拒 / 坏信封拒 / 白名单外 key 拒;③`game-runtime` 全量 `npm test` + `npm run validate`(manifest 门)+ gz 预算核;④`game-studio` `vue-tsc -b && vite build` 编译门。 ## 7. 三书 M 门降级口径的解除条件 本单落地(代码 merge 进 dev/2.0.0)即满足三书写的"正式 SDK Storage 定容 + schemaVersion"前置的**存档面**部分:卡牌 M2 入口门、割草 M4、夜市 M5 的切换对象从"影子存档"改为 `save:` 槽 + `setVersioned/getVersioned`。仍不解除的:跨 iframe storage 后端的 β 契约接线(save-progress alpha 后端是本地 adapter,postMessage 通道到插件 adapter 的接线是既有 β/第 9 契约组欠账,不属本单新增欠账——北极星游戏 M 门验收时走 SDK storage 通道即可,插件 adapter 接线随各款集成段做)与 O-6 版本语义全案(兼容检查器/意图重放)。 ## 8. 待拍(不阻塞) 无必须即刻拍的项。一项预留:若某款单槽逼近 65536 码元(仿真灌满内容量表后实测),按 §2 升级路"按子树分槽"另切小单。