Files
ft/CodexOutput/upserver/P2AlphaNullCompatibility/B2-original-reviewed-plan.md

140 lines
16 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.
# 101两类历史空态最小兼容方案(待完整独立评审)
2026-09-28,主会话。前置:[P2-α-A关闭裁决](../P2AlphaAudit/A5-执行复核裁决与关闭.md)。本单元简称P2-α-B,仅代表两类空态兼容,不等于整体迁移政策或阶段4B。
**当前只完成设计;尚未批准或修改生产/测试源码。** 用户已要求完成后自动进入下一步,本次据此推进完整方案准备;保留用户“方案完整评审后编码”的要求。
## 1. 问题、目标与取舍
已有本地PF101共55键,被当前原生产Decode拒绝的两项是:M15 `FishingChallengeData`的JSON null;M22 `GeneralEventPackData.selections`的JSON null。旧业务能产生这两种值,详见[已复核语义盘点](../P2AlphaAudit/A3-空态语义与未消费键裁定.md)。PF102共45键已Valid,且KV102已有后续真实游戏进度。
目标:只修两条加载表示差异,让解码与物化保留旧空态;其余字段、严格格式、集合隔离和事务准入不变。保留所有Raw键和值,不替用户改档。101能否整体Valid,以原样本和冻结配置实测为准;不为追求全绿不断扩大修复。
| 路线 | 取舍 | 决定 |
|---|---|---|
| 精确保留两类null表示 | 改4个已有生产文件;保留存在性、其他字段及列表深复制 | 推荐,作为本方案唯一申请范围 |
| 删除挑战键、礼包null规范成[]或重建礼包 | 表面改动少,丢原表示或覆盖已有礼包字段;存档写入范围更大 | 不采用 |
| 通用可空Schema/迁移框架或补全所有活动入口 | 扩大所有模块接受集合和生命周期改动,无法由两个样本验证 | 不采用 |
不需要新增架构、服务、状态机、配置开关或持久化版本。M15为避免把“键存在但null”冒充“键缺失”,只给现有内部DTO补一个只读标记;不引入通用三态框架。
## 2. 输入与开发边界
- 原样本、cfgData/Tables及关键源码由本目录`design-review-hashes.json`冻结;原A单元证据保持原文。PF101/102不再联网拉取,KV102仅用既有末次备份作历史证据。
- 生产白名单4个已有文件:`Client/Assets/Scripts/Services/PlayerData/PlayerKVSaveSchema.cs`、同目录`PlayerKVDecodedSave.cs`、`Client/Assets/Scripts/DataCenter/FishingChallengeManager.cs`、同目录`BargainPackData.cs`。只改下文指明的Decode/DTO/Replace;不改活动入口、计分、奖励、购买、保存或Reset逻辑。
- 测试白名单3个已有文件:`CodexOutput/upserver/KVGameLoadTests/ActivitySchemaTests.cs`、同目录`D3SchemaTests.cs`、`Client/Assets/Scripts/Services/PlayerData/PlayerKVEditorValidationHost.cs`(仅UNITY_EDITOR固定验证;包含Bargain专用比较器的null分支)。无新测试工程/新反射入口/新诊断类;不改csproj、asmdef或生成Tables。
- `KVGameLoadValidation.cs`原固定case入口、`KVGameplayLoopSetup.InspectPFBackup`、`unity-source-inspection.cs.txt`及其他测试文件只复用。设计/结果/源码修改前快照放本目录;共享入口/状态/TODO/日志逐检查点更新。
- 任何白名单扩展或第三种拒档原因需要停在具体差异并记录,不得自动放宽。暂不打包,无Play、许可、登录、远端读写或存档导入。地址固定`http://s101.local:18080/`,只用Unity CLI Loop。
## 3. M15:缺键、JSON null、合法对象三者分开
### 3.1 表示合同
| 输入 | IsPresent | 新IsExplicitNull | 受控Reset→Replace后的业务对象 |
|---|---|---|---|
| 缺FishingChallengeData键 | false | false | null;保留原Missing合同 |
| 键值为严格JSON null,允许JSON自身空白 | true | true | null,未激活 |
| 完整合法JSON对象 | true | false | 原全部字段物化和列表深复制 |
| C# null值、空字符串/空白、字符串"null"、数字、布尔、数组、坏JSON | 无成功DTO | 无 | Malformed,不能调用Apply |
DecodeChallenge在现有`errors.Capture(15,0)`内先调用原严格`Json(text)`一次,仅token.Type为Null走新工厂;其余仍交原Object及全部字段验证。不修改共用Json/Object/IntegerList,不能用宽松反序列化或`string.IsNullOrEmpty`兜底。对象内部任一原必填字段缺失/null仍失败。
PlayerKVChallengeState增加只读`IsExplicitNull`;既有构造末尾增加可选`bool explicitNull=false`以保留旧调用,赋值并禁止`explicitNull && !present`。新增`ExplicitNull()`工厂生成present=true、explicitNull=true、其余默认值及非null空只读列表。Missing保持present=false、explicitNull=false。对象构造仍默认false。此DTO为内部内存表示,不序列化到服务器。
FishingChallengeManager.ReplaceFromKVDecoded仅把原`!state.IsPresent`条件扩为“缺失或显式null”,两者赋业务对象null;合法对象初始化器不改。Reset继续由现有受控加载事务先调用,不在Decode或Replace中启动活动、清理全局状态或写档。仍拒绝state本身为null。
Raw字典由原PlayerKVDecodedSave复制:显式null保留键及原字符串,缺键仍缺键;不能以DTO旗标重建Raw。现有DoSave对业务null序列化为JSON null的行为保持;本轮验证其序列化含义而不调用网络保存。
### 3.2 调用前置、P3-1及限制
[142处逐引用前置锚点](M15-逐引用前置锚点.md)与[机器台账](M15-逐引用前置锚点.json)逐行链接原引用、近邻声明和条件;原始语句/分类仍以[已复核台账](../P2AlphaAudit/A3-M15-read-ledger.json)为准。近邻条件不冒充控制流支配证明;没有可证明门的调用明确保留为“需要有效活动上下文”。不因本次接受null而宣称这些入口都安全。
| 门或前态 | 当前源码锚点(行号以受审源码为准) | 本方案结论 |
|---|---|---|
| fresh/Reset | FishingChallengeManager:63、70~74;Replace:76~98 | 空态不创建默认挑战,不激活配置/异步行为 |
| 初始化 | 同文件CheckInit:237、Init:244、251/256/258、263 | 有活动时原Init可能创建对象;本方案不调用Init,也不承诺将来的活动开启永久保持null |
| 首页显示 | HomeBtnChallenge:67、75;Manager.ShouldOpen:139/143 | 正常OnEnable经过ShouldOpen,后续按钮回调仍依赖活动生命周期 |
| 状态观察 | Manager.ChallengeState:1028/1034;IsSettleState:1062/1066 | null返回state0;短路不进入时间判断 |
| 局部空判断 | Manager:281、572、605、844、950、963、1074、1089 | 仅对应函数/分支的保护,不覆盖所有异步续体 |
| 奖励准入 | UserService.KVFishingReward:127 | 非null才检查挑战状态,本方案不改奖励代码 |
| 直接/别名读取 | Manager.IsFirstOpen:110、IsTimeAllow:169等;ChallengeMatchSceneRoad:81~82 | 依赖已初始化活动及合法场景进入;未证明任意场景调用都安全 |
| 资源异步入口 | Manager.InChallenge:510、514及后续资源加载 | 状态0仍可能触发加载,限制保持;不批准101真实挑战验证 |
已有业务对象→Reset→显式null必须验证;fresh→显式null与fresh→缺键都为空但DTO/Raw存在性不同。禁止把此结论用于未Reset旧对象、热换号或任意跨账号异步回调。
## 4. M22:保留列表null而保留整个礼包
只在DecodeBargain局部读取`selections` token:属性存在且token.Type为Null才允许null;属性不存在(C# null)仍让原IntegerList拒绝。数组仍原IntegerList:字符串/对象/浮点/越界/元素null不接受。GeneralEventPackData根null、缺其余字段、未知或非PackType=3的redirectID仍走现有错误分类。
PlayerKVBargainState构造仅改变selections赋值:输入null保存null;否则仍复制为只读列表,包括空数组。Missing()仍传空数组,不改变“整个礼包缺键”的IsPresent=false和默认字段。
BargainPackData.ReplaceFromKVDecoded仅改变selections字段:state.selections为null则null,否则new List深复制。其余11整数、2布尔、1浮点及_kvLoadPresent保持原赋值。SnapshotForKVValidation现有构造调用自然保留null,不需要新增数据结构或setter。Reset保持新空List和_kvLoadPresent=false。不改旧LoadData/UpdateData/SaveData;序列化应保留selections:null,不能自动转[]。
| 场景 | DTO/实际对象/Snapshot | 其他合同 |
|---|---|---|
| 根key缺失 | IsPresent=false,列表空 | Reset与缺礼包既有合同 |
| 完整对象,selections=null | IsPresent=true,列表null | 所有其他字段保持原值 |
| 完整对象,selections=[] | IsPresent=true,独立空集合 | 不与null混同 |
| 完整对象,selections=[7,9] | 只读DTO、独立业务List、独立Snapshot | 任一可变列表改变不能影响另两者 |
| selections属性缺失/类型错或其余字段坏 | 原Malformed;错误redirect原UnsupportedSchema | 不输出可Apply状态 |
旧101 redirect=4056001,冻结配置PackType=3/MaxCount=1;只作现有输入证据,不硬编码账号ID、礼包ID或字符串样本特判。
## 5. 验收矩阵与复用测试
先更新获审测试并记录旧生产代码下红测,再实施上述4文件最小差异并绿测。旧测试中M15“根null拒绝”及M22“selections属性null拒绝”两条期望需要精确改写;其他对象字段null/缺字段及M26/M27必须仍拒绝,不能整体删除严格循环。
| 编号 | 测试与证据 | 通过条件 |
|---|---|---|
| T1 | ActivitySchemaTests原RunActivitySchemaTests增加M15根空态边界 | 缺键/JSON null/带空白null/对象四组DTO值正确;C# null、空、坏JSON、"null"字符串、数值、布尔、数组拒绝;对象每字段缺失/null仍拒绝。复用同一partial Tests已有Aggregate/ModernRaw,断言完整Decode的Data.Raw保留带空白null、模块外探针键原值及缺键差异,不修改AggregateTests文件 |
| T2 | D3SchemaTests原M22段与DTO集合测试 | null、[]、非空区分;缺属性/非法数组元素及其余字段错误继续拒绝;DTO非空深复制;null不抛异常;错误PackType不绕过检查 |
| T3 | 原Editor固定case M15-M26-M27,在RunActivities追加 | fresh/null、合法对象→Reset→null、再次对象→Reset→缺键均正确;IsInactiveForKVValidation为true;ChallengeState=0/ShouldOpen=false;JsonConvert序列化业务对象为null;M26/M27原断言不变 |
| T4 | 原Editor固定case M21-M22-M24,在RunD3追加 | 对象→null列表→[]→非空→Reset/缺键;每次Snapshot逐字段比对所有礼包字段与IsPresent;验证双向修改隔离,序列化保留null;原其他模块回归不删 |
| T5 | KVGameLoadTests全量,已有完整Decode/事务回归 | 运行精确DLL;旧严格语法/AD/鱼配置/失败不发布及其他模块不回归。T1/T2所在既有方法自动被现有Program执行,无新测试注册 |
| T6 | 原Editor固定B2-20完整事务case;原固定InspectPFBackup读两个未改样本 | B2-20回归通过;101两条目标诊断消失,102仍Valid;若出现第三问题记录并停止扩大。保留原样本值hash与Decode Raw复制的现有合同,附T1/T2 Raw输入前后相等断言;没有移除模块外键 |
| T7 | Unity CLI Loop重编译+现有ServiceHost编译命令 | Editor无新增编译错误,SERVICE_HOST成功;不更改非Editor历史问题或工程配置。失败保留首败输出,不用重复运行掩盖 |
| T8 | diff/hash/运行边界 | 生产4文件仅约定成员、测试3文件仅新用例/精确期望及Bargain专用比较分支;配置和备份不变;无Play/permit/正式存档接口请求;结束环境恢复 |
测试无效保护:红测必须是预期兼容断言失败,编译失败或错误apphost启动不能充当红证据。已有共享apphost曾指向其他程序集;本轮使用`dotnet <本次明确构建的KVGameLoadTests.dll>`并记录DLL hash/时间/测试组名。没有涉及的奖励测试不为凑数重跑;只有新失败或发现依赖时再扩大回归并说明理由。
测试比较器已发现必要适配:PlayerKVEditorValidationHost:107的Bargain专用Same使用SameList,而:117通用SameList直接取Count,不能用它比较两个null列表。只在Bargain专用Same的selections子表达式中区分两者都null、仅一方null、双方非null后调用原SameList;其余14字段比较仍执行。通用SameList与其他模块不改。新增null/null相等、null/[]不等、其他字段不同不等的断言,避免比较器抛错或漏检冒充业务兼容通过。
离线构建沿用`dotnet build CodexOutput/upserver/KVGameLoadTests/KVGameLoadTests.csproj --no-restore`,运行该工程实际输出DLL;SERVICE_HOST沿用`dotnet build CodexOutput/upserver/KVGameLoadTests/ServiceHost/ServiceHost.csproj --no-restore`。不依赖dotnet run选择共享apphost,不新增测试工程。原Editor入口接受的case名称不变。
T3/T4仅对隔离真实对象调用受控Reset/Replace/Snapshot,不调用Init/CheckInit、SaveData/DoSave、购买或资源场景。沿用原测试容器与finally恢复,不发布游戏句柄。M15直接IsInactive观察及不会联网的ShouldOpen/ChallengeState在空态验证;不造真实账户身份。若原case共享容器恢复失败,停止并记录,不能继续Inspect。
## 6. Editor与数据权限
执行前通过CLI读取版本、进程和playing/enabled/ready/run/permit;90064只是上轮已测进程,不假设审批后仍相同。必须空闲、无运行许可、无活动validation/GContext,保留11个历史claimed,不改启用静态字段绕门。
需要重编译使用现有Unity CLI Loop;编译完成确认空闲后跑固定用例与Inspect。若状态不满足或需要新进程,先确认已退出Play/排水/无许可,再正常关闭重启;不用强杀或launch --restart。不得在方案未审时预写测试或更改源码触发编译。
本方案只申请本地修订和离线/Editor隔离测试,不申请101真实登录、PF/KV导入、远端Get/Set/Delete、收费或补偿。101/102旧授权不替代本轮限定范围。正式迁移前身份/权威源/独占窗口/CAS缺口及产品处理政策仍未解决。
## 7. 三条P3、未知键和102分叉
P3-1见§3及142行附件。P3-2/P3-3见[定位尝试与102分叉时间线](定位尝试与102分叉时间线.md)。引用原盘点已封存证据,不重新搜索后把历史结果写成当前新证据。
五种未定位键仍保留,不在本轮Schema新增模块或删键。其他26种模块外键与Present覆盖不足保持专项未决。102从旧PF导入后经历装备、消费及收鱼,末次KV备份已分叉;本次本地PF102复测只是兼容回归,不是重新认定PF为当前权威,更不能把旧档写回。
## 8. 检查点及关闭条件
| 检查点 | 责任与交付 |
|---|---|
| B0 当前方案准备 | 完整设计、142引用锚点、定位/时间线附件、精确白名单、作者自审、提示词及受审hash;尚未编码 |
| B1 独立方案复核返回后 | 主会话核hash逐条裁决、明确批准版本;保存7个待改文件前态,记录Editor准入与配置/样本基线 |
| B2 红测与最小编码 | 仅7文件白名单;先记录预期红测,再修改4个生产文件;T1/T2及diff检查 |
| B3 验证 | T3~T8,记录当前UTC/版本/cfg指纹/原始输出,真实样本只读;发现第三问题登记,不自动扩大 |
| B4 作者交付 | 结果、首败、限制、最终diff与hash、独立实施复核提示词;更新共享文档 |
| B5 独立实施复核 | 主会话逐条裁决后关闭两类兼容;不关闭整体P2-α/G4,不推为101可正式启用 |
每个检查点更新开发日志和入口/TODO。若无阻塞可继续本已批准检查点,不反复征求同一授权。需要新增生产改动时交精确补充方案,不以“让101跑通”为理由顺手重构。
## 9. 作者自审
- 已完成上下文、三路线取舍、完整合同和测试设计;无视觉设计问题,无需视觉伴侣。既有用户约定确定目标,无需重复问选择;采用brainstorming内容方法,文档放既有upserver目录,不自动提交共享Git工作区。
- 接受根null与接受对象字段null严格分开;C# null/缺属性不是JSON null。M15新增旗标保持Raw存在性,M22所有其他值及深复制保留。
- 生产改动4个已有文件;测试复用3个已有文件和固定入口;不增加Schema框架、运行许可机制或迁移工具。
- 方案没有声称null会让所有玩法入口安全,没有批准101导入或修改当前102;报告附录中的未证明项仍是未证明。
- 审批对象为本方案及附件/白名单/基线;目前未执行B1~B5,待下一份独立方案报告。