mirror of
https://github.com/motajs/template.git
synced 2026-09-18 13:10:21 +08:00
docs(07-13): lock user adjudications for CR-02/IN-01/load-dynamics cleanup
This commit is contained in:
parent
674b325c55
commit
b37c218f2d
@ -8,7 +8,6 @@ depends_on:
|
||||
files_modified:
|
||||
- packages-user/data-system/src/combat/mapDamage.ts
|
||||
- packages-user/data-system/src/combat/mapDamage.test.ts
|
||||
- packages-user/data-system/src/combat/types.ts
|
||||
- packages-user/data-base/src/map/mapLayer.ts
|
||||
- packages-user/data-base/src/map/mapLayer.test.ts
|
||||
- packages-user/data-base/src/map/saveLoad.test.ts
|
||||
@ -16,71 +15,84 @@ autonomous: false
|
||||
requirements:
|
||||
- FIX-01
|
||||
estimate:
|
||||
tokens: 52000
|
||||
raw_tokens: 52000
|
||||
tasks: 4
|
||||
tokens: 54000
|
||||
raw_tokens: 54000
|
||||
tasks: 5
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "怪物来源的地图伤害在来源消失或范围收缩后不再残留:`markEnemyDirty` 路径下 `removeEnemyAffecting` 触达过的每个坐标索引都被标脏并按现有 `refreshIndex` 重新聚合(CR-02 关闭),`getReducedDamage`/`getSeparatedDamage` 不再返回幽灵伤害"
|
||||
- "转换视图集变为空时(伤害来源被移除,如光环/特殊属性消失)仍完成缓存失效,且该怪物的反向索引被显式登记,后续 `markEnemyDirty` 不走整表 `refreshAll()`(按 Task 0 Q1 裁决)"
|
||||
- "范围收缩(同一怪物产生的伤害点位减少)时,跌出范围的坐标不再保留包含已移除贡献的缓存值"
|
||||
- "`MapLayer.setMapRef` 的失效契约按 Task 0 Q2 裁决明确:`getMapRef()` 的持有者能据 `expired`/对象身份判断何时重取(`#06-17-8` / 审计 G 关闭或明写契约)"
|
||||
- "`MapLayer.loadDynamics` 不再让旧动态图块累积:读档后该图层的动态图块集合与存档一致(按 Task 0 Q3 裁决:清空旧块 / 只清存档外坐标 / 明写合并语义)"
|
||||
- "`pnpm test:ci` 全绿,不新增 `it.skip`、不弱化既有断言(既有 `mapDamage.test.ts` 的 102/103/104、`deleteEnemy`、`markEnemyDirty` 用例全部保持通过)"
|
||||
- "**CR-02 按 Q1=A 关闭(移除端标脏 + 空视图集登记)**:`removeEnemyAffecting` 收集被移除的坐标索引并在方法末尾逐个 `markDirtyIndex(index)`;因此来源消失(视图集变空)与范围收缩(同一怪物伤害点位减少)两条路径下,旧坐标都会经既有 `refreshIndex` 按剩余贡献重新聚合,`getReducedDamage`/`getSeparatedDamage` 不再返回幽灵伤害"
|
||||
- "**空视图集被显式登记**:`refreshEnemyAndClearCache` 与 `refreshEnemy` 不再在 `set.size === 0` 处提前返回——它们先 `enemyStore.set(view, set)`(空集也登记)再结束,使后续 `markEnemyDirty` 走局部路径(`refreshEnemyAndClearCache`)而不是整表 `refreshAll()`;两条路径各有自动化见证(`vi.spyOn(context, 'iterateEnemy')` 未被调用 + `converter.calls` 恰增 1)"
|
||||
- "**空集合的读法不得被误当作「未登记」**:`markEnemyDirty`(`:135-148`)与 `deleteEnemy`(`:150-167`)**不新增任何早退分支**——空的 `Set` 本身是真值,`if (!store)` 天然不误判;用例固定「空集登记后再次标记怪物脏仍局部刷新」与「空集合下 `deleteEnemy` 只做 `enemyStore.delete(view)`」"
|
||||
- "**IN-01 按 Q4=A 落地(两处清理)**:`deleteEnemy` 在删除每个视图时把该视图从 `point.affectedBy` 剪除、并清除其 `point.damages` 条目(不再依赖 `refreshIndex` 的惰性跳过);`deleteMapDamage` 在删除后该点的 `damages` 与 `affectedBy` 均为空时把该 `IPointInfo` 从 `sourcelessDamage` 中移除。其中 `deleteEnemy` 的剪除有**自动化见证**:删除后触发惰性 `refreshIndex` 时,被删视图的 `getDamageWithoutCheck`(`:414`)**不再被调用**(`vi.spyOn` 计数为 0)"
|
||||
- "**IN-01 的 `deleteMapDamage` 分支无公开可观测差异(如实登记,不伪造见证)**:`sourcelessDamage` 空条目的移除是簿记/内存修复,公开 API 的返回内容前后一致;其验证为「既有断言 + 新增公开契约回归全绿」+逐行代码对照 `<record>`,**禁止**用私有字段反射或连续 `as` 断言去制造伪见证"
|
||||
- "**`#06-17-8`(读档旧动态块累积)按 Q3=A1 关闭**:`MapLayer.loadState` 在派发三档压缩之前**先清空本图层全部既有动态块**,逐块复用 `deleteDynamic` 的语义——`syncStaticEvent(tile, false)`(`mapLayer.ts:133-145`)+ `removeTile(tile)`(`:118-124`)+ 触发 `onDeleteDynamic` 钩子(`types.ts:257`);读档后动态块集合严格等于存档(`[...iterateDynamicTiles()].length === 存档块数`),重复读档不累积,且**不**复用旧实例(全清 → 重建,与合并语义区分)"
|
||||
- "**读档清理触发的 `onDeleteDynamic` 为「触发式、不等待」**:`loadState` 是同步方法而 `IMapLayerHooks.onDeleteDynamic` 返回 `Promise<void>`,故清理路径按 `forEachHook` 调用钩子但**不** `await`(与可等待的 `deleteDynamic` 逐字不同);用例只固定「钩子被同步调用」这一事实,不等待其 Promise(A6)"
|
||||
- "**Q2 = A:`setMapRef` 零改动、零 jsDoc 改动**:`MapLayer.setMapRef`(`:371-391`)与 `getMapRef`(`:393-395`)逐字保留现契约(旧 `mapData` 标 `expired = true` + 整对象替换),其既有 jsDoc/注释一行不动;裁决为「保留现契约、无代码改动、无 jsDoc 改动」,登记于 Task 0 `<record>` 与 `07-13-SUMMARY.md`"
|
||||
- "**零 jsDoc 改动(用户 2026-09-17 长期规则:永远不要擅自改 jsDoc 注释)**:本计划唯一被授权的 jsDoc 落笔是 Task 3 新增私有方法 `clearDynamics` 的 jsDoc(dev.md:73 要求方法必须有 jsDoc);`combat/types.ts` 逐字不改(Q1=A 不需要任何接口/类型变更);测试文件的 `it` 前单行中文注释与文件头覆盖说明同步属 dev.md:85 的既定要求,不在禁令范围"
|
||||
- "`pnpm test:ci` 0 失败、不新增 `it.skip`、既有断言未弱化;基线(07-12 收尾)为 66 文件 / 729 通过 / 0 失败 / 1 跳过(跳过项仍只能是 `equipment.test.ts:306` 码 147,D-06);本计划对 `packages-user/client-base`、`packages-user/client-modules` 的既有未暂存改动**零触碰**"
|
||||
artifacts:
|
||||
- path: packages-user/data-system/src/combat/mapDamage.ts
|
||||
provides: "`removeEnemyAffecting` 返回/登记被移除的坐标索引并逐个 `markDirtyIndex`;`refreshEnemyAndClearCache` 在视图集为空时仍完成失效与反向索引登记(按 Q1 裁决)"
|
||||
- path: packages-user/data-system/src/combat/types.ts
|
||||
provides: "`IMapDamage.markEnemyDirty`/`deleteEnemy` 的失效契约 jsDoc(何时保证缓存失效、空视图集语义)"
|
||||
provides: "`removeEnemyAffecting` 收集被移除索引并逐个 `markDirtyIndex`(Q1=A);`refreshEnemyAndClearCache`/`refreshEnemy` 空集也 `enemyStore.set(view, set)` 后再结束;`deleteMapDamage` 移除空 `IPointInfo`、`deleteEnemy` 剪除 `point.affectedBy` 与 `point.damages`(Q4=A);`setMapRef` 同区的既有结构(`dirtyIndexes`/`reducedCache`/`viewStore`/`damageStore`/`enemyStore`)不新增缓存层"
|
||||
- path: packages-user/data-system/src/combat/mapDamage.test.ts
|
||||
provides: "CR-02 回归:来源消失(空视图集)与范围收缩两条路径的幽灵伤害断言"
|
||||
provides: "CR-02 回归 4 条(视图集变空后无幽灵伤害 / 范围收缩后跌出范围的坐标无旧缓存 / 空集后 `markEnemyDirty` 走局部刷新 / `refreshAll` 同样登记空集后走局部刷新)+ IN-01 回归 2 条(`deleteEnemy` 剪除的自动化见证 / `deleteMapDamage` 空点的公开契约回归)+ 文件头覆盖说明同步"
|
||||
- path: packages-user/data-base/src/map/mapLayer.ts
|
||||
provides: "`setMapRef`/`loadState`/`loadDynamics` 的失效与旧动态块处置(按 Q2/Q3 裁决)"
|
||||
provides: "`loadState` 入口新增「清空本图层全部既有动态块」步骤(Q3=A1),经新增私有方法 `clearDynamics` 逐块复用 `deleteDynamic` 语义(`syncStaticEvent(tile, false)` + `removeTile` + 触发 `onDeleteDynamic`,不等待);三档压缩共用同一入口;`setMapRef`/`getMapRef` 与其 jsDoc **零改动**(Q2=A)"
|
||||
- path: packages-user/data-base/src/map/mapLayer.test.ts
|
||||
provides: "`#06-17-8` 回归:`setMapRef` 的 `expired`/对象身份契约;旧动态块不再累积(实例级)"
|
||||
provides: "`#06-17-8` 实例级回归:读档前创建多于存档的动态块 → `loadState` 后 `iterateDynamicTiles()` 数等于存档、旧实例不再被管理(且读档后非同一实例,排除合并语义)、清理时 `onDeleteDynamic` 被同步触发"
|
||||
- path: packages-user/data-base/src/map/saveLoad.test.ts
|
||||
provides: "`#06-17-8` 回归:`MapLayer.loadState` 三档往返后动态图块集合等于存档(不累积)"
|
||||
provides: "`#06-17-8` 三档往返回归:动态块多于存档时读档后块数等于存档;同一存档连续两次 `loadState`(模拟重复读档)块数仍等于存档;既有 `restores the matrix and point events across all compressions` 与码 124 用例逐字保留"
|
||||
key_links:
|
||||
- "`removeEnemyAffecting`(`mapDamage.ts:240-256`)↔ `markDirtyIndex`(`:126-129`)——移除端不标脏是 CR-02 的根因;`refreshEnemyAndClearCache`(`:290-333`)与 `refreshEnemy`(`:338-372`)都经由它"
|
||||
- "`refreshEnemyAndClearCache` 的 `if (set.size === 0) return;`(`:300`)↔ `enemyStore.delete(view)`(`:255`)—— 视图集为空时不登记反向索引,后续 `markEnemyDirty`(`:135-148`)会因 `enemyStore.get(view)` 为空而走 `refreshAll()`(`:142`)"
|
||||
- "`refreshIndex`(`:395-427`)↔ `viewStore.get(store.sourceView)`(`:406-407`)——重新聚合只在 `viewStore` 仍登记的视图上重建,故被移除视图的贡献随 `point.affectedBy.delete(viewItem)`(`:249`)自然消失"
|
||||
- "`MapLayer.setMapRef`(`mapLayer.ts:371-391`)↔ `getMapRef()`(`:393-395`)/`MapLayerHookController.getMapData`(`:950-952`)——旧 `mapData` 被标 `expired` 后整对象被替换(`:380-386`)"
|
||||
- "`MapLayer.loadDynamics`(`:841-849`)↔ `iterateDynamicTiles`(`:512-514`)/`tilePosMap`/`posTileMap`(`:48-51`)/`removeTile`(`:118-124`)——读档只创建不清理,旧动态块累积"
|
||||
- "`MapLayer.deleteDynamic`(`:498-507`)的 `syncStaticEvent(tile, false)` + `removeTile` + `onDeleteDynamic` 钩子 ↔ 读档清理是否复用同一路径(决定静态事件同步与渲染端通知是否触发)"
|
||||
- "`removeEnemyAffecting`(`mapDamage.ts:240-256`)↔ `markDirtyIndex`(`:126-129`)——移除端不标脏是 CR-02 的根因;`refreshEnemyAndClearCache`(`:294`)与 `refreshEnemy`(`:339`)都经由它,故标脏放在移除端即可覆盖两条路径"
|
||||
- "`refreshEnemyAndClearCache` 的 `if (set.size === 0) return;`(`:300`)↔ `enemyStore.set(view, set)`(`:301`)/ `enemyStore.delete(view)`(`:255`)↔ `markEnemyDirty`(`:135-148`)的 `refreshAll()`(`:142`)——空集不登记会让下次 `markEnemyDirty` 因 `enemyStore.get(view)` 为空而整表重算;登记后走 `refreshEnemyAndClearCache` 局部路径"
|
||||
- "`refreshEnemyAndClearCache` 的重登失效(`:329-332` 的 `dirtyIndexes.delete`/`reducedCache.delete`)↔ `removeEnemyAffecting` 末尾的逐个 `markDirtyIndex`——重登的索引清脏、跌出范围的索引保留脏标记,下次读取时经 `refreshIndex`(`:395-427`)按剩余贡献重算"
|
||||
- "`refreshIndex`(`:395-427`)的 `point.affectedBy.forEach` → `view.getDamageWithoutCheck(locator)`(`:413-414`)↔ `deleteEnemy`(`:150-167`)是否剪除死视图——不剪除时惰性重建仍会调用已删视图的方法(IN-01 的可观测差异,即测试见证点)"
|
||||
- "`deleteMapDamage`(`:114-120`)↔ `sourcelessDamage`(`:45`)的空 `IPointInfo`——`affectedBy` 只在 `registerSourcedDamage`(`:266-285`)写入且只作用于 `sourcedDamage`,故无来源点的「两集合皆空」在实践中等价于 `damages.size === 0`;`sourcedDamage` 的空点条目**不在**本次清理范围(非目标)"
|
||||
- "`MapLayer.loadState`(`mapLayer.ts:922-931`)↔ 三档 `load*`(`:855-931`)↔ `if (save.dynamicBlocks)` 守卫(`:862`/`:886`/`:917`)——清理必须放在 `loadState` 入口而非 `loadDynamics` 内部,否则「存档不含动态块」时不会清理、读档结果与存档点不一致"
|
||||
- "新增 `clearDynamics` ↔ `deleteDynamic`(`:498-507`)/ `syncStaticEvent`(`:133-145`)/ `removeTile`(`:118-124`)/ `tilePosMap`+`posTileMap`(`:48-51`)/ `IMapLayerHooks.onDeleteDynamic`(`types.ts:257`,返回 `Promise<void>`)——复用同一删除语义,唯一差异是读档路径不等待钩子"
|
||||
- "`MapLayer.setMapRef`(`:371-391`)/ `getMapRef`(`:393-395`)/ `resize`+`resize2` 的另两处 `mapData.expired`(`:625`/`:668`)——Q2=A 裁定**零改动**:保留「标旧对象 `expired` + 整对象替换」现契约,契约文本落在 Task 0 `<record>` 与 SUMMARY(源码 jsDoc 按用户长期规则不改)"
|
||||
assumptions:
|
||||
- "A1(D-10 例外,本计划授权):本计划为修复计划,为 CR-02 与 `#06-17-8` 新增回归用例是必需项;既有断言不得弱化(`mapDamage.test.ts` 的 102/103/104/deleteEnemy/markEnemyDirty 用例、`map/saveLoad.test.ts` 的三档往返用例全部保持)"
|
||||
- "A2(不引入新状态语义):CR-02 的修法只在既有结构(`dirtyIndexes`/`reducedCache`/`viewStore`/`damageStore`/`enemyStore`)内完成,不新增缓存层、不改 `IMapDamageReducer`/`IMapDamageConverter` 契约"
|
||||
- "A3(项目未发布,与 07-09 的 A2 同口径):`IMapLayerSave`/存档形状不因本计划变更;动态块清理属读档行为而非格式变更"
|
||||
- "A4(范围守卫):仅修 CR-02 与 `#06-17-8`;`07-REVIEW.md` 的 IN-01(删除后 `point.damages`/`affectedBy` 残留、`sourcelessDamage` 空点残留)与 IN-04/IN-05 不在本轮 15 项清单,只在 Task 0 作为可选项列出、默认登记不修"
|
||||
- "A1(D-10 例外,本计划授权):本计划为修复计划,为 CR-02 与 IN-01 新增回归用例是必需项;**既有断言一字不改**(`mapDamage.test.ts` 的 102/103/104、`deleteEnemy`、`markEnemyDirty`、多来源叠加用例,`map/saveLoad.test.ts` 的三档往返与码 124 用例全部保持);仅同步 `mapDamage.test.ts:1` 的文件头覆盖说明与新增用例的 `it` 前注释(dev.md:85)"
|
||||
- "A2(不引入新状态语义):CR-02 与 IN-01 的修法只在既有结构(`dirtyIndexes`/`reducedCache`/`viewStore`/`damageStore`/`enemyStore`/`sourcelessDamage`/`sourcedDamage`)内完成,不新增缓存层、不改 `IMapDamageReducer`/`IMapDamageConverter` 契约、不改 `IMapDamage` 的方法签名"
|
||||
- "A3(项目未发布,与 07-09 的 A2 同口径):`IMapLayerSave`/存档形状不因本计划变更;动态块清理属读档行为而非格式变更,不新增存档版本字段、不保留兼容分支"
|
||||
- "A4(范围守卫):修 CR-02、IN-01(Q4=A)与 `#06-17-8` 的读档累积(Q3=A1)三项;`setMapRef` 按 Q2=A **零改动**;`sourcedDamage` 空点条目不删(非目标);`IN-04`/`IN-05` 不在本轮清单,只登记不修"
|
||||
- "A5(空集合不是「未登记」):空 `Set` 为真值,故 `markEnemyDirty`/`deleteEnemy`/`removeEnemyAffecting` **不需要**为空集新增分支;明令禁止把空集当作未登记处理(那会让 `markEnemyDirty` 回退到 `refreshAll()`,或让光环重新出现后永不刷新),该口径须有用例固定"
|
||||
- "A6(读档清理的钩子语义):`loadState` 为同步方法,`onDeleteDynamic` 返回 `Promise<void>`,故读档清理为「触发式、不等待」;用例只断言钩子被同步调用,不断言其 Promise 完成"
|
||||
- "A7(IN-01 的见证边界):`deleteEnemy` 的剪除有自动化见证(死视图不再被 `refreshIndex` 调用);`deleteMapDamage` 的空点移除**无公开可观测差异**,不用私有字段反射、不用连续 `as` 断言伪造见证,改以公开契约回归 + 逐行代码对照 `<record>` 确认并在 SUMMARY 登记"
|
||||
prohibitions:
|
||||
- "**禁止擅自修改/新增 jsDoc 注释**(用户 2026-09-17 长期规则,逐字:永远不要擅自改 jsDoc 注释)。本计划唯一被授权的 jsDoc 落笔:Task 3 新增私有方法 `clearDynamics` 的 jsDoc(dev.md:73 要求方法必须有 jsDoc,dev.md:118 要求私有方法置于调用者之前);测试文件的 `it` 前单行中文注释与文件头覆盖说明属 dev.md:85 的既定要求,不在禁令内。既有 jsDoc/注释一律逐字保留"
|
||||
- "`MapLayer.setMapRef`(`mapLayer.ts:371-391`)与 `getMapRef`(`:393-395`)**零改动**(Q2=A:保留「旧 `mapData` 标 `expired = true` + 整对象替换」现契约);本计划不得出现任何针对 `setMapRef` 的代码改动或注释改动,也不得改动 `resize`/`resize2` 的 `expired` 口径"
|
||||
- "不改 `packages-user/data-system/src/combat/types.ts`(`IMapDamage` 的声明与 jsDoc 逐字不改;Q1=A 不需要任何接口/类型变更)"
|
||||
- "不删除 `sourcedDamage` 中的空点条目(Q4=A 未要求;该惰性残留不产生错误结果,登记于 SUMMARY)"
|
||||
- "不改 `packages-user/data-base/src/map/gameMap.ts` / `mapState.ts`(审计判定 `GameMap`/`MapState.loadState` 为原地读档、安全)"
|
||||
- "不改 `packages-user/data-base/src/map/dynamicTile.ts`(07-05 已修 num 恢复)"
|
||||
- "不改 `packages-user/data-system/src/combat/context.ts` 的 `refreshEnemy`/`setEnemyAt` 调用点(只在 `mapDamage.ts` 内修复)"
|
||||
- "不改渲染端 `@user/client-*` 与 legacy 接线;不新增依赖、不新建源码文件、不新增 `import type`、不使用连续 `as` 断言"
|
||||
- "不改渲染端 `@user/client-*` 与 legacy 接线;不新增依赖、不新建源码文件、不新增 `import type`、不使用连续 `as` 断言(`as unknown as …`)"
|
||||
- "不新增 `it.skip`;不为转绿改写既有断言"
|
||||
- "不得暂存或提交与本计划无关的既有未暂存改动(`packages-user/client-base/src/types.ts`、`packages-user/client-modules/src/types.ts`):提交一律按显式路径 `git add <file>`;禁止 `git add -A`/`git add -a`、`git stash`、`git clean`、`git reset --hard`"
|
||||
---
|
||||
|
||||
<objective>
|
||||
闭合地图侧两条缺陷:`MapDamage` 的幽灵伤害(CR-02)与 `MapLayer` 的读档失效/旧动态块契约(`#06-17-8` / 审计 G)。
|
||||
闭合地图侧三条缺陷:`MapDamage` 的幽灵伤害(CR-02)与删除后的点残留(IN-01),以及 `MapLayer` 读档时的旧动态块累积(`#06-17-8` / 审计 G)。`setMapRef` 的失效契约按用户裁决**零改动**。
|
||||
|
||||
现状(逐条锚点):
|
||||
1. **CR-02(critical,`data-system/src/combat/mapDamage.ts:240-256, 290-333`)** `refreshEnemyAndClearCache()` 先 `removeEnemyAffecting(view)`(`:294`)——它删除该怪物的伤害对象并 `enemyStore.delete(view)`(`:255`),但**从不把被移除的坐标索引标脏、也不删对应 `reducedCache`**;随后只对新登记的索引做失效(`:329-332`),且视图集为空时在 `:300` 提前 `return`。后果:怪物光环/特殊属性消失(视图集变空)或范围收缩时,旧坐标的 `reducedCache` 永久停留在「含已移除贡献」的合并值——`getReducedDamage()`/`getSeparatedDamage()` 因索引不在 `dirtyIndexes` 而永不重算。`deleteEnemy()`(`:150-167`)标脏掩盖了整怪删除路径,`markEnemyDirty()`(`:135-148`,由 `EnemyContext.refreshEnemy()`/`setEnemyAt()` 触达)是真实可达路径。
|
||||
2. **`#06-17-8`(低,审计 G)** `MapLayer.setMapRef()`(`mapLayer.ts:371-391`)把旧 `mapData` 标 `expired` 后整对象替换、清 `staticTileCache`;`loadDynamics()`(`:841-849`)只按存档 `createDynamic` 新块、从不清理既有动态块——同一图层多次读档或读一份动态块更少的存档时旧块累积;`expired` 契约(持有者何时重取 `getMapRef()`)未成文。
|
||||
1. **CR-02(critical,`data-system/src/combat/mapDamage.ts:240-256, 290-333`)** `refreshEnemyAndClearCache()` 先 `removeEnemyAffecting(view)`(`:294`)——它删除该怪物的伤害对象并 `enemyStore.delete(view)`(`:255`),但**从不把被移除的坐标索引标脏、也不删对应 `reducedCache`**;随后只对新登记的索引做失效(`:329-332`),且视图集为空时在 `:300` 提前 `return`。后果:怪物光环/特殊属性消失(视图集变空)或范围收缩时,旧坐标的 `reducedCache` 永久停留在「含已移除贡献」的合并值——`getReducedDamage()`/`getSeparatedDamage()` 因索引不在 `dirtyIndexes` 而永不重算。`deleteEnemy()`(`:150-167`)标脏掩盖了整怪删除路径,`markEnemyDirty()`(`:135-148`,由 `EnemyContext.refreshEnemy()`/`setEnemyAt()` 触达)是真实可达路径。附带的隐式后果:空视图集不登记 `enemyStore` 条目,使该怪物下次 `markEnemyDirty` 走 `refreshAll()`(`:142`)整表重算。
|
||||
2. **IN-01(Info,`07-REVIEW.md`)** `deleteMapDamage()`(`:114-120`)删除无来源伤害后不清理已空的 `IPointInfo`;`deleteEnemy()`(`:150-167`)删除后仍把已删视图留在 `point.affectedBy`、把已删伤害留在 `point.damages`,依赖 `refreshIndex()` 的 `viewStore` 判空(`:417`)惰性跳过——属簿记/内存泄漏而非错误结果。
|
||||
3. **`#06-17-8`(低,审计 G)** `MapLayer.loadDynamics()`(`mapLayer.ts:841-849`)只按存档 `createDynamic` 新块、从不清理既有动态块——同一图层多次读档或读一份动态块更少的存档时旧块累积,读档结果与存档点不一致;`setMapRef()`(`:371-391`)的 `expired` 契约无成文文本(用户裁决保留现契约、不作任何改动)。
|
||||
|
||||
Purpose: 地图伤害是「怪物在哪里、谁该受伤」的唯一事实源,缓存残留会让玩家在光环消失后仍持续受伤(或显示已不存在的伤害);动态块累积会让读档后的地图与存档点不一致。两者都在「读档/刷新」的失效边界上,属同一主题。
|
||||
Purpose: 地图伤害是「怪物在哪里、谁该受伤」的唯一事实源,缓存残留会让玩家在光环消失后仍持续受伤(或显示已不存在的伤害);读档后动态块与存档点不一致会让同一存档反复读出的地图不同。三条都在「读档/刷新」的失效边界上,属同一主题。
|
||||
|
||||
Output: `mapDamage.ts`(失效补全)+ `combat/types.ts`(失效契约 jsDoc)+ `mapDamage.test.ts`(CR-02 回归);`mapLayer.ts`(`setMapRef`/`loadDynamics` 按裁决)+ `mapLayer.test.ts` + `saveLoad.test.ts`(`#06-17-8` 回归)。
|
||||
|
||||
**Artifacts this phase produces(本计划产出物)**:修改文件见 frontmatter `files_modified`;回归用例见 `must_haves.artifacts`;无新建源码文件、无新增依赖。
|
||||
Output(本计划产出物 = frontmatter `files_modified` 的 5 个文件):`mapDamage.ts`(Q1=A 标脏 + 空集登记;Q4=A 两处清理)+ `mapDamage.test.ts`(CR-02 与 IN-01 回归);`mapLayer.ts`(Q3=A1 读档前全清,新增私有 `clearDynamics`)+ `mapLayer.test.ts` + `saveLoad.test.ts`(`#06-17-8` 回归)。**`combat/types.ts` 不在改动清单**(Q1=A 无需接口变更,且用户长期规则禁止擅自改动 jsDoc);**`setMapRef` 零改动**(Q2=A)。无新建源码文件、无新增依赖。
|
||||
|
||||
**Phase 7 验证已失效**:`07-VERIFICATION.md`(2026-09-16T06:04:11Z)已因 07-09 与本轮 07-10..07-14 失效。本计划执行完毕后**必须重跑 `/gsd-verify-work`**(或 `gsd-verify-work 7`)重新出具验证结论。
|
||||
|
||||
Phase 7 裁决对应(逐条引用):D-01: 本计划是 Phase 7 第 13 个计划,承接 `07-REVIEW.md` CR-02 与 `07-LOADSTATE-AUDIT.md` G 行(`#06-17-8`)。D-02: 属 map 系统(`data-base/src/map`)与 combat 系统(`data-system/src/combat`)的地图伤害子系统,按严重度递减排序。D-03: `#06-01-3` before 语义,不涉及。D-04: `#06-06-1` 越图码 128,不涉及(本计划不改 `transferToDynamic`/`transferToStatic` 的诊断码)。D-05: 码 178 语义,不涉及。D-06: 码 147 保留,不涉及。D-07: path 接线,不涉及。D-08: enemy 复用映射,不涉及。D-09: 由 Task 0 关卡承载。D-10: 本计划为修复计划,回归用例为必需项(A1)。D-11: 失效契约与 `setMapRef` 语义变更需同步 jsDoc。D-12: 沿用 06-CONTEXT D-44 文件级三步门禁。D-13: 按缺陷原子提交(Task 1 / Task 2)。D-14: 两条均无 `WINDOWS.md` 条目,只登记不新建。
|
||||
Phase 7 裁决对应(逐条引用):D-01: 本计划是 Phase 7 第 13 个计划,承接 `07-REVIEW.md` 的 CR-02/IN-01 与 `07-LOADSTATE-AUDIT.md` G 行(`#06-17-8`)。D-02: 属 map 系统(`data-base/src/map`)与 combat 系统(`data-system/src/combat`)的地图伤害子系统,按严重度递减排序。D-03: `#06-01-3` before 语义,不涉及。D-04: `#06-06-1` 越图码 128,不涉及。D-05: 码 178 语义,不涉及。D-06: 码 147 保留,不涉及。D-07: path 接线,不涉及。D-08: enemy 复用映射,不涉及。D-09: 由 Task 0 关卡承载,用户 2026-09-17 已裁决并锁入 `<record>`(Q1=A / Q2=A 零改动 / Q3=A1 / Q4=A)。D-10: 本计划为修复计划,回归用例为必需项(A1)。D-11(契约变更需同步 jsDoc)在本计划**不适用**:Q2=A 明确「零代码、零 jsDoc 改动」,且用户 2026-09-17 立下长期规则「永远不要擅自改 jsDoc 注释」——故空集语义与 `setMapRef` 契约以 Task 0 `<record>` + `07-13-SUMMARY.md` 承载,不改源码注释。D-12: 沿用 06-CONTEXT D-44 文件级三步门禁。D-13: 按缺陷原子提交(Task 4 落三条:CR-02 / IN-01 / `#06-17-8`)。D-14: 三条均无 `WINDOWS.md` 条目,只登记不新建。
|
||||
|
||||
执行序:`depends_on: 07-12`(Wave 13)。门禁型依赖(D-09 逐计划裁决关卡 + D-12 全量测试基线逐计划重录);源码层面与 07-12、07-11 无耦合,但因 `pnpm test:ci` 基线需逐计划重录,不并波执行。
|
||||
|
||||
**基线(2026-09-16 实测)**:`pnpm test:ci` = 66 文件 / 690 通过 / 1 跳过(`equipment.test.ts:306` 码 147,D-06)。执行前重录当日基线并与本行对比。
|
||||
**隔离要求(执行前必读)**:工作树存在与本计划**无关**的既有未暂存改动——`packages-user/client-base/src/types.ts` 与 `packages-user/client-modules/src/types.ts`。本计划不得触碰、不得暂存、不得提交它们(见 prohibitions 最后一条)。
|
||||
|
||||
**基线(07-12 收尾实测,2026-09-17)**:`pnpm test:ci` = 66 文件 / **729 通过 / 0 失败 / 1 跳过**(`equipment.test.ts:306` 码 147,D-06)。执行前重录当日基线并与本行对比;本计划新增回归用例会使通过数上升,跳过项必须仍只有 1 条。
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@ -101,199 +113,263 @@ Phase 7 裁决对应(逐条引用):D-01: 本计划是 Phase 7 第 13 个
|
||||
@packages-user/data-system/src/combat/mapDamage.ts
|
||||
@packages-user/data-system/src/combat/mapDamage.test.ts
|
||||
@packages-user/data-base/src/map/mapLayer.ts
|
||||
@packages-user/data-base/src/map/mapLayer.test.ts
|
||||
@packages-user/data-base/src/map/saveLoad.test.ts
|
||||
|
||||
**执行起点提示(裁决,2026-09-17)**:Task 0 的 `<record>` 已由计划修订填入用户裁决,执行者**核对记录后直接进入 Task 1**,不再重复询问。要点:**Q1=A**(移除端标脏 + 空视图集登记);**Q2=A 零改动**(`setMapRef` 保留现契约,**不改代码、不改 jsDoc**,用户长期规则:永远不要擅自改 jsDoc 注释);**Q3=A1**(读档前全清既有动态块,复用 `deleteDynamic` 语义并触发 `onDeleteDynamic` 钩子);**Q4=A**(IN-01 一并修:`deleteMapDamage` 在两集合皆空时移除 `sourcelessDamage` 的空 `IPointInfo`;`deleteEnemy` 剪除被删视图的 `point.affectedBy` 条目并清除其 `point.damages` 条目)。
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 0: 契约裁决关卡(D-09)——CR-02 失效补全口径(标脏 + 空视图集是否登记)、`setMapRef` 失效契约(替换对象 vs 原地变更)、读档旧动态块处置、IN-01 是否纳入</name>
|
||||
<name>Task 0: 契约裁决关卡(D-09)——CR-02 失效补全口径(标脏 + 空视图集是否登记)、`setMapRef` 失效契约(替换对象 vs 原地变更)、读档旧动态块处置、IN-01 是否纳入【已于 2026-09-17 裁决:Q1=A / Q2=A(零改动)/ Q3=A1 / Q4=A;选项块保留为历史记录】</name>
|
||||
<files>(只读汇报,不修改任何文件)</files>
|
||||
<read_first>
|
||||
- packages-user/data-system/src/combat/mapDamage.ts(:58-72 内部结构、:114-133 `deleteMapDamage`/`markDirtyIndex`/`markDirty`、:135-148 `markEnemyDirty`、:150-167 `deleteEnemy`、:169-222 读取与合并缓存、:224-256 `clearSourceState`/`removeEnemyAffecting`、:258-285 `registerSourcedDamage`、:290-333 `refreshEnemyAndClearCache`、:338-372 `refreshEnemy`、:374-390 `refreshAll`、:395-427 `refreshIndex`)
|
||||
- packages-user/data-system/src/combat/types.ts(`IMapDamage` 的 `markEnemyDirty`/`deleteEnemy` 声明与 jsDoc 位置)
|
||||
- packages-user/data-system/src/combat/mapDamage.test.ts(:1-2 文件头覆盖说明、:19-90 夹具、后半段 `deleteEnemy`/`markEnemyDirty` 用例,注意文件头是否需同步更新)
|
||||
- packages-user/data-base/src/map/mapLayer.ts(:44-51 私有映射、:100-124 `removeTileFromPosMap`/`removeTile`、:371-395 `setMapRef`/`getMapRef`、:418-433 `createDynamic`、:461-507 删除动态块(含 `deleteDynamic` 的 `syncStaticEvent`/钩子)、:512-514 `iterateDynamicTiles`、:600-681 `isEqualToRef`/`compareWith`/`resize`/`resize2`(另两处 `mapData.expired`)、:841-849 `loadDynamics`、:855-931 三档 `load*`)
|
||||
- packages-user/data-base/src/map/mapLayer.test.ts、packages-user/data-base/src/map/saveLoad.test.ts(:176-220 `MapLayer save and load round trips` 与 124 告警用例)
|
||||
- .planning/phases/07-data-fixes/07-REVIEW.md(CR-02 与 IN-01 原文、建议修法)
|
||||
- packages-user/data-system/src/combat/mapDamage.test.ts(:1-2 文件头覆盖说明、:19-90 夹具与 `Map.prototype.getOrInsertComputed` 补丁、:101-203 `FakeRange`/`FakeView`/`FakeConverter`/`FakeReducer`、:378-546 sourceless 与 sourced 用例群、后半段 `deleteEnemy`/`markEnemyDirty` 用例)
|
||||
- packages-user/data-base/src/map/mapLayer.ts(:44-51 私有映射、:100-124 `removeTileFromPosMap`/`removeTile`、:128-145 `syncStaticEvent`、:371-395 `setMapRef`/`getMapRef`、:418-433 `createDynamic`、:460-507 删除与转换动态块(含 `deleteDynamic` 的 `syncStaticEvent`/钩子)、:512-514 `iterateDynamicTiles`、:600-681 `isEqualToRef`/`compareWith`/`resize`/`resize2`(另两处 `mapData.expired`)、:837-849 `loadDynamics`、:851-931 三档 `load*` 与 `loadState`)
|
||||
- packages-user/data-base/src/map/types.ts(:189-191 `IMapLayerData.expired`、:207-257 `IMapLayerHooks.onDeleteDynamic?`(返回 `Promise<void>`)、:470 `IHookable`)
|
||||
- packages-user/data-base/src/map/mapState.ts(:409-425 `MapState.loadState` 为**同步**逐层读档——决定读档清理无法 `await` 钩子)
|
||||
- packages-user/data-base/src/map/mapLayer.test.ts、packages-user/data-base/src/map/saveLoad.test.ts(:176-218 `MapLayer save and load round trips` 与码 124 用例、:109-112 `setLayerReference`)
|
||||
- .planning/phases/07-data-fixes/07-REVIEW.md(CR-02 原文与建议修法 `:98-132`;IN-01 原文与 `Fix` `:241-245`)
|
||||
- .planning/phases/07-data-fixes/07-LOADSTATE-AUDIT.md(G 行与「未确定项 2」)
|
||||
</read_first>
|
||||
<decision>
|
||||
地图侧两条修复的契约点:
|
||||
地图侧修复的契约点(**用户已于 2026-09-17 逐条裁决,锁定结果见本任务 `<record>`**):
|
||||
(Q1)CR-02 的失效补全口径(是否标脏;视图集为空时是否登记反向索引以免触发 `refreshAll()`);(Q2)`MapLayer.setMapRef` 的失效契约(替换 `mapData` 对象 + 旧对象标 `expired`,还是原地变更);(Q3)读档时旧动态块的处置(全清 / 只清存档外坐标 / 明写合并);(Q4)`IN-01`(删除后的点残留与 `sourcelessDamage` 空点)是否纳入本计划。
|
||||
</decision>
|
||||
<context>
|
||||
**本关卡已裁决(2026-09-17):下述选项块保留为历史记录与决策依据,不再需要用户在场确认;执行者核对 `<record>` 后直接进入 Task 1。**
|
||||
两个决定都直接改变对外可观测行为:
|
||||
`refreshEnemyAndClearCache` 的 `set.size === 0` 早退(`:300`)与 `enemyStore.delete(view)`(`:255`)组合出一个隐式后果——**视图集为空的怪物在下次 `markEnemyDirty` 时会走 `refreshAll()` 整表重算**(`:142`)。修复 CR-02 时是否顺手登记空视图集(`enemyStore.set(view, new Set())`)会决定这是「按需重算」还是「整表重算」。
|
||||
`setMapRef` 现在把旧 `mapData` 标 `expired` 再整对象替换(`:380-386`):这是外部持有者唯一的失效信号,但持有者拿到的对象上 `array` 仍是旧缓冲。是否改成原地变更(同引用原则的另一种形式)会决定外部持有者是「必须重取」还是「自动看到新缓冲」,两者不能同时成立。
|
||||
动态块清理涉及是否复用 `deleteDynamic` 的 `syncStaticEvent(tile, false)` 与 `onDeleteDynamic` 钩子(渲染端可能在监听),故必须由用户点名。
|
||||
`refreshEnemyAndClearCache` 的 `set.size === 0` 早退(`:300`)与 `enemyStore.delete(view)`(`:255`)组合出一个隐式后果——视图集为空的怪物在下次 `markEnemyDirty` 时会走 `refreshAll()` 整表重算(`:142`)。用户裁决 Q1=A:既补标脏,也顺手登记空视图集,使后续刷新走局部路径。
|
||||
`setMapRef` 现在把旧 `mapData` 标 `expired` 再整对象替换(`:380-386`)。用户裁决 Q2=A:**保留现契约,零改动**——并追加一条长期规则「永远不要擅自改 jsDoc 注释」,故连该契约的 jsDoc 也不写(契约文本改由本 `<record>` 与 `07-13-SUMMARY.md` 承载)。
|
||||
动态块清理涉及是否复用 `deleteDynamic` 的 `syncStaticEvent(tile, false)` 与 `onDeleteDynamic` 钩子(渲染端可能在监听)。用户裁决 Q3=A1:全清并**触发**钩子;因 `loadState` 为同步方法,钩子为触发式、不等待(A6)。
|
||||
Q4 用户裁决为 A:IN-01 一并纳入本计划。
|
||||
</context>
|
||||
<options>
|
||||
(**历史选项说明**:本块为裁决时的选项记录。除标注【已选】者外,其余选项一律为**未选·历史**,仅保留决策依据;`revise` 兜底未触发。裁决结果见 `<record>`。)
|
||||
<option id="q1-mark-dirty">
|
||||
<name>Q1 选项 A:【建议默认】`removeEnemyAffecting` 收集被移除的坐标索引并逐个 `markDirtyIndex(index)`;`refreshEnemyAndClearCache`(与 `refreshEnemy`)在视图集为空时**仍登记 `enemyStore.set(view, emptySet)`** 并完成失效,不再提前 `return` 跳过登记</name>
|
||||
<name>【已选(Q1=A)】Q1 选项 A:`removeEnemyAffecting` 收集被移除的坐标索引并逐个 `markDirtyIndex(index)`;`refreshEnemyAndClearCache`(与 `refreshEnemy`)在视图集为空时**仍登记 `enemyStore.set(view, emptySet)`** 并完成失效,不再提前 `return` 跳过登记</name>
|
||||
<pros>与 `07-REVIEW.md` 的建议修法一致;幽灵伤害在两条路径(来源消失 / 范围收缩)上一次消除;空视图集被登记后,后续 `markEnemyDirty` 走局部刷新而不是 `refreshAll()` 整表重算(避免性能与行为双重抖动)</pros>
|
||||
<cons>`enemyStore` 会开始保存「空集合」条目,`markEnemyDirty`/`deleteEnemy` 的读法需相应容错(`Set.size === 0` 分支);需在 jsDoc 写明空集语义</cons>
|
||||
<cons>`enemyStore` 会开始保存「空集合」条目;因空 `Set` 本身是真值,`markEnemyDirty`/`deleteEnemy` 的既有 `if (!store)` 天然不误判,故**无需**新增分支,但必须以用例固定该口径,防止后续被改成「空集即未登记」(A5)</cons>
|
||||
</option>
|
||||
<option id="q1-cache-only">
|
||||
<name>Q1 选项 B:只在移除端删除 `reducedCache`(不标脏、不登记空集),让 `getReducedDamage` 下次按剩余伤害重算</name>
|
||||
<name>【未选·历史】Q1 选项 B:只在移除端删除 `reducedCache`(不标脏、不登记空集),让 `getReducedDamage` 下次按剩余伤害重算</name>
|
||||
<pros>改动最小(一行 `reducedCache.delete` 的循环),不动 `dirtyIndexes`/`enemyStore` 结构</pros>
|
||||
<cons>`refreshIndex` 的重新聚合不再执行,`point.damages` 中的其它陈旧项不会被清理;`enemyStore` 为空的行为(下次 `markEnemyDirty` → `refreshAll()`)保持不变;CR-02 的「范围收缩」场景只被间接修复</cons>
|
||||
</option>
|
||||
<option id="q1-document">
|
||||
<name>Q1 选项 C:不改行为,只把「`markEnemyDirty` 之后旧坐标缓存可能残留」写进 `IMapDamage` 契约并登记</name>
|
||||
<name>【未选·历史】Q1 选项 C:不改行为,只把「`markEnemyDirty` 之后旧坐标缓存可能残留」写进 `IMapDamage` 契约并登记</name>
|
||||
<pros>零风险</pros>
|
||||
<cons>CR-02 是 review 判定的 correctness 缺陷(幽灵伤害可被玩家观测),与「本轮修复 15 项」的指令冲突(清单第 13 条)</cons>
|
||||
<cons>CR-02 是 review 判定的 correctness 缺陷(幽灵伤害可被玩家观测),与「本轮修复」的指令冲突(清单第 13 条)</cons>
|
||||
</option>
|
||||
<option id="q2-replace">
|
||||
<name>Q2 选项 A:【建议默认】保持「旧 `mapData` 标 `expired` + 替换为新对象」的既有契约,并把该契约写进 `IMapLayerData`/`getMapRef` 的 jsDoc(持有者见 `expired === true` 必须重取;`array` 引用可能失效)</name>
|
||||
<name>【已选(Q2=A)】Q2 选项 A:保持「旧 `mapData` 标 `expired` + 替换为新对象」的既有契约。**用户追加裁决:本计划不得改动 `setMapRef` 的代码,也不得为该契约改写/新增任何 jsDoc 注释**(长期规则:永远不要擅自改 jsDoc 注释)——契约文本落在本 `<record>` 与 SUMMARY</name>
|
||||
<pros>零行为变更、零风险;`expired` 语义与 `resize`/`resize2`(`:625`/`:668`)保持一致(三处都是替换 + 标旧)</pros>
|
||||
<cons>持有者必须主动轮询/检查 `expired`,契约靠文档而非类型约束</cons>
|
||||
<cons>持有者必须主动检查 `expired`;且该契约不进源码注释(按用户明令),只能靠计划/SUMMARY 存档与后续阅读者自觉</cons>
|
||||
</option>
|
||||
<option id="q2-inplace">
|
||||
<name>Q2 选项 B:改为原地变更(`this.mapData.expired = false; this.mapData.array = this.mapArray`),不再替换对象</name>
|
||||
<name>【未选·历史】Q2 选项 B:改为原地变更(`this.mapData.expired = false; this.mapData.array = this.mapArray`),不再替换对象</name>
|
||||
<pros>外部持有者的引用始终有效、自动看到新缓冲(同引用原则的又一处应用)</pros>
|
||||
<cons>`expired` 失效信号将**永不生效**(没有可观测的过期窗口),现有依赖该信号重取的调用方必须改成其它判据;`resize`/`resize2` 需一并改,改动面扩大</cons>
|
||||
</option>
|
||||
<option id="q3-clear-all">
|
||||
<name>Q3 选项 A:【建议默认】`loadState`(三档)在读入动态块之前**清空本图层全部既有动态块**(复用内部 `removeTile` 解除两张映射,并按裁决决定是否 `syncStaticEvent(tile, false)` 与触发 `onDeleteDynamic`),使读档后动态块集合等于存档</name>
|
||||
<pros>读档语义与「回到存档点」一致;多次读档不再累积;与 `MapLayer` 其它读档字段(静态块原地 `loadState`、矩阵 `setMapRef`)口径统一</pros>
|
||||
<cons>读档会销毁旧动态图块实例(外部持有者脱钩——与「读档即替换」语义一致,但需在 jsDoc 写明);若勾选触发 `onDeleteDynamic`,渲染端会在读档期收到一批删除通知</cons>
|
||||
<name>【已选(Q3=A1)】Q3 选项 A(A1 口径):`loadState` 在读入动态块之前**清空本图层全部既有动态块**——逐块复用 `deleteDynamic` 的语义(`syncStaticEvent(tile, false)` + `removeTile(tile)` + 触发 `onDeleteDynamic` 钩子),使读档后动态块集合等于存档;清理放在 `loadState` 入口(而非 `loadDynamics` 内部的 `if (save.dynamicBlocks)` 之后),以保证三档一致且「存档不含动态块」时也清理</name>
|
||||
<pros>读档语义与「回到存档点」一致;多次读档不再累积;与 `MapLayer` 其它读档字段口径统一;清理位置在入口使其对三档与缺字段情形**按构造**成立</pros>
|
||||
<cons>读档会销毁旧动态图块实例(外部持有者脱钩——与「读档即替换」语义一致,需在 SUMMARY 写明);渲染端会在读档期收到一批 `onDeleteDynamic` 通知,且因 `loadState` 同步而**不被等待**(A6)</cons>
|
||||
</option>
|
||||
<option id="q3-clear-missing">
|
||||
<name>Q3 选项 B:只清理「存档中不存在」的坐标上的动态块(合并语义),存档里出现的坐标保留并复用</name>
|
||||
<name>【未选·历史】Q3 选项 B:只清理「存档中不存在」的坐标上的动态块(合并语义),存档里出现的坐标保留并复用</name>
|
||||
<pros>旧实例尽量保留,减少脱钩</pros>
|
||||
<cons>同一坐标上「存档块数 < 现有块数」的场景(如同一坐标叠了 3 块、存档只有 1 块)仍需逐块比对,逻辑复杂且容易出现半合并状态;实现成本明显高于收益</cons>
|
||||
<cons>同一坐标上「存档块数 < 现有块数」的场景仍需逐块比对,逻辑复杂且容易出现半合并状态;实现成本明显高于收益</cons>
|
||||
</option>
|
||||
<option id="q3-document">
|
||||
<name>Q3 选项 C:不改行为,把「`loadState` 会与既有动态块合并/累积」写进 `IMapLayer`/`IMapLayerSave` 契约并登记</name>
|
||||
<name>【未选·历史】Q3 选项 C:不改行为,把「`loadState` 会与既有动态块合并/累积」写进 `IMapLayer`/`IMapLayerSave` 契约并登记</name>
|
||||
<pros>零代码风险</pros>
|
||||
<cons>`#06-17-8` 未修(审计 G 明确「旧动态块累积」);读档结果与存档点不一致,属可观测的正确性问题</cons>
|
||||
</option>
|
||||
<option id="q4-include">
|
||||
<name>Q4 选项 A:把 `IN-01`(`deleteMapDamage`/`deleteEnemy` 后的 `point.damages`/`affectedBy` 残留与 `sourcelessDamage` 空点)纳入本计划一并清理</name>
|
||||
<pros>CR-02 的清理路径与 IN-01 相邻(都在 `removeEnemyAffecting`/`deleteEnemy` 的清理语句上),一次改动收敛</pros>
|
||||
<cons>超出用户点名的 15 项清单(属 review Info 级),需要明确授权;会扩大 CR-02 的回归面</cons>
|
||||
<name>【已选(Q4=A)】Q4 选项 A:把 `IN-01` 纳入本计划一并清理——`deleteMapDamage` 在删除后该点两集合皆空时把 `IPointInfo` 从 `sourcelessDamage` 中移除;`deleteEnemy` 剪除已删视图的 `point.affectedBy` 条目并清除其 `point.damages` 条目</name>
|
||||
<pros>CR-02 的清理路径与 IN-01 相邻(都在 `removeEnemyAffecting`/`deleteEnemy`/`deleteMapDamage` 的清理语句上),一次改动收敛;`deleteEnemy` 的剪除有自动化见证(死视图不再被 `refreshIndex` 调用)</pros>
|
||||
<cons>超出用户点名的 15 项清单(属 review Info 级),需明确授权(用户已授权);`deleteMapDamage` 的空点移除无公开可观测差异,须如实登记其为簿记修复(A7)</cons>
|
||||
</option>
|
||||
<option id="q4-register">
|
||||
<name>Q4 选项 B:【建议默认】`IN-01` 只登记不修(本轮清单未含)</name>
|
||||
<name>【未选·历史】Q4 选项 B:`IN-01` 只登记不修(本轮清单未含)</name>
|
||||
<pros>严格守住清单边界;CR-02 的改动面最小(只补失效,不动移除语义)</pros>
|
||||
<cons>`point.affectedBy`/`sourcelessDamage` 的空条目泄漏保留在册(review 已判定为 lazy cleanup 而非错误结果)</cons>
|
||||
</option>
|
||||
<option id="revise">
|
||||
<name>(兜底)修订方案后再执行</name>
|
||||
<name>【兜底·未触发】修订方案后再执行</name>
|
||||
<pros>避免带错误契约落地</pros>
|
||||
<cons>按 D-09 退出本次修改、修订本 PLAN.md 后重新执行,不得自行另辟他法</cons>
|
||||
</option>
|
||||
</options>
|
||||
<record>
|
||||
待用户在关卡处逐条填写(Q1..Q4:勾选选项 id 或给出裁决文字)。
|
||||
**空的 `<record>` 表示尚未裁决**:执行者必须停在本任务,不得按「建议默认」推进。
|
||||
填写后 Task 1–3 一律按本记录执行;若裁决与 Task 1–3 的分支描述冲突,暂停并请用户澄清。
|
||||
用户裁决(2026-09-17,逐条;本节由计划修订填入,执行者核对后直接进入 Task 1,不再重复询问):
|
||||
- **Q1 = A(标脏 + 登记空集)**:`removeEnemyAffecting`(`mapDamage.ts:240-256`)收集被移除的坐标索引,并在方法末尾对每个索引调用 `markDirtyIndex(index)`;`refreshEnemyAndClearCache`(`:290-333`)与 `refreshEnemy`(`:338-372`)**不再在 `set.size === 0` 时早退**——它们先登记 `enemyStore.set(view, emptySet)` 并完成既有失效(移除端的标脏已在 `removeEnemyAffecting` 内发生),使该怪物后续的 `markEnemyDirty` 走**局部刷新**(`refreshEnemyAndClearCache`)而不是 `refreshAll()`(`:142`)。空视图集的读法口径:空 `Set` 为真值,`markEnemyDirty`/`deleteEnemy` 的既有 `if (!store)` 天然不误判,**不得**为空集新增早退分支(A5)。
|
||||
- **Q2 = A(保留替换 + `expire`)——但附用户明令的例外:不修 jsDoc**:`MapLayer.setMapRef`(`:371-391`)**保持现有行为**(旧 `mapData` 标 `expired = true` + 整对象替换为新对象,与 `resize`/`resize2` 的 `:625`/`:668` 口径一致)**且保持其现有注释与 jsDoc——本计划对 `setMapRef` 无代码改动、无 jsDoc 改动**(用户逐字强调:按 A 走,但是永远不要擅自改 jsDoc 注释)。契约登记:本记录 + `07-13-SUMMARY.md`(源码注释不改,见 D-11 在本计划不适用的说明)。
|
||||
- **用户长期规则(对本计划全程生效)**:**禁止擅自修改/新增 jsDoc 注释**(除非该计划任务被明确授权)。本计划唯一被授权的 jsDoc 落笔是 Task 3 新增私有方法 `clearDynamics` 的 jsDoc(dev.md:73 要求方法必须有 jsDoc);测试文件的 `it` 前单行中文注释与文件头覆盖说明属 dev.md:85 的既定要求,不在禁令范围。
|
||||
- **Q3 = A1(全清 + 触发钩子)**:读档时在装入动态块之前**清空本图层全部既有动态块**,逐块复用 `deleteDynamic` 的语义——`syncStaticEvent(tile, false)` + `removeTile(tile)` + 触发 `onDeleteDynamic` 钩子(渲染端可能正在监听,**必须触发**)。读档结果等于存档点(同一存档反复读、或读到动态块更少的存档,均**不累积**)。清理位于 `loadState` 入口(三档共用、且「存档不含 `dynamicBlocks`」时照样清理)。因 `loadState` 同步、`onDeleteDynamic` 返回 `Promise<void>`,钩子为**触发式、不等待**(A6)。
|
||||
- **Q4 = A(一并修 IN-01)**:`deleteMapDamage`(`:114-120`)在该点 `damages` 与 `affectedBy` 均为空时,把该 `IPointInfo` 从 `sourcelessDamage`(`:45`)中移除;`deleteEnemy`(`:150-167`)把被删视图从 `point.affectedBy` 剪除并清除其 `point.damages` 条目,**不再**依赖 `refreshIndex`(`:395-427`)的 `viewStore` 判空惰性跳过。`sourcedDamage` 中的空点条目**不删除**(非目标)。
|
||||
填写后 Task 1–4 一律按本记录执行;若执行期发现与本记录冲突,暂停并请用户澄清。
|
||||
</record>
|
||||
<resume-signal>逐条给出 Q1..Q4 的裁决(例如「Q1=A, Q2=A, Q3=A, Q4=B」);若要修订方案,回复修订点,执行者退出并按 D-09 修订本 PLAN.md</resume-signal>
|
||||
<action>向用户汇报上述 4 个契约点,重点说明 Q1 对 `refreshAll()` 触发频率的影响、Q2 对 `getMapRef()` 持有者的影响、Q3 对渲染端 `onDeleteDynamic` 通知的影响。不得在裁决前修改任何文件。</action>
|
||||
<resume-signal>已裁决(2026-09-17):Q1=A, Q2=A(保留替换 + `expired`,**零代码改动、零 jsDoc 改动**), Q3=A1(全清 + 触发 `onDeleteDynamic`), Q4=A(一并修 IN-01)。执行者核对 `<record>` 后直接进入 Task 1;若记录与用户实际意图不符,暂停并请用户澄清后再继续</resume-signal>
|
||||
<action>本关卡已裁决:向执行者转述 `<record>` 的五条口径——Q1=A(标脏 + 空集登记)、Q2=A(`setMapRef` **零代码、零 jsDoc** 改动)、用户长期规则(禁止擅自修改/新增 jsDoc,唯一例外是 Task 3 新增私有方法的 jsDoc)、Q3=A1(读档前全清 + **必须触发** `onDeleteDynamic`,钩子不等待)、Q4=A(IN-01 两处清理,`sourcedDamage` 空点不删)——核对无误后直接进入 Task 1。执行者**不得**在 Task 1 之前修改任何文件,也**不得**按任何【未选·历史】选项推进。</action>
|
||||
<verify>
|
||||
<human-check>`<record>` 中 Q1..Q4 均有明确裁决,且与用户实际意图一致</human-check>
|
||||
<fails_when>记录为空或与用户实际意图不符 —— 必须暂停并请用户澄清,不得按「建议默认」自行推进</fails_when>
|
||||
<human-check>`<record>` 中 Q1..Q4 均有明确裁决(含 Q2「保留现契约、零代码改动、零 jsDoc 改动」与用户长期规则、Q3「钩子必须触发」两条关键口径),且与用户实际意图一致</human-check>
|
||||
<fails_when>记录为空或与用户实际意图不符 —— 必须暂停并请用户澄清,不得按「建议默认」或【未选·历史】选项自行推进</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Q1(CR-02 失效口径)、Q2(`setMapRef` 失效契约)、Q3(旧动态块处置)、Q4(IN-01 纳入与否)均有用户裁决并写入 `<record>`
|
||||
- Q3 已明确是否复用 `deleteDynamic` 的 `syncStaticEvent`/`onDeleteDynamic` 行为
|
||||
- 未在裁决前修改任何文件
|
||||
- Q1(CR-02:标脏 + 空集登记)、Q2(`setMapRef`:保留替换 + `expired`,零代码、零 jsDoc 改动)、Q3(旧动态块:全清 + 触发钩子)、Q4(IN-01 纳入并两处清理)四条裁决均已写入 `<record>`
|
||||
- Q3 已明确复用 `deleteDynamic` 的 `syncStaticEvent(tile, false)` + `removeTile` + `onDeleteDynamic`,且明写钩子**必须被触发**(因 `loadState` 同步而不等待)
|
||||
- 用户长期规则「禁止擅自修改/新增 jsDoc 注释(除明确授权)」已写入 `<record>` 并落到 frontmatter `prohibitions`
|
||||
- 未在 Task 1 之前修改任何文件
|
||||
</acceptance_criteria>
|
||||
<done>Q1..Q4 裁决已完整写入 `<record>`;执行者据记录直接进入 Task 1,不再重复询问</done>
|
||||
<done>Q1..Q4 裁决与用户长期规则已完整写入 `<record>`;执行者据记录直接进入 Task 1,不再重复询问</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: CR-02 —— `MapDamage` 失效补全(移除端标脏 + 空视图集登记),含「来源消失」与「范围收缩」两条幽灵伤害回归</name>
|
||||
<files>packages-user/data-system/src/combat/mapDamage.ts, packages-user/data-system/src/combat/types.ts, packages-user/data-system/src/combat/mapDamage.test.ts</files>
|
||||
<name>Task 1: CR-02(Q1=A)—— `MapDamage` 失效补全(移除端标脏 + 空视图集登记),含「来源消失」与「范围收缩」两条幽灵伤害回归及「不再整表刷新」见证</name>
|
||||
<files>packages-user/data-system/src/combat/mapDamage.ts, packages-user/data-system/src/combat/mapDamage.test.ts</files>
|
||||
<read_first>
|
||||
- packages-user/data-system/src/combat/mapDamage.ts(:126-148 `markDirtyIndex`/`markEnemyDirty`、:150-167 `deleteEnemy` 的标脏参照、:224-256 `clearSourceState`/`removeEnemyAffecting`、:290-333 `refreshEnemyAndClearCache`、:338-372 `refreshEnemy`、:395-427 `refreshIndex`)
|
||||
- packages-user/data-system/src/combat/types.ts(`IMapDamage` 方法声明与 jsDoc 位置)
|
||||
- packages-user/data-system/src/combat/mapDamage.test.ts(:1-2 文件头覆盖说明、夹具(假视图/转换器/reducer)、`deleteEnemy` 与 `markEnemyDirty` 现有用例)
|
||||
- packages-user/data-system/src/combat/mapDamage.ts(:126-133 `markDirtyIndex`/`markDirty`、:135-148 `markEnemyDirty` 的 `refreshAll()` 分支、:150-167 `deleteEnemy` 的标脏参照、:240-256 `removeEnemyAffecting`、:266-285 `registerSourcedDamage`、:290-333 `refreshEnemyAndClearCache`(:294 移除、:300 早退、:301 登记、:329-332 重登失效)、:338-372 `refreshEnemy`、:374-390 `refreshAll`、:395-427 `refreshIndex`)
|
||||
- packages-user/data-system/src/combat/mapDamage.test.ts(:1 文件头覆盖说明、:19-43 全局补丁、:101-203 `FakeRange`(`indexes` 内容可变)/`FakeView`/`FakeConverter`(`views` 数组内容可变、`calls` 计数)/`FakeReducer`(`calls` 计数)、:282-360 `createFixture`(`damageView` 为 `FakeView(7, [index(1,0)])`)、:502-546 既有 `markEnemyDirty`/`deleteEnemy` 用例)
|
||||
- .planning/phases/07-data-fixes/07-REVIEW.md(CR-02 原文 `:98-132`,含建议修法与「`deleteEnemy` 标脏掩盖了整怪删除路径」的说明)
|
||||
</read_first>
|
||||
<action>
|
||||
按 Task 0 的 Q1 裁决修复。**若 Q1 = 选项 A**:
|
||||
(1) `removeEnemyAffecting`(`:240-256`):在内层 `store.damages.forEach((dam, index) => …)`(`:246-252`)中把 `index` 收集进一个 `Set<number>`;方法结束前对每个收集到的索引调用 `this.markDirtyIndex(index)`;返回类型与调用点按实现需要调整(两个调用点:`:294`、`:339`);
|
||||
(2) `refreshEnemyAndClearCache`(`:290-333`)与 `refreshEnemy`(`:338-372`):把 `if (set.size === 0) return;` 改为——**先** `this.enemyStore.set(view, set)`(空集也登记,使后续 `markEnemyDirty` 走局部路径而非 `refreshAll()`),再在 `set.size === 0` 时直接结束(无需继续遍历视图);非空时保持既有登记与新索引失效(`:329-332` 的 `dirtyIndexes.delete`/`reducedCache.delete` 不变);
|
||||
(3) `enemyStore` 的读法容错:`markEnemyDirty`(`:135-148`)与 `deleteEnemy`(`:150-167`)在 `store` 为**空集合**时不得视为「未登记」——`markEnemyDirty` 对空集合应直接结束(无视图可刷),`deleteEnemy` 对空集合应只做 `enemyStore.delete(view)`;`removeEnemyAffecting` 对空集合的遍历保持无副作用;
|
||||
(4) `IMapDamage`(`combat/types.ts`)的 `markEnemyDirty`/`deleteEnemy` jsDoc 补写失效契约:调用后该怪物此前影响的坐标索引缓存保证失效;该怪物的反向索引(含空集)保持登记,后续刷新为局部而非整表。
|
||||
**若 Q1 = 选项 B**:只在 `removeEnemyAffecting` 中收集索引并 `this.reducedCache.delete(index)`(不动 `dirtyIndexes`、不登记空集),并按裁决在 jsDoc 写明其局限。
|
||||
**若 Q1 = 选项 C**:不改行为,仅在 jsDoc 写明已知限制并在 SUMMARY 登记。
|
||||
回归用例(`mapDamage.test.ts`,中文单行注释;文件头覆盖说明同步更新):
|
||||
(a) **来源消失**:注册带伤害的怪物视图 → `getReducedDamage(loc)` 预热缓存 → 使该怪物的转换视图集变为空(例如让假转换器返回空数组后调用 `refreshEnemy`/`markEnemyDirty`,或移除其特殊属性)→ 再次 `getReducedDamage(loc)` 断言为 `null`(或不再包含已移除贡献)、`getSeparatedDamage(loc)` 为空;
|
||||
(b) **范围收缩**:假视图的范围/命中点位在第二次转换时变小 → `markEnemyDirty(view)` 后断言跌出范围的坐标 `getReducedDamage` 不再包含已移除贡献(用「多点位 + 局部收缩」构造,避免与 (a) 重复);
|
||||
(c) **整怪删除回归不破**:既有 `removes enemy-sourced damage when the enemy is deleted`(`:106-…` 附近)用例保持通过;
|
||||
(d) **不触发整表重算**(Q1 选项 A 时):在空视图集场景下再次 `markEnemyDirty(view)`,断言行为仍为局部刷新(例如通过 reducer/转换器调用次数或 `enemyStore` 可观测状态断言,不使用对私有字段的反射)。
|
||||
按 Task 0 `<record>` 的 Q1=A 修复(**无分支、无「建议默认」**):
|
||||
(1) **移除端标脏**:在 `removeEnemyAffecting`(`mapDamage.ts:240-256`)内新增局部 `const removed = new Set<number>();`,在内层 `store.damages.forEach((dam, index) => …)`(`:246-252`)回调的**第一条语句**处 `removed.add(index)`(该索引上确实登记了本视图的贡献,无论 `sourcedDamage` 是否仍有对应 point 条目);方法末尾(`this.enemyStore.delete(view)` 之前或之后均可,但必须在遍历结束后)对集合内每个索引调用 `this.markDirtyIndex(index)`。方法签名保持 `private removeEnemyAffecting(view: IEnemyView<TEnemy>): void`(**不**改成返回 `Set`,不新增对外契约);两个调用点(`:294`、`:339`)逐字不变。
|
||||
(2) **空视图集登记**:`refreshEnemyAndClearCache`(`:290-333`)与 `refreshEnemy`(`:338-372`)中,把
|
||||
`const set = new Set<IMapDamageView<any>>(views);` 之后的 `if (set.size === 0) return;` 与 `this.enemyStore.set(view, set);` 两行**换序**为:先 `this.enemyStore.set(view, set);`(空集也登记),再 `if (set.size === 0) return;`。`!this.converter` 与 `!createReadonlyHandler` 两个既有失败分支保持原样(它们不是「空视图集」路径,不属本次裁决)。`refreshEnemyAndClearCache` 的 `collection` 与 `collection.forEach(v => { this.dirtyIndexes.delete(v); this.reducedCache.delete(v); })`(`:302`、`:329-332`)**逐字不改**——重登的索引清脏,跌出范围的索引保留 `removeEnemyAffecting` 留下的脏标记,下次读取时经 `refreshIndex` 按剩余贡献重算。
|
||||
(3) **不改 `markEnemyDirty` 与 `deleteEnemy`**:空的 `Set` 本身是真值,故 `if (!store)`(`:138`、`:152`)不会把「已登记的空集合」误判为「未登记」——`markEnemyDirty` 会走 `refreshEnemyAndClearCache` 局部路径,`deleteEnemy` 对空集合只执行 `enemyStore.delete(view)`。**禁止**为空集新增任何早退分支(那会让 `markEnemyDirty` 回退到 `refreshAll()`,或让光环重新出现后永不刷新);该口径以用例固定(A5)。
|
||||
(4) **不修改 `combat/types.ts`**:`IMapDamage` 的方法声明与 jsDoc 逐字不改(Q1=A 不需要任何接口/类型变更),本任务**零 jsDoc 改动**(用户长期规则)。
|
||||
(5) **回归用例(`mapDamage.test.ts` 新增 4 条;既有用例逐字保留)**,每条 `it` 前写一行中文注释(dev.md:85),并同步 `:1` 的文件头覆盖说明(追加「CR-02 空视图集/范围收缩」字样):
|
||||
(a) **来源消失 → 幽灵伤害消失**:`useReducer` + `useConverter` 后先 `getReducedDamage(fixture.locator)?.damage` 为 7(预热缓存);令 `fixture.converter.views.length = 0`(`readonly` 字段的**数组内容**可变)后调用 `fixture.damage.markEnemyDirty(fixture.view)`;断言 `getReducedDamage(fixture.locator)` 为 `null` 且 `[...getSeparatedDamage(fixture.locator)]` 长度为 0。修复前该断言会读到残留的 7。
|
||||
(b) **范围收缩 → 跌出范围的坐标无旧缓存**:用局部变量持有视图 `const view = new FakeView(7, [indexA, indexB])`(`indexA = context.indexer.locToIndex(fixture.locator.x, fixture.locator.y)`;`indexB` 取另一合法坐标如 `locToIndex(2, 1)`),`useReducer` + `useConverter(new FakeConverter([view]))`;对两个坐标各预热一次缓存(均为 7);把 `view.range.indexes` 收缩为只含 `indexA`(`indexes.length = 1` 或等效)后 `markEnemyDirty(fixture.view)`;断言 `getReducedDamage(locatorB)` 为 `null`、`[...getSeparatedDamage(locatorB)]` 为空,且 `getReducedDamage(fixture.locator)?.damage` 仍为 7。
|
||||
(c) **空集后 `markEnemyDirty` 走局部刷新(`refreshEnemyAndClearCache` 路径)**:`useReducer` + `useConverter` → `fixture.converter.views.length = 0` → `markEnemyDirty(fixture.view)`(完成空集登记)→ `const iterateSpy = vi.spyOn(fixture.context, 'iterateEnemy'); const calls = fixture.converter.calls;` → 再次 `markEnemyDirty(fixture.view)` → 断言 `expect(iterateSpy).not.toHaveBeenCalled()`(`refreshAll()` 才会遍历怪物,`:387`)且 `fixture.converter.calls === calls + 1`。
|
||||
(d) **`refreshAll` 同样登记空集(`refreshEnemy` 路径)**:`useReducer` + `useConverter` → `fixture.converter.views.length = 0` → `fixture.damage.refreshAll()` → 同 (c) 的 spy/`calls` 断言(再次 `markEnemyDirty(fixture.view)` 不触发 `iterateEnemy`、`converter.calls` 恰增 1)。修复前该路径因 `refreshEnemy` 早退而不登记,`markEnemyDirty` 会退回 `refreshAll()`,断言失败。
|
||||
(e) **整怪删除回归不破**:既有 `removes enemy-sourced damage when the enemy is deleted`(`:533-546`)与 `refreshes sourced damage when a registered enemy is marked dirty`(`:503-515`)、102/103/104 用例逐字保留、必须继续通过。
|
||||
注意:全部断言只经公开 API 与 `vi.spyOn` 观测(`converter.calls` 为既有字段),**不得**反射私有字段、不得使用连续 `as` 断言、不新增 `it.skip`、不弱化既有断言。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm exec vitest run packages-user/data-system/src/combat/mapDamage.test.ts</automated>
|
||||
<fails_when>来源消失或范围收缩后 `getReducedDamage` 仍返回含已移除贡献的缓存值(幽灵伤害);或既有 `deleteEnemy`/102/103/104 用例出现回归 —— 按 D-09 退出并修订计划,不得改用「让调用方手动 markDirty」的替代方案</fails_when>
|
||||
<fails_when>来源消失或范围收缩后 `getReducedDamage` 仍返回含已移除贡献的缓存值(幽灵伤害);或空集登记后再次 `markEnemyDirty` 仍调用 `context.iterateEnemy()`(退回 `refreshAll()`);或既有 `deleteEnemy`/102/103/104/多来源叠加用例出现回归 —— 按 D-09 退出并修订计划,不得改用「让调用方手动 markDirty」「在 markEnemyDirty 里对空集早退」等替代方案</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 移除端触达的每个坐标索引都被失效(Q1 选项 A:`markDirtyIndex`;选项 B:`reducedCache.delete`)
|
||||
- 视图集为空时按裁决完成失效与反向索引处置,并有 `markEnemyDirty` 重复调用不触发 `refreshAll()` 的断言(选项 A)
|
||||
- `IMapDamage` 的 jsDoc 与最终契约一致;既有 mapDamage 用例全部保持通过
|
||||
- `removeEnemyAffecting` 对每个被移除的坐标索引都调用 `markDirtyIndex`;方法签名与两个调用点逐字不变
|
||||
- `refreshEnemyAndClearCache` 与 `refreshEnemy` 均为「先 `enemyStore.set(view, set)` 再 `if (set.size === 0) return;`」;`collection` 的失效循环逐字未改
|
||||
- 空视图集后再次 `markEnemyDirty` 有 `iterateEnemy` 未被调用 + `converter.calls` 恰增 1 的双重断言(`markEnemyDirty` 与 `refreshAll` 两入口各一条)
|
||||
- `markEnemyDirty`/`deleteEnemy` 的代码逐字未改(未新增空集分支);`combat/types.ts` 未被改动;本条任务零 jsDoc 改动
|
||||
- 既有 `mapDamage.test.ts` 用例全部保持通过,无新增 `it.skip`
|
||||
</acceptance_criteria>
|
||||
<done>怪物光环/特殊属性消失或范围收缩后不再有幽灵地图伤害,且失效路径保持局部刷新(CR-02 按裁决关闭)</done>
|
||||
<done>怪物光环/特殊属性消失或范围收缩后不再有幽灵地图伤害,且空视图集被登记后失效路径保持局部刷新(CR-02 按 Q1=A 关闭)</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: `#06-17-8` —— `MapLayer.setMapRef` 失效契约 + 读档旧动态块处置(按 Q2/Q3 裁决)</name>
|
||||
<files>packages-user/data-base/src/map/mapLayer.ts, packages-user/data-base/src/map/mapLayer.test.ts, packages-user/data-base/src/map/saveLoad.test.ts</files>
|
||||
<name>Task 2: IN-01(Q4=A)—— 删除后的点残留清理(`deleteEnemy` 剪除 `affectedBy`/`damages`;`deleteMapDamage` 移除空 `IPointInfo`),含死视图不再被重建的自动化见证</name>
|
||||
<files>packages-user/data-system/src/combat/mapDamage.ts, packages-user/data-system/src/combat/mapDamage.test.ts</files>
|
||||
<read_first>
|
||||
- packages-user/data-base/src/map/mapLayer.ts(:44-51 私有映射、:100-124 `removeTileFromPosMap`/`removeTile`、:133-145 `syncStaticEvent`、:371-395 `setMapRef`/`getMapRef`、:418-433 `createDynamic`、:461-507 删除动态块、:512-514 `iterateDynamicTiles`、:600-681 `resize`/`resize2`(另两处 `expired`)、:814-822 `saveState`、:841-849 `loadDynamics`、:855-931 三档 `load*`)
|
||||
- packages-user/data-base/src/map/mapLayer.test.ts(现有动态块与 `setMapRef` 用例)
|
||||
- packages-user/data-base/src/map/saveLoad.test.ts(:176-220 `MapLayer save and load round trips` 与码 124 用例)
|
||||
- packages-user/data-base/src/map/types.ts(`IMapLayerData`/`IMapLayer`/`IMapLayerSave` 的 jsDoc 位置)
|
||||
- packages-user/data-system/src/combat/mapDamage.ts(:15-20 `IPointInfo`、:44-47 `sourcelessDamage`/`sourcedDamage`、:104-120 `addMapDamage`/`deleteMapDamage`、:150-167 `deleteEnemy`、:240-256 `removeEnemyAffecting`(已有 `affectedBy.delete`/`damages.delete` 的写法参照)、:395-427 `refreshIndex`(:403-411 遍历 `point.damages`、:413-426 遍历 `point.affectedBy` 并调用 `view.getDamageWithoutCheck(locator)`、:417 的 `viewStore` 惰性跳过))
|
||||
- packages-user/data-system/src/combat/mapDamage.test.ts(:378-407 sourceless 用例群、:517-546 `deleteEnemy` 用例、:282-360 `createFixture`(`damageView` 为 `FakeView` 实例,可 `vi.spyOn`))
|
||||
- .planning/phases/07-data-fixes/07-REVIEW.md(IN-01 原文与 `Fix` `:241-245`)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **Q2 裁决落地(`setMapRef` 契约)**:选项 A → 保持替换 + 标旧对象 `expired`(`:380-386`)的实现不变,在 `IMapLayerData`/`getMapRef()` 的 jsDoc 写明「持有者见 `expired === true` 必须重取;`array` 引用可能失效」,并确保 `resize`/`resize2`(`:625`/`:668`)的三处行为与文档一致(不修改其行为,只核对口径);选项 B → 三处一并改为原地变更(`this.mapData.expired = false` + `this.mapData.array = this.mapArray`),并同步更新 jsDoc 与任何依赖 `expired` 信号的既有用例(逐条在 SUMMARY 说明)。
|
||||
(2) **Q3 裁决落地(`loadDynamics`/`loadState`)**:选项 A → 在 `loadDynamics`(`:841-849`)入口先清空本图层全部既有动态块:遍历 `iterateDynamicTiles()` 收集快照 → 逐个复用内部 `removeTile(tile)`(`:118-124`,解除 `tilePosMap`/`posTileMap`),并按裁决决定是否对每个块执行 `syncStaticEvent(tile, false)`;再按存档创建新块(既有逻辑)。**注意**:`setMapRef` 已清 `staticTileCache`,`syncStaticEvent` 在清理时是否有意义由裁决决定;若裁决要求触发 `onDeleteDynamic`,须用 `forEachHook` 与 `deleteDynamic`(`:498-507`)保持一致的参数顺序(`hook.onDeleteDynamic?.(tile)`)。选项 B → 只清理存档中不存在坐标上的动态块;选项 C → 不改行为,只在 jsDoc 写明合并/累积语义并登记。
|
||||
(3) **三档一致性**:清理逻辑放在 `loadDynamics` 内(三档共用 `:862-863`/`:886-887`/`:917-918` 三处调用点),避免三档行为分叉;若裁决要求「无 `dynamicBlocks` 字段时也清理」,则在三档 `load*` 的 `loadDynamics` 调用处按存档字段缺失情况补齐。
|
||||
回归用例:
|
||||
(a) `mapLayer.test.ts`:`createDynamic` 若干块 → 记录实例 → `setMapRef`(按 Q2 裁决断言 `expired`/对象身份/`array` 可见性);读档前动态块数为 N(按 Q3 裁决断言清理后的集合与实例状态);
|
||||
(b) `saveLoad.test.ts`:同一图层先创建多于存档的动态块 → `loadState`(三档各自循环)→ 断言 `[...iterateDynamicTiles()].length` 等于存档中的块数(不累积);再 `loadState` 第二次(模拟重复读档)→ 数量仍等于存档块数;既有 `restores the matrix and point events across all compressions` 与码 124 用例保持通过。
|
||||
按 Task 0 `<record>` 的 Q4=A 落地(**无分支**)。本任务在 Task 1 之后执行,只在此基础上追加;不得回改 Task 1 已定的口径。
|
||||
(1) **`deleteEnemy` 剪除(`:150-167`)**:在内层 `affecting.damages.forEach((dam, index) => { … })` 中,于 `this.damageStore.delete(dam);` 旁补上来源侧的剪除——先 `const point = this.sourcedDamage.get(index);`,若存在则 `point.affectedBy.delete(viewItem);` 与 `point.damages.delete(dam);`。`collection.add(index)` 与 `this.viewStore.delete(viewItem)`、`this.enemyStore.delete(view)`、方法末尾的 `collection.forEach(v => this.markDirtyIndex(v))` 逐字保留。写法与 `removeEnemyAffecting`(`:246-252`)的既有剪除保持一致(同一份语义、两处实现),便于对照阅读。**不删除** `sourcedDamage` 中的空点条目(非目标,A4)。
|
||||
(2) **`deleteMapDamage` 移除空点(`:114-120`)**:在 `current.damages.delete(info);` 之后、`this.markDirtyIndex(index);` 之前补一步——当 `current.damages.size === 0 && current.affectedBy.size === 0` 时 `this.sourcelessDamage.delete(index);`。说明:无来源点的 `affectedBy` 结构上从不被写入(只有 `registerSourcedDamage`(`:266-285`)会写 `affectedBy`,且只作用于有来源点),故「两集合皆空」在实践中等价于 `damages.size === 0`;按裁决字面写作两者合取。`markDirtyIndex(index)` 必须保留(缓存失效与本次清理是两件事)。
|
||||
(3) **不修改 `combat/types.ts`**;本任务**零 jsDoc 改动**(用户长期规则)。
|
||||
(4) **回归用例(`mapDamage.test.ts` 新增 2 条;既有用例逐字保留)**,每条 `it` 前一行中文注释(dev.md:85),并同步 `:1` 的文件头覆盖说明(追加「IN-01 删除后点残留」字样):
|
||||
(a) **`deleteEnemy` 剪除的自动化见证**(这是 IN-01 唯一有公开可观测差异的一面):`useReducer` + `useConverter`(`damageView` 覆盖 `fixture.locator` 所在索引)后,`const spy = vi.spyOn(fixture.damageView, 'getDamageWithoutCheck');` → `fixture.damage.deleteEnemy(fixture.view);` → `expect(fixture.damage.getReducedDamage(fixture.locator)).toBeNull();`(该读取会触发脏索引的惰性 `refreshIndex`)→ 断言 `expect(spy).not.toHaveBeenCalled();`。原理:修复前被删视图仍在 `point.affectedBy` 中,`refreshIndex`(`:413-414`)会对它调用 `getDamageWithoutCheck`;剪除后不再触达。再断言 `[...getSeparatedDamage(fixture.locator)]` 长度为 0(公开结果正确)。
|
||||
(b) **`deleteMapDamage` 空点分支的两条臂**:`addMapDamage` 两条无来源伤害于同一点 → `getReducedDamage` 为两者之和 → 删其中一条后为剩下那条的值(非空臂)→ 删最后一条后 `getReducedDamage` 为 `null`、`[...getSeparatedDamage]` 为 0(空点臂)→ 再次 `addMapDamage` 后 `getReducedDamage` 为新值(证明清理未破坏后续使用)。
|
||||
**见证边界(A7,必须照办)**:`deleteMapDamage` 的空点移除**没有公开可观测差异**(`getReducedDamage`/`getSeparatedDamage` 的返回值前后一致)。**禁止**用私有字段反射、`as unknown as` 连续断言、`Object.getOwnPropertyDescriptor` 等手法为它制造伪见证;其落地由本任务与 `<record>` 的逐行对照确认,并在 Task 4 的 SUMMARY 中如实登记。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm exec vitest run packages-user/data-system/src/combat/mapDamage.test.ts</automated>
|
||||
<fails_when>删除怪物后惰性 `refreshIndex` 仍调用被删视图的 `getDamageWithoutCheck`(剪除未生效);或 `deleteMapDamage` 删净后该点仍返回伤害、或再次 `addMapDamage` 后读不到新值;或 Task 1 的 CR-02 用例与既有用例出现回归 —— 按 D-09 退出并修订计划</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `deleteEnemy` 对被删视图执行 `point.affectedBy.delete(viewItem)` 与 `point.damages.delete(dam)`(当 `sourcedDamage` 尚有该 index 的 point 条目时);其余语句逐字保留
|
||||
- `deleteMapDamage` 在两集合皆空时移除该 `sourcelessDamage` 条目,且保留 `markDirtyIndex(index)`
|
||||
- 新增用例证明「删除后惰性 `refreshIndex` 不再触达被删视图」(`vi.spyOn` 计数为 0)与「`deleteMapDamage` 两条臂的公开契约正确」
|
||||
- `sourcedDamage` 的空点条目未被删除(非目标);`combat/types.ts` 未改动;本任务零 jsDoc 改动;无新增 `it.skip`
|
||||
</acceptance_criteria>
|
||||
<done>删除怪物/视图与删除无来源伤害后不再遗留可被惰性重建触达的残留条目(IN-01 按 Q4=A 关闭),且公开契约回归全绿</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: `#06-17-8`(Q3=A1)—— `loadState` 读档前全清既有动态块(复用 `deleteDynamic` 语义 + 触发 `onDeleteDynamic`);`setMapRef` 零改动(Q2=A)</name>
|
||||
<files>packages-user/data-base/src/map/mapLayer.ts, packages-user/data-base/src/map/mapLayer.test.ts, packages-user/data-base/src/map/saveLoad.test.ts</files>
|
||||
<read_first>
|
||||
- packages-user/data-base/src/map/mapLayer.ts(:44-51 `tilePosMap`/`posTileMap`、:100-124 `removeTileFromPosMap`/`removeTile`、:128-145 `syncStaticEvent`、:418-433 `createDynamic`、:460-478 `transferToStatic`、:498-507 `deleteDynamic`(`removeTile` → `forEachHook(hook => hook.onDeleteDynamic?.(tile))` → `await Promise.all(hooks)`)、:512-514 `iterateDynamicTiles`、:837-849 `loadDynamics`、:851-931 三档 `load*` 与 `loadState`(`:862`/`:886`/`:917` 的 `if (save.dynamicBlocks)` 守卫))
|
||||
- packages-user/data-base/src/map/types.ts(:207-257 `IMapLayerHooks`,尤其 `onDeleteDynamic?(tile: IDynamicTile): Promise<void>`;:257 的返回类型是「不等待」约束的来源)
|
||||
- packages-user/data-base/src/map/mapState.ts(:409-425 `MapState.loadState` 同步逐层读档——证明读档清理不能 `await` 钩子)
|
||||
- packages-user/data-base/src/map/mapLayer.test.ts(:86-119 `createFixture`(2×2、事件层别名 `event`)、:341-359 动态创建与 `onCreateDynamic` 钩子用例、:563-580 `deleteDynamic` 与 `onDeleteDynamic` 钩子用例的写法、:595-668 脏标记与门用例)
|
||||
- packages-user/data-base/src/map/saveLoad.test.ts(:47-52 `SAVE_COMPRESSIONS`、:60-112 `createMapFixture`/`setLayerReference`、:176-218 `MapLayer save and load round trips` 与码 124 用例)
|
||||
- .planning/phases/07-data-fixes/07-LOADSTATE-AUDIT.md(G 行「旧动态块累积」与「未确定项 2」)
|
||||
</read_first>
|
||||
<action>
|
||||
按 Task 0 `<record>` 的 Q3=A1 落地(**无分支**)。本任务**不碰 `setMapRef`**(Q2=A:零代码改动、零 jsDoc 改动)。
|
||||
(1) **新增私有方法 `clearDynamics()`**:放在存读档 region 内、**其调用者 `loadState`(`:922-931`)之前**(建议紧随 `loadDynamics`(`:841-849`)之后;dev.md:118 要求私有方法置于调用者之前)。方法体:先用快照数组收集当前全部动态块(`const tiles = [...this.iterateDynamicTiles()];`,避免遍历中改动 `posTileMap`),再逐块按 `deleteDynamic`(`:498-507`)的语义清理——`this.syncStaticEvent(tile, false);` → `this.removeTile(tile);` → `this.forEachHook(hook => hook.onDeleteDynamic?.(tile));`。要点:
|
||||
- **必须触发** `onDeleteDynamic`(Q3=A1 裁决,渲染端可能在监听);参数顺序与 `deleteDynamic`/`transferToStatic` 一致(只传 `tile`)。
|
||||
- **不等待**钩子:本方法是同步的(`loadState` 同步、`MapState.loadState` 同步),而 `onDeleteDynamic` 返回 `Promise<void>`(`types.ts:257`)——按 A6 保留 `forEachHook` 的调用但不 `await`,并在该方法的 jsDoc 里写明「读档清理为触发式、不等待钩子完成」(这是本计划**唯一被授权**的 jsDoc 落笔)。
|
||||
- 该方法**只**解除两张索引映射与触发钩子;不得改 `createDynamic`/`deleteDynamic`/`transferToStatic`。
|
||||
(2) **`loadState` 首行调用**:在 `loadState`(`:922`)分派三档压缩**之前**加入 `this.clearDynamics();`。理由(必须写进实现说明/SUMMARY):三档 `load*` 的 `loadDynamics` 调用点都包在 `if (save.dynamicBlocks)` 守卫内(`:862`/`:886`/`:917`),若把清理放进 `loadDynamics` 内部,则「存档不含动态块」时不会清理、读档结果与存档点不一致;放在 `loadState` 入口使三档与缺字段情形**按构造**一致。`loadDynamics`(`:841-849`)本身逐字不改。
|
||||
(3) **`setMapRef`/`getMapRef`/`resize`/`resize2` 零改动**:这三处 `mapData.expired` 口径(`:380-386`、`:625`、`:668`)与其注释逐字保留;本任务不得出现任何与之相关的 diff。
|
||||
(4) **回归用例**:
|
||||
(a) `mapLayer.test.ts` 新增一条(`it` 前一行中文注释):
|
||||
- 夹具 + `layer.addHook({ onDeleteDynamic: async tile => { deleted.push(tile); } }).load();`(沿用 `:563-580` 的写法;`async` 钩子体在首个 `await` 前**同步**执行,故 `loadState` 返回后 `deleted` 已可断言,必要时 `await Promise.resolve()` 兜底);
|
||||
- `const first = layer.createDynamic(1, 0, 0); const second = layer.createDynamic(1, 1, 0);` → `const saved = layer.saveState(SaveCompression.NoCompression);`(存档含 2 块)→ 再创建 `const extra = layer.createDynamic(1, 0, 1);`(超量)→ `layer.loadState(saved, SaveCompression.NoCompression);`
|
||||
- 断言:`[...layer.iterateDynamicTiles()].length === 2`(等于存档块数,`extra` 已被清理);`[...layer.getDynamicTilesAt(0, 1)]` 为空(超量块不再被索引);`deleted` 含 `first`/`second`/`extra`(清理时**钩子被触发**,Q3=A1 的验收点);读档后的实例**不是**原实例之一(`expect([...layer.iterateDynamicTiles()]).not.toContain(first)`——排除「合并语义」,证明走的是全清 + 重建)。
|
||||
(b) `saveLoad.test.ts` 新增一条(`it` 前一行中文注释,放在 `MapLayer save and load round trips` describe 内):
|
||||
- `for (const compression of SAVE_COMPRESSIONS)` 循环(沿用文件既有风格),每轮 `createMapFixture()` + `setLayerReference(layer)`;
|
||||
- `layer.createDynamic(1, 0, 0);` → `const saved = layer.saveState(compression);`(存档 1 块)→ `layer.createDynamic(1, 1, 0); layer.createDynamic(1, 1, 0);`(读到 3 块)→ `layer.loadState(saved, compression);` → 断言 `[...layer.iterateDynamicTiles()].length === 1`;**再** `layer.loadState(saved, compression);` → 断言仍为 1(重复读档不累积)。
|
||||
- 既有 `restores the matrix and point events across all compressions`(`:176-199`)与码 124 用例(`:201-217`)、以及 `StaticTile`/`DynamicTile`/`GameMap`/`MapState` 各 describe 用例**逐字保留、必须继续通过**。
|
||||
注意:无新增依赖、无新建源码文件、不新增 `it.skip`、不弱化既有断言。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm exec vitest run packages-user/data-base/src/map/mapLayer.test.ts packages-user/data-base/src/map/saveLoad.test.ts</automated>
|
||||
<fails_when>重复读档(或读到动态块更少的存档)后 `iterateDynamicTiles()` 的块数大于存档块数(累积);或 `setMapRef` 的失效契约与 Q2 裁决/文档不一致;或既有矩阵/事件/码 124 用例出现回归 —— 按 D-09 退出并修订计划</fails_when>
|
||||
<fails_when>重复读档(或读到动态块更少的存档)后 `iterateDynamicTiles()` 的块数大于存档块数(累积);或读档清理未触发 `onDeleteDynamic`(Q3=A1 的验收点缺失);或读档后的动态块复用了清空前的旧实例(说明走了合并语义);或既有矩阵/事件/码 124/StaticTile/DynamicTile/GameMap/MapState 用例出现回归 —— 按 D-09 退出并修订计划</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Q2 裁决落地:`setMapRef`(及 `resize`/`resize2` 的口径)与 jsDoc 一致,并有 `getMapRef()` 失效行为的断言
|
||||
- Q3 裁决落地:读档后动态图块集合等于存档(不累积)并有断言;重复读档的断言通过
|
||||
- 三档压缩的清理行为一致;既有 `mapLayer.test.ts`/`saveLoad.test.ts` 断言未弱化
|
||||
- `loadState` 在分派三档之前调用新增的私有 `clearDynamics()`;三档与「存档不含 `dynamicBlocks`」情形均被覆盖(按构造一致)
|
||||
- 清理逐块复用 `deleteDynamic` 语义:`syncStaticEvent(tile, false)` + `removeTile(tile)` + 触发 `onDeleteDynamic`(**必须触发**),且不等待钩子;用例断言钩子被触发
|
||||
- 读档后动态块集合等于存档(`iterateDynamicTiles()` 计数)、重复读档不累积、旧实例不再被管理
|
||||
- `setMapRef`/`getMapRef`/`resize`/`resize2` 与其 jsDoc/注释**零改动**;`loadDynamics` 逐字未改;`combat/types.ts` 未改动
|
||||
- 本任务仅新增一处被授权的 jsDoc(`clearDynamics`),其余注释逐字保留;无新增 `it.skip`、既有断言未弱化
|
||||
</acceptance_criteria>
|
||||
<done>`MapLayer` 的失效契约成文且读档不再累积旧动态块(`#06-17-8` / 审计 G 按裁决关闭)</done>
|
||||
<done>读档不再累积旧动态块(读档结果等于存档点、重复读档不累积),清理复用 `deleteDynamic` 语义并触发渲染端钩子,而 `setMapRef` 保持零改动(`#06-17-8` 按 Q3=A1 关闭,Q2=A 零改动登记)</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: D-12/D-44 门禁 + 全量套件 + 缺口登记(CR-02 / `#06-17-8` / IN-01 处置)与 Phase 7 验证失效说明</name>
|
||||
<name>Task 4: D-12/D-44 门禁 + 全量套件 + 原子提交 + 裁决/缺口登记(CR-02 / IN-01 / `#06-17-8` 闭合,Q2 零改动,IN-04/IN-05 保留在册)与 Phase 7 验证失效说明</name>
|
||||
<files>.planning/phases/07-data-fixes/07-13-SUMMARY.md</files>
|
||||
<read_first>
|
||||
- .planning/phases/07-data-fixes/07-REVIEW.md(CR-02 与 IN-01 编号)
|
||||
- .planning/phases/07-data-fixes/07-REVIEW.md(CR-02 `:98-132`、IN-01 `:241-245`、IN-04/IN-05 编号)
|
||||
- .planning/phases/07-data-fixes/07-LOADSTATE-AUDIT.md(G 行)
|
||||
- .planning/phases/07-data-fixes/07-VERIFICATION.md(头部 verified/status)
|
||||
- .planning/ROADMAP.md(Phase 7 段 Plans 与 Wave 列表)
|
||||
- .planning/WINDOWS.md(确认两条无入账 id)
|
||||
- .planning/WINDOWS.md(确认三条无入账 id)
|
||||
- .planning/phases/07-data-fixes/07-13-PLAN.md(Task 0 `<record>`,用于逐条对照)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **门禁(D-12 / D-44 文件级)**:对 `files_modified` 中每个改动的文件执行 `pnpm exec eslint --fix <文件>` 后 `pnpm exec eslint <文件>` 0 错误;**文件级类型门禁按 06-RESEARCH Pitfall 4 的判定方式**:`pnpm exec vue-tsc --noEmit 2>&1 | Select-String -Pattern "<改动文件相对路径>"` 无命中(**不得**用「退出码非 0」判定失败);`pnpm test:ci` 全绿(对比基线 66 文件 / 690 通过 / 1 跳过,跳过项仍只能是 `equipment.test.ts:306` 码 147)。
|
||||
(2) **原子提交(D-13)**:Task 1(CR-02)与 Task 2(`#06-17-8`)各自独立提交(`fix(07-13): …`)。
|
||||
(3) **缺口登记**:SUMMARY 逐条标注 CR-02 与 `#06-17-8` 已闭合(附 commit);`IN-01` 按 Q4 裁决标注「已修」或「保留在册」;`IN-04`/`IN-05` 标注「本轮清单外,保留在册」。`WINDOWS.md` 不新建、不 `fixed`/`waive`。
|
||||
(4) **Phase 7 验证失效**:SUMMARY 明写 `07-VERIFICATION.md` 已因 07-09 + 07-10..07-14 失效,结束后必须重跑 `/gsd-verify-work`(或 `gsd-verify-work 7`);`ROADMAP.md` 的 Phase 7 计划数与 Wave 列表同步。
|
||||
(1) **门禁(D-12 / D-44 文件级)**:对 `files_modified` 中每个改动的文件执行 `pnpm exec eslint --fix <文件>` 后 `pnpm exec eslint <文件>` 0 错误;**文件级类型门禁按 06-RESEARCH Pitfall 4 的判定方式**:`pnpm exec vue-tsc --noEmit 2>&1 | Select-String -Pattern "<改动文件相对路径>"` 无命中(**不得**用「退出码非 0」判定失败);`pnpm test:ci` **0 失败**、跳过项仍只有 `equipment.test.ts:306` 一条(码 147,D-06)。执行前先重录当日基线并与本计划 objective 的「66 文件 / 729 通过 / 0 失败 / 1 跳过」对比;本计划新增用例后通过数应上升、失败数必须为 0。
|
||||
(2) **原子提交(D-13,按缺陷落三条)**:Task 1 = `fix(07-13): mark removed indexes dirty and register empty view sets`;Task 2 = `fix(07-13): prune deleted views and empty sourceless points`;Task 3 = `fix(07-13): clear existing dynamic tiles before loading`。提交**一律按显式路径** `git add <file>`;**禁止** `git add -A`/`git add -a`、`git stash`、`git clean`、`git reset --hard`。工作树存在与本计划无关的既有未暂存改动(`packages-user/client-base/src/types.ts`、`packages-user/client-modules/src/types.ts`)——**不得**暂存、不得提交、不得改动它们;提交前后用 `git status --short` 核对暂存区只含本计划文件。
|
||||
(3) **Q2 零改动核对(必做,不得只依赖裸 `git diff` 的空输出)**:提交前用工作树对比 `git diff -- packages-user/data-base/src/map/mapLayer.ts packages-user/data-system/src/combat/types.ts`;提交后用**显式区间**对比「本计划起点 sha(`depends_on` 的前一个计划收尾提交)..HEAD」,并断言输出中**不含** `setMapRef`/`getMapRef`/`resize`/`resize2`/`expired` 相关行、也不含 `combat/types.ts` 的任何改动;核对结论写入 SUMMARY 的对照表。同时核对全计划除 Task 3 新增的 `clearDynamics` jsDoc 外**无任何 jsDoc 改动**(用户长期规则)。
|
||||
(4) **缺口登记(SUMMARY 逐条)**:CR-02 已闭合(Q1=A,附 commit);IN-01 已闭合(Q4=A,两处清理,附 commit——并如实标注 `deleteMapDamage` 的空点移除为簿记修复、无公开可观测差异,其见证为公开契约回归 + 代码对照);`#06-17-8`(读档旧动态块累积)已闭合(Q3=A1,附 commit);**Q2 = 「保留现契约、无代码改动、无 jsDoc 改动」**(`setMapRef` 保持「标旧对象 `expired` + 整对象替换」,与 `resize`/`resize2` 一致;契约文本登记于 Task 0 `<record>` 与本 SUMMARY,源码注释按用户长期规则未改);`sourcedDamage` 空点条目为**已知惰性残留**(Q4=A 非目标,不产生错误结果);`IN-04`/`IN-05` 本轮清单外、保留在册。`WINDOWS.md` 不新建、不 `fixed`/`waive`。
|
||||
(5) **Phase 7 验证失效**:SUMMARY 明写 `07-VERIFICATION.md` 已因 07-09 + 07-10..07-14 失效,结束后必须重跑 `/gsd-verify-work`(或 `gsd-verify-work 7`);`ROADMAP.md` 的 Phase 7 计划数与 Wave 列表同步。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm test:ci</automated>
|
||||
<fails_when>`pnpm test:ci` 出现新增失败或新增 `it.skip`;或 `vue-tsc --noEmit` 输出含本计划改动文件的类型错误 —— 按 D-09 退出并修订计划</fails_when>
|
||||
<fails_when>`pnpm test:ci` 出现新增失败或新增 `it.skip`(跳过数 ≠ 1);或 `vue-tsc --noEmit` 输出含本计划改动文件的类型错误;或暂存区/提交中出现 `packages-user/client-base/**`、`packages-user/client-modules/**` 的既有改动;或 `setMapRef`/`getMapRef`/`combat/types.ts` 出现任何 diff —— 按 D-09 退出并修订计划</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 改动文件 `eslint` 0 错误、`vue-tsc --noEmit` 该文件 0 类型错误、`pnpm test:ci` 全绿且跳过项仍仅 1 条
|
||||
- Task 1/2 各有独立提交并含对应锚点
|
||||
- SUMMARY 完成缺口登记(CR-02、`#06-17-8`、IN-01/IN-04/IN-05 处置)与 Phase 7 验证失效说明
|
||||
- 改动文件 `eslint` 0 错误、`vue-tsc --noEmit` 该文件 0 类型错误、`pnpm test:ci` 0 失败且跳过项仍仅 1 条(`equipment.test.ts:306` 码 147)
|
||||
- Task 1/2/3 各有独立原子提交(`fix(07-13): …`),且暂存内容只含本计划文件(无关的 `client-base`/`client-modules` 改动未被触碰)
|
||||
- Q2 零改动已核对:提交后用显式区间 `git diff BASE..HEAD -- packages-user/data-base/src/map/mapLayer.ts packages-user/data-system/src/combat/types.ts`(BASE = 本计划起点提交的显式 sha,即前一计划收尾提交),断言输出中不含 `setMapRef`/`getMapRef`/`resize`/`resize2`/`expired` 相关行、也不含 `combat/types.ts` 的任何改动(提交前若需查未提交树,命令形状相同但去掉区间);除 Task 3 新增 `clearDynamics` 的 jsDoc 外无任何 jsDoc 改动
|
||||
- SUMMARY 完成缺口登记(CR-02、IN-01、`#06-17-8`、Q2 零改动、`sourcedDamage` 空点残留、IN-04/IN-05)与 Phase 7 验证失效说明
|
||||
- `WINDOWS.md` 未被本计划改动
|
||||
</acceptance_criteria>
|
||||
<done>D-44 三步门禁全绿、原子提交完成、缺口登记与 Phase 7 验证失效说明写入 SUMMARY</done>
|
||||
<done>D-44 三步门禁全绿、三条按缺陷原子提交、Q2 零改动经 `git diff BASE..HEAD -- <path>` 核对为空(BASE = 起点提交 sha)、缺口登记与 Phase 7 验证失效说明写入 SUMMARY</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@ -304,7 +380,8 @@ Phase 7 裁决对应(逐条引用):D-01: 本计划是 Phase 7 第 13 个
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| 怪物状态变更 → 地图伤害缓存 | 光环/特殊属性增删与范围变化由战斗侧驱动,缓存失效不全即产生与真实状态不一致的伤害 |
|
||||
| 存档数据 → 地图图层 | 读档决定矩阵、静态块、动态块;清理不全即与存档点不一致 |
|
||||
| 渲染端/外部持有者 → 读档清理钩子 | 读档清空动态块会触发 `onDeleteDynamic`,渲染端可能在监听;钩子不被等待,读档不被其阻塞 |
|
||||
| 存档数据 → 地图图层 | 读档决定矩阵、静态块、动态块;清理不全即与存档点不一致(重复读档累积) |
|
||||
| 图层内部状态 → 外部持有者 | `getMapRef()` / `iterateDynamicTiles()` / 图块实例被外部(渲染端、事件层)持有 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
@ -312,28 +389,31 @@ Phase 7 裁决对应(逐条引用):D-01: 本计划是 Phase 7 第 13 个
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-7-37 | Tampering | `MapDamage.removeEnemyAffecting`(`mapDamage.ts:240-256`) | high | mitigate | Task 1:收集被移除索引并逐个 `markDirtyIndex`,使旧坐标按 `refreshIndex` 重新聚合(CR-02 主修复) |
|
||||
| T-7-38 | Tampering | `refreshEnemyAndClearCache` 的空视图集早退(`:300`) | high | mitigate | Task 1:空集也登记 `enemyStore.set(view, set)` 并完成失效,避免缓存永久残留与 `refreshAll()` 级联 |
|
||||
| T-7-39 | Repudiation | 空集登记后的 `markEnemyDirty`/`deleteEnemy` 读法 | low | mitigate | Task 1:`markEnemyDirty`/`deleteEnemy`/`removeEnemyAffecting` 对空集合显式容错,并有重复调用断言 |
|
||||
| T-7-40 | Tampering | `MapLayer.loadDynamics` 不清理旧动态块(`:841-849`) | medium | mitigate | Task 2:按裁决在读入前清理既有动态块(复用内部 `removeTile`),三档一致,并有重复读档断言 |
|
||||
| T-7-41 | Repudiation | `setMapRef` 的失效信号 `expired` 无成文契约(`:371-391`) | low | mitigate | Task 2 按 Q2 裁决:保持替换+标过期并写入 jsDoc,或原地变更并同步全部依赖点(逐条登记) |
|
||||
| T-7-42 | Denial of Service | 清空动态块时触发大量 `onDeleteDynamic`/`syncStaticEvent` | low | mitigate | Task 0 Q3 已把「是否复用 `deleteDynamic` 的同步与钩子」列为裁决点;实现按裁决执行并在测试中断言通知口径 |
|
||||
| T-7-38 | Tampering | `refreshEnemyAndClearCache` 的空视图集早退(`:300`)与 `refreshEnemy`(`:345`) | high | mitigate | Task 1:两处均改为「先 `enemyStore.set(view, set)` 再 `if (set.size === 0) return;`」,避免缓存永久残留与 `refreshAll()` 级联;两条路径各有 `iterateEnemy` spy + `converter.calls` 断言 |
|
||||
| T-7-39 | Repudiation | 空集登记后 `markEnemyDirty`/`deleteEnemy` 的既有读法 | low | mitigate | Task 1:**不新增分支**(空 `Set` 为真值),并以用例固定「空集≠未登记」——防止后续被改为「空集即未登记」而退回整表重算或永不刷新 (A5) |
|
||||
| T-7-40 | Tampering | `MapLayer.loadDynamics` 不清理旧动态块(`:841-849`) | medium | mitigate | Task 3:`loadState` 入口全清既有动态块(新增私有 `clearDynamics`,复用 `deleteDynamic` 语义),三档与缺 `dynamicBlocks` 情形按构造一致;有重复读档与「旧实例不再被管理」断言 |
|
||||
| T-7-41 | Repudiation | `setMapRef` 的失效信号 `expired` 无成文源码契约(`:371-391`) | low | accept | Q2=A 用户裁决:**保留现契约、零代码改动、零 jsDoc 改动**(长期规则:永远不要擅自改 jsDoc)。契约文本改由 Task 0 `<record>` 与 `07-13-SUMMARY.md` 承载;`resize`/`resize2` 三处口径一致,风险维持在 low |
|
||||
| T-7-42 | Denial of Service | 读档全清动态块时触发大量 `onDeleteDynamic` 与 `syncStaticEvent` | low | mitigate | Task 3:钩子为**触发式、不等待**(`loadState` 同步,A6),读档不被渲染端异步钩子阻塞;`syncStaticEvent(tile, false)` 的写入随后被矩阵读档覆盖,保留它只为与 `deleteDynamic` 语义逐字一致 |
|
||||
| T-7-43 | Denial of Service | `deleteEnemy` 不剪除 `point.affectedBy`/`point.damages`、`deleteMapDamage` 遗留空 `IPointInfo` | low | mitigate | Task 2(Q4=A):`deleteEnemy` 剪除死视图与其伤害条目(有 `getDamageWithoutCheck` 不被调用的自动化见证);`deleteMapDamage` 在两集合皆空时移除该点;`sourcedDamage` 空点为已登记非目标 |
|
||||
| T-7-SC | Tampering | npm/pnpm 依赖安装 | high | mitigate | 本计划不新增依赖、不改 `package.json`;出现安装需求即暂停并要求用户确认包合法性 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
1. `pnpm exec vitest run packages-user/data-system/src/combat/mapDamage.test.ts packages-user/data-base/src/map/mapLayer.test.ts packages-user/data-base/src/map/saveLoad.test.ts` 全绿。
|
||||
2. `pnpm test:ci` 全绿:66 文件 / 690 通过 / 1 跳过(仅 `equipment.test.ts:306` 码 147)。
|
||||
2. `pnpm test:ci` 0 失败:基线(07-12 收尾)66 文件 / 729 通过 / 0 失败 / 1 跳过;本计划新增用例使通过数上升,跳过项仍只为 `equipment.test.ts:306` 码 147(D-06)。
|
||||
3. `pnpm exec eslint <files_modified 中每个改动文件>` 0 错误;`pnpm exec vue-tsc --noEmit` 输出不含这些文件的类型错误。
|
||||
4. Task 0 的 `<record>` 中 Q1..Q4 均有裁决,代码与裁决逐条一致(SUMMARY 对照表)。
|
||||
5. 无新增依赖、无新建源码文件、`WINDOWS.md` 未改动、`gameMap.ts`/`mapState.ts`/`dynamicTile.ts`/`context.ts` 未改动。
|
||||
6. Phase 7 验证失效与重跑要求已在 `07-13-SUMMARY.md` 明写,`ROADMAP.md` 已更新。
|
||||
4. Task 0 的 `<record>` 中 Q1..Q4 均有裁决,代码与裁决逐条一致(SUMMARY 对照表):Q1=A(标脏 + 空集登记)、Q2=A(`setMapRef` 零代码零 jsDoc 改动;用显式区间 `<起点 sha>..HEAD -- <path>` 核对 `mapLayer.ts`/`combat/types.ts` 的输出不含 `setMapRef`/`getMapRef`/`resize`/`resize2`/`expired` 相关行)、Q3=A1(读档全清 + 触发 `onDeleteDynamic`)、Q4=A(IN-01 两处清理)。
|
||||
5. 无新增依赖、无新建源码文件、`combat/types.ts` 未改动、`WINDOWS.md` 未改动、`gameMap.ts`/`mapState.ts`/`dynamicTile.ts`/`context.ts` 未改动;无关的未暂存改动(`client-base`/`client-modules` 的 `types.ts`)未被触碰或提交。
|
||||
6. 除 Task 3 新增私有方法 `clearDynamics` 的 jsDoc(本计划唯一授权落笔)外,本计划无任何 jsDoc/注释改动(用户长期规则)。
|
||||
7. Phase 7 验证失效与重跑要求已在 `07-13-SUMMARY.md` 明写,`ROADMAP.md` 已更新。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- CR-02 按 Q1 裁决闭合:来源消失与范围收缩两条路径都不再有幽灵伤害,且失效路径保持局部刷新。
|
||||
- `#06-17-8` 按 Q2/Q3 裁决闭合:失效契约成文;读档后动态块集合等于存档(不累积),三档一致。
|
||||
- `IN-01`/`IN-04`/`IN-05` 按裁决登记或修复,并在 SUMMARY 逐条记录。
|
||||
- `pnpm test:ci` 全绿、无新增 `it.skip`、既有断言未弱化;D-44 三步门禁全绿。
|
||||
- CR-02 按 Q1=A 闭合:移除端标脏使来源消失与范围收缩两条路径都不再有幽灵伤害;空视图集被登记后 `markEnemyDirty` 保持局部刷新(两入口各有用例见证)。
|
||||
- IN-01 按 Q4=A 落地:`deleteEnemy` 剪除死视图(有自动化见证)与 `deleteMapDamage` 空点移除(簿记修复,公开契约回归全绿);`sourcedDamage` 空点为已登记非目标。
|
||||
- `#06-17-8` 按 Q3=A1 闭合:读档前全清既有动态块(复用 `deleteDynamic` 语义、触发 `onDeleteDynamic`、不等待)、三档与缺字段情形一致、重复读档不累积、旧实例不被复用。
|
||||
- Q2 零改动:`setMapRef`/`getMapRef` 保留「标旧对象 `expired` + 整对象替换」现契约,无代码改动、无 jsDoc 改动;契约登记于 Task 0 `<record>` 与 SUMMARY。
|
||||
- `pnpm test:ci` 0 失败、跳过项仍仅 1 条、无新增 `it.skip`、既有断言未弱化;D-44 三步门禁全绿;三条按缺陷原子提交且未牵连工作树中的无关改动。
|
||||
- Phase 7 验证失效与重跑要求已记录。
|
||||
</success_criteria>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user