games-development-ai/docs/plans/2026-07-06-002-feat-W-SAVE-CAP-存档定容与schemaVersion-plan.md
lili 2ce0ed3a9d feat(save-cap): W-SAVE-CAP 存档面定容+schemaVersion——单槽版本信封,北极星三款共享基建
契约先行(sdk-interface.d.ts+镜像 additive):白名单新增恰等键族 save:{gameId}:{versionId}(≤65536 码元,
写入必须 {schemaVersion≥1, data} 信封,宿主闸 fail-closed);idle: 族 4096 逐字不动;弃多 key 分片。
宿主闸抽纯函数模块 storageGate.ts(四闸合一可 node 直测);save-progress 插件 v1.0.0→v1.1.0 additive:
setVersioned/getVersioned 逐级迁移/缺级兜底/降档读新档不毁档/无版本旧档按 0 版进链+探针两计数。
证据:插件单测 18/18、game-runtime 31/31、宿主闸负向演示脚本入仓真跑全断言过、vue-tsc+vite build 绿。
三书 M 门影子存档降级口径解除条件就位(卡牌 M2/割草 M4/夜市 M5 切换)。设计页 docs/plans/2026-07-06-002。
2026-07-06 09:24:44 -07:00

55 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 结构化分片(不选)**:按子树拆多个 keyA 胜在三点:单键单写是一致快照,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 升级路"按子树分槽"另切小单。