docs(phase-07): lock 07-09 decisions - option A registry, delete attachAttribute, readonly attribute

Task 0 becomes a resolved decision record (options marked historical, locked results in <record>): registry moves into HeroAttribute with HeroState thin delegation and clone() carrying it; attachAttribute deleted from IHeroState/HeroState plus its sole test, attribute made readonly so loadState never rebinds; equipment modifiers excluded from the attribute save via addModifier(name, modifier, false); IHeroStateSave.attribute becomes IHeroAttributeSave<THero> with top-level modifiers removed (no compat branch); #06-17-1/#06-17-2 regressions authorized; #06-17-3..8 registered only.
This commit is contained in:
unanmed 2026-09-16 18:04:54 +08:00
parent c39f43c6da
commit 94f6a70e38

View File

@ -12,6 +12,7 @@ files_modified:
- packages-user/data-base/src/hero/equipment.ts
- packages-user/data-base/src/hero/attribute.test.ts
- packages-user/data-base/src/hero/saveLoad.test.ts
- packages-user/data-base/src/hero/state.test.ts
- packages-user/data-state/test/saveablesRoundTrip.test.ts
- packages-user/data-state/test/dataClosure.test.ts
autonomous: false
@ -26,32 +27,40 @@ must_haves:
truths:
- "读档后最终属性仍含装备加成:三档压缩下 `hero.getModifiableAttribute().getFinalAttribute('atk')` 等于读档前的值(`#06-17-1` 由 5 变 0 的缺陷消失)"
- "`hero.getModifiableAttribute()` 在读档前后是同一实例(`===` 成立),属性对象不再被 `loadState` 替换"
- "属性引用不可变:`HeroState.attribute` 为 `readonly`,`loadState` 中不出现对 `this.attribute` 的赋值;`attachAttribute` 已从 `IHeroState` 与 `HeroState` 完全删除(含其唯一用例),任何持有者都无法把绑定换到另一个属性实例,跨实例引用污染在类型层与实现层同时被排除"
- "`HeroEquipment` 构造期捕获的属性引用始终有效:读档后再 `equip`/`unequip` 产生的加成落在活属性上,`getFinalAttribute` 随 `add`/`set` 同步"
- "战斗侧:`enemyContext.getBindedHero()` 与 `hero.getModifiableAttribute()` 同实例,读档后读到的是活属性而非读档前属性(`#06-17-2`)"
- "`HeroAttribute` 自身实现 `ISaveableContent<IHeroAttributeSave<THero>>`:`saveState` 深拷贝基础值并枚举保存启用的修饰器状态,`loadState` 在自身实例上重置基础值、清空并重建修饰器、重算 final"
- "`HeroState.saveState` 委托 `this.attribute.saveState(...)`,`IHeroStateSave` 不再含顶层 `modifiers` 字段;`HeroState.loadState` 不再 `new HeroAttribute`,也不再给 `this.attribute` 赋值"
- "修饰器工厂注册表由 `HeroAttribute` 拥有(用户裁决选项 A):`IHeroAttribute` 提供 `registerModifier`/`createModifier`/`createAndInsertModifier`,`HeroAttribute.loadState` 用**自身注册表**重建修饰器(未命中静默跳过),`clone()` 把注册表条目复制给克隆对象,克隆体可独立存读档"
- "`HeroState.saveState` 委托 `this.attribute.saveState(...)`,`IHeroStateSave` 不再含顶层 `modifiers` 字段;`HeroState.loadState` 不再 `new HeroAttribute`,也不再给 `this.attribute` 赋值;`registerModifier`/`createModifier`/`createAndInsertModifier` 为对属性的薄委托,既有 `hero.registerModifier(...)` 调用点零改动(`saveLoad.test.ts:513/543`、`state.test.ts:134`、`saveablesRoundTrip.test.ts:115`、`saveablesReal.perf.ts:361`)"
- "装备修饰器不进入属性存档:即使其类型已在修饰器注册表中注册,三档压缩往返后 atk 增量也不翻倍"
- "`pnpm test:ci` 全绿,且本计划不新增任何 `it.skip`、不弱化任何既有断言"
artifacts:
- path: packages-user/data-base/src/hero/types.ts
provides: "新存档类型 `IHeroAttributeSave<THero>`(`values` + `modifiers`);`IHeroAttribute` 扩展 `ISaveableContent<IHeroAttributeSave<THero>>` 与修饰器工厂注册表成员;`IHeroStateSave.attribute` 改为 `IHeroAttributeSave<THero>` 并移除顶层 `modifiers`"
provides: "新存档类型 `IHeroAttributeSave<THero>`(`values` + `modifiers`);`IHeroAttribute` 扩展 `ISaveableContent<IHeroAttributeSave<THero>>` 并获得修饰器工厂注册表成员(`registerModifier`/`createModifier`/`createAndInsertModifier`);`IHeroStateSave.attribute` 改为 `IHeroAttributeSave<THero>` 并移除顶层 `modifiers`;`IHeroState` **删除** `attachAttribute`"
- path: packages-user/data-base/src/hero/attribute.ts
provides: "修饰器工厂注册表(按 Task 0 裁决归属);`saveState` 深拷贝基础值 + 保存启用修饰器枚举;`loadState` 原地重置基础值、清空并重建修饰器、重算 final(实例身份不变)"
provides: "**拥有**修饰器工厂注册表(用户裁决选项 A,`private readonly registry`);`saveState` 深拷贝基础值 + 保存启用修饰器枚举;`loadState` 用自身注册表原地重置基础值、清空并重建修饰器、重算 final(实例身份不变);`clone()` 复制注册表条目"
- path: packages-user/data-base/src/hero/state.ts
provides: "`saveState` 委托属性存档、`loadState` 委托属性原地读档且不再替换 `this.attribute`;`registerModifier`/`createModifier`/`createAndInsertModifier` 委托属性注册表"
provides: "`saveState` 委托属性存档、`loadState` 委托属性原地读档且不再替换 `this.attribute`;`attribute` 改为 `readonly`;**删除** `attachAttribute`;`registerModifier`/`createModifier`/`createAndInsertModifier` 薄委托到属性注册表"
- path: packages-user/data-base/src/hero/equipment.ts
provides: "`loadEquipEffect` 以 `save = false` 挂载装备修饰器,使装备加成只经 `HeroEquipment.loadState` 的重新装备恢复"
- path: packages-user/data-base/src/hero/attribute.test.ts
provides: "`HeroAttribute` 自身存读档的原地性、三档恢复、存档禁用修饰器与实例身份回归"
- path: packages-user/data-base/src/hero/saveLoad.test.ts
provides: "`#06-17-1` 三档回归(读档后装备加成仍在、属性对象同实例、读档后再装备仍作用于活属性)与存档形状迁移后的既有断言"
- path: packages-user/data-base/src/hero/state.test.ts
provides: "删除 `attachAttribute` 用例(该能力已按用户裁决移除);保留 `exposes shared, readonly and isolated attribute views` 与注册表委托用例(码 116)"
- path: packages-user/data-state/test/dataClosure.test.ts
provides: "`#06-17-2` 回归:顶层 `CoreState` 经 `hero.loadState` 后 `enemyContext.getBindedHero()` 仍与活属性同实例且反映读档值"
key_links:
- "`HeroState.attribute` 的 `readonly` 约束(`state.ts:55`)↔ `HeroState.loadState` 的 `this.attribute.loadState(state.attribute, compression)`(原地)——没有赋值语句,属性实例无法被换绑"
- "`HeroState.loadState` 的 `this.attribute.loadState(...)`(原地)↔ `HeroEquipment` 构造期捕获的 `attribute`(`equipment.ts:21-23`、`state.ts:71`)——同实例即同引用,装备加成不再落到孤儿属性"
- "修饰器工厂注册表(`HeroAttribute.registry`,由 `state.ts:41-44` 原样搬入)↔ `HeroAttribute.loadState` 的修饰器重建(按 `save.type` 取工厂、`cons()`、`modifier.loadState(save.state, compression)`)——属性存读档不需要任何外部注入,自洽"
- "`IHeroAttribute` 新增的 `registerModifier`/`createModifier`/`createAndInsertModifier` ↔ `HeroState` 的同名薄委托(`state.ts:106-130`)——`IHeroState` 公开签名不变,既有 `hero.registerModifier(...)` 调用点(含零断言 perf 文件)零改动"
- "`HeroAttribute.clone()` 的注册表条目复制 ↔ `getIsolatedAttribute()` / `compareEquip()` 的克隆属性——克隆体持有同一套注册记录,克隆属性上的存读档同样自洽"
- "`IHeroState` 删除 `attachAttribute`(`types.ts:825-829`)↔ `HeroState` 删除其实现(`state.ts:88-92`)↔ `state.test.ts:118-127` 用例删除——能力不存在,也就无从换绑"
- "`HeroAttribute.saveState` 的 `getModifierSaveEnabled` 过滤(原 `state.ts:150-157`)↔ `loadEquipEffect` 的 `addModifier(name, modifier, false)`(`equipment.ts:83`)——装备修饰器排除在属性存档外"
- "`HeroState.loadState` 属性 → `items.loadState` → `equip.loadState`(`state.ts:186-189`)的调用序 ↔ `equipment.ts:336-346` 清空 equips 后重新 `equip`,装备修饰器在属性读档之后重新施加到同一实例"
- "修饰器工厂注册表(现 `state.ts:41-44`)↔ `HeroAttribute.loadState` 的修饰器重建(按 `save.type` 取工厂、`cons()`、`modifier.loadState(save.state, compression)`)"
- "`EnemyContext.bindedHero`(`data-system/src/combat/context.ts:87`,`core.ts:219` 一次性 `bindHero(heroAttribute)`)↔ 原地读档后属性实例不变"
- "`IHeroStateSave.attribute` 的新形状 ↔ `saveablesRoundTrip.test.ts:254` 的快照分离断言(`heroSave.attribute.values.hp`)与 `saveLoad.test.ts:549` 的存档禁用断言(`saved.attribute.modifiers`)"
assumptions:
@ -59,12 +68,15 @@ must_haves:
- "A2(项目未发布、无向后兼容要求,与 07-03 的 A10 同口径):`IHeroStateSave` / `IHeroAttributeSave` 形状可自由变更,不保留任何旧形状兼容分支,不新增存档版本字段"
- "A3(同引用原则,用户已裁决):`HeroAttribute` 在自身实例上完成存读档,使 `HeroEquipment.attribute` 与 `EnemyContext.bindedHero` 这类长生命周期引用跨读档始终有效;A/B 两缺陷同根因、同修"
- "A4(装备加成归属装备子系统):装备修饰器由 `HeroEquipment.loadState` 重新装备恢复,不进属性存档,与 `equipment.ts:341` 既有注释「装备修饰器不进存档」一致;这是 A/B 修复后不产生重复计入的必要条件"
- "A5(相邻缺口,只登记不修):`attachAttribute`(`state.ts:90-92`)仍会替换属性实例、从而再次让 `HeroEquipment` 的捕获引用脱钩;本计划只登记"
- "A6(相邻缺口,只登记不修):`HeroAttribute.clone()`(`attribute.ts:257-271`)未把 `modifierName` / `modifierNosave` 复制到克隆对象,克隆属性上的 `getModifierIndex` / `markModifierDirty` 对克隆修饰器不生效"
- "A7(注册表归属为 Task 0 裁决项):Task 1–3 的 action 以【选项 A】为默认写法;若用户裁决为 B 或 C,按 Task 0 `<record>` 中逐条列出的差异执行,其余步骤不变"
- "A5(用户裁决:删除而非登记):`attachAttribute`(`types.ts:825-829` 声明 + `state.ts:88-92` 实现)会替换属性实例、让 `HeroEquipment` 的捕获引用脱钩;用户裁决**彻底删除该能力**,并同时把 `HeroState.attribute` 改为 `readonly`,使换绑在实现层不可能发生;其唯一用例 `state.test.ts:118-127` 一并删除(相邻的 `getIsolatedAttribute` 用例 `:104-116` 保留)"
- "A6(相邻缺口,只登记不修):`HeroAttribute.clone()`(`attribute.ts:257-271`)按 A7 需复制注册表条目,但仍**不**复制 `modifierName` / `modifierNosave`,克隆属性上的 `getModifierIndex` / `markModifierDirty` 对克隆修饰器不生效;本计划只登记,不顺手扩大修复面"
- "A7(注册表归属,用户已裁决为选项 A,无差异分支):修饰器工厂注册表**移入 `HeroAttribute`**(`private readonly registry`),`IHeroAttribute` 新增 `registerModifier`/`createModifier`/`createAndInsertModifier`(含既有 `logger.warn(116)` 未知类型分支),`HeroState` 的三个同名方法退化为薄委托(`IHeroState` 公开签名不变,既有调用点零改动);`HeroAttribute.loadState` 用**自身注册表**重建修饰器;`clone()` 复制注册表条目。Task 1–3 一律按本口径书写,不存在 B/C 分支"
- "A8(只读绑定,用户已裁决):`HeroState.attribute` 由构造器可写属性改为 `readonly`,`HeroState.loadState` 只调用 `this.attribute.loadState(...)`、永不出现 `this.attribute = ...`;删除 `attachAttribute` 后全仓无任何生产/测试代码依赖换绑能力(删除前唯一使用者是 `state.test.ts:118-127`)"
prohibitions:
- "不修复 C–H(`#06-17-3` replay `setReplayArray` 漏 `expireStreams`、`#06-17-4` `equipStore` 重建实例脱钩、`#06-17-5` `flag/system` 重建字段脱钩、`#06-17-6` followers 重建脱钩、`#06-17-7` `data-fallback` hero Proxy、`#06-17-8` `mapLayer.setMapRef` 换缓冲):这六条对应文件一行不动,只在 SUMMARY 登记"
- "不改 `data-system` 的战斗生产代码:不重绑 `EnemyContext.bindedHero`、不加 `DamageSystem`/`CombatFlow` 的重新绑定钩子——B 靠同引用原则自然成立"
- "不重新引入 `attachAttribute`:`IHeroState` 的声明(`types.ts:825-829`)、`HeroState` 的实现(`state.ts:88-92`)与其唯一用例(`state.test.ts:118-127`)一律删除;不保留 deprecated 标记、别名或改名版本(如 `setAttribute` / `useAttribute`),也不新增任何等价的换绑入口"
- "不给 `this.attribute` 赋值:`attribute` 声明为 `readonly` 后,`loadState` 中不得出现 `this.attribute = ...`,也不得用 `Object.assign` / `Reflect.set` / 类型断言等手段绕过只读约束"
- "不修改渲染端 `@user/client-*`、不改 legacy 渲染接线、不改 `packages-user/data-fallback`"
- "不引入新依赖、不新建文件、不新增错误码、不新增 warn/error 调用(未知修饰器类型沿用既有静默跳过语义)"
- "不弱化断言、不改写为可跑绿假象;不新增 `it.skip`;除存档形状迁移导致的两处字段路径外,既有断言一字不改"
@ -80,13 +92,13 @@ must_haves:
Purpose: 属性是勇士数值的唯一事实源,装备、Buff、战斗计算都通过它读取;实例被替换会让「读档后数值正确性」取决于谁持有引用,任何绕过 getter 的持有者都会静默读到过期数据。同引用原则把这类缺陷从根因上消除,而不是逐个补重绑调用点。
Output: `hero/types.ts` 新增 `IHeroAttributeSave<THero>` 并调整 `IHeroStateSave` 形状(顶层 `modifiers` 合并进属性存档);`hero/attribute.ts` 实现自身存读档(原地);`hero/state.ts` 改为委托且不再替换属性;`hero/equipment.ts` 令装备修饰器不进属性存档;`#06-17-1` / `#06-17-2` 的回归用例(三档压缩 + 战斗侧同引用);Phase 7 重开登记与 C–H 未修登记。
Output: `hero/types.ts` 新增 `IHeroAttributeSave<THero>` 并调整 `IHeroStateSave` 形状(顶层 `modifiers` 合并进属性存档)、`IHeroAttribute` 获得注册表成员、`IHeroState` 删除 `attachAttribute`;`hero/attribute.ts` **拥有**修饰器工厂注册表并实现自身存读档(原地,注册表随 `clone()` 传递);`hero/state.ts` 改为薄委托、`attribute` 变 `readonly` 且不再替换实例、删除 `attachAttribute`;`hero/equipment.ts` 令装备修饰器不进属性存档;`#06-17-1` / `#06-17-2` 的回归用例(三档压缩 + 战斗侧同引用);Phase 7 重开登记与 C–H 未修登记。
**本计划重开已标记 `Complete` 的 Phase 7**:`07-VERIFICATION.md`(2026-09-16)在 07-09 执行前即已失效,执行后必须重跑 `/gsd-verify-work`(或 `gsd-verify-work 7`)重新出具验证结论;`ROADMAP.md` 的 Phase 7 计划数、Wave 列表与进度表已同步标注为未完成。
执行序:本计划在 07-08 之后串行执行(`depends_on: 07-08`,对应 Wave 9)。`depends_on` 表示 D-09 逐步用户确认与 D-12/D-44 全量门禁的串行约束,源码层面与 path 系统无耦合。Task 0 为 D-09 预执行汇报关卡并承载两项待裁决事项,未获用户确认前不得进入 Task 1。
执行序:本计划在 07-08 之后串行执行(`depends_on: 07-08`,对应 Wave 9)。`depends_on` 表示 D-09 逐步用户确认与 D-12/D-44 全量门禁的串行约束,源码层面与 path 系统无耦合。Task 0 为 D-09 预执行汇报关口,其待裁决事项已由用户全部裁决并锁入 Task 0 `<record>`(选项 A + 装备修饰器不入属性存档 + 删除 `attachAttribute` 并改 `readonly` + 形状变更确认 + 回归用例授权)——执行者据该记录直接进入 Task 1,不再二次询问。
Phase 7 裁决对应(逐条引用):D-01: 本计划是 Phase 7 第 9 个计划,承接 07-LOADSTATE-AUDIT 的 `#06-17-1`(A)/`#06-17-2`(B)。D-02: 属 hero 系统(`packages-user/data-base/src/hero`)。D-03: 其它系统契约裁决,不涉及。D-04: 其它系统契约裁决,不涉及。D-05: 其它系统契约裁决,不涉及。D-06: 其它系统契约裁决,不涉及。D-07: 其它系统契约裁决,不涉及。D-08: 其它系统契约裁决,不涉及。D-09: 由 Task 0 的预执行汇报关卡承载。D-10: 本计划为其唯一例外(A1,经用户授权为 `#06-17-1`/`#06-17-2` 新增回归用例)。D-11: 形状变更后同步 jsDoc 与 `equipment.ts` 注释。D-12: 沿用 06-CONTEXT D-44 的文件级三步门禁。D-13: 按根因族原子提交(Task 1 / Task 2 / Task 3)。D-14: `#06-17-1`/`#06-17-2` 无 WINDOWS.md 条目,不新建、只登记对照。
Phase 7 裁决对应(逐条引用):D-01: 本计划是 Phase 7 第 9 个计划,承接 07-LOADSTATE-AUDIT 的 `#06-17-1`(A)/`#06-17-2`(B)。D-02: 属 hero 系统(`packages-user/data-base/src/hero`)。D-03: 其它系统契约裁决,不涉及。D-04: 其它系统契约裁决,不涉及。D-05: 其它系统契约裁决,不涉及。D-06: 其它系统契约裁决,不涉及。D-07: 其它系统契约裁决,不涉及。D-08: 其它系统契约裁决,不涉及。D-09: 由 Task 0 的预执行汇报关口承载,用户已裁决并锁入 `<record>`。D-10: 本计划为其唯一例外(A1,经用户授权为 `#06-17-1`/`#06-17-2` 新增回归用例)。D-11: 形状变更后同步 jsDoc 与 `equipment.ts` 注释。D-12: 沿用 06-CONTEXT D-44 的文件级三步门禁。D-13: 按根因族原子提交(Task 1 / Task 2 / Task 3)。D-14: `#06-17-1`/`#06-17-2` 无 WINDOWS.md 条目,不新建、只登记对照。
</objective>
<execution_context>
@ -120,108 +132,120 @@ Phase 7 既有裁决与本计划的对应关系(逐条引用,便于 D-01..D-
- **D-01**(修复范围):本计划是 Phase 7 的第 9 个计划,承接 07-LOADSTATE-AUDIT 登记的 `#06-17-1`(A)与 `#06-17-2`(B);20 条原始清单不受影响。
- **D-02**(按系统切分 PLAN):本计划属 hero 系统(`packages-user/data-base/src/hero`),是既有 8 个系统计划的补充计划。
- **D-03 / D-04 / D-05 / D-06 / D-07 / D-08**(其它系统的契约裁决:combat 战前脚本语义、map 码 128、save 码 178、hero 码 147 保留、path 用户接线、enemy 复用映射):本计划不涉及,相关文件一行不动。
- **D-09**(每个计划执行前向用户汇报方案并获确认;失败即退出并修订计划):由 Task 0 的 `checkpoint:decision` 承载,未确认前不得进入 Task 1。
- **D-10**(测试处置:只取消 skip、不额外新增用例):本计划的唯一例外为 A1——经用户显式授权为 `#06-17-1` / `#06-17-2` 新增回归用例(Task 0 汇报第 ⑥ 条待确认);C–H 一律不新增用例。
- **D-09**(每个计划执行前向用户汇报方案并获确认;失败即退出并修订计划):由 Task 0 的 `checkpoint:decision` 承载;用户已裁决全部待定项并锁入其 `<record>`,执行者据记录直接进入 Task 1,不再询问。
- **D-10**(测试处置:只取消 skip、不额外新增用例):本计划的唯一例外为 A1——经用户显式授权为 `#06-17-1` / `#06-17-2` 新增回归用例(Task 0 `<record>` 第 10 条);C–H 一律不新增用例。
- **D-11**(文档/注释同步):`IHeroStateSave` / `IHeroAttributeSave` 形状变更后同步 `types.ts` 的 jsDoc 与 `equipment.ts:341` 的注释,使文档与实现一致。
- **D-12**(门禁沿用 06-CONTEXT D-44 的文件级三步):每个任务收尾执行 eslint / vue-tsc(按文件过滤)/ `pnpm test:ci`。
- **D-13**(按缺陷/根因原子提交):Task 1 属性自持存读档、Task 2 `#06-17-1` 根因闭合、Task 3 `#06-17-2` 回归与门禁各成原子提交。
- **D-14**(WINDOWS.md 收口):`#06-17-1` / `#06-17-2` 在 `WINDOWS.md` 中**无条目**(07-LOADSTATE-AUDIT 与 06-TEST-FINDINGS 均未登记账本 id),故不新建条目、不做 `fixed`/`waive` 动作,改为在 SUMMARY 记录 findings↔ledger 对照。
- **06-CONTEXT D-43**(阶段化):本计划按 4 个阶段化任务推进(裁决记录 → 属性自持存读档 → 容器委托与装备归档 → 战斗侧回归与门禁),前序未完成不得进入后续。
- **06-CONTEXT D-43**(阶段化):本计划按 4 个阶段化任务推进(裁决记录 → 属性自持存读档 + 注册表归属性 → 容器薄委托与绑定只读、删除 `attachAttribute`、装备归档 → 战斗侧回归与门禁),前序未完成不得进入后续。
- **06-CONTEXT D-44**:门禁与验证节奏的判定口径(见 D-12 行)。
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 0: D-09 预执行汇报 —— 同引用原则修复方案 + 修饰器注册表归属裁决 + 装备修饰器是否进属性存档</name>
<name>Task 0: D-09 裁决记录(用户已裁决:选项 A + 装备修饰器不入属性存档 + 删除 `attachAttribute` 并改 `readonly` + 形状变更确认)—— 同引用原则修复方案</name>
<files>(只读汇报,不修改任何文件)</files>
<read_first>
- packages-user/data-base/src/hero/attribute.ts(:48-63 私有字段与构造器、:159-251 修饰器处理与存取开关、:257-279 克隆与结构化输出)
- packages-user/data-base/src/hero/state.ts(:41-44 注册表、:71 装备接线、:90-92 attachAttribute、:106-130 注册表方法、:148-195 saveState/loadState)
- packages-user/data-base/src/hero/equipment.ts(:21-26 捕获属性、:76-96 loadEquipEffect/unloadEquipEffect、:190 装备时施加、:336-346 loadState)
- packages-user/data-base/src/hero/state.ts(:41-44 现注册表、:55 `attribute` 构造器属性、:71 装备接线、:88-92 属性相关 region 与 attachAttribute、:106-130 注册表方法、:148-195 saveState/loadState)
- packages-user/data-base/src/hero/types.ts(:786-801 `IHeroStateSave`、:803-874 `IHeroState` 含 `:825-829` attachAttribute 声明)
- packages-user/data-base/src/hero/equipment.ts(:21-26 捕获属性、:76-96 loadEquipEffect/unloadEquipEffect、:190 装备时施加、:336-346 loadState 与 :341 注释)
- packages-user/data-base/src/hero/state.test.ts(:104-116 保留的 getIsolatedAttribute 用例、:118-127 待删除的 attachAttribute 用例、:130-148 注册表委托用例)
- .planning/phases/07-data-fixes/07-LOADSTATE-AUDIT.md(A / B 行、触发序列、处置)
- packages-user/data-common/src/save/types.ts(:12-25 ISaveableContent 契约)
</read_first>
<action>
按 D-09,执行前向用户汇报修复方案与两项待裁决事项,获确认后才进入 Task 1。汇报内容如下(**历史记录 + 待裁决**):
以下为 D-09 预执行汇报稿(**历史记录**:本检查点的全部待定项均已由用户裁决,锁定结果见本任务 `<record>` 与 `<decision>`;执行者据此直接进入 Task 1,不再重新询问、不得自行改选)。
① **根因与修法(同引用原则,用户已裁决方向)**:`HeroState.loadState` 目前 `new HeroAttribute(...)` 替换实例(`:177/185`),而 `HeroEquipment` 构造期以 `private readonly` 捕获旧实例(`:21-23`、`state.ts:71`)、`core.ts:219` 又一次性把旧实例绑给 `enemyContext`。修法为**让属性自己在原实例上存读档**:`HeroAttribute<THero> implements ISaveableContent<IHeroAttributeSave<THero>>`,`loadState` 原地重置基础值 → 清空并重建修饰器 → 重算 final;`HeroState.loadState` 改为调用 `this.attribute.loadState(state.attribute, compression)` 且**不再给 `this.attribute` 赋值**。所有长生命周期引用因此天然有效,不需要任何重绑调用点。同时修复 A(`#06-17-1`)与 B(`#06-17-2`)。
② **存档形状变更(A2,项目未发布,无向后兼容要求)**:新增 `IHeroAttributeSave<THero> = { values: THero; modifiers: readonly IModifierStateSave<THero>[] }`;`IHeroStateSave.attribute` 由 `THero` 改为 `IHeroAttributeSave<THero>`,并删除顶层 `modifiers`(即把现在的 `attribute` + `modifiers` 两个字段合并到属性自己的存档里)。不保留旧形状兼容分支、不新增存档版本字段。**这是持久化格式变更**,请确认。
② **存档形状变更(A2,项目未发布,无向后兼容要求)—— 用户已确认**:新增 `IHeroAttributeSave<THero> = { values: THero; modifiers: readonly IModifierStateSave<THero>[] }`;`IHeroStateSave.attribute` 由 `THero` 改为 `IHeroAttributeSave<THero>`,并删除顶层 `modifiers`(即把现在的 `attribute` + `modifiers` 两个字段合并到属性自己的存档里)。**用户已确认该破坏性变更**(`<record>` 第 9 条):不保留任何旧形状兼容分支、不新增存档版本字段,对仓外已存在的旧存档不承诺可读。
③ **【待裁决 1,关键】修饰器工厂注册表归属**。现状:注册表在 `HeroState`(`state.ts:41-44`,`Map<string, () => IHeroModifier>`),`registerModifier`/`createModifier`/`createAndInsertModifier`(`:106-130`)属于 `IHeroState`;`HeroAttribute.loadState` 现在也需要它来重建修饰器。三个选项:
- **选项 A(推荐)**:把注册表移入 `HeroAttribute`(`private readonly registry`),`IHeroAttribute` 新增 `registerModifier` / `createModifier` / `createAndInsertModifier`(含既有 `logger.warn(116)` 未知类型分支,原样搬运),`HeroState` 的三个方法**委托**给 `this.attribute`,`IHeroState` 的公开签名不变(既有调用点 `state.hero.registerModifier(...)` 全部不受影响)。理由:与「属性拥有自己的修饰器」一致,属性存读档自洽,不需要跨对象传工厂。已知代价:`IHeroAttribute` 接口变大;`clone()` 需把注册表条目复制给克隆对象;`attachAttribute` 换上无注册表的新属性时需要决定是否转移注册记录(见 ④)。
- **选项 B**:注册表仍由 `HeroState` 拥有,通过构造参数或 `useRegistry(...)` 注入到 `HeroAttribute`(共享同一个 Map),`clone()` 传递同一引用;`attachAttribute` 后由 `HeroState` 重新注入。理由:注册动作的唯一入口仍在 `HeroState`;代价:同一 Map 有两个写入方(`HeroState` 注册 / 属性读取),属性离开 `HeroState` 后注册表为空(脱离宿主即不可用),`attachAttribute` 需额外接线才不空。
- **选项 C**:注册表留在 `HeroState`、`HeroAttribute` 只负责值状态:`IHeroAttribute.loadState(state, compression, factory?)` 增加一个可选的修饰器工厂解析参数(保持与 `ISaveableContent` 结构兼容),由 `HeroState.loadState` 传入解析器;**或**退化为「`HeroState` 驱动重建 + 属性提供原地原语」。理由:改动面最小、注册表归属不变;代价:**偏离用户已锁定的「属性自身存读档」方向**——单独调用 `attribute.loadState(state, compression)`(不传工厂)时修饰器不会恢复,属性实例的存读档不自洽,需要在文档中明写该限定。
③ **【原待裁决 1,关键】修饰器工厂注册表归属 —— 用户已裁决为「选项 A」**。现状:注册表在 `HeroState`(`state.ts:41-44`,`Map<string, () => IHeroModifier>`),`registerModifier`/`createModifier`/`createAndInsertModifier`(`:106-130`)属于 `IHeroState`;`HeroAttribute.loadState` 现在也需要它来重建修饰器。三个历史选项:
- **选项 A(【已选】)**:把注册表移入 `HeroAttribute`(`private readonly registry`),`IHeroAttribute` 新增 `registerModifier` / `createModifier` / `createAndInsertModifier`(含既有 `logger.warn(116)` 未知类型分支,原样搬运),`HeroState` 的三个方法**薄委托**给 `this.attribute`,`IHeroState` 的公开签名不变(既有调用点 `state.hero.registerModifier(...)` 全部不受影响)。理由:与「属性拥有自己的修饰器」一致,属性存读档自洽,不需要跨对象传工厂。已知代价:`IHeroAttribute` 接口变大;`clone()` 需把注册表条目复制给克隆对象。**复核补充事实**:`registerModifier` 今日**无任何生产调用者**,仅测试与零断言 perf 文件使用(`saveLoad.test.ts:513/543`、`state.test.ts:134`、`saveablesRoundTrip.test.ts:115`、`saveablesReal.perf.ts:361`),故薄委托足以让全部调用点零改动。
- **选项 B(未选,历史选项)**:注册表仍由 `HeroState` 拥有,通过构造参数或 `useRegistry(...)` 注入到 `HeroAttribute`(共享同一个 Map),`clone()` 传递同一引用;`attachAttribute` 后由 `HeroState` 重新注入。代价:同一 Map 有两个写入方,属性离开 `HeroState` 后注册表为空(脱离宿主即不可用),`attachAttribute` 需额外接线才不空。
- **选项 C(未选,历史选项)**:注册表留在 `HeroState`、`HeroAttribute` 只负责值状态:`IHeroAttribute.loadState(state, compression, factory?)` 增加一个可选的修饰器工厂解析参数(保持与 `ISaveableContent` 结构兼容),由 `HeroState.loadState` 传入解析器;**或**退化为「`HeroState` 驱动重建 + 属性提供原地原语」。代价:偏离「属性自身存读档」方向——单独调用 `attribute.loadState(state, compression)`(不传工厂)时修饰器不会恢复,属性实例的存读档不自洽。
④ **选项 A 下的 `attachAttribute` 子问题(若选 A 需一并答复)**:`attachAttribute`(`state.ts:90-92`)会替换属性实例,替换后新属性注册表为空、`createModifier` 会报 116。两种处置:(i) `HeroState` 保留一份「注册记录」并在 `attachAttribute` 时重放到新属性(代价:注册表信息两处登记);(ii) 明确 `attachAttribute` 为遗留逃生口、不转移注册表,并把它登记为相邻缺口(A5),由后续计划决定是否废弃该方法。**默认采用 (ii)**;注意 `attachAttribute` 本身仍会破坏 `HeroEquipment` 的捕获引用,本计划不修(A5)。
④ **【原选项 A 的子问题】`attachAttribute` 处置 —— 用户裁决超出 (i)/(ii) 两案:彻底删除该能力**。原两案为 (i) `HeroState` 保留「注册记录」并在 `attachAttribute` 时重放到新属性(代价:注册表信息两处登记);(ii) 明确其为遗留逃生口、不转移注册表并登记为相邻缺口。用户裁决:**删除 `attachAttribute`**(`IHeroState` 声明 `types.ts:825-829` + `HeroState` 实现 `state.ts:88-92`,全仓复核无其它使用者),同时把 `HeroState.attribute`(现为构造器可写属性 `state.ts:55` 的 `public attribute: IHeroAttribute<THero>`)改为 **`readonly`**,`HeroState.loadState` 只调用 `this.attribute.loadState(...)`、永不重绑;其唯一使用者 `state.test.ts:118-127`(`replaces the bound attribute through attachAttribute`,含 `:118` 的注释)**删除**,相邻的 `getIsolatedAttribute` 用例 `:104-116` 保留。由此 (i)/(ii) 之争以及「新属性注册表为空会报 116」「换绑破坏 `HeroEquipment` 捕获引用」两个衍生问题一并消失(原 A5 缺口随删除而关闭),注册表归属也不再有两处登记的代价。
⑤ **【待裁决 2】装备修饰器是否进属性存档**。现状:`loadEquipEffect` 以 `addModifier(name, modifier, true)`(`equipment.ts:83`)挂载装备修饰器,因此它们**会**进入 `state.modifiers` 存档;读档时若其类型不在注册表(生产环境注册表为空)则被静默跳过,再由 `equip.loadState` 重新装备恢复,表面上无重复。**但原地读档后两侧会落到同一属性实例上**:若某个装备修饰器类型恰好已在注册表中注册(例如 `@system/value`、`@system/percentage`,测试夹具 `saveablesReal.perf.ts:361` 就同时注册了 `@system/value` 并装备了 4 件装备),属性读档会重建一份、`equip.loadState` 再挂一份,加成**翻倍**。修法(推荐):把 `loadEquipEffect` 的第三个实参由 `true` 改为 `false`,使装备修饰器不进属性存档——这与 `equipment.ts:341` 既有注释「装备修饰器不进存档」一致,且装备子系统有自己的存档负责恢复。请确认;若否决,需另选去重方案(本计划不建议)。
⑤ **【原待裁决 2】装备修饰器是否进属性存档 —— 用户已裁决为「不进属性存档」**。现状:`loadEquipEffect` 以 `addModifier(name, modifier, true)`(`equipment.ts:83`)挂载装备修饰器,因此它们**会**进入 `state.modifiers` 存档;读档时若其类型不在注册表(生产环境注册表为空)则被静默跳过,再由 `equip.loadState` 重新装备恢复,表面上无重复。**但原地读档后两侧会落到同一属性实例上**:若某个装备修饰器类型恰好已在注册表中注册(例如 `@system/value`、`@system/percentage`——测试夹具 `saveablesReal.perf.ts:361` 就同时注册了 `@system/value` 并装备了 4 件装备),属性读档会重建一份、`equip.loadState` 再挂一份,加成**翻倍**。修法:把 `loadEquipEffect` 的第三个实参由 `true` 改为 `false`,使装备修饰器不进属性存档——这与 `equipment.ts:341` 既有注释「装备修饰器不进存档」一致,装备子系统有自己的存档负责恢复。**用户已裁决同意该修法**(`<record>` 第 8 条),不采用任何替代去重方案。
⑥ **回归覆盖(A1 授权,D-10 例外)**:新增 `#06-17-1` 三档回归(读档后 `atk` 仍含装备加成、`getModifiableAttribute()` 同实例、读档后再装备仍作用于活属性)、「注册表命中不翻倍」对照、`#06-17-2` 战斗侧回归(`enemyContext.getBindedHero()` 与活属性同实例并反映读档值)。这是修复计划,测试为必需项;不对 C–H 新增用例。
⑥ **回归覆盖(A1 授权,D-10 例外)—— 用户已授权**(`<record>` 第 10 条):新增 `#06-17-1`(装备修饰器跨 `hero.loadState` 存活于全部三档压缩;`getModifiableAttribute()` 读档前后为**同一实例**)、「注册表命中不翻倍」对照、`#06-17-2`(`enemyContext.getBindedHero()` 读档后仍是活属性、**无需任何重绑**)回归。这是修复计划,测试为必需项;不对 C–H 新增用例。
⑦ **不做的事(范围守卫)**:C–H(`#06-17-3..8`)只登记不修;不改 `data-system` 战斗代码(不重绑 `bindedHero`);不改渲染端与 `data-fallback`;不新建文件、不新增依赖、不新增错误码。
⑦ **不做的事(范围守卫)**:C–H(`#06-17-3..8`)只登记不修(replay `setReplayArray` / `flag/system` / `equipStore` / followers / `mapLayer` / `data-fallback` hero Proxy 对应文件一行不动);不改 `data-system` 战斗代码(不重绑 `bindedHero`);不改渲染端与 `data-fallback`;不新建文件、不新增依赖、不新增错误码。
⑧ **失败纪律(D-09)**:任一任务方案失败必须退出本次修改、修订本 PLAN.md 后重新执行,不得自行另辟他法。
**待用户答复**:③(A/B/C 与 ④ 的 (i)/(ii))、⑤(同意/否决)、②(存档形状变更确认)、⑥(新增回归用例授权确认)。答复后写入下方 `<record>` 并直接进入 Task 1。
**裁决状态(已锁定)**:③④⑤②⑥ 全部已由用户裁决并写入下方 `<record>`;执行者据 `<record>` 直接进入 Task 1,不再询问、不得自行改选。
</action>
<decision>同引用原则修复方案的范围与两项裁决:修饰器工厂注册表归属(A/B/C)与装备修饰器是否排除出属性存档(推荐排除)</decision>
<decision>同引用原则修复方案的锁定范围(D-09 已由用户全部裁决):修饰器工厂注册表**移入 `HeroAttribute`**(选项 A)+ `HeroEquipment` 的装备修饰器**不进属性存档** + **删除 `attachAttribute` 并把 `HeroState.attribute` 改为 `readonly`** + 存档形状破坏性变更确认 + 授权 `#06-17-1`/`#06-17-2` 回归用例</decision>
<context>
`#06-17-1`(A,高)与 `#06-17-2`(B,高)同根因:`HeroState.loadState` 替换属性实例,而 `HeroEquipment`(`private readonly`)与 `EnemyContext`(一次性 `bindHero`)持有旧实例。用户已裁决方向为「同引用原则」:`HeroAttribute` 自身实现 `ISaveableContent`,存读档在自身实例上原地完成。剩余待定的是注册表归属(决定 `IHeroAttribute` 接口是否变大、`attachAttribute` 如何处理)与装备修饰器的存档归属(决定是否出现重复计入)。两者都影响接口/存档内容,属用户主导的接口设计范畴,故在执行前裁决。
`#06-17-1`(A,高)与 `#06-17-2`(B,高)同根因:`HeroState.loadState` 替换属性实例,而 `HeroEquipment`(`private readonly`)与 `EnemyContext`(一次性 `bindHero`)持有旧实例。用户已裁决方向为「同引用原则」:`HeroAttribute` 自身实现 `ISaveableContent`,存读档在自身实例上原地完成。执行前待定的三项分别是注册表归属(决定 `IHeroAttribute` 接口是否变大)、`attachAttribute` 的存废(决定换绑入口是否保留)与装备修饰器的存档归属(决定是否重复计入)——均属用户主导的接口/仓储设计范畴,用户已逐条裁决:**注册表归 `HeroAttribute`(选项 A)**、**彻底删除 `attachAttribute` 并把 `attribute` 改为 `readonly`**、**装备修饰器不进属性存档**;并确认存档形状的破坏性变更、授权两项缺陷的回归用例。
</context>
<options>
<option id="A+exclude-equip">
<name>【推荐】注册表移入 `HeroAttribute`(选项 A,(ii))+ 装备修饰器不进属性存档 + 存档形状变更确认 + 授权回归用例</name>
<pros>属性存读档自洽(`attribute.loadState` 不需任何外部注入即可重建修饰器);`IHeroState` 公开签名不变、既有调用点零改动;彻底消除装备加成重复计入;`attachAttribute` 的相邻缺口如实登记</pros>
<cons>`IHeroAttribute` 接口新增三个成员;`clone()` 需复制注册表条目;`attachAttribute` 后新属性注册表为空(登记为缺口);存档形状破坏性变更(项目未发布,可接受)</cons>
<name>【已选】注册表移入 `HeroAttribute`(选项 A)+ 删除 `attachAttribute` 且 `attribute` 只读 + 装备修饰器不进属性存档 + 存档形状变更确认 + 授权回归用例</name>
<pros>属性存读档自洽(`attribute.loadState` 不需任何外部注入即可重建修饰器);`IHeroState` 公开签名不变、既有调用点零改动;属性绑定在类型层(`readonly`)与实现层(无 `attachAttribute`)双双不可变,跨实例引用污染被根除;彻底消除装备加成重复计入</pros>
<cons>`IHeroAttribute` 接口新增三个成员;`clone()` 需复制注册表条目;删除 `attachAttribute` 属公开 API 移除(全仓仅一处测试使用);存档形状破坏性变更(项目未发布,可接受)</cons>
</option>
<option id="B+exclude-equip">
<name>注册表注入 `HeroAttribute`(选项 B,(ii))+ 装备修饰器不进属性存档</name>
<name>(历史选项,未选)注册表注入 `HeroAttribute`(选项 B)+ 装备修饰器不进属性存档</name>
<pros>注册入口仍集中在 `HeroState`;接口仅新增注册表注入点</pros>
<cons>同一 Map 双写入方;属性脱离 `HeroState` 时注册表为空;`attachAttribute` 需额外注入才不空</cons>
<cons>同一 Map 双写入方;属性脱离 `HeroState` 时注册表为空;需额外注入才不空</cons>
</option>
<option id="C+exclude-equip">
<name>注册表留在 `HeroState`,`loadState` 增加可选工厂解析参数(选项 C)+ 装备修饰器不进属性存档</name>
<name>(历史选项,未选)注册表留在 `HeroState`,`loadState` 增加可选工厂解析参数(选项 C)+ 装备修饰器不进属性存档</name>
<pros>改动面最小、注册表归属不变</pros>
<cons>偏离用户已锁定的「属性自身存读档」方向:不传工厂时修饰器不恢复,属性存读档不自洽,须在文档明写限定</cons>
<cons>偏离「属性自身存读档」方向:不传工厂时修饰器不恢复,属性存读档不自洽,须在文档明写限定</cons>
</option>
<option id="keep-equip-in-save">
<name>装备修饰器仍进属性存档(否决 ⑤ 的推荐)</name>
<name>(历史选项,未选)装备修饰器仍进属性存档</name>
<pros>存档内容不变</pros>
<cons>注册表命中时装备加成翻倍(属性读档重建 + `equip.loadState` 再挂),需要另做去重,本计划不建议</cons>
<cons>注册表命中时装备加成翻倍(属性读档重建 + `equip.loadState` 再挂),需要另做去重</cons>
</option>
<option id="keep-attach-attribute">
<name>(历史选项,未选)保留 `attachAttribute`、只把换绑风险登记为相邻缺口(原 (ii))</name>
<pros>不动公开 API</pros>
<cons>换绑入口继续存在,`HeroEquipment` 捕获引用与注册表归属的空窗风险只被「登记」而不被消除;用户裁决改为彻底删除该能力并加 `readonly`</cons>
</option>
<option id="revise">
<name>修订方案后再执行</name>
<name>(历史选项,未选)修订方案后再执行</name>
<pros>避免带错误方案落地</pros>
<cons>退出本计划并修订 PLAN.md 后重新执行(D-09)</cons>
</option>
</options>
<record>
待用户答复后填写(执行者须原样记录用户选择,不得自行代选):
1. 注册表归属裁决 = (A / B / C)
2. 选项 A 的 `attachAttribute` 处置 = ((i) 转移注册记录 / (ii) 登记缺口,默认 (ii))
3. 装备修饰器不进属性存档 = (同意 / 否决),若否决记录用户指定的替代方案
4. 存档形状变更(`IHeroStateSave.attribute: IHeroAttributeSave<THero>`,删除顶层 `modifiers`)= (确认 / 修订)
5. 回归用例新增授权(A1,D-10 例外)= (授权 / 限定范围)
若裁决为 B 或 C,Task 1–3 的差异(其余步骤不变):
- 选项 B:Task 1 在 `HeroAttribute` 增加 `private registry: Map<...>` 与 `useRegistry(registry)`(或构造参数)而非新增 `registerModifier`/`createModifier`/`createAndInsertModifier`;`IHeroAttribute` 只新增 `useRegistry`;Task 2 中 `HeroState` 保留自己的 `registry` 字段并在构造器内 `attribute.useRegistry(this.registry)`、`attachAttribute` 时对新属性再次注入;`clone()` 把同一注册表引用传给克隆对象。
- 选项 C:Task 1 不新增注册表成员,`loadState(state, compression, factory?)` 增加可选工厂解析参数(签名保持与 `ISaveableContent` 结构兼容),并在 `types.ts` 的 jsDoc 中明写「不传工厂时修饰器不恢复」;Task 2 中 `HeroState.loadState` 传入 `type => this.registry.get(type) ?? null`,`saveState` 不变;`IHeroState` 的 `registerModifier`/`createModifier`/`createAndInsertModifier` 实现留在 `HeroState`(本计划不动)。
用户裁决(**锁定结果**,执行者不得再询问、不得自行改选):
1. **注册表归属 = 选项 A**:`HeroAttribute` 拥有 `type → () => IHeroModifier` 注册表;`IHeroAttribute` 获得注册 API(`registerModifier` / `createModifier` / `createAndInsertModifier`);`HeroAttribute.loadState` 用**自身注册表**重建修饰器。
2. **`HeroState.registerModifier`(及 `createModifier`/`createAndInsertModifier`)退化为对 `this.attribute.*` 的薄委托**,保持既有测试调用点 `hero.registerModifier(...)` 不变:`hero/saveLoad.test.ts:513/543`、`hero/state.test.ts:134`、`saveablesRoundTrip.test.ts:115`、`saveablesReal.perf.ts:361`(复核确认:`registerModifier` 今日无任何生产调用者)。
3. **`HeroAttribute.clone()` 必须把注册表条目复制给克隆对象**,使克隆体可独立存读档。
4. **删除 `attachAttribute`**:移除 `IHeroState` 的声明(`hero/types.ts:829`,含 `:825-829` 的 jsDoc)与 `HeroState` 的实现(`hero/state.ts:90-92`)。
5. **`HeroState.attribute` 改为 `readonly`**(现为构造器可写属性 `hero/state.ts:55`),使绑定永不可换 → 引用不可能跨实例污染。
6. **`HeroState.loadState` 绝不重绑 `this.attribute`**:改为 `this.attribute.loadState(state.attribute, compression)`(原地)。
7. **删除 `attachAttribute` 的唯一用例** `hero/state.test.ts:118-127`(`replaces the bound attribute through attachAttribute`,含 `:118` 注释);相邻的 `getIsolatedAttribute` 用例 `:104-116` **保留**。
8. **装备修饰器不进属性存档**:`loadEquipEffect`(`hero/equipment.ts:80-85`)调用 `addModifier(name, modifier, false)`,与 `equipment.ts:341` 既有注释一致;`HeroEquipment.loadState` 在**同一活实例**上重新装备恢复加成。
9. **存档形状破坏性变更确认**:`IHeroStateSave.attribute` 改为 `IHeroAttributeSave<THero>`,顶层 `modifiers` 字段移除(并入属性存档);**不保留兼容分支**(项目未发布)。
10. **回归用例授权(A1,D-10 例外)**:新增 `#06-17-1`(装备修饰器跨 `hero.loadState` 存活于全部三档压缩;`getModifiableAttribute()` 读档前后同一实例)与 `#06-17-2`(`enemyContext.getBindedHero()` 读档后仍是活属性、无重绑)回归。
11. **范围守卫确认**:`#06-17-3..8`(C–H)**只登记不修**——不碰 replay/flag/equipStore/followers/map-layer、`data-system` 战斗代码、渲染端、`data-fallback`。
12. **无 B/C 差异分支**:本记录取代原「若裁决为 B 或 C」的差异表;Task 1–3 一律按选项 A 执行。
</record>
<resume-signal>用户已答复 ③④⑤②⑥ —— 执行者把答复原文写入 `<record>` 后直接进入 Task 1,不再重复询问;若答复与 `<record>` 中记录的差异表不符,暂停并请用户澄清。若用户选择 `revise`,退出本计划并修订 PLAN.md。</resume-signal>
<resume-signal>裁决已完成并锁入本计划(见 `<record>`):执行者直接进入 Task 1,不再询问用户、不得自行改选注册表归属或恢复 `attachAttribute`。若选项 A 方案在实现中失败,按 D-09 退出并修订本 PLAN.md,不得另辟他法;若发现本记录与用户实际意图不符,暂停并请用户澄清。</resume-signal>
<verify>
<human-check>用户已确认:注册表归属(A/B/C 及 A 下的 (i)/(ii))、装备修饰器是否排除出属性存档、存档形状变更、回归用例新增授权</human-check>
<fails_when>未获用户确认即进入 Task 1,或 `<record>` 与用户实际答复不符,或执行者自行代选注册表归属</fails_when>
<human-check>裁决记录(`<record>`)与用户最终选择一致:选项 A(注册表移入 `HeroAttribute` + `HeroState` 薄委托 + `clone()` 携带注册表)+ 删除 `attachAttribute` 并把 `attribute` 改为 `readonly` + 装备修饰器不进属性存档 + 存档形状破坏性变更确认 + 授权 `#06-17-1`/`#06-17-2` 回归用例 + `#06-17-3..8` 只登记不修;已由用户裁决,无需再次询问</human-check>
<fails_when>记录与用户实际意图不符 —— 暂停并请用户澄清,不得自行改写范围;或执行者在执行中重新向用户索取已裁决事项的确认、自行改选注册表归属、恢复 `attachAttribute`</fails_when>
</verify>
<acceptance_criteria>
- D-09 预执行汇报已完成:根因、修法、存档形状变更、两项待裁决、范围守卫与失败纪律均已陈述
- 用户对 ③(注册表归属 + `attachAttribute` 处置)、⑤(装备修饰器归档)、②(形状变更)、⑥(回归授权)的答复已逐条写入 `<record>`
- 若裁决非选项 A,`<record>` 中对应的差异表已明确列出 Task 1–3 需要替换的具体步骤
- 执行者据 `<record>` 直接进入 Task 1,不再重复询问
- D-09 裁决已记录在案:注册表归属 = 选项 A(`HeroAttribute` 拥有注册表、`IHeroAttribute` 获得注册 API、`HeroState` 薄委托、`clone()` 复制注册表条目)
- `attachAttribute` 的处置已记录为**删除**(`IHeroState` 声明 `types.ts:825-829` + `HeroState` 实现 `state.ts:88-92` + 其唯一用例 `state.test.ts:118-127`),且 `HeroState.attribute` 改 `readonly`、`loadState` 不重绑已记录
- 装备修饰器不进属性存档(`addModifier(name, modifier, false)`)已记录,并明确 `HeroEquipment.loadState` 在**同一活实例**上重新装备恢复
- 存档形状破坏性变更(`attribute: IHeroAttributeSave<THero>`、删除顶层 `modifiers`、不保留兼容分支)已确认;回归用例新增授权(A1,D-10 例外)已记录
- 范围守卫已确认:C–H(`#06-17-3..8`)只登记不修,不碰 replay/flag/equipStore/followers/map-layer、`data-system` 战斗代码、渲染端、`data-fallback`
- `<record>` 中已声明**无 B/C 差异分支**;执行者据 `<record>` 直接进入 Task 1,不再重复询问
</acceptance_criteria>
<done>用户裁决已完整记录(注册表归属 + `attachAttribute` 处置 + 装备修饰器归档 + 形状变更确认 + 回归授权),执行者据此进入 Task 1</done>
<done>D-09 裁决结果已完整记录(选项 A 注册表归属 + 薄委托 + clone 携带注册表 + 删除 `attachAttribute` 且 `attribute` 只读 + 装备修饰器不入属性存档 + 形状变更确认 + 回归授权 + C–H 只登记);执行者据此直接进入 Task 1,不再重新索取确认</done>
</task>
<task type="auto">
<name>Task 1: `HeroAttribute` 实现 `ISaveableContent` —— 新存档类型 `IHeroAttributeSave` + 原地 `saveState` / `loadState` + 注册表(按 Task 0 裁决)</name>
<name>Task 1: `HeroAttribute` 实现 `ISaveableContent` —— 新存档类型 `IHeroAttributeSave` + 原地 `saveState` / `loadState` + 注册表归属性(用户裁决选项 A)</name>
<files>packages-user/data-base/src/hero/types.ts, packages-user/data-base/src/hero/attribute.ts, packages-user/data-base/src/hero/attribute.test.ts</files>
<read_first>
- packages-user/data-base/src/hero/attribute.ts(全文件::48-63 字段与构造器、:80-102 `recalculateAttribute`、:159-163 `iterateModifiers`、:180-201 `addModifier`、:241-251 存档开关、:257-279 `clone`/`toStructured`)
@ -231,19 +255,19 @@ Phase 7 既有裁决与本计划的对应关系(逐条引用,便于 D-01..D-
- packages-user/data-base/src/hero/attribute.test.ts(:1-130 既有夹具与用例风格、`TestModifier`、`createAttribute`)
</read_first>
<action>
让勇士属性自己承担存读档(同引用原则的核心一步)。**默认按 Task 0 裁决的【选项 A + 排除装备修饰器】书写;若裁决为 B 或 C,按 Task 0 `<record>` 的差异表替换注册表相关步骤,其余步骤不变。**
让勇士属性自己承担存读档(同引用原则的核心一步)。**注册表归属已由用户裁决为选项 A(Task 0 `<record>` 第 1–3 条),本任务按该口径书写,不存在 B/C 分支。**
(1) `hero/types.ts` 新增存档类型(jsDoc 写在接口源头,中文,写明「基础属性值 + 保存启用的修饰器状态」;成员之间按功能换行):
- `export interface IHeroAttributeSave<THero>`,成员两个:`readonly values: THero;`(勇士基础属性值,即未经过任何 Buff 或装备加成的属性,须为深拷贝)、`readonly modifiers: readonly IModifierStateSave<THero>[];`(保存启用的属性修饰器状态)。复用既有 `IModifierStateSave<THero>`(:61-68),不改它的形状。
(2) `hero/types.ts` 让 `IHeroAttribute<THero>` 同时实现存档契约:声明改为 `extends IReadonlyHeroAttribute<THero>, ISaveableContent<IHeroAttributeSave<THero>>`(`ISaveableContent` 已从 `@user/data-common` 引入,确认顶部 import 列表无需新增)。**不要**把存读档加到 `IReadonlyHeroAttribute`(战斗侧只读视图不应暴露 `loadState`)。
(3) `hero/types.ts` 按【选项 A】给 `IHeroAttribute` 增加三个注册表成员(jsDoc 中文,签名与 `IHeroState` 现有的一份逐字一致,便于 Task 2 委托):
(3) `hero/types.ts` 给 `IHeroAttribute` 增加三个注册表成员(用户裁决选项 A;jsDoc 中文,签名与 `IHeroState` 现有的一份逐字一致,便于 Task 2 薄委托):
- `registerModifier(type: string, cons: <K extends keyof THero>() => IHeroModifier<THero[K]>): void`(注册一个修饰器工厂函数)
- `createModifier<T, V>(type: string): IHeroModifier<T, V> | null`(创建指定类型的修饰器实例)
- `createAndInsertModifier<K extends keyof THero, V>(type: string, name: K): IHeroModifier<THero[K], V> | null`(创建并插入至本属性,默认进入存档)
(4) `hero/attribute.ts` 按【选项 A】把注册表搬进来:
(4) `hero/attribute.ts` 把注册表搬进来(用户裁决选项 A;`state.ts` 侧的字段与实现由 Task 2 删除,本任务只做「复制进来」以保证 Task 1 单独提交时套件仍全绿):
- 新增字段 `private readonly registry: Map<string, <K extends keyof THero>() => IHeroModifier<THero[K]>> = new Map();`,jsDoc 与 `state.ts:40` 一致。
- 从 `state.ts:106-130` **原样搬入** `registerModifier` / `createModifier` / `createAndInsertModifier`(保留 `logger.warn(116, type)` 未知类型分支与返回 `null` 的语义、保留 `this.attribute.addModifier(name, modifier)` 的默认 `save = true`);搬入后把 `createAndInsertModifier` 内的 `this.attribute.addModifier(...)` 改为 `this.addModifier(...)`(属性自身即宿主)。
- 按 dev.md(私有方法放在调用它的方法之前、处于合理 region)把这三个方法放进「修饰器处理」region,并为其补 `#region` 内位置;实现处**不重复**写 jsDoc(接口源头已写)。
@ -279,7 +303,7 @@ Phase 7 既有裁决与本计划的对应关系(逐条引用,便于 D-01..D-
- `hero/types.ts` 存在 `IHeroAttributeSave<THero>`(`values: THero` + `modifiers: readonly IModifierStateSave<THero>[]`),且 `IHeroAttribute<THero>` 声明继承 `ISaveableContent<IHeroAttributeSave<THero>>`;`IReadonlyHeroAttribute` 未被加入存读档成员
- `hero/attribute.ts` 存在 `saveState(compression): IHeroAttributeSave<THero>`(`values` 为深拷贝、`modifiers` 按 `getModifierSaveEnabled` 过滤)与 `loadState(state, compression): void`;`loadState` 中不出现 `new HeroAttribute`、不给 `this.attribute` / `this.finalAttribute` 赋值
- `loadState` 依次完成基础值原地重置、`modifier`/`modifierName`/`modifierNosave` 清空、按注册表重建修饰器、逐键 `recalculateAttribute`
- 按裁决(默认选项 A)`HeroAttribute` 拥有注册表并实现 `registerModifier`/`createModifier`/`createAndInsertModifier`(未知类型仍 `logger.warn(116)` 并返回 `null`),`clone()` 复制注册表条目且不改变既有克隆行为
- `HeroAttribute` 拥有注册表(用户裁决选项 A)并实现 `registerModifier`/`createModifier`/`createAndInsertModifier`(未知类型仍 `logger.warn(116)` 并返回 `null`),`clone()` 复制注册表条目且不改变既有克隆行为(仍不复制 `modifierName`/`modifierNosave`,A6)
- `hero/attribute.test.ts` 新增三档压缩恢复、实例身份、存档禁用与快照独立四类回归,每个 `it` 前有中文单行注释;既有断言未被改动
- `pnpm exec vitest run packages-user/data-base/src/hero/attribute.test.ts` 全绿;本任务改动文件 eslint 0 错误、vue-tsc 过滤后 0 类型错误
</acceptance_criteria>
@ -287,17 +311,18 @@ Phase 7 既有裁决与本计划的对应关系(逐条引用,便于 D-01..D-
</task>
<task type="auto">
<name>Task 2: `HeroState` 委托 + 原地读档 + 存档形状更新 + 装备修饰器不入属性存档(`#06-17-1` 根因闭合)</name>
<files>packages-user/data-base/src/hero/state.ts, packages-user/data-base/src/hero/types.ts, packages-user/data-base/src/hero/equipment.ts, packages-user/data-base/src/hero/saveLoad.test.ts</files>
<name>Task 2: `HeroState` 委托 + `attribute` 改 `readonly` + 删除 `attachAttribute` + 原地读档 + 存档形状更新 + 装备修饰器不入属性存档(`#06-17-1` 根因闭合)</name>
<files>packages-user/data-base/src/hero/state.ts, packages-user/data-base/src/hero/types.ts, packages-user/data-base/src/hero/equipment.ts, packages-user/data-base/src/hero/saveLoad.test.ts, packages-user/data-base/src/hero/state.test.ts</files>
<read_first>
- packages-user/data-base/src/hero/state.ts(:148-195 saveState/loadState 现状、:88-130 属性相关与注册表方法)
- packages-user/data-base/src/hero/types.ts(:786-801 现 `IHeroStateSave`、:803-874 `IHeroState`)
- packages-user/data-base/src/hero/state.ts(:40-44 注册表字段、:52-72 构造器与装备接线、:88-132 属性相关 region 与注册表方法、:148-195 saveState/loadState 现状)
- packages-user/data-base/src/hero/types.ts(:786-801 现 `IHeroStateSave`、:803-874 `IHeroState`(含 :810 `attribute` 只读视图声明与 :825-829 `attachAttribute` 声明))
- packages-user/data-base/src/hero/state.test.ts(:104-116 保留的 `getIsolatedAttribute` 用例、:118-127 待删除的 `attachAttribute` 用例、:130-148 注册表委托用例(码 116))
- packages-user/data-base/src/hero/equipment.ts(:76-96 loadEquipEffect/unloadEquipEffect、:329-346 saveState/loadState、:341 既有注释)
- packages-user/data-base/src/hero/saveLoad.test.ts(:508-559 `HeroState save and load round trips`、:561-609 容器覆盖与 `#06-09-2` 用例、:66-205 夹具 `createState`/`createBaseAttr`/`createEquipItem`/`registerItem`/`createEquipEnv`/`createHeroState`)
- packages-user/data-base/src/hero/attribute.ts(Task 1 产出的 `saveState`/`loadState` 签名,导入路径与调用方式)
- packages-user/data-base/src/hero/attribute.ts(Task 1 产出的 `saveState`/`loadState`/注册表成员签名,导入路径与调用方式)
</read_first>
<action>
把 `HeroState` 从「替换实例」改为「委托原地」,并让装备修饰器脱离属性存档。**默认按 Task 0 裁决的【选项 A + 排除装备修饰器】书写;若裁决为 B/C 或否决 ⑤,按 Task 0 `<record>` 执行对应差异。**
把 `HeroState` 从「替换实例」改为「委托原地」,把属性绑定变为不可变(删除 `attachAttribute` + `readonly`),并让装备修饰器脱离属性存档。**注册表归属、`attachAttribute` 存废与装备修饰器归档均已由用户裁决(Task 0 `<record>` 第 1–9 条),本任务按选项 A 口径书写,不存在 B/C 或「保留 attachAttribute」分支。**
(1) `hero/types.ts` 的 `IHeroStateSave<THero>`(:786-801):
- 把 `readonly attribute: THero;` 改为 `readonly attribute: IHeroAttributeSave<THero>;`(jsDoc 相应改为「勇士属性状态,含基础属性值与保存启用的修饰器」)。
@ -316,40 +341,48 @@ Phase 7 既有裁决与本计划的对应关系(逐条引用,便于 D-01..D-
- 保持其余调用序不变:`location.loadState` → `rendering.loadState` → `items.loadState` → `equip.loadState` → `followers.removeAllFollowers()` + 逐个 `addFollower` + `follower.loadState`。属性读档必须在 `items.loadState`/`equip.loadState` 之前(装备修饰器靠 `equip.loadState` 的重新装备恢复)。
- 从 import 列表移除已不再使用的 `HeroAttribute` 与 `IModifierStateSave`(保留 `IHeroAttribute` 等仍在用的类型)。
(4) `hero/state.ts` 的注册表方法(按选项 A):删除 `:40-44` 的 `private readonly registry` 字段,`registerModifier` / `createModifier` / `createAndInsertModifier`(:106-130)改为一行委托到 `this.attribute.*`(保留 jsDoc 在 `IHeroState` 源头、保留返回类型与 `null` 语义)。`IHeroState` 的公开签名不变,既有调用点(含 `state.test.ts:134-146` 的 `@system/value` 与码 116 用例)应零改动通过。
- `attachAttribute`(:90-92)保持替换语义不动,按 Task 0 `<record>` 的 (i)/(ii) 执行;默认 (ii):不转移注册表,并在 SUMMARY 登记 A5 缺口。
(4) `hero/state.ts` 的注册表方法(用户裁决选项 A):删除 `:40-44` 的 `private readonly registry` 字段,`registerModifier` / `createModifier` / `createAndInsertModifier`(`:106-130`)改为**薄委托**到 `this.attribute.*`(`return this.attribute.registerModifier(type, cons)` / `return this.attribute.createModifier<T, V>(type)` / `return this.attribute.createAndInsertModifier<K, V>(type, name)`)。`IHeroState` 的公开签名不变,既有调用点零改动通过:`saveLoad.test.ts:513/543`、`state.test.ts:134`(含 `:142-146` 的码 116 未知类型用例,`logger.warn(116)` 现由 `HeroAttribute.createModifier` 发出,语义不变)、`saveablesRoundTrip.test.ts:115`、`saveablesReal.perf.ts:361`(零断言 perf 文件不改)。委托可按 `this.attribute` 的类型直接调用:`HeroState` 类内的字段类型是 `IHeroAttribute<THero>`(`:55`),已由 Task 1 获得三个成员。
(5) `hero/equipment.ts` 的 `loadEquipEffect`(:80-85):把 `this.attribute.addModifier(name, modifier, true)` 的第三个实参改为 `false`,并补中文单行注释说明原因:「装备修饰器由 `HeroEquipment.loadState` 的重新装备恢复,故不进入属性存档,避免与属性读档的重建重复计入」。同步复核 `equipment.ts:341` 的既有注释与实现现在一致(若不准确一并修正文案)。**不**改 `unloadEquipEffect`、不改 `equip`/`unequip`/`compareEquip`/`saveState`/`loadState` 的其它逻辑。
(5) **删除 `attachAttribute` 并把属性绑定改为只读(用户裁决,Task 0 `<record>` 第 4–7 条)**:
- `hero/types.ts`:删除 `IHeroState` 的 `attachAttribute(attribute: IHeroAttribute<THero>): void;` 及其 jsDoc(`:825-829`)。不复用为别名、不保留 deprecated 版本、不改名为其它换绑方法。
- `hero/state.ts`:删除 `attachAttribute` 实现(`:88-92`)——`#region 属性相关` 区块内只保留 `getModifiableAttribute` / `getAttribute` / `getIsolatedAttribute` 与注册表委托;把构造器参数 `public attribute: IHeroAttribute<THero>`(`:55`)改为 **`public readonly attribute: IHeroAttribute<THero>`**。
- `hero/state.ts` 的 `loadState`:**不得**出现 `this.attribute = ...`(`readonly` 会在编译期拦住;vue-tsc 过滤 0 错误可证)。原 `:185` 的 `this.attribute = newAttribute;` 随 `newAttribute` 一起消失。
- `hero/state.test.ts`:删除 `replaces the bound attribute through attachAttribute` 用例(`:118-127`,含 `:118` 的中文注释);**保留** `exposes shared, readonly and isolated attribute views`(`:104-116`,其 `getIsolatedAttribute` 断言在 Task 1 的 `clone()` 携带注册表后仍成立)与 `HeroState modifier registry` 用例(`:130-148`,走委托后仍应绿)。
- 删除后复核全仓无 `attachAttribute` 残留(接口、实现、调用点、注释、文档均为零);本计划不新增任何替代换绑入口。
(6) `hero/saveLoad.test.ts` 的形状迁移与回归:
(6) `hero/equipment.ts` 的 `loadEquipEffect`(`:80-85`):把 `this.attribute.addModifier(name, modifier, true)` 的第三个实参改为 `false`,并补中文单行注释说明原因:「装备修饰器由 `HeroEquipment.loadState` 的重新装备恢复,故不进入属性存档,避免与属性读档的重建重复计入」。同步复核 `equipment.ts:341` 的既有注释与实现现在一致(若不准确一并修正文案)。**不**改 `unloadEquipEffect`、不改 `equip`/`unequip`/`compareEquip`/`saveState`/`loadState` 的其它逻辑;`HeroEquipment.loadState` 仍靠 `equip(uid, index)` 在**同一活属性实例**上重新施加装备修饰器。
(7) `hero/saveLoad.test.ts` 的形状迁移与回归:
- 既有断言纠偏(形状迁移,唯一允许的既有断言改动):`:549` 的 `expect(saved.modifiers).toHaveLength(1)` 改为 `expect(saved.attribute.modifiers).toHaveLength(1)`。其余断言(`:510-538`、`:553-557`、`:563-585`、`:589-609`)一律不动。
- 新增 `describe('HeroState same-reference attribute load (#06-17-1)')`,每个 `it` 前写中文单行注释(A1 授权):
- 「三档压缩读档后装备加成仍在且属性对象同实例」:用既有夹具 `createEquipEnv()` + `registerItem(env, createEquipItem(10, 'sword', [0], [['atk', 5]]))`(`createBaseAttr()` 的 `atk` 为 10,故 `atk` 最终为 15)构造 `HeroState`、`hero.equip.setSlots(['weapon'])`、`uid = hero.items.equipment.add(10)`、`hero.equip.equip(uid, 0)`;断言 `getFinalAttribute('atk') === 15`;`const attrBefore = hero.getModifiableAttribute();`;`const saved = hero.saveState(compression)`;打乱活状态(`hero.equip.unequip(0)`、`hero.getModifiableAttribute().set('atk', 1)`);`hero.loadState(saved, compression)`;断言 `hero.getModifiableAttribute()` **是** `attrBefore`(`toBe`)、`getFinalAttribute('atk')` 回到 15、`getEquipped(0)` 仍为 `uid`、基础 `atk` 回到 10。
- 「读档后装备仍作用于活属性」:承接上一 `it`,在 `loadState` 之后再 `hero.equip.unequip(0)` 并断言 `getFinalAttribute('atk')` 回到 10(证明 `HeroEquipment` 捕获的引用是活属性),再 `hero.equip.equip(uid, 0)` 断言回到 15。
- 「注册表命中时装备加成不翻倍」:额外 `hero.registerModifier('@system/value', () => new ValueModifier(5))` 并 `hero.createAndInsertModifier('@system/value', 'atk')`(英雄侧 +5),再装备同一件 `[['atk', 5]]` 装备;三档压缩往返后断言 `getFinalAttribute('atk') === 20`(基础 10 + 英雄 5 + 装备 5,**不是** 25),且 `getModifiers('atk')` 的修饰器数量为 2。
不做的事:不改 `EnemyContext` / `DamageSystem` / 战斗代码;不改 `HeroEquipment` 的捕获方式(保持 `private readonly`);不改 `equipStore.ts`(`#06-17-4` 只登记);不改 `follower` / `items` 的 `loadState`;不新增错误码;不新增 `it.skip`。
不做的事:不新增任何换绑入口(不保留、不 deprecated、不改名 `attachAttribute`),不给 `this.attribute` 赋值(`readonly` 已从类型层封死);不改 `EnemyContext` / `DamageSystem` / 战斗代码;不改 `HeroEquipment` 的捕获方式(保持 `private readonly`);不改 `equipStore.ts`(`#06-17-4` 只登记);不改 `follower` / `items` 的 `loadState`;不新增错误码;不新增 `it.skip`。
验证:`pnpm exec vitest run packages-user/data-base/src/hero/saveLoad.test.ts packages-user/data-base/src/hero/state.test.ts packages-user/data-base/src/hero/equipment.test.ts` 全绿(重点确认 `#06-09-2` 容器用例、码 116 注册表用例、`attachAttribute` 用例不回归)→ D-12/D-44 文件级门禁。提交信息:`fix(07-09): #06-17-1 load hero attribute in place and keep equipment bonuses`。
验证:`pnpm exec vitest run packages-user/data-base/src/hero/saveLoad.test.ts packages-user/data-base/src/hero/state.test.ts packages-user/data-base/src/hero/equipment.test.ts` 全绿(重点确认 `#06-09-2` 容器用例与码 116 注册表用例经薄委托后仍绿、`attachAttribute` 用例已删除且无残留编译错误、`getIsolatedAttribute` 用例不回归)→ D-12/D-44 文件级门禁。提交信息:`fix(07-09): #06-17-1 load hero attribute in place and keep equipment bonuses`。
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/hero/saveLoad.test.ts packages-user/data-base/src/hero/state.test.ts packages-user/data-base/src/hero/equipment.test.ts</automated>
<fails_when>非零退出,或摘要行出现 "failed"(读档后装备加成丢失或翻倍、`getModifiableAttribute()` 非同实例、注册表委托后码 116 用例回归、`attachAttribute` 用例回归),或输出出现 "no tests found"</fails_when>
<fails_when>非零退出,或摘要行出现 "failed"(读档后装备加成丢失或翻倍、`getModifiableAttribute()` 非同实例、注册表薄委托后码 116 用例回归、`getIsolatedAttribute` 用例回归、`attachAttribute` 残留导致 vue-tsc 报错),或输出出现 "no tests found"</fails_when>
</verify>
<acceptance_criteria>
- `hero/state.ts` 的 `loadState` 中不出现 `new HeroAttribute`,也不出现 `this.attribute =`;属性读档通过 `this.attribute.loadState(state.attribute, compression)` 完成,且位于 `items.loadState`/`equip.loadState` 之前
- `hero/state.ts` 的 `attribute` 为 `readonly`;`loadState` 中不出现 `new HeroAttribute`,也不出现 `this.attribute =`;属性读档通过 `this.attribute.loadState(state.attribute, compression)` 完成,且位于 `items.loadState`/`equip.loadState` 之前
- `attachAttribute` 在 `IHeroState`(`types.ts`)与 `HeroState`(`state.ts`)中均已删除,全仓无该标识符残留;`state.test.ts` 的 `replaces the bound attribute through attachAttribute` 用例(`:118-127`)已删除,`exposes shared, readonly and isolated attribute views`(`:104-116`)保留且绿
- `hero/state.ts` 的 `saveState` 的 `attribute` 字段为 `this.attribute.saveState(compression)`,返回值不再含顶层 `modifiers`;`IModifierStateSave`/`HeroAttribute` 的未使用 import 已移除(eslint 0 错误可证)
- `registerModifier`/`createModifier`/`createAndInsertModifier` 为对 `this.attribute.*` 的薄委托(未命中类型仍由属性侧 `logger.warn(116)` 并返回 `null`);`saveLoad.test.ts:513/543`、`state.test.ts:134`、`saveablesRoundTrip.test.ts:115`、`saveablesReal.perf.ts:361` 四处调用点零改动通过
- `hero/types.ts` 的 `IHeroStateSave<THero>` 为 `attribute: IHeroAttributeSave<THero>` 且顶层 `modifiers` 已删除;`IHeroAttributeSave` 的 jsDoc 说明与实现一致
- `hero/equipment.ts` 的 `loadEquipEffect` 以 `addModifier(name, modifier, false)` 挂载并带中文原因注释;`:341` 注释与实现一致
- `saveLoad.test.ts:549` 已迁移为 `saved.attribute.modifiers`,其余既有断言一字未改;新增三条回归(同实例 + 加成保留、读档后装备作用于活属性、注册表命中不翻倍)且每个 `it` 前有中文单行注释
- 三个既有 hero 测试文件全绿;本任务改动文件 eslint 0 错误、vue-tsc 过滤后 0 类型错误
</acceptance_criteria>
<done>读档后属性实例不变、装备加成保留且不翻倍,`#06-17-1` 根因闭合;`IHeroStateSave` 形状变更已落地并登记;已一个原子提交入库</done>
<done>属性绑定不可换(`readonly` + `attachAttribute` 连同其用例已删除)、读档后属性实例不变、装备加成保留且不翻倍,`#06-17-1` 根因闭合;注册表薄委托后既有调用点与码 116 语义不变;`IHeroStateSave` 形状变更已落地并登记;已一个原子提交入库</done>
</task>
<task type="auto">
<name>Task 3: `#06-17-2` 战斗侧回归 + 顶层形状迁移 + D-12/D-44 门禁 + 全量套件 + 缺口登记与 Phase 7 重开说明</name>
<files>packages-user/data-state/test/saveablesRoundTrip.test.ts, packages-user/data-state/test/dataClosure.test.ts, packages-user/data-base/src/hero/types.ts, packages-user/data-base/src/hero/attribute.ts, packages-user/data-base/src/hero/state.ts, packages-user/data-base/src/hero/equipment.ts, packages-user/data-base/src/hero/attribute.test.ts, packages-user/data-base/src/hero/saveLoad.test.ts</files>
<files>packages-user/data-state/test/saveablesRoundTrip.test.ts, packages-user/data-state/test/dataClosure.test.ts, packages-user/data-base/src/hero/types.ts, packages-user/data-base/src/hero/attribute.ts, packages-user/data-base/src/hero/state.ts, packages-user/data-base/src/hero/equipment.ts, packages-user/data-base/src/hero/attribute.test.ts, packages-user/data-base/src/hero/saveLoad.test.ts, packages-user/data-base/src/hero/state.test.ts</files>
<read_first>
- packages-user/data-state/test/saveablesRoundTrip.test.ts(:239-256 快照分离断言、:1-60 夹具与压缩常量)
- packages-user/data-state/test/dataClosure.test.ts(:1-30 导入、:180-191 现有 hero 往返用例、:46-90 `vi.hoisted` 与 `REPLAY_COMMAND_ORDER`)
@ -373,14 +406,16 @@ Phase 7 既有裁决与本计划的对应关系(逐条引用,便于 D-01..D-
(4) 在 SUMMARY 显式登记(不得静默丢弃):
- **修复内容**:`#06-17-1`(A)与 `#06-17-2`(B)按同引用原则闭合——`HeroAttribute implements ISaveableContent<IHeroAttributeSave<THero>>`,`loadState` 原地完成;`HeroState.loadState` 不再替换实例;`EnemyContext.bindedHero` / `HeroEquipment.attribute` 无需重绑即始终有效。
- **引用不可变(用户裁决)**:`attachAttribute` 已从 `IHeroState`(`types.ts:825-829`)与 `HeroState`(`state.ts:88-92`)**删除**,其唯一用例 `state.test.ts:118-127` 一并删除(全仓零残留);`HeroState.attribute` 改为 `readonly`,`loadState` 不重绑——换绑入口在生产代码中已不存在,原 A5 相邻缺口随删除关闭(不再作为「只登记不修」项)。
- **注册表归属(用户裁决选项 A)**:`HeroAttribute` 拥有 `registry` 并实现 `registerModifier`/`createModifier`/`createAndInsertModifier`(未知类型仍 `logger.warn(116)` 返回 `null`),`HeroState` 三方法为薄委托,`IHeroState` 公开签名不变;`clone()` 复制注册表条目(克隆体可独立存读档)。`registerModifier` 复核确认无生产调用者,四处既有调用点零改动。
- **形状变更**:`IHeroStateSave.attribute: THero` → `IHeroAttributeSave<THero>`(`values` + `modifiers`),顶层 `modifiers` 移除;`hero/equipment.ts` 的装备修饰器改为不进属性存档。A2:项目未发布、无向后兼容要求、不保留兼容分支;对仓外已存在的旧存档不承诺可读。
- **账本对照**:`#06-17-1` / `#06-17-2` 在 `.planning/WINDOWS.md` 中**无对应条目**(`Select-String` 复核 `WINDOWS.md` 无 `06-17`),按 07-03 的处置先例**不新建条目**(新增会抬高 `open_count` 门禁),逐条注明「无 ledger entry」;`WINDOWS.md` id 28(`saveablesReal.perf.ts` deviation)与本计划无关,保持 `open`。
- **未修的 C–H 清单**(只登记不修,附各自文件与现状):`#06-17-3`(`data-common/src/replay/array.ts:801-826` `setReplayArray` 漏 `expireStreams`)、`#06-17-4`(`hero/equipStore.ts:271/:278` 重建实例脱钩)、`#06-17-5`(`flag/system.ts:66/:68` 重建字段脱钩)、`#06-17-6`(`hero/state.ts:190/:192` followers 重建脱钩)、`#06-17-7`(`data-fallback/src/hero.ts:8-23` Proxy 闭包持旧属性,是否触发取决于仓外 `resetHero`)、`#06-17-8`(`map/mapLayer.ts:371-391` `setMapRef` 换缓冲)。
- **相邻缺口(只登记不修)**:A5 `attachAttribute` 仍会替换属性实例并让 `HeroEquipment` 的捕获引用脱钩(本计划保语义不动);A6 `HeroAttribute.clone()` 未复制 `modifierName`/`modifierNosave`。
- **相邻缺口(只登记不修)**:A6 `HeroAttribute.clone()` 复制了注册表条目(A7 要求),但仍未复制 `modifierName`/`modifierNosave`。原 A5(`attachAttribute` 换绑)**已由用户裁决删除而关闭**,不再列入未修清单。
- **Phase 7 重开说明**:本计划在 Phase 7 标记 `Complete`(2026-09-16)之后追加,阶段验证因此**失效**——`07-VERIFICATION.md` 的结论(含「8/8 plans」「orphaned requirements: none」等)不再覆盖当前工作树,执行完成后必须重跑 `/gsd-verify-work`(或 `gsd-verify-work 7`)重新出具结论;`07-VALIDATION.md` 的 Per-Task Verification Map 未登记 `#06-17-1` / `#06-17-2`,由该次重验证一并处理(本计划不改该文件)。
- **门禁结果**:eslint / vue-tsc / `pnpm test:ci` 的实测结论,以及 `pnpm test:ci` 的用例计数(改前为 66 文件 / 680 passed / 1 skipped,新增用例后计数应上升、skipped 不得上升)。
- **门禁结果**:eslint / vue-tsc / `pnpm test:ci` 的实测结论,以及 `pnpm test:ci` 的用例计数(改前为 66 文件 / 680 passed / 1 skipped,新增用例后计数应上升、skipped 不得上升;`attachAttribute` 用例删除会使总数减少 1,须如实记入对照)。
不做的事:不放宽 `pnpm test:ci`;不新增 `it.skip`;不改 `.planning/WINDOWS.md`(不新建条目、不打 `fixed`——无对象可结清);不改 `07-VALIDATION.md` / `07-VERIFICATION.md`(留给重验证环节;本计划只写 SUMMARY);不改零断言 perf 文件。
不做的事:不放宽 `pnpm test:ci`;不新增 `it.skip`;不重新引入 `attachAttribute` 或任何换绑入口;不改 `.planning/WINDOWS.md`(不新建条目、不打 `fixed`——无对象可结清);不改 `07-VALIDATION.md` / `07-VERIFICATION.md`(留给重验证环节;本计划只写 SUMMARY);不改零断言 perf 文件。
验证:`pnpm test:ci` 全绿且 skipped 数不增加 → `git status` 最终仅含本计划产物。提交信息:`test(07-09): #06-17-2 combat-side same-reference hero attribute regression`(门禁与 SUMMARY 可并入同一提交或按其根因族拆分,按 D-13)。
</action>
@ -393,9 +428,9 @@ Phase 7 既有裁决与本计划的对应关系(逐条引用,便于 D-01..D-
- `dataClosure.test.ts` 新增 `#06-17-2` 回归:读档前 `getBindedHero()` 与活属性同实例、读档后仍同实例且 `getFinalAttribute('atk')` 为 5(三档压缩),且全程无重绑调用
- `pnpm exec eslint --fix` + `pnpm exec eslint` 对全部改动文件 0 错误;`pnpm exec vue-tsc --noEmit` 按改动文件路径过滤 0 类型错误
- `pnpm test:ci` 全绿,skipped 数不增加,本计划未新增 `it.skip`、未弱化既有断言
- SUMMARY 已登记:A/B 修复、存档形状变更(A2)、`#06-17-1`/`#06-17-2` 无 ledger entry 的对照、C–H 六条未修清单、相邻缺口 A5/A6、Phase 7 重开与 `07-VERIFICATION.md` 失效须重验证、门禁与用例计数实测结果
- SUMMARY 已登记:A/B 修复、引用不可变(`attachAttribute` 删除 + `attribute` 只读 + 用例删除)、注册表归属选项 A(含 `clone()` 携带注册表)、存档形状变更(A2)、`#06-17-1`/`#06-17-2` 无 ledger entry 的对照、C–H 六条未修清单、相邻缺口 A6(A5 已关闭)、Phase 7 重开与 `07-VERIFICATION.md` 失效须重验证、门禁与用例计数实测结果
</acceptance_criteria>
<done>`#06-17-1` / `#06-17-2` 各自有可跑的回归见证(属性层三档 + 战斗侧三档),顶层形状迁移完成,D-12/D-44 三步与全量套件通过,未修项与 Phase 7 重开说明全部登记于 SUMMARY</done>
<done>`#06-17-1` / `#06-17-2` 各自有可跑的回归见证(属性层三档 + 战斗侧三档),属性绑定不可换(`readonly` + `attachAttribute` 已删除)、注册表归 `HeroAttribute`、顶层形状迁移完成,D-12/D-44 三步与全量套件通过,未修项与 Phase 7 重开说明全部登记于 SUMMARY</done>
</task>
</tasks>
@ -408,6 +443,7 @@ Phase 7 既有裁决与本计划的对应关系(逐条引用,便于 D-01..D-
| 存档数据 → 属性读档 | 存档中的基础属性值与修饰器类型是不受信输入:类型可能未注册、键集合可能与当前 `THero` 形状不一致 |
| 属性子系统 ↔ 装备子系统 | 两者都会向同一个属性实例挂载修饰器;归属划分不清就会重复计入数值 |
| 长生命周期持有者 ↔ 属性实例 | `HeroEquipment`(构造期捕获)、`EnemyContext`(一次性绑定)持有属性引用,实例被替换即静默读到过期数据 |
| 属性绑定 ↔ 换绑入口 | 只要存在「换绑属性实例」的公开入口(原 `attachAttribute`),任何持有者都可能被切到另一个属性,注册表与装备/战斗引用随之跨实例污染 |
| 包管理器 → 仓库 | 本计划无安装动作 |
## STRIDE Threat Register
@ -415,47 +451,52 @@ Phase 7 既有裁决与本计划的对应关系(逐条引用,便于 D-01..D-
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-7-13 | Tampering | `hero/attribute.ts` 的 `loadState` 原地重置 | high | mitigate | Task 1 的基础值重置只改 `this.attribute` 的成员:先删除不在 `state.values` 的自有键(`Object.prototype.hasOwnProperty.call` 判定,避免原型链误删),再逐键赋入 `structuredClone(state.values)`;不对 `private readonly` 引用整体重新赋值,最终属性与基础属性对象身份保持不变 |
| T-7-14 | Tampering | `hero/attribute.ts` 的修饰器重建 | medium | mitigate | Task 1 重建前 `clear()` 三个簿记容器,遍历 `state.modifiers` 时仅接受注册表命中的 `type`(未命中静默跳过,与既有 `state.ts:179-180` 同语义、不新增错误码),并在重建后逐键 `recalculateAttribute` 使 final 不含被移除修饰器的陈旧值 |
| T-7-14 | Tampering | `hero/attribute.ts` 的修饰器重建 | medium | mitigate | Task 1 重建前 `clear()` 三个簿记容器,遍历 `state.modifiers` 时仅接受注册表命中的 `type`(未命中静默跳过,与既有 `state.ts:179-180` 同语义、不新增错误码),并在重建后逐键 `recalculateAttribute` 使 final 不含被移除修饰器的陈旧值;注册表随 `clone()` 复制,克隆体重建行为与本体一致 |
| T-7-15 | Tampering | 装备加成重复计入 | high | mitigate | Task 2 让 `loadEquipEffect` 以 `save = false` 挂载装备修饰器,装备加成只经 `HeroEquipment.loadState` 的重新装备恢复;Task 2 新增「注册表命中时加成不翻倍」三档回归(英雄 5 + 装备 5 → 20 而非 25)锁定该不变量 |
| T-7-16 | Tampering | 持久化形状破坏性变更 | low | accept | `IHeroStateSave.attribute` 改为 `IHeroAttributeSave<THero>`、顶层 `modifiers` 移除(A2);项目未发布、无向后兼容要求,不保留兼容分支、不引入版本字段,但在 SUMMARY 显式登记,且约定对仓外旧存档不承诺可读 |
| T-7-17 | Tampering | `HeroState.attribute` 绑定可换(原 `attachAttribute`) | medium | mitigate | Task 2 删除 `IHeroState.attachAttribute` 声明与 `HeroState` 实现(全仓无其它调用者),并将 `HeroState.attribute` 改为 `readonly`;`loadState` 只调用 `this.attribute.loadState(...)`。换绑入口在类型层与实现层同时不存在,`HeroEquipment.attribute` / `EnemyContext.bindedHero` 不可能被切到另一实例;原 A5 缺口随之关闭,且禁止以别名/deprecated 形式复原 |
| T-7-SC | Tampering | npm/pip/cargo 安装 | high | accept | 本计划不安装任何包(`package.json` / `pnpm-lock.yaml` 不变);若执行中出现安装需求,立即停止并汇报 |
</threat_model>
<verification>
- `pnpm exec vitest run packages-user/data-base/src/hero/attribute.test.ts` 全绿(属性层三档恢复、实例身份、存档禁用、快照独立)
- `pnpm exec vitest run packages-user/data-base/src/hero/saveLoad.test.ts packages-user/data-base/src/hero/state.test.ts packages-user/data-base/src/hero/equipment.test.ts` 全绿(含 `#06-09-2`、码 116 与 `attachAttribute` 既有用例不回归)
- `pnpm exec vitest run packages-user/data-base/src/hero/saveLoad.test.ts packages-user/data-base/src/hero/state.test.ts packages-user/data-base/src/hero/equipment.test.ts` 全绿(含 `#06-09-2` 与码 116 用例经薄委托后不回归;`attachAttribute` 用例已删除、`getIsolatedAttribute` 用例保留且绿)
- 属性绑定不可换:`hero/state.ts` 的 `attribute` 为 `readonly`、`loadState` 内无 `this.attribute = ...`;`attachAttribute` 在 `types.ts` / `state.ts` / `state.test.ts` 及全仓均无残留
- 注册表归 `HeroAttribute`:`IHeroAttribute` 提供 `registerModifier`/`createModifier`/`createAndInsertModifier`,`HeroState` 为薄委托,`clone()` 携带注册表条目(属性层回归可证)
- `pnpm exec vitest run packages-user/data-state/test/dataClosure.test.ts packages-user/data-state/test/saveablesRoundTrip.test.ts` 全绿(`#06-17-2` 战斗侧同引用 + 顶层形状迁移)
- `pnpm test:ci` 全绿且 skipped 数不增加;本计划不新增 `it.skip`
- 全部改动文件:`pnpm exec eslint --fix` 后 `pnpm exec eslint` 0 错误;`pnpm exec vue-tsc --noEmit` 按改动文件路径过滤 0 类型错误
- `git status` 最终仅含本计划 `files_modified` 内的文件改动与 SUMMARY
- SUMMARY 可见:A/B 修复记录、存档形状变更(A2)、`#06-17-1`/`#06-17-2` 无 ledger entry 对照、C–H 六条未修清单、相邻缺口 A5/A6、Phase 7 重开与 `07-VERIFICATION.md` 失效须重验证
- SUMMARY 可见:A/B 修复记录、引用不可变(`attachAttribute` 删除 + `readonly` + 用例删除)、注册表归属选项 A(`clone()` 携带注册表)、存档形状变更(A2)、`#06-17-1`/`#06-17-2` 无 ledger entry 对照、C–H 六条未修清单、相邻缺口 A6(A5 已随删除关闭)、Phase 7 重开与 `07-VERIFICATION.md` 失效须重验证
</verification>
<success_criteria>
1. `#06-17-1` 闭合:三档压缩下 `hero.loadState(saved, compression)` 后 `getFinalAttribute('atk')` 与读档前一致(装备加成不再丢失),且 `getModifiableAttribute()` 读档前后为同一实例
2. `#06-17-2` 闭合:`hero.loadState` 之后 `enemyContext.getBindedHero()` 与 `hero.getModifiableAttribute()` 同实例并反映读档值,全程无任何重绑调用
3. `HeroAttribute<THero> implements ISaveableContent<IHeroAttributeSave<THero>>`:`saveState` 深拷贝基础值 + 枚举保存启用修饰器,`loadState` 原地重置并重建,实例身份与 final 一致性均由回归锁定
4. `HeroState.saveState` 委托属性存档、`loadState` 委托属性原地读档且不再替换实例;`IHeroStateSave` 形状变更(`attribute: IHeroAttributeSave<THero>`、无顶层 `modifiers`)已落地并在 SUMMARY 登记
5. 装备修饰器不进属性存档,注册表命中时加成不翻倍(回归对照通过)
6. C–H(`#06-17-3..8`)与相邻缺口(`attachAttribute`、`clone` 簿记)明确登记为未修,未被静默忽略;渲染端、`data-fallback`、`data-system` 战斗代码、零断言 perf 文件一行未动
7. Phase 7 重开已登记:`ROADMAP.md` 计划数/Wave/进度状态更新,SUMMARY 说明 `07-VERIFICATION.md` 结论失效并须重跑验证
8. 门禁三步与 `pnpm test:ci` 全过,提交按 D-13 的根因族粒度入库
4. `HeroState.saveState` 委托属性存档、`loadState` 委托属性原地读档且不再替换实例;`attribute` 为 `readonly` 且 `attachAttribute`(接口 + 实现 + 其用例)已删除;`IHeroStateSave` 形状变更(`attribute: IHeroAttributeSave<THero>`、无顶层 `modifiers`)已落地并在 SUMMARY 登记
5. 注册表由 `HeroAttribute` 拥有(`IHeroAttribute` 提供注册 API、`HeroState` 薄委托、`clone()` 携带注册表),四处既有 `hero.registerModifier(...)` 调用点零改动
6. 装备修饰器不进属性存档,注册表命中时加成不翻倍(回归对照通过)
7. C–H(`#06-17-3..8`)与相邻缺口(`clone` 簿记)明确登记为未修,未被静默忽略(原 A5 换绑缺口已由删除 `attachAttribute` 关闭);渲染端、`data-fallback`、`data-system` 战斗代码、零断言 perf 文件一行未动
8. Phase 7 重开已登记:`ROADMAP.md` 计划数/Wave/进度状态更新,SUMMARY 说明 `07-VERIFICATION.md` 结论失效并须重跑验证
9. 门禁三步与 `pnpm test:ci` 全过,提交按 D-13 的根因族粒度入库
</success_criteria>
## Artifacts this phase produces
| 类型 | 路径 | 内容 |
|------|------|------|
| 生产源码(修改) | `packages-user/data-base/src/hero/types.ts` | 新增 `IHeroAttributeSave<THero>`(`values` + `modifiers`);`IHeroAttribute` 扩展 `ISaveableContent` 与注册表成员;`IHeroStateSave.attribute` 改为 `IHeroAttributeSave<THero>` 并移除顶层 `modifiers` |
| 生产源码(修改) | `packages-user/data-base/src/hero/attribute.ts` | 修饰器工厂注册表(默认选项 A);`saveState` 深拷贝基础值 + 保存启用修饰器枚举;`loadState` 原地重置基础值、清空并重建修饰器、逐键重算 final;`clone()` 复制注册表条目 |
| 生产源码(修改) | `packages-user/data-base/src/hero/state.ts` | `saveState` 委托 `attribute.saveState`;`loadState` 委托 `attribute.loadState` 且不再替换 `this.attribute`;注册表方法委托属性;移除未使用 import |
| 生产源码(修改) | `packages-user/data-base/src/hero/equipment.ts` | `loadEquipEffect` 以 `save = false` 挂载装备修饰器(装备加成只经重新装备恢复) |
| 测试(修改) | `packages-user/data-base/src/hero/attribute.test.ts` | 属性自身存读档:三档恢复、实例身份、存档禁用、快照独立 |
| 生产源码(修改) | `packages-user/data-base/src/hero/types.ts` | 新增 `IHeroAttributeSave<THero>`(`values` + `modifiers`);`IHeroAttribute` 扩展 `ISaveableContent` 并获得注册表成员;`IHeroStateSave.attribute` 改为 `IHeroAttributeSave<THero>` 并移除顶层 `modifiers`;`IHeroState` 删除 `attachAttribute` |
| 生产源码(修改) | `packages-user/data-base/src/hero/attribute.ts` | **拥有**修饰器工厂注册表(用户裁决选项 A);`saveState` 深拷贝基础值 + 保存启用修饰器枚举;`loadState` 原地重置基础值、清空并重建修饰器、逐键重算 final;`clone()` 复制注册表条目 |
| 生产源码(修改) | `packages-user/data-base/src/hero/state.ts` | `attribute` 改 `readonly`;删除 `attachAttribute`;`saveState` 委托 `attribute.saveState`;`loadState` 委托 `attribute.loadState` 且不再替换 `this.attribute`;注册表方法薄委托属性;移除未使用 import |
| 生产源码(修改) | `packages-user/data-base/src/hero/equipment.ts` | `loadEquipEffect` 以 `save = false` 挂载装备修饰器(装备加成只经同一活实例上的重新装备恢复) |
| 测试(修改) | `packages-user/data-base/src/hero/attribute.test.ts` | 属性自身存读档:三档恢复、实例身份、存档禁用、快照独立、注册表随 `clone()` 传递 |
| 测试(修改) | `packages-user/data-base/src/hero/saveLoad.test.ts` | `#06-17-1` 三档回归(加成保留 + 同实例 + 读档后再装备)+ 注册表命中不翻倍对照 + 一处形状迁移断言 |
| 测试(修改) | `packages-user/data-state/test/dataClosure.test.ts` | `#06-17-2` 战斗侧同引用回归(三档) |
| 测试(修改) | `packages-user/data-base/src/hero/state.test.ts` | 删除 `attachAttribute` 用例(能力已移除);保留 `getIsolatedAttribute` 与注册表薄委托(码 116)用例 |
| 测试(修改) | `packages-user/data-state/test/dataClosure.test.ts` | `#06-17-2` 战斗侧同引用回归(三档,全程无重绑) |
| 测试(修改) | `packages-user/data-state/test/saveablesRoundTrip.test.ts` | 快照分离断言迁移到 `attribute.values` |
| 追踪(修改,编排层负责) | `.planning/ROADMAP.md` | Phase 7 计划列表加入 07-09、计划数 8/9、新增 Wave 9、进度表状态改回未完成 |
| 摘要 | `.planning/phases/07-data-fixes/07-09-SUMMARY.md` | 同引用修法记录、形状变更(A2)、无 ledger entry 对照、C–H 未修清单、相邻缺口 A5/A6、Phase 7 重开与验证失效说明、门禁与用例计数 |
| 摘要 | `.planning/phases/07-data-fixes/07-09-SUMMARY.md` | 同引用修法记录、引用不可变(`attachAttribute` 删除 + `readonly` + 用例删除)、注册表归属选项 A、形状变更(A2)、无 ledger entry 对照、C–H 未修清单、相邻缺口 A6(A5 已关闭)、Phase 7 重开与验证失效说明、门禁与用例计数 |
<output>
Create `.planning/phases/07-data-fixes/07-09-SUMMARY.md` when done