mirror of
https://github.com/motajs/template.git
synced 2026-10-02 00:20:18 +08:00
docs(07-15): create review-recheck fix plan (CR-01/WR-01/WR-02/WR-03)
This commit is contained in:
parent
ceb5d1e9ad
commit
fa8d9563d6
519
.planning/phases/07-data-fixes/07-15-PLAN.md
Normal file
519
.planning/phases/07-data-fixes/07-15-PLAN.md
Normal file
@ -0,0 +1,519 @@
|
||||
---
|
||||
phase: 07-data-fixes
|
||||
plan: 15
|
||||
type: execute
|
||||
wave: 15
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- packages-user/data-base/src/hero/equipment.ts
|
||||
- packages-user/data-base/src/hero/equipment.test.ts
|
||||
- packages-user/data-common/src/replay/array.ts
|
||||
- packages-user/data-common/src/replay/array.test.ts
|
||||
- packages-user/data-base/src/hero/attribute.ts
|
||||
- packages-user/data-base/src/hero/attribute.test.ts
|
||||
autonomous: false
|
||||
gap_closure: true
|
||||
requirements:
|
||||
- FIX-01
|
||||
estimate:
|
||||
tokens: 52000
|
||||
raw_tokens: 52000
|
||||
tasks: 6
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**CR-01 按 Task 0 Q1 裁决闭合**:`HeroEquipment.compareEquip` 的差值始终等于 `data-base/src/hero/types.ts:824-834` 的契约「装备 A 时的属性减装备 B 时的属性」,**包含「被比较的两件装备中有一件正是当前现役槽位里的装备」这一场景**;见证锚点(base atk=10、英雄侧 `ValueModifier(7, -1)`、现役剑 `atk + 5`,活属性 final `atk = 22`、另一把斧 `atk + 12`):`compareEquip(sword, axe, 0).atk === -7` 且对称调用 `compareEquip(axe, sword, 0).atk === +7`"
|
||||
- "**CR-01 的观测见证是「不再出现告警 108」**:修复前该场景下 `HeroAttribute.addModifier`(`attribute.ts:195-199`)因修饰器 `owner` 已绑定活属性而拒绝并告警 108,修复后比较过程**不产生任何 108**(用例用 `logger.catch` 的 `info` 码表断言其不含 108),且活属性上的原修饰器 `owner` 在调用前后始终指向活属性、其在活属性上的索引始终非负"
|
||||
- "**CR-01 的实现口径(Q1=A)**:每个被比较装备的修饰器在加入比较克隆之前都先 `clone()`,比较完成后按**同一批克隆对象**逐个删除;活属性上已绑定的原修饰器对象**从不**被加入比较克隆,也**从不**被 `bindAttribute(null)` 解绑(B 分支被明确排除);`compareEquip` 的返回类型、两个 146 早退分支、现役槽位删除块(`equipment.ts:274-290`)与 `:319` 的既有注释逐字保留"
|
||||
- "**WR-01 按 Task 0 Q2 裁决闭合**:`normalizeParam` 对无法编码的参数类型发告警 148 后返回 `null`,与该方法自身 jsDoc「当参数无法用现有编码表示时返回 `null`」及 `normalizeParamList`(`array.ts:318-335`)既有的「丢弃的项不保留占位」语义一致——该参数不进入参数计数、不写入任何字节、也不推进参数字节游标"
|
||||
- "**WR-01 的外部可观测后果**:`array.add(1, [undefined])` 后该命令的参数计数字节为 0、`array.get(0).params` 为 `[]`;随后 `array.add(2, [20])` 的字节不被第一步覆盖(`array.get(1).params` 为 `[20]`),读流两步依次读回 `[]` 与 `[20]` 且流索引为 1、2(修复前第一步计数为 1 并读到第二步的字节)"
|
||||
- "**WR-02 按 Task 0 Q3 裁决闭合**:构造函数的两个扩容乘数校验由 `< 1` 收紧为 `<= 1`,与告警 149 自身文案「need to be greater than 1, but got $2」一致,非法值仍复用既有回退倍率 2;乘数恰为 1 时 `checkBufferExpand`(`array.ts:158-211`)必然终止(不再以相同实参自递归)"
|
||||
- "**WR-02 的外部可观测后果**:`commandExpandMultiplier`/`paramExpandMultiplier` 为 1 由「被静默接受(随后首次扩容即栈溢出)」变为「告警 149 两次并回退为 2」,即在 `initCommandLength: 2`/`initParamLength: 8` 下连续追加 15 步可完整完成,且每一步都能按原顺序读回"
|
||||
- "**WR-03 按 Task 0 Q4 裁决闭合**:`HeroAttribute.clone()`(`cloneModifier` 非 false 时)把克隆修饰器接入与 `addModifier`(`attribute.ts:190-211`)同一套簿记——写入 `cloned.modifierName`、`bindAttribute(cloned)`、继承源修饰器的存档开关(`modifierNosave`)、并以 `recalculateAttribute(name)` 重算该属性;`cloneModifier: false` 的早退分支与注册表复制(`:303-306`)逐字保留"
|
||||
- "**WR-03 的外部可观测后果(克隆属性的可序列化面变化)**:`cloned.iterateModifiers()` 能列出全部克隆修饰器、`cloned.getModifierIndex(克隆修饰器)` 返回非负、克隆修饰器 `setValue(...)` 经 `owner` 通知克隆重算 `getFinalAttribute`、`cloned.saveState(...)` 的 `modifiers` 不再为空数组——修复前这四处全为退化行为(空遍历、-1、final 陈旧、`modifiers: []`)"
|
||||
- "本计划为修复计划(D-10 的授权例外 A1):4 条缺陷各有**正确预期**回归用例(共新增 5 条 `it`),每条在修复前失败、修复后通过;**既有断言一字不改**、不新增 `it.skip`、不删除既有用例、不为转绿弱化断言"
|
||||
- "`pnpm test:ci` 0 失败、跳过项仍恰为 1 条(`packages-user/data-base/src/hero/equipment.test.ts` 的码 147 用例,D-06);本计划不新增测试文件,故文件数不变、通过数按新增用例数上升(基线须在执行时重录,见 objective 的基线段)"
|
||||
- "四条缺陷各一次原子提交(D-13),提交一律按显式路径 `git add <file>`;本计划 `files_modified` 之外的任何工作树改动(含既有未暂存修改/删除与未跟踪文件)一律不触碰、不暂存、不提交"
|
||||
artifacts:
|
||||
- path: packages-user/data-base/src/hero/equipment.ts
|
||||
provides: "`compareEquip`(`:255-331`)按 Q1=A 改为「克隆加入 / 按同一批克隆删除」:新增局部数组收集 `[name, copy]`,加入端 `const copy = modifier.clone()` 后 `clone.addModifier(name, copy)`,删除端遍历该数组 `clone.deleteModifier(name, copy)`;现役槽位删除块(`:274-290`)与方法返回值、146 早退分支、`:319` 注释零改动"
|
||||
- path: packages-user/data-base/src/hero/equipment.test.ts
|
||||
provides: "CR-01 回归 1 条(现役装备参与比较的两个方向 + 无 108 见证 + 活属性原修饰器未被解绑)+ `:1` 文件头覆盖说明的追加同步"
|
||||
- path: packages-user/data-common/src/replay/array.ts
|
||||
provides: "`normalizeParam`(`:230-316`)的回落分支改为发 148 后 `return null`(删去 `paramType: 0 / paramValue: 0 / byteLength: 0` 的记录);构造函数(`:113-127`)两处乘数校验由 `< 1` 收紧为 `<= 1`;其余编解码路径与所有既有注释零改动"
|
||||
- path: packages-user/data-common/src/replay/array.test.ts
|
||||
provides: "WR-01 回归 1 条(丢弃后计数为 0、后续命令字节不被覆盖、读流对齐)+ WR-02 回归 1 条(乘数恰为 1 时告警 149 两次且扩容终止、15 步全部读回)+ `:1` 文件头覆盖说明的追加同步"
|
||||
- path: packages-user/data-base/src/hero/attribute.ts
|
||||
provides: "`clone()`(`:296-314`)的 `cloneModifier` 分支按 Q4=A 落笔四处簿记:`cloned.modifierName.set(copy, name)`、`copy.bindAttribute(cloned)`、源修饰器不可存档时 `cloned.modifierNosave.add(copy)`、每个属性名一次 `cloned.recalculateAttribute(name)`;注册表复制与 `cloneModifier: false` 早退逐字保留"
|
||||
- path: packages-user/data-base/src/hero/attribute.test.ts
|
||||
provides: "WR-03 回归 2 条(遍历/定位/`setValue` 通知/存档四项簿记;克隆修饰器继承存档开关)+ `:1` 文件头覆盖说明的追加同步"
|
||||
key_links:
|
||||
- "`equipment.ts:297-300` 与 `:310-313` 的 `clone.addModifier(name, modifier)` ↔ `attribute.ts:195-199` 的 `if (modifier.owner)` 拒绝分支(告警 108)↔ `equipment.ts:80-86` `loadEquipEffect` 已用 `addModifier(name, modifier, false)` 把这些同一对象绑定到活属性——「活对象不可复用到比较克隆」是 CR-01 的唯一根因"
|
||||
- "`equipment.ts:274-290` 的现役删除块(`this.attribute.getModifierIndex(modifier)` 取活属性索引 + `clone.getModifiers(name)[index]` 取克隆同槽位对象)↔ 新增的克隆加入/删除配对——删除块按「克隆体同槽位顺序」定位目标的口径(07-12 Q4 (i) 已定)必须保持;新增的克隆不得改变克隆属性数组的同槽位顺序(`addModifier` 按优先级降序稳定排序,源数组本就按该序,故同序)"
|
||||
- "`array.ts:230-316` `normalizeParam` 的回落分支 ↔ `array.ts:322-335` `normalizeParamList` 的 `if (param) normalized.push(param)` ↔ `array.ts:341-343` `calculateParamsLength` ↔ `array.ts:411` 的 `index += param.byteLength` ↔ `array.ts:443`/`:474`/`:491`/`:570` 的 `paramUsed` 推进——只有返回 `null` 才能让「参数计数 = 实际写入个数 = 字节推进」三者自洽"
|
||||
- "`array.ts:113-127` 的两处乘数校验 ↔ `array.ts:164-211` `checkBufferExpand` 的 `expanded` 重入(`:207-210`)↔ 码表 149 文案(`packages/common/src/logger.json`)——`Math.ceil(n * 1) === n` 使 `nextSize === n`,`expanded` 恒真且实参不变,构成不退化的自递归"
|
||||
- "`attribute.ts:296-314` `clone()` ↔ `attribute.ts:190-211` `addModifier` 的四处簿记(`modifierList` 插入 + 排序、`modifierName.set`、`modifierNosave`、`bindAttribute(this)` + `markDirty`)↔ `attribute.ts:169-173` `iterateModifiers`(读 `modifierName`)↔ `:182-188` `getModifierIndex`(读 `modifierName` + `modifier`)↔ `:248-252` `markModifierDirty`(读 `modifierName`)↔ `:328-339` `saveState`(读 `iterateModifiers` + `getModifierSaveEnabled`)"
|
||||
- "`attribute.ts:299` 的 `const { cloneModifier = true } = option` 早退 ↔ `:303-306` 的注册表复制——`cloneModifier: false` 路径本计划零新增,既有用例 `carries the modifier registry into cloned attributes`(`attribute.test.ts:417-431`)逐字保留"
|
||||
- "`attribute.clone()` 的下游消费点 `equipment.ts:271`(`compareEquip` 的比较克隆)与 `data-system/src/combat/damage.ts:152`(`getModifiableClone()` 的会心伤害搜索)——本计划不修改任一消费点,只让克隆体自身簿记自洽"
|
||||
assumptions:
|
||||
- "A1(D-10 例外,本计划授权):本计划为修复计划,为 4 条已确认缺陷新增**正确预期**回归是必需项(共 5 条 `it`,分布见 artifacts);既有断言一字不改;仅同步各测试文件 `:1` 的文件头覆盖说明(追加本轮覆盖字样)与新增用例的 `it` 前单行中文注释(dev.md:86)"
|
||||
- "A2(不新增对外契约):修法只在既有方法体内完成,不改任何接口声明或类型(`data-base/src/hero/types.ts`、`data-common/src/replay/types.ts` 逐字不改)、不新增/修改/复用告警码、不改 `packages/common/src/logger.json`、不新增类/方法/字段/导出(生产代码零新增符号)"
|
||||
- "A3(项目未发布,与 07-09 A2 同口径):录像参数格式与存档形状不因本计划变更;本计划不新增版本字段、不留兼容分支"
|
||||
- "A4(范围守卫):只修 `07-VERIFICATION.md` 的 Review-Recheck Gaps 所载 4 条(CR-01 / WR-01 / WR-02 / WR-03);`07-REVIEW-recheck.md` 的 4 条 Info(IN-01..IN-04)已由用户直接提交修完(`34ba9e8`、`219ac49`、`3a3a6ac`、`a2a8e6e`),本计划对 `packages-user/data-common/src/replay/system.ts`、`packages-user/data-base/src/flag/**`、`packages-user/data-system/src/combat/mapDamage.ts` **零触碰**;`07-REVIEW.md` 的既有裁决(WR-04 设计如此、IN-04/IN-05 不在清单、`#06-05-3` 保留码 147)不变"
|
||||
- "A5(WR-01 的丢弃语义有同口径先行见证):`array.test.ts:665`「drops the unrepresentable bigint param and keeps the rest of the command」与 `:688`「keeps the read stream aligned after dropping an unrepresentable bigint」已确立「`normalizeParam` 返回 `null` → `normalizeParamList` 丢弃 → 计数与偏移自洽」的语义;WR-01 的修复与新增用例必须与之同口径,**不得**引入第二种「占位」语义(Q2=B 分支即被排除)"
|
||||
- "A6(CR-01 的非现役路径零回归可复算):既有用例 `diffs the final attributes of two equipment instances`(`equipment.test.ts:419-432`)与 `keeps foreign modifiers when the equipped instance was rebuilt`(`:435-467`)在修复后数值不变(逐条复算为 `atk 5 / def -3` 与 `atk 12 / def -3`),故二者可作为「非现役比较路径零回归」的见证;若执行期复算不符,按 D-09 退出并修订计划"
|
||||
- "A7(WR-03 的落笔方式):采用「按 `addModifier` 的四处簿记逐项直接落笔」(同一类内访问私有字段)而非调用泛型方法 `cloned.addModifier(name, copy)`——后者会因 `this.modifier` 的 `IHeroModifier[]`(默认泛型 `unknown`)无法满足 `IHeroModifier<THero[K]>` 而必须补 `// @ts-expect-error 泛型无法推导`(dev.md:92 允许,但属额外声明面);逐项落笔不引入类型断言/指令,且与 `07-REVIEW-recheck.md:170-183` 的建议片段一致"
|
||||
- "A8(flag 假设,必须保留在册,不得视为已解决):本阶段无 SPEC,故 specless 探针回退运行;该探针对 `FIX-01` 只返回一行 `unclassified — review manually`(`coverage.unresolved = 1`),**未**给出可判定的边界/禁止项分类,也**不得**用任何回填(backstop)自动消化。本计划以「4 条缺陷各自的正确预期回归用例 + 文件级三步门禁 + `pnpm test:ci` 0 失败」作为 `FIX-01` 在本轮的唯一可操作验证面,并明示「探针对 `FIX-01` 的未决分类状态保留在册」"
|
||||
- "A9(无用户环境准备):本计划无外部服务、无环境变量、无仪表盘配置、无需人工在仓外执行任何步骤,故 frontmatter 不含 `user_setup`"
|
||||
- "A10(比较克隆所用 `clone()` 的隐含前提):CR-01 的实现依赖「被比较修饰器的 `clone()` 返回一个 `owner` 为 `null` 的新对象」(`BaseHeroModifier` 的子类默认如此,`ValueModifier`/`PercentageModifier` 见 `modifier.ts:18-19`/`:37-38`);若某装饰修器的 `clone()` 返回已绑定 owner 的对象,`addModifier` 会按既有 108 分支拒绝——本计划不为此新增分支,回归用例的「不含 108」断言即为该前提的守卫"
|
||||
prohibitions:
|
||||
- "不得修改任何既有 jsDoc 或既有注释(用户 2026-09-17 长期规则 + AGENTS.md)。本计划唯一被允许的注释落笔:各测试文件 `:1` 文件头覆盖说明的**追加**、新增用例 `it` 前的单行中文注释(dev.md:86)、以及实现确需说明非显然「为什么」时**新增**的单行注释(dev.md:84);既有注释行一律逐字保留"
|
||||
- "不得新增 `it.skip`、不得弱化或改写既有断言、不得删除既有用例、不得为转绿改写既有期望值"
|
||||
- "不得修改 `packages-user/data-base/src/hero/types.ts` 与 `packages-user/data-common/src/replay/types.ts`(接口与 jsDoc 逐字不改);不得新增/修改告警码;不得改动 `packages/common/src/logger.json`"
|
||||
- "不得触碰 `packages-user/data-common/src/replay/system.ts`、`packages-user/data-base/src/flag/**`、`packages-user/data-system/src/combat/mapDamage.ts`(用户的 IN-01..IN-04 修复已提交)"
|
||||
- "不得新增依赖、不得改 `package.json`/`pnpm-lock.yaml`;不得新建源码或测试文件(本计划零新增文件,只在既有 6 个文件内改动);不得使用 `try-catch-finally`(dev.md:121)"
|
||||
- "不得使用连续 `as` 断言(`as unknown as …`)、不得新增 `import type`、不得新增字符串特殊标识符、不得新增类/方法/字段/导出"
|
||||
- "不得暂存或提交 `files_modified` 之外的任何路径(含工作树中已存在的无关未暂存修改/删除与未跟踪文件):提交一律 `git add <显式路径>`;禁止 `git add -A`/`git add -a`、`git stash`、`git clean`、`git reset --hard`"
|
||||
- "Task 0 的 `<record>` 为空时**不得**进行任何文件修改(未裁决即停);执行期若发现与裁决冲突,暂停并请用户澄清,不得自行改用任何【未选】分支或另辟他法(D-09)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
闭合 `07-VERIFICATION.md` 的 `### Review-Recheck Gaps` 所载 4 条缺陷(CR-01 / WR-01 / WR-02 / WR-03),即 `07-REVIEW-recheck.md` 在第二次修复批次(07-09..07-13)上新增的 1 Critical + 3 Warning:让每一处改动与**同文件内已写明的契约**(jsDoc、码表文案、兄弟路径语义)重新对齐,并为每条配上正确预期的回归用例。
|
||||
|
||||
现状(逐条锚点,行号基于本计划所读版本):
|
||||
|
||||
1. **CR-01(critical,`packages-user/data-base/src/hero/equipment.ts:255-331`)** `compareEquip` 用 `clone.addModifier(name, modifier)`(`:297-300`、`:310-313`)把**被比较装备自己的修饰器对象**加入比较克隆。而这些对象正是 `loadEquipEffect`(`:80-86`)已经用 `attribute.addModifier(name, modifier, false)` 绑定到活属性上的同一批对象;`HeroAttribute.addModifier`(`attribute.ts:195-199`)对 `owner` 已设置的修饰器发告警 108 并直接返回——于是「被比较的装备恰是现役槽位里的那件」时,它的加成完全不进入比较克隆。实算(base atk 10、英雄侧 `ValueModifier(7, -1)`、现役剑 `atk + 5` 使活属性 final 为 22、另一把斧 `atk + 12`):`compareEquip(sword, axe, 0)` 返回 `-12`,而 `types.ts:824-834` 的契约「输出装备 A 时的属性减装备 B 时的属性」要求 `-7`;对称调用返回 `+12` 而契约要求 `+7`。既有两条 `compareEquip` 用例之所以通过,只是因为它们比较的两件装备都不是现役那件。`compareEquip` 目前只有测试调用(属 `IHeroEquipment` 的公开 API),影响为潜伏的正确性缺陷。
|
||||
2. **WR-01(warning,`packages-user/data-common/src/replay/array.ts:310-315`)** `normalizeParam` 对 `boolean | number | bigint | string` 之外的值(如 `undefined`)发告警 148 后返回一条 `{ paramType: 0, paramValue: 0, byteLength: 0 }` 的记录,与该方法自身 jsDoc(`:228`「当参数无法用现有编码表示时返回 `null`」)及 `normalizeParamList`(`:318-335`,注释明写「丢弃的项不保留占位」)都矛盾。后果是编码/解码失步:该记录因非空而被保留并计入 `normalized.length`(故命令的参数计数把它算进去),`setParamArray` 的 type-0 分支实际写入 2 字节(`:377-379`),而游标 `index += param.byteLength` 推进 0——`calculateParamsLength` 少算 2 字节,后续所有命令的 `indexArray` 都指向错误字节。复现:`array.add(1, [undefined])` 后 `paramUsed` 仍为 0,`array.add(2, [20])` 覆盖第一步的字节,`array.get(0).params` 读成 `[20]`。
|
||||
3. **WR-02(warning,`array.ts:158-211`)** 构造函数只拒绝 `< 1` 的扩容乘数(`:113-127`,且告警文案自己写着「need to be greater than 1」)。乘数恰为 1 时 `Math.ceil(byteLength * 1) === byteLength`,`nextSize` 等于当前大小,于是分配了一个同尺寸缓冲、置 `expanded = true`,并在 `:207-210` 以**完全相同的实参**自递归——首次需要扩容即无限递归/栈溢出。默认(`system.ts:37-38` 用 1.2)与测试(2)都绕开了它。
|
||||
4. **WR-03(warning,`packages-user/data-base/src/hero/attribute.ts:296-314`)** `clone()` 把 `v.clone()` 出来的修饰器**直接**塞进 `cloned.modifier`,既不写 `cloned.modifierName`、也不 `bindAttribute(cloned)`、也不继承 `modifierNosave`。所有消费 `modifierName` 的克隆路径因此全是错的:`cloned.iterateModifiers()` 为空 → `cloned.saveState()` 序列化出 `modifiers: []`(静默丢掉全部克隆修饰器);`cloned.getModifierIndex(m)` 返回 `-1`;克隆修饰器 `setValue(...)` 因 `owner` 为 `null` 无法通知克隆,`cloned.getFinalAttribute(name)` 在改动后陈旧。该路径经公开的 `HeroState.getIsolatedAttribute()`(`state.ts:85-87`)与 `equipment.ts:271` 可达。
|
||||
|
||||
Purpose: 这四条的共同根因(`07-VERIFICATION.md:337` 已记录)是「第二批修复以让目标用例转绿为目标,而没有把契约、簿记与边界条件与同文件已写明的设计语言对齐」。修复它们让「比较两个装备」「编码一条录像参数」「构造一个可扩容的录像数组」「克隆一份勇士属性」这四个对外能力回到各自文档承诺的口径,并使每个口径都有可判定的自动化见证,从而让 `FIX-01` 的收口不再依赖人工判断。
|
||||
|
||||
Output(本计划产出物 = frontmatter `files_modified` 的 6 个既有文件,**零新建文件、零新增依赖**):`hero/equipment.ts` + `hero/equipment.test.ts`(CR-01);`replay/array.ts` + `replay/array.test.ts`(WR-01 与 WR-02);`hero/attribute.ts` + `hero/attribute.test.ts`(WR-03)。生产代码四处改动全部落在既有方法体内。
|
||||
|
||||
**Artifacts this phase produces(本计划产出物,符号级清单)**:
|
||||
|
||||
- **生产代码:零新增符号**(无新类、新方法、新字段、新导出、新告警码、新接口成员)。四处改动均为既有方法体内的实现修正:`HeroEquipment.compareEquip`、`ReplayArray.normalizeParam`、`ReplayArray` 构造函数、`HeroAttribute.clone`。
|
||||
- **新增测试用例(5 条 `it`,名称即交付契约)**:
|
||||
- `packages-user/data-base/src/hero/equipment.test.ts` — `diffs the final attributes when one compared item is currently equipped`(CR-01)
|
||||
- `packages-user/data-common/src/replay/array.test.ts` — `drops the unencodable param and keeps the later commands aligned`(WR-01)
|
||||
- `packages-user/data-common/src/replay/array.test.ts` — `warns code 149 and still expands when an expand multiplier is exactly 1`(WR-02)
|
||||
- `packages-user/data-base/src/hero/attribute.test.ts` — `keeps the modifier bookkeeping of cloned attributes`(WR-03)
|
||||
- `packages-user/data-base/src/hero/attribute.test.ts` — `keeps the save-ability of cloned modifiers`(WR-03 的存档开关一面)
|
||||
- **各测试文件 `:1` 的文件头覆盖说明追加**本轮覆盖字样(dev.md:86 的测试范围同步要求)。
|
||||
- **产物**:`.planning/phases/07-data-fixes/07-15-SUMMARY.md`(Task 5)。
|
||||
|
||||
**Phase 7 验证已失效**:`07-VERIFICATION.md`(`_Verified: 2026-09-17T10:42:49Z_`)在记录本批 Review-Recheck Gaps 后已不对应当前代码,本计划执行完毕后**必须重跑 `/gsd-verify-work`(或 `gsd-verify-work 7`)**重新出具验证结论,并复核 4 条 gap 的关闭状态。
|
||||
|
||||
Phase 7 裁决对应(逐条引用):D-01: 本计划是 Phase 7 第 15 个计划,承接 `07-VERIFICATION.md` 的 Review-Recheck Gaps(CR-01 / WR-01 / WR-02 / WR-03)。D-02: 属 hero 系统(`data-base/src/hero`,CR-01 与 WR-03,Critical 优先)与 replay 系统(`data-common/src/replay`,WR-01、WR-02),按严重度递减排序,逐条汇报、逐条门禁、逐条回滚。D-03(`before` 语义)/ D-04(越图码 128)/ D-05(码 178)/ D-06(码 147 保留)/ D-07(path 接线用户负责)/ D-08(enemy 复用映射)均不涉及。D-09: 由 Task 0 关卡承载(本计划**尚未裁决**,`<record>` 为空,执行者必须停在此处)。D-10: 本计划为修复计划,正确预期回归用例为必需项(A1,与 07-09/07-13 同一例外口径)。D-11(契约变更需同步 jsDoc):本计划**无契约变更**——CR-01 是让实现符合 `types.ts:824-834` 的既有文案,WR-01 是让实现符合方法自身 jsDoc,WR-02 是让校验符合码表 149 的既有文案,WR-03 是让 `clone()` 符合 `types.ts:117`「深拷贝此勇士属性对象」与 `iterateModifiers`/`getModifierIndex`/`saveState` 的既有语义;四条都不需要改动任何 jsDoc(且用户 2026-09-17 长期规则禁止擅自改动 jsDoc)。D-12: 沿用 06-CONTEXT D-44 文件级三步门禁。D-13: 按缺陷原子提交(Task 1..4 各一条 `fix(07-15): …`)。D-14: 这 4 条评审发现**没有** `.planning/WINDOWS.md` 账本条目——本计划**不新建**条目、不做 `fixed`/`waive`,只在 `07-15-SUMMARY.md` 登记「发现 ID → 缺陷 → 修复提交」的映射。
|
||||
|
||||
<assumption_delta_decision>
|
||||
- **noun**: N/A(本计划不改变任何核心身份模型的名词/概念,无单数→复数或近义迁移)
|
||||
- **decision**: `no-change`
|
||||
- **rationale**: 假设-增量检测器在本阶段唯一命中的信号是字面量 `fallback`,它来自包名 `packages-user/data-fallback`(legacy 兼容层,用户已删除/即将删除);该命中是**包名字符串**造成的假阳性,不是身份模型名词的迁移。本计划只改 `hero/equipment.ts`、`replay/array.ts`、`hero/attribute.ts` 及其测试,不新增、不重命名任何领域概念;故裁决为 `no-change`,不需要任何名词对齐或词汇表更新。
|
||||
- **note**: 该检测器由 orchestrator 运行(不是用户运行),故本条为流程性决策记录,不改变任何用户裁决。
|
||||
</assumption_delta_decision>
|
||||
|
||||
执行序:`depends_on: []`(Wave 15)。四条修复源码层面互不耦合(hero 侧两条、replay 侧两条),但不并波执行:本计划 `autonomous: false`,Task 0 是 D-09 的逐计划裁决关卡,且四条各自需要「文件级门禁 + 原子提交 + `pnpm test:ci` 基线核对」,与 07-13/07-14 同一节奏。
|
||||
|
||||
**隔离要求(执行前必读)**:用户正在本仓库并行工作,且工作树的改动集合在规划与执行之间会继续变化,故**不以任何固定清单为准**。**通用规则(唯一判据)**:凡是**不在**本计划 `files_modified` 六个路径内的改动,一律不触碰、不暂存、不提交;每次提交前后用 `git status --short` 核对暂存区只含本计划当次声明的文件;用户自己的改动(例如 `packages/common/src/logger.json`、`packages-user/data-*/**`、`.planning/milestone.lock` 等)无论内容如何都属用户所有,本计划只读不写,且**不得**以「工作树中某文件有改动」为由判定本计划越界——越界判定一律只看 `git diff BASE_SHA..HEAD -- <该路径>` 是否为空。计划期(2026-09-17 规划会话)最后一次观察到的无关改动为:`packages-user/data-common/src/types.ts`、`packages-user/data-system/src/path/finder.ts`、`packages-user/data-system/src/path/graph.ts`、`packages-user/data-system/src/path/graph.test.ts`、`packages-user/data-system/src/path/performance.test.ts`、`packages-user/data-system/src/path/types.ts`、`packages/common/src/logger.json`(均为修改),以及未跟踪的 `.planning/milestone.lock`——**仅作参考**,执行时以 `git status --short` 为准。
|
||||
|
||||
**基线(计划期记录,未经本次规划运行复测)**:`pnpm test:ci` = 66 文件 / 737 通过 / 0 失败 / 1 跳过;唯一跳过项是 `packages-user/data-base/src/hero/equipment.test.ts` 的码 147 用例(设计如此,D-06)。**执行前必须重录当日基线**并与本行对比:若文件数或跳过项与 737/1 的口径不符,先查明原因(本计划不新增文件、只新增用例,故文件数应不变、跳过数应仍为 1),基线异常即按 D-09 退出汇报。本计划新增 5 条 `it`,故通过数应 = 当日基线通过数 + 5,失败数必须为 0。
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
|
||||
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/REQUIREMENTS.md
|
||||
@.planning/phases/07-data-fixes/07-CONTEXT.md
|
||||
@.planning/phases/07-data-fixes/07-VERIFICATION.md
|
||||
@.planning/phases/07-data-fixes/07-REVIEW-recheck.md
|
||||
@.planning/phases/07-data-fixes/07-PATTERNS.md
|
||||
@.planning/codebase/TESTING.md
|
||||
@.planning/codebase/CONVENTIONS.md
|
||||
@dev.md
|
||||
@packages-user/data-base/src/hero/equipment.ts
|
||||
@packages-user/data-base/src/hero/equipment.test.ts
|
||||
@packages-user/data-common/src/replay/array.ts
|
||||
@packages-user/data-common/src/replay/array.test.ts
|
||||
@packages-user/data-base/src/hero/attribute.ts
|
||||
@packages-user/data-base/src/hero/attribute.test.ts
|
||||
|
||||
**执行起点提示**:本计划**尚未裁决**(Task 0 的 `<record>` 为空)。执行者**必须**在 Task 0 停下,向用户汇报 Q1..Q4 的四个口径取舍与各自的外部可观测后果,取得逐条裁决后由计划修订把裁决写进 `<record>`,再重新执行 Task 1..5。**不得**按任何「建议默认」自动推进。
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 0: 契约裁决关卡(D-09)—— CR-01 比较克隆如何取得修饰器(克隆后加入并删除 / 临时解绑再重绑 / 仅文档)、无法编码参数的处置(返回 null 丢弃 / 写入 2 字节并改文档为占位)、扩容乘数边界(收紧为 <= 1 / 保留 >= 1 但钳制为严格增大)、`clone()` 要恢复多少簿记(四处全恢复 / 仅 modifierName + bindAttribute / 停止克隆修饰器)</name>
|
||||
<files>(只读汇报,不修改任何文件)</files>
|
||||
<read_first>
|
||||
- packages-user/data-base/src/hero/equipment.ts(:74-97 `loadEquipEffect`/`unloadEquipEffect`、:153-198 `equip`、:255-331 `compareEquip`,尤其 :271 的比较克隆、:274-290 现役槽位删除块、:297-308 A 的加入-采样-删除、:310-317 B 的加入-采样、:319 的既有注释)
|
||||
- packages-user/data-base/src/hero/attribute.ts(:51-64 私有结构 `modifier`/`modifierName`/`modifierNosave`/`finalAttribute`、:169-188 `iterateModifiers`/`getModifiers`/`getModifierIndex`、:190-228 `addModifier`/`deleteModifier`、:244-264 `markDirty`/`markModifierDirty`/`setModifierSaveEnabled`/`getModifierSaveEnabled`、:296-314 `clone`、:328-339 `saveState`)
|
||||
- packages-user/data-base/src/hero/types.ts(:25-68 `IHeroModifier` 与 `clone()` jsDoc、:86-89 `IHeroAttributeCloneOption`、:116-127 `clone`/`getModifiableClone` jsDoc、:134-151 `iterateModifiers`/`getModifierIndex` jsDoc、:824-834 `compareEquip` 契约「输出装备 A 时的属性减装备 B 时的属性」)
|
||||
- packages-user/data-base/src/hero/equipStore.ts(:43-61 `rebuildModifiers` 与 `getModifiers` 返回**修饰器对象本身**)
|
||||
- packages-user/data-base/src/hero/state.ts(:77-87 `getModifiableAttribute`/`getIsolatedAttribute`——克隆的公开可达路径)
|
||||
- packages-user/data-base/src/hero/modifier.ts(:18-19 `ValueModifier.clone`、:37-38 `PercentageModifier.clone` 均返回 `owner` 为 `null` 的新对象)
|
||||
- packages-user/data-common/src/replay/array.ts(:104-141 构造函数与乘数校验、:154-211 `checkBufferExpand` 与 :207-210 的重入、:225-316 `normalizeParam` 与 :310-315 的回落记录、:318-335 `normalizeParamList`、:341-343 `calculateParamsLength`、:364-413 `setParamArray` 与 :377-379 的 type-0 分支、:429-447 `add`)
|
||||
- packages-user/data-common/src/replay/types.ts(:167-182 `IReplayArrayConfig` 的乘数成员 jsDoc、:184-215 `IReplayArray` 的 `add` 契约「向录像末尾追加一条录像步」)
|
||||
- packages-user/data-common/src/replay/system.ts(:32-43 `ReplaySystem` 的实配:初始 1000/10000、乘数 1.2、上限 1_000_000/10_000_000)
|
||||
- packages-user/data-base/src/enemy/manager.ts(:200-215 既有用法参照:复用映射注册时 `enemy.clone()`——本计划不改该文件)
|
||||
- packages-user/data-common/src/replay/array.test.ts(:611-618 既有 148 用例、:640-701 既有 152 与「丢弃」两条用例、:1028-1039 既有 149 用例、:1109-1133 既有扩容用例)
|
||||
- packages-user/data-base/src/hero/equipment.test.ts(:417-496 既有 `compareEquip` 用例群与码 146 用例)
|
||||
- packages-user/data-base/src/hero/attribute.test.ts(:293-343 克隆与进度用例群、:345-431 存读档与注册表用例群)
|
||||
- .planning/phases/07-data-fixes/07-REVIEW-recheck.md(CR-01 `:74-121`(含建议修法)、WR-01 `:125-144`、WR-02 `:146-157`、WR-03 `:159-183`)
|
||||
- .planning/phases/07-data-fixes/07-VERIFICATION.md(`### Review-Recheck Gaps` `:335-378`,含 4 条的 Missing / Impact / Fix 与 `## Recommended Fix Plans` 的 07-15 任务分解)
|
||||
- packages/common/src/logger.json(码 148「Unknown replay param type」、码 149「need to be greater than 1」、码 108「has already been added once」的**权威文案**)
|
||||
</read_first>
|
||||
<decision>
|
||||
四处修复各自的口径取舍(本计划**尚未裁决**,需用户逐条拍定后写入 `<record>`):
|
||||
(Q1,CR-01)`compareEquip` 如何取得被比较装备的修饰器,使「被比较的那件正是现役装备」时其加成能真正进入比较克隆;
|
||||
(Q2,WR-01)无法用现有编码表示的参数类型应如何处置(丢弃,还是保留一个 2 字节占位并把文档改成「占位」语义);
|
||||
(Q3,WR-02)扩容乘数的合法边界取在哪(把校验收紧到 `<= 1` 以匹配码 149 的既有文案,还是保留 `>= 1` 但把下一次尺寸钳制为严格增大);
|
||||
(Q4,WR-03)`HeroAttribute.clone()` 必须恢复多少簿记(`modifierName`、`bindAttribute`、`modifierNosave`、`recalculateAttribute` 四项全恢复,还是只恢复前两项,或干脆停止克隆修饰器)。
|
||||
</decision>
|
||||
<context>
|
||||
**本计划尚未裁决。下述四个口径中,凡改变对外可观测行为者,其后果必须在汇报中讲清;任一分支被裁定后,Task 1..4 一律按 `<record>` 执行。**
|
||||
|
||||
**Q1 的两种口径都把 CR-01 视为必须修复的正确性缺陷**(`types.ts:824-834` 是事实源,`compareEquip` 的唯一实证消费者是测试,但它是 `IHeroEquipment` 的公开 API):A 分支在比较克隆上「先克隆再加入、按同一批克隆删除」,活属性上已绑定的原对象**从不**进入比较克隆,也**从不**被解绑;B 分支先对活属性上的原对象 `bindAttribute(null)`、加完再重新 `bindAttribute(this.attribute)`。B 分支的外部可观测后果:`compareEquip` 期间活属性上的修饰器会短暂处于**未绑定**状态(若比较过程被中断或期间有任何一次 `setValue`/`markModifierDirty` 观测,活属性的簿记会瞬时不一致),且必须精确恢复原 owner——A 分支没有这个窗口,代价是每个被比较修饰器多一次 `clone()`(`ValueModifier`/`PercentageModifier` 的 clone 是 `new` 一个等价对象)。C 分支(不改行为、只写文档)会把一个**能算出错误差值**的公开 API 留成「调用方必须自行避免」的约定,与「本轮修复已确认缺陷」的指令冲突。
|
||||
|
||||
**Q2 改变对外可观测的参数计数与字节布局**(这是两个分支的分水岭):A 分支让「命令参数计数 = 实际写入的参数个数 = 参数字节推进」三者自洽(`array.ts:438`/`:469`/`:487`/`:566` 写入的计数取 `normalized.length`,`:411` 的游标取 `byteLength` 之和),代价是**该参数彻底消失**(`array.get(i).params` 少一项),语义为「无法表示的参数被丢弃」——这正是 `array.ts:328` 的既有注释「丢弃的项不保留占位」与方法自身 jsDoc 所写的口径,且 `array.test.ts:665`/`:688` 已为 bigint 溢出确立了同款语义(A5)。B 分支保留一个 type-0 的 2 字节占位(解码回来是一个 `false`),好处是参数**位置**不移动、`params.length` 与写入前一致;代价是必须改掉 `normalizeParam` 与 `normalizeParamList` 两处既有 jsDoc/注释(用户 2026-09-17 长期规则禁止擅自改动 jsDoc,且「丢弃」注释要一并改写),并且解码端会把一个「未知类型」静默还原成布尔 `false`——与码 148 的文案「Unknown replay param type」相比,A 的「丢弃并告警」语义更诚实,也与该文件已确立的「无法表示即丢弃」范式一致。
|
||||
|
||||
**Q3 收紧被接受的配置范围**(这是两个分支的分水岭):A 分支把 `< 1` 改成 `<= 1`,使「乘数恰为 1」从「被静默接受、随后首次扩容栈溢出」变为「告警 149 两次并回退为 2」——与码 149 的既有文案「need to be greater than 1, but got $2」逐字一致;已核查全仓:`logger.warn(149` 只有 `array.ts:115`/`:123` 两处写入点,测试只有 `array.test.ts:1030` 一条断言(用乘数 0),生产实配只有 `system.ts:37-38` 的 1.2,**没有任何既有配置或既有用例使用乘数 1**,故收紧不破坏任何既有绿用例。B 分支保留 `>= 1` 的接受范围、改为把 `nextSize` 钳制为 `Math.max(current + 1, …)`,好处是「乘数 1 表示不扩容」这一直觉语义得以保留(实际表现为「最少增长 1」),代价是码 149 的文案与校验条件将永久不一致(文案说必须大于 1,实现却接受 1),且 `:170-173`/`:193-196` 两处 `Math.min(Math.ceil(…)…)` 都要加 `Math.max` 钳制、改动面更大。
|
||||
|
||||
**Q4 改变克隆属性被序列化出来的内容**(这是最容易被外部观测的分支):A 分支恢复 `addModifier` 的全部四处簿记——`modifierName`(决定 `iterateModifiers` 能列出谁、`getModifierIndex` 能否定位、`markModifierDirty` 能否反查属性名)、`bindAttribute(cloned)`(决定克隆修饰器 `setValue` 后克隆的 `finalAttribute` 是否重算)、`modifierNosave`(决定克隆修饰器的**存档开关**是否与源一致,即 `clone.saveState().modifiers` 的条数)、`recalculateAttribute`(决定克隆的 final 与 base 一致)。后果:`cloned.saveState()` 从 `modifiers: []` 变为包含全部(未被标为不存档的)克隆修饰器;`cloned.iterateModifiers()` 从空变为全量;`cloned.getModifierIndex(...)` 从恒为 `-1` 变为真实索引;对克隆修饰器 `setValue(...)` 从「克隆 final 陈旧」变为「克隆重算」。克隆的下游消费点 `equipment.ts:271`(CR-01 的比较克隆,一次性使用)与 `data-system/src/combat/damage.ts:152`(`getModifiableClone()` 的会心伤害搜索循环,只用基础属性读写、不触碰修饰器)都不读取克隆的存档面,故 A 分支对既有行为是纯补齐。B 分支只恢复 `modifierName` + `bindAttribute`,仍不继承存档开关——于是克隆属性的「哪个修饰器不入档」与源属性不一致(源上被标为不存档的装备修饰器在克隆上会重新变回可存档)。C 分支(`clone()` 不再复制修饰器)会使 `getIsolatedAttribute()` 与 `compareEquip` 的比较语义直接丢失修饰器,等于把缺陷换成更大缺陷,不符合 `types.ts:117` 的「深拷贝此勇士属性对象」。
|
||||
|
||||
**观察(未确认、不登记,仅请用户在 Q3 裁决时知悉)**:Q3=A 只覆盖「乘数恰为 1」这一条被登记的分支。使 `Math.min(Math.ceil(size * multiplier), max)` 等于当前 size 的其它配置(例如 `initCommandLength: 0` 或 `initParamLength: 0`)同样会让 `expanded` 恒真、以相同实参自递归。该情形未被 `07-REVIEW-recheck.md` 登记,按 AGENTS.md「发现问题必须经用户确认后才可记录」,本计划**只作为观察提出、不登记为缺陷、不修**。若用户希望一并封口,请在 Q3 裁决中改选 B(严格增大的钳制口径)。
|
||||
</context>
|
||||
<options>
|
||||
<option id="q1-clone">
|
||||
<name>Q1 选项 A:【建议默认】每个被比较装备的修饰器先 `clone()` 再加入比较克隆,比较完成后按**同一批克隆对象**删除(活属性上已绑定的原对象从不进入克隆、从不被解绑)</name>
|
||||
<pros>与 `07-REVIEW-recheck.md:101-121` 的建议修法一致;活属性的簿记在整个比较过程中零扰动(原修饰器的 `owner` 与索引始终不变),比较期间不存在任何「未绑定窗口」;改动只在 `compareEquip` 方法体内(加入端加一次 `clone()`、删除端改为遍历收集到的克隆数组),现役删除块与全部既有注释零改动</pros>
|
||||
<cons>每个被比较修饰器多一次 `clone()`(比较是 UI 打开对比面板时的一次性调用,非热点循环);依赖「`clone()` 返回 `owner` 为 `null` 的新对象」这一既有前提(A10),若某装饰修器违反该前提,`addModifier` 会按既有 108 分支拒绝——回归用例的「不含 108」断言即是该前提的守卫</cons>
|
||||
</option>
|
||||
<option id="q1-unbind">
|
||||
<name>Q1 选项 B:加入前对活属性上的原修饰器 `bindAttribute(null)`,加入并删除后重新 `bindAttribute(this.attribute)`</name>
|
||||
<pros>不新增 `clone()` 调用;比较克隆上承载的仍是同一批原对象(对象身份不变)</pros>
|
||||
<cons>比较期间活属性簿记被临时破坏:原修饰器在活属性上仍占位(`modifierName`/`modifier` 未动)却 `owner` 为 `null`,任何在此窗口内发生的 `setValue`/`markModifierDirty` 都会失去通知目标;必须精确恢复原 owner(异常/提前 return 路径下极易漏恢复 —— 而本仓库 `dev.md:121` 明令不写 `try-catch-finally`,没有兜底机制);与「活属性是唯一事实源、其簿记不可被观测为不一致」的原则相悖</cons>
|
||||
</option>
|
||||
<option id="q1-document">
|
||||
<name>Q1 选项 C:不改行为,只在 `compareEquip` 相关文档/登记中写明「被比较的装备不得是现役装备」</name>
|
||||
<pros>零代码风险、零回归面</pros>
|
||||
<cons>把「会算出错误差值」留成一个公开 API 的调用约定(`types.ts:824-834` 的契约没有任何此类限制),与「本轮修复已确认缺陷」的指令冲突;且用户长期规则禁止擅自改动 jsDoc,该约定连源码注释都写不进去</cons>
|
||||
</option>
|
||||
<option id="q2-null">
|
||||
<name>Q2 选项 A:【建议默认】`normalizeParam` 的回落分支发告警 148 后返回 `null`(与该方法自身 jsDoc 的「无法用现有编码表示时返回 null」及 `normalizeParamList` 的「丢弃的项不保留占位」一致)</name>
|
||||
<pros>三者(参数计数 / 实际写入个数 / 字节推进)自洽,`indexArray` 不再指向错误字节;与 `array.test.ts:665`/`:688` 已确立的 bigint 溢出「丢弃」语义同口径;一行改动(删掉那条 0 记录改为 `return null`),零 jsDoc/注释改动(既有的两处文档写的正是这个口径);`array.test.ts:611-618` 的既有 148 断言(只断言告警与 `array.length`)继续通过</pros>
|
||||
<cons>该参数从 `get(i).params` 中彻底消失(参数**个数**变化),调用方若依赖参数位置必须自行判长——但「无法表示」本就意味着调用方传入的不是合法录像参数,且与 bigint 溢出路径的既有行为一致</cons>
|
||||
</option>
|
||||
<option id="q2-placeholder">
|
||||
<name>Q2 选项 B:回落分支返回「实际写入的字节数」(`byteLength: 2`),并把我文档改写为「保留一个占位」</name>
|
||||
<pros>参数个数与写入前一致、后续参数位置不移动;命令参数计数与实际写入个数仍然自洽</pros>
|
||||
<cons>必须改动两处既有 jsDoc/注释(`normalizeParam` 自身与 `normalizeParamList` 的「丢弃的项不保留占位」),与用户 2026-09-17「永远不要擅自改 jsDoc」的长期规则直接冲突;解码端会把未知类型静默还原为布尔 `false`(type 0),使「未知类型」在往返后表现为一个看似合法的布尔值,与码 148 的文案语义相反;与 bigint 溢出路径的既有「丢弃」语义分叉出第二种口径(违反 A5)</cons>
|
||||
</option>
|
||||
<option id="q3-strict">
|
||||
<name>Q3 选项 A:【建议默认】构造函数两处乘数校验由 `< 1` 收紧为 `<= 1`(非法值仍告警 149 并回退为 2),使其与码 149 文案「need to be greater than 1」一致</name>
|
||||
<pros>校验条件与码表文案、与类内失败回退逻辑三者一致;已核查全仓(`logger.warn(149` 仅 `array.ts:115`/`:123` 两处;测试仅 `array.test.ts:1030` 用乘数 0;生产实配仅 `system.ts` 的 1.2),收紧不破坏任何既有配置或用例;改动仅两个字符</pros>
|
||||
<cons>「乘数 1 表示不扩容」这一直觉写法不再被接受(会告警 149 并回退为 2),即配置的合法范围被收紧——这正是「与既有文案一致」的代价;本选项**不**封口其它使 `nextSize === 当前 size` 的退化配置(见 context 的「观察(未确认、不登记)」段)</cons>
|
||||
</option>
|
||||
<option id="q3-clamp">
|
||||
<name>Q3 选项 B:保留 `>= 1` 的接受范围,把下一尺寸钳制为严格增大(`Math.max(current + 1, Math.min(Math.ceil(current * multiplier), max))`)</name>
|
||||
<pros>保留「乘数 1 = 最小增长」的直觉语义;同时**顺带封口**所有「下一尺寸等于当前尺寸」的退化配置(含 `initCommandLength`/`initParamLength` 为 0 的情形),因为钳制保证每轮至少增长 1 字节/1 条指令</pros>
|
||||
<cons>码 149 的文案将永久与校验条件不一致(文案说必须大于 1,实现却接受 1 且不再告警);`checkBufferExpand` 的两处尺寸计算都要加钳制,且必须分别处理「命令数组以指令条数为单位、参数数组以字节为单位」的差异(`current + 1` 在命令侧要乘 `commandSize`),改动面与审阅面都大于 A;本分支改变了码 149 的触发条件(乘数 1 不再告警),需在 SUMMARY 登记「无既有断言受影响,但告警触发范围收窄」</cons>
|
||||
</option>
|
||||
<option id="q4-full">
|
||||
<name>Q4 选项 A:【建议默认】克隆修饰器接入 `addModifier` 的全部四处簿记:`cloned.modifierName.set(copy, name)`、`copy.bindAttribute(cloned)`、继承源修饰器的存档开关(源上不可存档时 `cloned.modifierNosave.add(copy)`)、并 `cloned.recalculateAttribute(name)`</name>
|
||||
<pros>克隆属性与源属性在「可遍历 / 可定位 / 可通知重算 / 可存档」四个面上完全同构,`iterateModifiers`、`getModifierIndex`、`markModifierDirty`、`saveState` 四条既有消费路径一次性全部正确;与 `07-REVIEW-recheck.md:170-183` 建议的四处簿记一致(并补齐其中未写到的存档开关);不引入类型断言/`@ts-expect-error`(A7);对下游两个消费点(`equipment.ts:271` 的比较克隆、`damage.ts:152` 的会心搜索克隆)为纯补齐</pros>
|
||||
<cons>克隆属性的 `saveState()` 输出形状改变(`modifiers` 从恒为空数组变为真实列出可存档修饰器)——这是**预期的正确化**,但属对外可观测的序列化面变化,须在 SUMMARY 明写;`clone()` 的每个属性名多一次 `recalculateAttribute`(本就在既有循环里,仅簿记增加)</cons>
|
||||
</option>
|
||||
<option id="q4-partial">
|
||||
<name>Q4 选项 B:只恢复 `modifierName` + `bindAttribute`(不继承存档开关、不改其它)</name>
|
||||
<pros>覆盖评审建议片段里显式写到的两项;改动更小</pros>
|
||||
<cons>克隆属性的「哪个修饰器不入档」与源属性不一致:源上被标为不存档的修饰器(例如 `HeroEquipment.loadEquipEffect` 用 `addModifier(name, modifier, false)` 加入的装备修饰器)在克隆上会重新变回可存档,于是 `cloned.saveState()` 与「源属性存档语义的深拷贝」这一 `types.ts:117` 的承诺不符;评审的 `Fix` 段落本身也未把「存档开关」写全,选 B 等于只修一半</cons>
|
||||
</option>
|
||||
<option id="q4-drop">
|
||||
<name>Q4 选项 C:`clone()` 不再复制修饰器(只克隆基础属性)</name>
|
||||
<pros>彻底消除「克隆体簿记不自洽」这一整类问题</pros>
|
||||
<cons>`clone()` 的既有语义(`IHeroAttributeCloneOption.cloneModifier` 默认 `true` 明确表示要克隆修饰器)被推翻,`getIsolatedAttribute()`、`compareEquip` 的比较语义与 `getModifiableClone()` 的公开契约同时受损;需改接口 jsDoc(用户长期规则禁止);且既有用例 `clones base only or with modifiers independently` 会失败——等于用更大缺陷替换缺陷</cons>
|
||||
</option>
|
||||
<option id="revise">
|
||||
<name>【兜底】修订方案后再执行</name>
|
||||
<pros>避免带错误契约落地</pros>
|
||||
<cons>按 D-09 退出本次修改、修订本 PLAN.md 后重新执行,不得自行另辟他法(例如自行改用「占位」「临时解绑」「只克隆基础属性」等未选分支,或新增未裁决的相邻改动)</cons>
|
||||
</option>
|
||||
</options>
|
||||
<record>
|
||||
空 —— **尚未裁决**。本块为空表示「未裁决:停止」。执行者在收到用户逐条裁决前**不得**修改任何文件、**不得**按任何【建议默认】或【未选】分支推进;裁决取得后由计划修订把 Q1..Q4 的结论(含每条选定的分支与由此产生的对外可观测后果)逐条写入本块,然后重新执行 Task 1..5。
|
||||
</record>
|
||||
<resume-signal>逐条给出 Q1..Q4 的裁决(例如「Q1=A, Q2=A, Q3=A, Q4=A」);若要修订方案,回复修订点,执行者退出并按 D-09 修订本 PLAN.md 后重新执行</resume-signal>
|
||||
<action>向用户汇报四处口径,逐条讲清取舍与**对外可观测后果**:Q1(CR-01)A 分支在比较克隆上克隆加入/克隆删除、活属性零扰动,B 分支会制造「修饰器暂时未绑定」的窗口且本仓库禁用 try-catch 无法兜底,C 分支等于把一个会算错差值的公开 API 留成调用约定,并给出实算对照(现役剑场景下 `-12` vs 契约要求的 `-7`);Q2(WR-01)A 分支丢弃该参数(命令参数计数、`get(i).params` 个数与后续字节偏移同时变化,与 bigint 溢出路径同口径),B 分支保留 2 字节占位并必须改写两处既有 jsDoc/注释;Q3(WR-02)A 分支把乘数 1 从「静默接受(随后栈溢出)」改为「告警 149 并回退为 2」(收紧被接受的配置范围,已核查无既有配置/用例受影响),B 分支保留乘数 1 但改为严格增大并顺带封口其它退化配置,代价是码 149 文案与实现永久不一致;Q4(WR-03)A 分支恢复四处簿记,后果是**克隆属性 `saveState()` 的 `modifiers` 不再为空**、克隆修饰器可遍历可定位且 `setValue` 能通知克隆重算,B 分支不继承存档开关(源上不入档的修饰器在克隆上会变回可存档),C 分支会破坏 `cloneModifier` 的既有语义与既有用例。同时转述 context 中标注为「未确认、不登记」的观察(乘数恰为 1 之外的退化配置),请用户决定是否在 Q3 选 B 一并封口。**不得在裁决前修改任何文件**;不得自行采纳任何建议默认值。</action>
|
||||
<verify>
|
||||
<human-check>`<record>` 中 Q1..Q4 均有用户明确裁决(含 Q2 对参数计数/字节布局的影响、Q3 对可接受配置范围的影响、Q4 对克隆属性序列化内容的影响均已被告知并确认),且与用户实际意图一致</human-check>
|
||||
<fails_when>`<record>` 仍为空(未裁决)—— 必须停在 Task 0、不得修改任何文件;或记录与用户实际意图不符 —— 必须暂停并请用户澄清,不得按「建议默认」或任何【未选】分支自行推进</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Q1(CR-01 的比较克隆取修饰器方式)、Q2(WR-01 的无法编码参数处置)、Q3(WR-02 的扩容乘数边界)、Q4(WR-03 的 `clone()` 簿记范围)四条裁决均已由用户给出并写入 `<record>`
|
||||
- 每条裁决都写明选定的分支与其对外可观测后果(Q2 的参数计数/字节偏移、Q3 的可接受配置范围、Q4 的克隆属性序列化内容),Q1 的裁决不得是「不改行为只写文档」以外的含糊表述
|
||||
- `<record>` 非空之前,工作树中本计划的 6 个目标文件**零改动**(可用 `git status --short` 核对这 6 个路径无新增改动)
|
||||
- context 中标注「未确认、不登记」的观察已如实转述给用户,且未在计划任何位置登记为新缺陷
|
||||
</acceptance_criteria>
|
||||
<done>Q1..Q4 的用户裁决(含每条的可观测后果)已完整写入 `<record>`;执行者据记录进入 Task 1,不再重复询问</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: CR-01(Q1=A)—— `compareEquip` 改为「先克隆再加入、按同一批克隆删除」,使现役装备参与比较时差值符合契约(含双向差值与无 108 见证)</name>
|
||||
<files>packages-user/data-base/src/hero/equipment.ts, packages-user/data-base/src/hero/equipment.test.ts</files>
|
||||
<read_first>
|
||||
- packages-user/data-base/src/hero/equipment.ts(:74-86 `loadEquipEffect` 用 `addModifier(name, modifier, false)` 把装备修饰器绑定到活属性、:92-97 `unloadEquipEffect`、:255-331 `compareEquip` 全方法,尤其 :271 的比较克隆、:274-290 现役删除块、:297-308 A 的加入-采样-删除三段、:310-317 B 的加入-采样两段、:319 的既有注释)
|
||||
- packages-user/data-base/src/hero/attribute.ts(:190-211 `addModifier` 的 `if (modifier.owner)` 拒绝分支与告警 108、:213-228 `deleteModifier` 的 `indexOf` 定位与解绑、:169-188 `iterateModifiers`/`getModifierIndex`)
|
||||
- packages-user/data-base/src/hero/equipStore.ts(:43-61 `rebuildModifiers` 与 `getModifiers` 返回修饰器对象本身——证明加入克隆的正是活属性上那一批对象)
|
||||
- packages-user/data-base/src/hero/equipStore.ts(:57-61 `getModifiers` 的返回类型 `Iterable<[keyof SelectType<THero, number>, IHeroModifier<number>]>`,用于确定新增局部数组的元素类型)
|
||||
- packages-user/data-base/src/hero/types.ts(:824-834 `compareEquip` 的契约「输出装备 A 时的属性减装备 B 时的属性」;:199-231 `addModifier`/`deleteModifier`/`deleteModifierByIndex` 的声明)
|
||||
- packages-user/data-base/src/hero/equipment.test.ts(:1 文件头覆盖说明、:102-177 `createEnv`/`registerItem` 夹具、:417-467 既有两条 `compareEquip` 用例——修复后必须逐字保留且数值不变)
|
||||
- .planning/phases/07-data-fixes/07-REVIEW-recheck.md(CR-01 `:74-121`,含实算轨迹与建议修法)
|
||||
</read_first>
|
||||
<action>
|
||||
按 Task 0 `<record>` 的 Q1=A 落地(**无分支、无「建议默认」**)。本任务只改 `compareEquip` 的方法体内部,不改方法签名、返回值类型、146 早退分支与现役删除块。
|
||||
(1) **A 的加入端(`:297-300`)**:在循环前新增收集数组 `const addedA: [SelectKey<THero, number>, IHeroModifier<number>][] = [];`(`SelectKey` 在本文件已用于 `:295`,为全局类型;元素类型须与 `stateA.getModifiers()` 迭代出的元组一致,**不得**用 `any`)。循环体改为三行:`const copy = modifier.clone();` → 保留原有那一行 `// @ts-expect-error 泛型无法推导` 注释**逐字不动**(它仍对应下一行的类型错误) → `clone.addModifier(name, copy);` → `addedA.push([name, copy]);`。原 `modifier` 变量仍用于 `clone()`,不得被加入比较克隆。
|
||||
(2) **A 的删除端(`:305-308`)**:把「再次遍历 `stateA.getModifiers()` 并 `clone.deleteModifier(name, modifier)`」改为「遍历 `addedA` 并 `clone.deleteModifier(name, copy)`」;同样保留该处原有的 `// @ts-expect-error 泛型无法推导` 注释逐字不动。删除目标必须是**上一步加入的那个克隆对象**(`deleteModifier` 以 `indexOf` 定位,同一对象即可命中)。
|
||||
(3) **A 的采样端(`:301-304`)逐字不改**:仍遍历 `stateA.getModifiers()` 的键名并对每个键取 `clone.getFinalAttribute(name)`、写入 `keys`。
|
||||
(4) **B(`:310-317`)只改加入端**:循环体同样先 `const copy = modifier.clone();` 再 `clone.addModifier(name, copy);`(保留该处既有的 `// @ts-expect-error 泛型无法推导` 注释逐字不动);**B 不做删除**——`:319` 的既有注释「第二次没必要再删除了,因为这个 clone 对象不会再被使用到」逐字保留,B 也不新增收集数组。
|
||||
(5) **逐字保留的部分**:`:255-269` 的两个 146 早退分支、`:271` 的 `this.attribute.clone()`、`:273-290` 的现役删除块(`this.attribute.getModifierIndex(modifier)` + `clone.getModifiers(name)[index]` + 跳过负索引与空对象的两个 `continue`,以及其 `:282-285` 注释)、`:322-330` 的差值收集与 `?? final` 回退。
|
||||
(6) **禁止**:不得对活属性上的原修饰器调用 `bindAttribute(null)`(Q1=B 已排除);不得把 `stateA`/`stateB` 的原修饰器对象直接加入比较克隆;不得修改 `attribute.ts`(WR-03 见 Task 4);不得改动 `hero/types.ts` 与任何 jsDoc。
|
||||
(7) **回归用例(`equipment.test.ts` 新增 1 条;既有用例逐字保留)**:`it` 前写一行中文注释(dev.md:86),并同步 `:1` 文件头覆盖说明(**追加**「现役装备参与比较」字样,既有文字不改)。用例名 `diffs the final attributes when one compared item is currently equipped`,体例沿用既有 `compareEquip` 用例的夹具(`createEnv` + `registerItem` + `setSlots`),场景与断言:
|
||||
- 装备与英雄侧修饰器:`registerItem(env, createItem(10, 'sword', [0], [['atk', 5]]));`(现役剑)、`registerItem(env, createItem(11, 'axe', [0], [['atk', 12]]));`、`env.equipment.setSlots(['weapon']);`、`const sword = env.store.add(10);`、`env.attribute.addModifier('atk', new ValueModifier(7, -1));`(英雄侧优先级 -1,排在装备修饰器之后)、`env.equipment.equip(sword, 0);`、`expect(env.attribute.getFinalAttribute('atk')).toBe(22);`(10 + 5 + 7)、`const axe = env.store.add(11);`
|
||||
- 取活属性上现役剑的修饰器引用:`const swordModifier = [...env.store.get(sword)!.getModifiers()][0][1];`(沿用 `:457` 的既有写法)
|
||||
- 双向比较包在同一个 `logger.catch` 里:`const { ret, info } = logger.catch(() => [env.equipment.compareEquip(sword, axe, 0), env.equipment.compareEquip(axe, sword, 0)]);`
|
||||
- 契约断言(正确预期):`expect(ret[0].atk).toBe(-7);`(现役剑 22 减换斧 29)与 `expect(ret[1].atk).toBe(7);`(对称方向 29 减 22);并断言 `expect(Object.keys(ret[0])).toEqual(['atk']);`
|
||||
- 观测见证:`expect(info.map(v => v.code)).not.toContain(108);`(修复前每个方向各触发一次「修饰器已有 owner」的 108 拒绝)
|
||||
- 活属性零扰动:`expect(env.attribute.getModifierIndex(swordModifier)).toBeGreaterThanOrEqual(0);` 与 `expect(env.attribute.getFinalAttribute('atk')).toBe(22);`
|
||||
- 既有两条用例(`:419-432` 与 `:435-467`)必须继续通过且数值不变(`5 / -3` 与 `12 / -3`,见 A6)
|
||||
断言只经公开 API 与 `logger.catch` 观测;**不得**反射私有字段、不得使用连续 `as`、不新增 `it.skip`、不弱化既有断言。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm exec vitest run packages-user/data-base/src/hero/equipment.test.ts packages-user/data-base/src/hero/state.test.ts</automated>
|
||||
<fails_when>现役装备参与比较时 `compareEquip(sword, axe, 0).atk` 不等于 -7(或对称方向不等于 7);或比较过程仍产生告警 108;或活属性上原修饰器的索引变为负 / 活属性 final 数值被改变;或既有 `diffs the final attributes of two equipment instances`、`keeps foreign modifiers when the equipped instance was rebuilt`、两条码 146 用例与 `state.test.ts` 出现回归 —— 按 D-09 退出并修订计划,不得改用「临时解绑活属性修饰器」「比较前先卸下现役装备」「只写文档」等未选分支</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `compareEquip` 加入端对每个被比较修饰器先 `clone()` 再加入(A 与 B 两处),A 的删除端遍历收集数组删除**同一批克隆对象**;`stateA`/`stateB` 的原修饰器对象从未被加入比较克隆、从未被 `bindAttribute(null)`
|
||||
- 两处既有的 `// @ts-expect-error 泛型无法推导` 注释行、`:282-285` 的现役删除块注释、`:319` 的「第二次没必要再删除」注释、两个 146 早退分支、`:322-330` 的差值回退逻辑逐字保留
|
||||
- 新增用例断言双向差值(`-7` 与 `+7`)、键集合、不含 108、活属性修饰器索引仍非负且 final 仍为 22
|
||||
- `equipment.test.ts:1` 的文件头覆盖说明为**追加**(既有文字一字未改);既有用例全部保持通过;无新增 `it.skip`
|
||||
- 未改动 `hero/attribute.ts`、`hero/types.ts` 或任何 jsDoc
|
||||
</acceptance_criteria>
|
||||
<done>被比较的装备即使正是现役槽位里的那件,`compareEquip` 也返回契约要求的差值(`-7` / `+7`),比较过程不再触发告警 108,且活属性的簿记零扰动(CR-01 按 Q1=A 关闭)</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: WR-01(Q2=A)—— `normalizeParam` 的不可编码回落分支改为返回 `null`(与自身 jsDoc 及 `normalizeParamList` 的丢弃语义一致),含参数计数与字节偏移对齐回归</name>
|
||||
<files>packages-user/data-common/src/replay/array.ts, packages-user/data-common/src/replay/array.test.ts</files>
|
||||
<read_first>
|
||||
- packages-user/data-common/src/replay/array.ts(:225-230 `normalizeParam` 的 jsDoc「当参数无法用现有编码表示时返回 `null`」、:273-291 bigint 溢出以 `return null` 丢弃的既有范式、:292-315 字符串分支与回落记录、:318-335 `normalizeParamList` 的「丢弃的项不保留占位」注释与 `if (param)` 过滤、:337-343 `calculateParamsLength`、:345-362 `setCommandArray`、:364-413 `setParamArray`(:377-379 的 type-0 两字节写入、:411 的 `index += param.byteLength`)、:429-447 `add`、:778-787 `get`)
|
||||
- packages-user/data-common/src/replay/types.ts(:184-215 `IReplayArray.add` 契约、`ReplayParamValue` 的取值面)
|
||||
- packages-user/data-common/src/replay/array.test.ts(:1 文件头覆盖说明、:37-49 `createArray` 夹具备注「默认给足初始容量避免无关扩容」、:27-35 `IArrayOverrides`、:65-80 `expectStepTyped` 与 `expectParamTyped` 断言助手、:611-618 既有 148 用例(**必须逐字保留并继续通过**)、:664-701 既有两条「丢弃」用例(本任务与之同口径)、:703-773 既有计数截断用例群)
|
||||
- .planning/phases/07-data-fixes/07-REVIEW-recheck.md(WR-01 `:125-144`,含复现步骤与建议修法)
|
||||
- packages/common/src/logger.json(码 148 的权威文案「Unknown replay param type: $1, value(stringified): $2.」)
|
||||
</read_first>
|
||||
<action>
|
||||
按 Task 0 `<record>` 的 Q2=A 落地(**无分支**)。
|
||||
(1) **`normalizeParam` 的回落分支(`:310-315`)**:保留 `logger.warn(148, typeof param, String(param));` 逐字不动,把紧跟其后的 `return { paramType: 0, paramValue: 0, byteLength: 0 };` 改为 `return null;`。除这一处外,`normalizeParam` 的其余分支(boolean / int 宽度 / int64 / float / bigint 含溢出告警 152 与 `return null` / 字符串长短两态)逐字不改;方法签名 `INormalizedParam | null` 已是既有声明,无需改动。
|
||||
(2) **不改 `normalizeParamList`**:其 `if (param) normalized.push(param)` 与 `:328` 的既有注释「丢弃的项不保留占位,保证命令参数计数与实际写入的参数个数一致」正是本修复要兑现的口径,逐字保留。
|
||||
(3) **不改 `setParamArray` / `calculateParamsLength` / `setCommandArray` / `add`/`insert`/`set`**:修复后 type-0 记录只会来自 boolean 分支(`byteLength: 2`),故 `:377-379` 的 type-0 两字节写入与 `:411` 的游标推进天然自洽;这两处的既有注释逐字保留。
|
||||
(4) **零 jsDoc / 零注释改动**:本任务**不新增也不修改**任何 jsDoc 或注释(`normalizeParam` 的 jsDoc 已写明返回 `null`;`normalizeParamList` 的注释已写明丢弃语义)。
|
||||
(5) **回归用例(`array.test.ts` 新增 1 条;既有用例逐字保留)**:`it` 前写一行中文注释(dev.md:86),并同步 `:1` 文件头覆盖说明(**追加**「不可编码参数丢弃后的计数与偏移对齐」字样,既有文字不改)。用例名 `drops the unencodable param and keeps the later commands aligned`,体例沿用既有 `array.test.ts` 用例(`createArray()` 默认夹具备注、`logger.catch`、`expectStepTyped`/`expectParamTyped` 断言助手),步骤与断言:
|
||||
- `const array = createArray();` → `const { info } = logger.catch(() => array.add(1, [undefined!]));` → `array.add(2, [20]);`
|
||||
- 告警仍在:`expect(info.map(v => v.code)).toContain(148);`
|
||||
- 被丢弃的参数不占位:`expect(new Uint8Array(array.getCommandArray())[0]).toBe(0);`(第一步的参数计数为 0)与 `expect(array.get(0)).toEqual({ command: 1, params: [], index: 0 });`
|
||||
- 后续命令的字节未被覆盖:`expect(array.get(1)).toEqual({ command: 2, params: [20], index: 1 });`
|
||||
- 读流对齐:`const stream = array.createReadStream(0);` → `expectStepTyped(stream.read()!, 1, [], 1);` → `expectStepTyped(stream.read()!, 2, [20], 2);` → `expect(stream.read()).toBeNull();` → `expect(stream.index).toBe(2);`(读流的 `index` 为位置 + 1,见既有助手说明)
|
||||
- 既有 `warns code 148 for an unknown param type`(`:611-618`)与两条 bigint 丢弃用例(`:664-701`)必须继续通过
|
||||
断言只经公开 API 观测;**不得**反射私有字段、不得使用连续 `as`、不新增 `it.skip`、不弱化既有断言。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm exec vitest run packages-user/data-common/src/replay/array.test.ts</automated>
|
||||
<fails_when>第一步的命令参数计数仍为 1 或 `array.get(0).params` 仍非空(回落记录未被丢弃);或第二步的参数被覆盖 / `array.get(1).params` 不等于 `[20]`;或读流两步读回的参数与流索引不符;或既有 148 用例、两条 bigint 丢弃用例、`drop` 系列与计数截断用例出现回归 —— 按 D-09 退出并修订计划,不得改用「保留 2 字节占位并改文档」(Q2=B 已排除)或「在 `setParamArray` 里特判 type-0」等替代方案</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `normalizeParam` 的回落分支为「`logger.warn(148, typeof param, String(param));` + `return null;`」,不再构造任何 0 长度记录
|
||||
- `normalizeParamList`、`calculateParamsLength`、`setParamArray`、`setCommandArray`、`add`/`insert`/`set` 以及本文件的全部 jsDoc/注释逐字未改
|
||||
- 新增用例断言:148 仍触发、第一步参数计数为 0、`get(0).params` 为 `[]`、`get(1).params` 为 `[20]`、读流两步依次为 `[]` 与 `[20]` 且流索引为 1 与 2
|
||||
- `array.test.ts:1` 的文件头覆盖说明为**追加**(既有文字一字未改);既有用例全部保持通过;无新增 `it.skip`
|
||||
</acceptance_criteria>
|
||||
<done>无法用现有编码表示的参数被一致地丢弃(不再写入字节、不再计入命令参数计数、不再使后续命令的字节偏移错位),且告警 148 的既有语义不变(WR-01 按 Q2=A 关闭)</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: WR-02(Q3=A)—— 构造函数两处扩容乘数校验由 `< 1` 收紧为 `<= 1`(与码 149 文案一致),含「乘数恰为 1 时告警并终止扩容」回归</name>
|
||||
<files>packages-user/data-common/src/replay/array.ts, packages-user/data-common/src/replay/array.test.ts</files>
|
||||
<read_first>
|
||||
- packages-user/data-common/src/replay/array.ts(:104-141 构造函数,尤其 :113-127 两处乘数校验与回退值 2、:146-152 `getCommandSize`、:154-211 `checkBufferExpand`(:166-187 命令侧与 :189-205 参数侧的尺寸计算、:207-210 的重入))
|
||||
- packages-user/data-common/src/replay/system.ts(:32-43 生产实配:初始 1000/10000、乘数 1.2、上限 1_000_000/10_000_000)
|
||||
- packages-user/data-common/src/replay/types.ts(:167-182 `IReplayArrayConfig` 各成员的 jsDoc——乘数两条只写「扩容乘数」,无「大于 1」的成文约束,故本修复**不需要**改动任何 jsDoc)
|
||||
- packages-user/data-common/src/replay/array.test.ts(:1 文件头覆盖说明、:37-49 `createArray` 夹具(默认乘数 2、`commandMaxLength: 64`、`paramMaxLength: 512`)、:1028-1039 既有 149 用例(用乘数 0,**必须逐字保留并继续通过**)、:1109-1133 既有「初始容量不足时自动扩容」用例(`initCommandLength: 2`/`initParamLength: 8` 的既有夹具体例,本任务沿用)、:65-80 `expectStepTyped` 断言助手)
|
||||
- .planning/phases/07-data-fixes/07-REVIEW-recheck.md(WR-02 `:146-157`,含两种建议修法)
|
||||
- packages/common/src/logger.json(码 149 的权威文案「Replay $1 array expand multiplier need to be greater than 1, but got $2.」——本修复的事实源)
|
||||
</read_first>
|
||||
<action>
|
||||
按 Task 0 `<record>` 的 Q3=A 落地(**无分支**)。
|
||||
(1) **两处校验各改一个字符**:`array.ts:113` 的 `if (config.commandExpandMultiplier < 1) {` 改为 `if (config.commandExpandMultiplier <= 1) {`;`:121` 的 `if (config.paramExpandMultiplier < 1) {` 改为 `if (config.paramExpandMultiplier <= 1) {`。两处的分支体(`const str = …toString();`、`logger.warn(149, 'command'|'param', str);`、回退赋值 `this.commandExpand = 2;` / `this.paramExpand = 2;`)与 `else` 分支**逐字保留**——告警参数顺序与标签不变,回退倍率仍为 2。
|
||||
(2) **不改 `checkBufferExpand`**:本分支**不**新增尺寸钳制(那是 Q3=B 的口径,已排除);`:170-173`/`:193-196` 的尺寸计算与 `:207-210` 的重入逐字保留。修复后「乘数 > 1」保证 `Math.ceil(n * multiplier) > n`(`n` 为正整数),重入以严格更大尺寸推进至收敛或触达上限告警 150。
|
||||
(3) **零 jsDoc / 零注释改动**:`replay/types.ts` 与 `array.ts` 的注释逐字保留(本修复让实现向码 149 的既有文案靠拢,没有任何文档需要改)。
|
||||
(4) **全仓影响面(执行前复核,结论须写入 SUMMARY)**:`logger.warn(149` 只有 `array.ts:115`/`:123` 两处写入点;对 149 的断言只有 `array.test.ts:1030` 一条(用乘数 0,仍会命中新条件);`*ExpandMultiplier` 的赋值点只有 `system.ts:37-38`(1.2)与 `array.test.ts` 的夹具默认值 2 及若干显式覆盖——**没有任何既有配置或既有用例使用乘数恰好为 1**,故收紧不破坏既有绿用例。执行时须重新核对以上四处(`logger.warn(149`、测试中的码 149 断言、`*ExpandMultiplier` 的赋值点),若发现新出现的乘数 1 使用点,按 D-09 退出汇报。
|
||||
(5) **回归用例(`array.test.ts` 新增 1 条;既有用例逐字保留)**:`it` 前写一行中文注释(dev.md:86),并同步 `:1` 文件头覆盖说明(**追加**「乘数恰为 1 的边界」字样,既有文字不改)。用例名 `warns code 149 and still expands when an expand multiplier is exactly 1`,步骤与断言:
|
||||
- 用 `logger.catch` 包住构造并取其返回值作被测对象(沿用 `:630` 既有写法):`const { ret, info } = logger.catch(() => createArray({ initCommandLength: 2, initParamLength: 8, commandExpandMultiplier: 1, paramExpandMultiplier: 1 }));` → `const array = ret;`
|
||||
- 两个乘数各告警一次且按下标有序:`expect(info.map(v => v.code)).toEqual([149, 149]);`
|
||||
- 扩容必须终止且完整可用(修复前此处因 `Math.ceil(n * 1) === n` 以相同实参自递归而栈溢出):`for (let i = 0; i < 15; i++) { array.add(i, [i]); }` → `expect(array.length).toBe(15);`
|
||||
- 全部步骤按原顺序读回:`const stream = array.createReadStream(0);` → 对 `i` 从 0 到 14 逐条 `expectStepTyped(stream.read()!, i, [i], i + 1);` → `expect(stream.read()).toBeNull();` → `expect(stream.index).toBe(15);`
|
||||
- 既有 `warns code 149 for an illegal expand multiplier`(`:1030-1039`,乘数 0)与既有扩容用例(`:1110-1133`)必须继续通过
|
||||
断言只经公开 API 观测;**不得**反射私有字段、不得使用连续 `as`、不新增 `it.skip`、不弱化既有断言。
|
||||
(6) **与 Task 2 的文件共享约束**:本任务与 Task 2 改动同两个文件,必须**先后串行**分别执行、分别门禁、分别提交(D-13),**不得**把两条缺陷合并为一次提交;本任务执行前 Task 2 的改动须已提交,执行后须在 `array.ts`/`array.test.ts` 上重跑文件级门禁。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm exec vitest run packages-user/data-common/src/replay/array.test.ts</automated>
|
||||
<fails_when>构造乘数为 1 时没有产生两条告警码 149(说明校验未收紧);或构造后连续追加 15 步无法完成 / 触发栈溢出 / 读回的步骤与参数不符;或 `checkBufferExpand` 出现任何尺寸钳制改动;或既有 149(乘数 0)与扩容用例出现回归 —— 按 D-09 退出并修订计划,不得改用「保留 >= 1 并钳制下一尺寸」(Q3=B 已排除)或「在 `checkBufferExpand` 里加递归深度上限」等替代方案</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `array.ts` 的两处乘数校验为 `<= 1`,分支体(告警 149 的标签与回退倍率 2)与 `else` 分支逐字未改
|
||||
- `checkBufferExpand` 的尺寸计算与重入逐字未改(本任务零钳制)
|
||||
- 新增用例断言 `[149, 149]`、15 步全部入册、读流 15 条逐条类型化读回且流索引为 15
|
||||
- 全仓影响面复核结论(149 的两处写入点、测试中的 149 断言、`*ExpandMultiplier` 赋值点均不含乘数 1)已核对并写入 SUMMARY
|
||||
- `array.test.ts:1` 的文件头覆盖说明为**追加**(既有文字一字未改);既有用例全部保持通过;无新增 `it.skip`;本文件零 jsDoc/注释改动
|
||||
- 与 Task 2 分别提交(两条 `fix(07-15): …`),未合并为一次提交
|
||||
</acceptance_criteria>
|
||||
<done>扩容乘数恰为 1 不再被静默接受:它以告警 149 两次并回退为倍率 2 的方式被拒绝,扩容过程必然终止且全部步骤可完整读回(WR-02 按 Q3=A 关闭)</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: WR-03(Q4=A)—— `HeroAttribute.clone()` 恢复 `modifierName` / `bindAttribute` / 存档开关 / `recalculateAttribute` 四处簿记,含遍历-定位-通知-存档四项回归</name>
|
||||
<files>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(:51-64 四个私有结构、:90-112 `recalculateAttribute`、:169-188 `iterateModifiers`/`getModifiers`/`getModifierIndex`、:190-228 `addModifier` 的四处簿记与 `deleteModifier`、:244-264 `markDirty`/`markModifierDirty`/`setModifierSaveEnabled`/`getModifierSaveEnabled`、:296-318 `clone`/`getModifiableClone`、:328-339 `saveState`、:341-365 `loadState`)
|
||||
- packages-user/data-base/src/hero/types.ts(:25-68 `IHeroModifier` 的 `owner`/`clone()`/`bindAttribute` 声明、:86-89 `IHeroAttributeCloneOption`、:116-127 `clone`/`getModifiableClone` 的 jsDoc「深拷贝此勇士属性对象」、:134-151 `iterateModifiers`/`getModifierIndex` 的 jsDoc、:152-164 `setModifierSaveEnabled`/`getModifierSaveEnabled` 的 jsDoc)
|
||||
- packages-user/data-base/src/hero/modifier.ts(:18-19 `ValueModifier.clone`、:37-38 `PercentageModifier.clone`——均返回 `owner` 为 `null` 的新对象)
|
||||
- packages-user/data-base/src/hero/state.ts(:85-87 `getIsolatedAttribute` 经 `getModifiableClone()` 暴露克隆)
|
||||
- packages-user/data-base/src/hero/attribute.test.ts(:1 文件头覆盖说明、:55-110 `TestModifier`(`priority` 默认 0)/`createAttribute`/`createNumericAttribute` 夹具、:293-316 既有克隆用例(**必须逐字保留并继续通过**)、:416-431 既有注册表用例(**必须逐字保留并继续通过**)、:345-414 存读档用例群)
|
||||
- packages-user/data-base/src/hero/equipment.ts(:271 `compareEquip` 的比较克隆、:82-86 `loadEquipEffect` 以 `save = false` 加入装备修饰器——克隆属性存档开关差异的来源)
|
||||
- .planning/phases/07-data-fixes/07-REVIEW-recheck.md(WR-03 `:159-183`,含建议修法片段)
|
||||
</read_first>
|
||||
<action>
|
||||
按 Task 0 `<record>` 的 Q4=A 落地(**无分支**)。本任务只改 `clone()` 的 `cloneModifier` 分支,不改方法签名、不改注册表复制与早退分支、不改 `saveState`/`loadState`。
|
||||
(1) **`clone()` 的 `cloneModifier` 分支(`:308-312`)改为按 `addModifier` 的四处簿记逐项落笔**:遍历 `this.modifier` 的 `[name, modifiers]`,把 `modifiers.map(v => …)` 的回调体从「仅 `v.clone()`」扩展为四步——`const copy = v.clone();` → `copy.bindAttribute(cloned);`(与 `addModifier` 的 `modifier.bindAttribute(this)` 同义,使克隆修饰器 `setValue` 能经 `owner` 通知克隆重算)→ `cloned.modifierName.set(copy, name);`(与 `addModifier` 的 `this.modifierName.set(modifier, name)` 同义,使克隆体的 `iterateModifiers`/`getModifierIndex`/`markModifierDirty` 可用)→ 当源修饰器不可存档时 `cloned.modifierNosave.add(copy);`(判据取既有公开读取 `!this.getModifierSaveEnabled(v)`,与 `addModifier` 的 `save === false` 分支同义);回调返回 `copy`。随后 `cloned.modifier.set(name, arr);` 与 `cloned.recalculateAttribute(name);` **逐字保留**在循环体内。
|
||||
(2) **禁止**:不得改成调用泛型方法 `cloned.addModifier(name, copy)`(A7:会引入额外的 `// @ts-expect-error 泛型无法推导` 声明面);不得触碰 `cloneModifier: false` 早退(`:307`)与注册表复制(`:303-306`);不得改动 `saveState`/`loadState`/`iterateModifiers`/`getModifierIndex`/`markModifierDirty`/`setModifierSaveEnabled`/`getModifierSaveEnabled`;不得改动 `hero/types.ts` 或任何 jsDoc。
|
||||
(3) **等价性说明(写进 SUMMARY,不写成注释)**:`addModifier` 的优先级排序在原实现里是「不排序」、在本实现里是「保持源数组顺序」——源数组本身由 `addModifier` 按优先级降序稳定排序,故克隆数组的顺序与源逐位一致,行为等价;`this.modifier` 中若存在「空数组」的值(例如某属性名的修饰器被删净),新实现不会为该名建立空的 `modifier` 条目,但 `getModifiers(name)` 对「无条目」与「空数组」都返回空迭代,`recalculateAttribute` 对二者都令 final 等于 base,故无可观测差异。**不得**为零散边界新增分支或用例。
|
||||
(4) **回归用例(`attribute.test.ts` 新增 2 条;既有用例逐字保留)**:每条 `it` 前写一行中文注释(dev.md:86),并同步 `:1` 文件头覆盖说明(**追加**「克隆体修饰器簿记与存档开关」字样,既有文字不改)。
|
||||
- **用例 1**:`keeps the modifier bookkeeping of cloned attributes`——
|
||||
`const attribute = createNumericAttribute();` → `attribute.addModifier('hp', new TestModifier(5));` → `attribute.addModifier('atk', new TestModifier(3, 10));` → `const clone = attribute.clone();` → `const clonedHp = [...clone.getModifiers('hp')][0];`
|
||||
断言组:(a) 遍历——`expect([...clone.iterateModifiers()].map(v => v[0]).sort()).toEqual(['atk', 'hp']);`;(b) 定位——`expect(clone.getModifierIndex(clonedHp)).toBe(0);`;(c) 通知重算——`clonedHp.setValue(9);` 后 `expect(clone.getFinalAttribute('hp')).toBe(109);`(base 100 + 9)且 `expect(attribute.getFinalAttribute('hp')).toBe(105);`(源属性不受克隆修饰器影响);(d) 存档——`expect(clone.saveState(SaveCompression.NoCompression).modifiers.map(v => v.name).sort()).toEqual(['atk', 'hp']);`
|
||||
- **用例 2**:`keeps the save-ability of cloned modifiers`——
|
||||
`const attribute = createNumericAttribute();` → `attribute.addModifier('hp', new TestModifier(5), false);`(源上标记为不存档)→ `const clone = attribute.clone();` → `const clonedHp = [...clone.getModifiers('hp')][0];`
|
||||
断言组:`expect(clone.getModifierSaveEnabled(clonedHp)).toBe(false);` 与 `expect(clone.saveState(SaveCompression.NoCompression).modifiers).toHaveLength(0);`
|
||||
- 既有 `clones base only or with modifiers independently`(`:295-316`)、`carries the modifier registry into cloned attributes`(`:417-431`)、`tracks the per-modifier save flag`(`:261`)、`recomputes only the owning attribute when a modifier value changes`(`:277`)以及全部存读档用例必须继续通过
|
||||
断言只经公开 API 观测;**不得**反射私有字段、不得使用连续 `as`、不新增 `it.skip`、不弱化既有断言。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm exec vitest run packages-user/data-base/src/hero/attribute.test.ts packages-user/data-base/src/hero/equipment.test.ts packages-user/data-base/src/hero/state.test.ts</automated>
|
||||
<fails_when>`clone.iterateModifiers()` 仍为空 / `clone.getModifierIndex(克隆修饰器)` 仍为 -1 / 克隆修饰器 `setValue` 后克隆 final 未重算 / `clone.saveState().modifiers` 仍为空数组 / 克隆修饰器的存档开关未继承源属性;或既有克隆与注册表用例、Task 1 的 CR-01 用例、`state.test.ts` 的隔离属性用例出现回归 —— 按 D-09 退出并修订计划,不得改用「只恢复 modifierName + bindAttribute」(Q4=B)或「clone 不再复制修饰器」(Q4=C)等未选分支,也不得为此新增分支或用例</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `clone()` 的 `cloneModifier` 分支对每个克隆修饰器执行 `bindAttribute(cloned)`、`cloned.modifierName.set(copy, name)`、源不可存档时 `cloned.modifierNosave.add(copy)`,并按属性名调用 `cloned.recalculateAttribute(name)`
|
||||
- `cloneModifier: false` 早退与注册表复制、`saveState`/`loadState`、以及本文件全部 jsDoc/注释逐字保留
|
||||
- 两条新增用例分别覆盖「遍历 / 定位 / 通知重算 / 存档」四项与「继承存档开关」一项,且断言只经公开 API
|
||||
- `attribute.test.ts:1` 的文件头覆盖说明为**追加**(既有文字一字未改);既有用例全部保持通过;无新增 `it.skip`
|
||||
- 未改动 `hero/types.ts`、未新增任何类型断言/`@ts-expect-error`、未引入 `addModifier` 泛型调用
|
||||
</acceptance_criteria>
|
||||
<done>克隆属性与源属性在「可遍历 / 可定位 / 可通知重算 / 可存档(含存档开关)」四个面上同构,克隆不再静默丢失其修饰器(WR-03 按 Q4=A 关闭)</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 5: D-12/D-44 文件级门禁 + 全量套件与基线核对 + 四条按缺陷原子提交 + 评审映射登记(CR-01 / WR-01 / WR-02 / WR-03)+ Phase 7 验证失效说明</name>
|
||||
<files>.planning/phases/07-data-fixes/07-15-SUMMARY.md</files>
|
||||
<read_first>
|
||||
- .planning/phases/07-data-fixes/07-15-PLAN.md(Task 0 `<record>` 的 Q1..Q4 裁决,用于逐条对照;以及 A1..A10 假设与 prohibitions)
|
||||
- .planning/phases/07-data-fixes/07-REVIEW-recheck.md(CR-01 `:74-121`、WR-01 `:125-144`、WR-02 `:146-157`、WR-03 `:159-183` 的编号与标题)
|
||||
- .planning/phases/07-data-fixes/07-VERIFICATION.md(头部 `_Verified: 2026-09-17T10:42:49Z_`、`### Review-Recheck Gaps` `:335-363`、`## Recommended Fix Plans` `:365-378`)
|
||||
- .planning/phases/07-data-fixes/07-SECURITY.md(本阶段的威胁登记口径与 T-7-xx 编号现状:已用到 T-7-47)
|
||||
- .planning/ROADMAP.md(Phase 7 段的 Plans 列表与 Wave 分组——本计划只做**追加**:计划行与 Wave 15 段)
|
||||
- .planning/WINDOWS.md(确认这 4 条评审发现**没有**账本条目;本计划不新建、不 `fixed`/`waive`)
|
||||
- .planning/codebase/TESTING.md(`pnpm test:ci` 口径、`logger.catch` 断言写法、无覆盖率工具)
|
||||
- .planning/phases/07-data-fixes/07-14-PLAN.md 的 Task 2(门禁/基线/SUMMARY 的既有写法参照)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **门禁(D-12 / D-44,文件级三步)**:对 `files_modified` 的 6 个文件执行——(a) `pnpm exec eslint --fix <6 个文件>` 后 `pnpm exec eslint <6 个文件>` **0 错误**(CRLF 与 Prettier 由 `--fix` 处理,不手工调换行);(b) **文件级类型门禁按 06-RESEARCH Pitfall 4 的判定方式**:`pnpm exec vue-tsc --noEmit 2>&1 | Select-String -Pattern "<改动文件相对路径>"` **无命中**(逐个文件过滤;**不得**用「退出码非 0」判定失败,仓库存在既有渲染/legacy 类型错误,不属本阶段);(c) `pnpm test:ci` **0 失败**、跳过项仍**恰为 1 条**(`packages-user/data-base/src/hero/equipment.test.ts` 的码 147 用例,D-06)。
|
||||
(2) **基线与增量核对**:执行前先重录当日基线;与本计划 objective 记录的参照(**计划期记录、未经本次规划运行复测**:66 文件 / 737 通过 / 0 失败 / 1 跳过)对比。本计划**不新增文件**,故文件数应不变(66);新增 5 条 `it`,故通过数应 = 当日基线通过数 + 5;失败数必须为 0;跳过数必须仍为 1。任何偏离先查明原因并按 D-09 退出汇报,**不得**通过删改用例或放宽断言来对齐数字。
|
||||
(3) **原子提交(D-13,按缺陷四条)**:Task 1 = `fix(07-15): compare equipment without rebinding the equipped modifiers`;Task 2 = `fix(07-15): drop unencodable replay params instead of a zero-length record`;Task 3 = `fix(07-15): reject an expand multiplier of exactly one`;Task 4 = `fix(07-15): restore modifier bookkeeping when cloning an attribute`。提交**一律按显式路径** `git add <显式路径>`;**禁止** `git add -A`/`git add -a`、`git stash`、`git clean`、`git reset --hard`。每次提交前后用 `git status --short` 核对暂存区只含本计划当次声明的文件。
|
||||
(4) **范围隔离核对(必做)**:用显式路径区间对比本计划起点 sha..HEAD——`git diff <BASE_SHA>..HEAD --stat -- packages-user/data-base/src/hero/equipment.ts packages-user/data-base/src/hero/equipment.test.ts packages-user/data-common/src/replay/array.ts packages-user/data-common/src/replay/array.test.ts packages-user/data-base/src/hero/attribute.ts packages-user/data-base/src/hero/attribute.test.ts`(`BASE_SHA` = 本计划开始执行时的 HEAD 显式 sha),并另跑一次不带路径的 `git diff <BASE_SHA>..HEAD --stat` 确认**没有**本计划之外的文件进入提交(工作树中既有的无关未暂存修改/删除与未跟踪文件不得被暂存或提交)。同时核对:未新建任何源码/测试文件、未改 `package.json`/`pnpm-lock.yaml`、未改 `hero/types.ts` 与 `replay/types.ts`、未改 `replay/system.ts`/`flag/**`/`combat/mapDamage.ts`、未新增/修改告警码;`packages/common/src/logger.json` 在 `git diff BASE_SHA..HEAD -- packages/common/src/logger.json` 下为空(即本计划未改动它——该文件已由用户自行修改,**不得**以工作树存在改动判定越界)。
|
||||
(5) **注释门槛核对(必做)**:对本计划 6 个文件的 diff 逐行确认——除「各测试文件 `:1` 文件头覆盖说明的**追加**」与「新增用例 `it` 前的单行中文注释」外,**没有任何** jsDoc/注释行被修改或删除;实现确需说明非显然「为什么」时只允许**新增**单行注释(dev.md:84)。核对结论写入 SUMMARY 的对照表。
|
||||
(6) **评审映射登记(SUMMARY 逐条,D-14)**:登记「`07-REVIEW-recheck.md` 发现 ID → 缺陷 → 修复提交 → 选定的裁决分支」,四条分别为 CR-01(Q1 分支)/ WR-01(Q2 分支)/ WR-02(Q3 分支)/ WR-03(Q4 分支);如实登记本计划对 `07-VERIFICATION.md` `### Review-Recheck Gaps` 四条的实际闭合状态。**`WINDOWS.md` 不新建条目、不做 `fixed`/`waive`**(这 4 条本来就没有账本条目)。SUMMARY 另须写明:A6 的既有 `compareEquip` 两条用例数值复算结果;A10 的 `clone()` 前提与其守卫断言;WR-02 的全仓影响面复核结论(149 仅两处写入点、测试仅 `:1030` 一条断言、乘数赋值点仅 1.2 与 2);WR-03 造成的**对外可观测序列化面变化**(克隆属性 `saveState().modifiers` 由空数组变为真实列表,属预期正确化);以及 A8 的 `FIX-01` 探针未决假设(specless 探针只给出 `unclassified — review manually`,本计划不以其判定结果替代人工确认,该未决状态保留在册)。
|
||||
(7) **Phase 7 验证失效与 ROADMAP 追加**:SUMMARY 明写 `07-VERIFICATION.md`(`_Verified: 2026-09-17T10:42:49Z_`)已被本批改动与更早的 07-09..07-14 失效,本计划执行完毕后**必须重跑 `/gsd-verify-work`(或 `gsd-verify-work 7`)**重新出具验证结论;`ROADMAP.md` 的 Phase 7 段以 **Edit 定点追加**方式同步(Plans 计数 + 新增 `07-15-PLAN.md` 计划行 + Wave 15 段),**不得**重排、删除或改写任何既有计划行与既有说明段。`STATE.md` 的同步(若有)同样只做定点追加。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm test:ci</automated>
|
||||
<fails_when>`pnpm test:ci` 出现任何失败;或跳过数不等于 1(新增了 `it.skip`,或 D-06 的码 147 用例被取消 skip);或文件数与「本计划不新增文件」不符;或 `vue-tsc --noEmit` 输出含本计划 6 个文件的任一路径;或某个提交的暂存内容含本计划之外的文件(含工作树中既有的无关改动);或 6 个文件的 diff 中出现被修改/删除的既有 jsDoc/注释行 —— 按 D-09 退出并修订计划,不得通过删改用例、放宽断言或撤回已提交改动来对齐门禁</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 6 个文件 `eslint` 0 错误、`vue-tsc --noEmit` 按文件路径过滤 0 命中、`pnpm test:ci` 0 失败且跳过项恰为 1 条
|
||||
- 基线增量与预期一致(文件数不变;通过数 = 当日基线 + 5;失败 0;跳过 1),偏离已查明原因或已按 D-09 退出
|
||||
- Task 1..4 各有独立原子提交(`fix(07-15): …`),暂存内容只含本计划声明的文件;`git diff BASE_SHA..HEAD --stat` 不含本计划之外的文件
|
||||
- 注释门槛核对通过:除文件头覆盖说明的追加与新增用例的单行注释外,无任何 jsDoc/既有注释改动
|
||||
- SUMMARY 含四条评审映射、Q1..Q4 裁决对照、A6/A10/WR-02 影响面/WR-03 序列化面变化、A8 探针未决假设,以及「`WINDOWS.md` 未新建条目、未 `fixed`/`waive`」的明写
|
||||
- SUMMARY 含 Phase 7 验证失效与重跑要求;`ROADMAP.md` 为定点追加(既有行未被重排/删除/改写)
|
||||
- 未新建源码/测试文件、未新增依赖、未新增告警码、本计划提交区间内 `logger.json` 无改动(按区间判定,不看工作树)
|
||||
</acceptance_criteria>
|
||||
<done>D-44 三步门禁全绿、基线增量与预期一致、四条缺陷按缺陷原子提交且未牵连任何无关改动、评审映射与 Phase 7 验证失效说明写入 `07-15-SUMMARY.md`,`ROADMAP.md` 定点追加完成</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| 活属性 ↔ 比较克隆 | `compareEquip` 把被比较装备的修饰器复制到临时克隆上;复制不当会让已绑定活属性的对象泄漏进克隆(或反向解绑活属性对象),使比较结果与真实性不一致 |
|
||||
| 录像命令 ↔ 参数字节偏移 | 同一段参数缓冲区承载全部命令的字节,`indexArray` 是唯一的偏移事实源;任一命令的「计数 / 实际写入 / 游标推进」三者不一致即静默错位(后续所有命令的解码都受影响) |
|
||||
| 配置 → 缓冲区扩容 | `IReplayArrayConfig` 的乘数来自调用方;退化取值会让扩容路径不收敛(首次需要增长即栈溢出 / 不可用) |
|
||||
| 克隆属性 → 公开序列化面 | `getModifiableClone()` / `getIsolatedAttribute()` 把克隆暴露给外部;克隆的 `saveState()`、`iterateModifiers()`、`getModifierIndex()` 输出即其对外契约 |
|
||||
| 包管理器 → 仓库 | 本计划无安装动作(`package.json`/`pnpm-lock.yaml` 不变) |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-7-48 | Tampering | `equipment.ts` `compareEquip` 的 `clone.addModifier(name, modifier)`(`:297-300`/`:310-313`) | high | mitigate | Task 1(Q1=A):加入端先 `modifier.clone()`、删除端按同一批克隆对象删除,活属性上的原修饰器既不进入克隆也不被解绑;用例断言双向差值(`-7`/`+7`)、不含告警 108、活属性修饰器索引仍非负 |
|
||||
| T-7-49 | Tampering | `array.ts` `normalizeParam` 的零长度回落记录(`:310-315`) | high | mitigate | Task 2(Q2=A):回落分支改为 `return null`,由 `normalizeParamList` 的既有丢弃语义接管,使「参数计数 = 写入个数 = 游标推进」自洽;用例断言计数为 0、后续命令字节未被覆盖、读流两步对齐 |
|
||||
| T-7-50 | Denial of Service | `array.ts` 构造函数乘数校验(`:113-127`)与 `checkBufferExpand` 重入(`:207-210`) | high | mitigate | Task 3(Q3=A):校验由 `< 1` 收紧为 `<= 1`(与码 149 文案一致),使乘数 1 走回退倍率 2,重入尺寸严格增大;用例断言 `[149, 149]` 与 15 步扩容后全部读回 |
|
||||
| T-7-51 | Tampering | `attribute.ts` `clone()` 绕过 `modifierName`/绑定(`:296-314`) | medium | mitigate | Task 4(Q4=A):克隆修饰器接入 `bindAttribute` + `modifierName` + `modifierNosave` + `recalculateAttribute` 四处簿记;用例断言遍历/定位/`setValue` 通知/存档四项 |
|
||||
| T-7-52 | Repudiation | 克隆属性 `saveState()` 的 `modifiers` 由空数组变为真实列表(Q4=A 的预期正确化) | low | accept | 该变化是「克隆即深拷贝」的应有语义(`types.ts:117`),无安全影响;其对外可观测性由 Task 5 在 `07-15-SUMMARY.md` 明写登记(含 A8 的探针未决假设),不额外增加代码缓解 |
|
||||
| T-7-53 | Repudiation | 新增用例以 `logger.catch` 观测告警,可能掩盖真实告警 | low | mitigate | 四条用例均**显式断言告警码**(148 仍触发、比较过程不含 108、构造产生 `[149, 149]`),不使用「吞掉告警」的写法;`logger.catch` 只用于取 `ret`/`info`,不断言「无告警」之外的模糊条件 |
|
||||
| T-7-SC | Tampering | npm/pnpm 依赖安装 | high | mitigate | 本计划不新增依赖、不改 `package.json`/`pnpm-lock.yaml`;出现安装需求即暂停并要求用户确认包合法性 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
1. 四条聚焦命令全绿:`pnpm exec vitest run packages-user/data-base/src/hero/equipment.test.ts packages-user/data-base/src/hero/state.test.ts`、`pnpm exec vitest run packages-user/data-common/src/replay/array.test.ts`、`pnpm exec vitest run packages-user/data-base/src/hero/attribute.test.ts packages-user/data-base/src/hero/equipment.test.ts packages-user/data-base/src/hero/state.test.ts`,以及 Task 5 末尾的 `pnpm test:ci`。
|
||||
2. `pnpm test:ci` **0 失败**、跳过项**恰为 1 条**(`packages-user/data-base/src/hero/equipment.test.ts` 码 147,D-06);文件数不变(本计划不新增文件),通过数 = 当日基线通过数 + 5,基线偏离已查明原因或已按 D-09 退出。
|
||||
3. `pnpm exec eslint` 对本计划 6 个文件 0 错误;`pnpm exec vue-tsc --noEmit` 按文件路径过滤 0 命中(不得用退出码判定)。
|
||||
4. Task 0 的 `<record>` 中 Q1..Q4 均有用户裁决,代码/用例与裁决逐条一致(SUMMARY 对照表):CR-01(Q1)、WR-01(Q2)、WR-02(Q3)、WR-03(Q4);未采纳任何【未选】分支。
|
||||
5. 只改动 `files_modified` 的 6 个文件;未新建源码/测试文件、未新增依赖、未改 `package.json`/`pnpm-lock.yaml`、未改 `hero/types.ts`/`replay/types.ts`/`replay/system.ts`/`flag/**`/`combat/mapDamage.ts`、未新增或修改告警码、本计划提交区间内 `logger.json` 无改动(按区间判定,不看工作树);工作树中与用户无关或属于用户的既有改动(未暂存修改/删除/未跟踪文件)未被触碰、未被暂存、未被提交。
|
||||
6. 除「各测试文件 `:1` 文件头覆盖说明的追加」与「新增用例 `it` 前单行中文注释」外,本计划 6 个文件无任何 jsDoc/既有注释改动(用户 2026-09-17 长期规则)。
|
||||
7. 无新增 `it.skip`、既有断言未被弱化、既有用例未被删除;D-06 的码 147 用例仍保留 skip。
|
||||
8. `07-15-SUMMARY.md` 含四条评审映射与闭合状态、`WINDOWS.md` 未新建/未 `fixed`/未 `waive` 的明写、Phase 7 验证失效与重跑要求;`ROADMAP.md` 已定点追加 Wave 15 与 `07-15-PLAN.md` 行。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- CR-01 按 Q1=A 闭合:被比较的装备即使是现役槽位里的那件,`compareEquip` 也返回契约要求的双向差值(`-7` / `+7`),比较过程不产生告警 108,活属性簿记零扰动(有自动化见证)。
|
||||
- WR-01 按 Q2=A 闭合:无法用现有编码表示的参数被一致丢弃(不计入参数计数、不写入字节、不影响后续命令的偏移),实现与其自身 jsDoc 及 `normalizeParamList` 的既有语义一致。
|
||||
- WR-02 按 Q3=A 闭合:扩容乘数为 1 时告警 149 并回退为倍率 2,扩容必然终止且步骤可完整读回;全仓复核确认无既有配置或用例使用乘数 1。
|
||||
- WR-03 按 Q4=A 闭合:克隆属性与源属性在「可遍历 / 可定位 / 可通知重算 / 可存档(含存档开关)」四个面上同构,且该序列化面的变化已在 SUMMARY 明写。
|
||||
- 每条缺陷各有正确预期回归用例(共 5 条),修复前失败、修复后通过;`pnpm test:ci` 0 失败、跳过项仍仅 1 条、无新增 `it.skip`、既有断言未弱化;D-44 三步门禁全绿;四条按缺陷原子提交且未牵连工作树中的无关改动。
|
||||
- Phase 7 验证失效与重跑要求已记录(须重跑 `/gsd-verify-work`),评审映射登记在 `07-15-SUMMARY.md`,`ROADMAP.md` 定点追加完成。
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/07-data-fixes/07-15-SUMMARY.md` when done
|
||||
</output>
|
||||
Loading…
Reference in New Issue
Block a user