docs(07-13): complete map invalidation boundaries plan

This commit is contained in:
unanmed 2026-09-17 17:38:23 +08:00
parent 13234b762a
commit d92e405dc2
3 changed files with 334 additions and 24 deletions

View File

@ -278,7 +278,7 @@ Plans:
3. pnpm test:ci 全绿且不新增跳过用例,数据范围 check:type / check:circular 门禁通过
4. 改动仅限数据端packages 与 packages-user/data-*),不改动渲染端 @user/client-* 与 legacy 渲染接线,双端分离约束保持
**Plans**: 12/14 plans executed — 07-01..07-09 已执行07-09 为阶段标记 Complete 后追加的重开计划,已执行并有 SUMMARY07-10..07-14 为第二轮追加(把 CH 与 CR-01/CR-02 由「只登记」转为「修复」,**尚未执行**)(按 D-02 一系统一计划;每个计划以 D-09 预执行汇报关卡开头,`autonomous: false`
**Plans**: 13/14 plans executed — 07-01..07-09 已执行07-09 为阶段标记 Complete 后追加的重开计划,已执行并有 SUMMARY07-10..07-14 为第二轮追加(把 CH 与 CR-01/CR-02 由「只登记」转为「修复」,**尚未执行**)(按 D-02 一系统一计划;每个计划以 D-09 预执行汇报关卡开头,`autonomous: false`
> **阶段重开2026-09-16**`07-LOADSTATE-AUDIT.md` 登记的同引用审计条目 `#06-17-1`A`#06-17-2`B在本阶段收口后追加为计划 `07-09`Phase 7 因此由 `Complete` 回到未完成;执行 07-09 后 `07-VERIFICATION.md`2026-09-16 结论仅覆盖 8/8 计划的工作树)**失效,必须重跑 `/gsd-verify-work`** 重新出具验证结论。
@ -290,7 +290,7 @@ Plans:
- [x] 07-10-PLAN.md — replayCR-01`set()` 索引数组损坏,与 `delete()` 对齐)/ 审计 H `#06-17-3``setReplayArray` 漏 `expireStreams`/ WR-01bigint 长度字节溢出)/ WR-02参数计数用未截断长度/ WR-03编解码格式版本/ WR-07`insert`/`delete`/`set` 越界校验)
- [x] 07-11-PLAN.md — 容器同引用:`#06-17-4``equipStore` 重建实例脱钩)/ `#06-17-5``flag/system` 字段脱钩)/ `#06-17-6`followers 重建脱钩)
- [x] 07-12-PLAN.md — 装备/属性存档正确性WR-04装备修饰器活值未持久化/ WR-05`deleteModifierByIndex` 簿记残留)/ WR-06`HeroEquipment.loadState` 读档写录像)
- [ ] 07-13-PLAN.md — 地图CR-02`MapDamage` 幽灵伤害:来源消失/范围收缩后缓存残留)/ `#06-17-8``MapLayer.setMapRef` 失效契约 + 读档旧动态块累积)
- [x] 07-13-PLAN.md — 地图CR-02`MapDamage` 幽灵伤害:来源消失/范围收缩后缓存残留)/ `#06-17-8``MapLayer.setMapRef` 失效契约 + 读档旧动态块累积)
- [ ] 07-14-PLAN.md — legacy`#06-17-7``data-fallback` `core.status.hero` 代理闭包持有读档前属性)
- [x] 07-09-PLAN.md — hero`#06-17-1` / `#06-17-2` 同引用修复(`HeroAttribute` 自身实现 `ISaveableContent`,属性存读档在自身实例上原地完成;`IHeroStateSave` 形状变更 + 装备修饰器不入属性存档)
- [x] 07-01-PLAN.md — combat`#06-01-1` / `#06-01-2` / `#06-01-3` / `#06-01-4`(含 `#06-15-1`
@ -348,11 +348,11 @@ Plans:
**Wave 12** *(blocked on Wave 11)*
- [ ] 07-12-PLAN.md — 装备/属性存档正确性WR-04 / WR-05 / WR-06
- [x] 07-12-PLAN.md — 装备/属性存档正确性WR-04 / WR-05 / WR-06
**Wave 13** *(blocked on Wave 12)*
- [ ] 07-13-PLAN.md — 地图失效边界与动态块CR-02 / `#06-17-8`
- [x] 07-13-PLAN.md — 地图失效边界与动态块CR-02 / `#06-17-8`
**Wave 14** *(blocked on Wave 13)*
@ -371,4 +371,4 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7
| 4. 渲染适配与双布局 | 0/TBD | Not started | - |
| 5. Legacy 移植 | 0/TBD | Not started | - |
| 6. 单元测试 | 18/18 | In Progress| |
| 7. 数据端缺陷修复 | 12/14 | In Progress| |
| 7. 数据端缺陷修复 | 13/14 | In Progress| |

View File

@ -1,19 +1,19 @@
---
gsd_state_version: "1.0"
milestone: v1.0
current_phase: 1
current_phase_name: 事件系统
status: planning
stopped_at: Phase 7 complete, ready to plan Phase 1
last_updated: "2026-09-16T05:22:43.856Z"
last_activity: 2026-09-16
last_activity_desc: Phase 7 complete, transitioned to Phase 1
state_head: 35f6fed9b4780fc7fdf7e024be3d588ed4f6b88c
current_phase: 07
current_phase_name: 数据端缺陷修复
status: executing
stopped_at: "Executed 07-13 (CR-02 / IN-01 / #06-17-8); 07-14 remains"
last_updated: "2026-09-17T09:34:52.902Z"
last_activity: 2026-09-17
last_activity_desc: 07-13 executed (CR-02 / IN-01 / #06-17-8); 07-14 remains
state_head: 13234b762aa7bb1efb45d97720414af7d46267b7
progress:
total_phases: 7
completed_phases: 1
total_plans: 60
completed_plans: 60
completed_phases: 0
total_plans: 69
completed_plans: 68
milestone_name: milestone
---
@ -24,14 +24,14 @@ milestone_name: milestone
See: .planning/PROJECT.md (updated 2026-09-10)
**Core value:** 引擎能完整跑通一部魔塔——开局到结局,存档、战斗、地图、事件、剧情全链路可玩。
**Current focus:** Phase 7 — 数据端缺陷修复
**Current focus:** Phase 07 — 数据端缺陷修复
## Current Position
Phase: 1 — 事件系统
Plan: Not started
Status: Ready to plan
Last activity: 2026-09-16 — Phase 7 complete, transitioned to Phase 1
Phase: 07 (数据端缺陷修复) — EXECUTING
Plan: 13 of 14
Status: Ready to execute
Last activity: 2026-09-17 — 07-13 executed (CR-02 / IN-01 / `#06-17-8`); 07-14 remains
Progress: [█████░░░░░] 50%
@ -94,6 +94,7 @@ Progress: [█████░░░░░] 50%
| Phase 06 P13 | 14min | 3 tasks | 4 files |
| Phase 06 P14 | 12min | 3 tasks | 2 files |
| Phase 06 P15 | 12min | 2 tasks | 3 files |
| Phase 07-data-fixes P13 | 56min | 5 tasks | 5 files |
## Accumulated Context
@ -203,6 +204,10 @@ Recent decisions affecting current work:
- [Phase 06]: 06-15G-06-01-D`EnemyContext.deleteAura`)补测首次实测即为红灯——`addAura` 生效atk 2→5断言通过`deleteAura`(同一 `FakeAura` 实例)+ 再次 `buildup` 后 atk 仍为 5期望回到基础值 2故按 D-05 保留正确预期并标记 `it.skip`,不弱化断言也不改写为可跑绿假象
- [Phase 06]: 06-15#06-15-1 与既有 #06-01-4 同根因(`buildup()` 只清空光环拓扑、未像 `refreshEnemy()` 那样先 `view.reset()`findings 中交叉引用 #06-01-4 而非另立独立缺陷编号;修复 #06-01-4 后本用例可直接取消 skip
- [Phase 06]: 06-15删除全局光环判定必须传同一光环实例`globalAuraList` 为 Set 身份比较)且必须注册 `FakeConverter([])` 打开光环流水线(否则 `buildupBase()` 不执行导致假绿);本计划无新增码、无新增依赖、不测 saveState/loadState、不改动任何生产/核心源码
- [Phase 07]: [Phase 07] 07-13 Q1=AMapDamage 移除端逐个 markDirtyIndex + 空视图集 enemyStore.set空 Set 为真值不新增分支A5
- [Phase 07]: [Phase 07] 07-13 Q2=AsetMapRef/getMapRef 保留「标旧对象 expired + 整对象替换」现契约,零代码、零 jsDoc 改动(契约文本落在 <record> 与 07-13-SUMMARY
- [Phase 07]: [Phase 07] 07-13 Q3=A1MapLayer.loadState 入口 clearDynamics 全清既有动态块(复用 deleteDynamic 语义、触发 onDeleteDynamic、不等待A6
- [Phase 07]: [Phase 07] 07-13 Q4=AIN-01 两处清理deleteEnemy 剪除 affectedBy/damagesdeleteMapDamage 空点移除为无公开可观测差异的簿记修复A7sourcedDamage 空点不删)
### Roadmap Evolution
@ -238,6 +243,6 @@ None yet.
## Session Continuity
Last session: 2026-09-15T06:00:01.304Z
Stopped at: Phase 7 complete, ready to plan Phase 1
Resume file: .planning/phases/07-data-fixes/07-CONTEXT.md
Last session: 2026-09-17T09:34:35.942Z
Stopped at: Executed 07-13 (CR-02 / IN-01 / #06-17-8); 07-14 remains
Resume file: None

View File

@ -0,0 +1,305 @@
---
phase: 07-data-fixes
plan: 13
subsystem: data-layer
tags: [map-damage, ghost-damage, cache-invalidation, dirty-index, empty-view-set, point-residue, load-state, dynamic-tiles, cr-02, in-01, 06-17-8, regression]
requires:
- phase: 07-data-fixes
provides: "07-12 equipment/attribute save correctness (WR-05 / WR-06 / compareEquip + IN-02 test alignment) and its D-44 file-level gate baseline (66 files / 729 passed / 0 failed / 1 skipped)"
- phase: 07-data-fixes
provides: "07-11 container same-reference load and 07-09's same-reference principle (in-place loadState)"
- phase: 06-unit-tests
provides: "D-44 file-level three-step gate, the mapDamage/mapLayer/saveLoad suites, and D-06's retained code-147 skip"
provides:
- "CR-02 closed (Q1=A): removeEnemyAffecting collects every removed coordinate index and calls markDirtyIndex for each, so BOTH the source-disappeared and the range-shrunk paths invalidate through the existing refreshIndex; refreshEnemyAndClearCache and refreshEnemy register an empty Set instead of early-returning, so a later markEnemyDirty stays on the local path rather than falling back to refreshAll()"
- "IN-01 closed (Q4=A): deleteEnemy prunes the removed view from point.affectedBy and drops its point.damages entry (no longer relying on refreshIndex's viewStore lazy skip); deleteMapDamage removes the sourcelessDamage IPointInfo once both damages and affectedBy are empty, keeping markDirtyIndex"
- "#06-17-8 closed (Q3=A1): MapLayer.loadState clears every existing dynamic tile before dispatching the three compression tiers, reusing deleteDynamic semantics (syncStaticEvent(tile,false) + removeTile + onDeleteDynamic) without awaiting the hook; a load never accumulates tiles and never reuses old instances"
- "Q2 = A registered: setMapRef/getMapRef keep the existing 'expire the old mapData object + whole-object replacement' contract with ZERO code change and ZERO jsDoc change; the contract text lives in the Task 0 <record> and this SUMMARY"
- "sourcedDamage empty point entries remain by decision (Q4=A non-goal) — registered as a known lazy residue that produces no wrong results"
affects: [07-data-fixes, data-system-combat, data-base-map, render-side-holders, phase-07-verification]
actuals:
tokens: 3502
tasks: 5
commits: 6
plan_head_before: b37c218f2d4c4ac2158d478795e2738d4edfbd69
tech-stack:
added: []
patterns:
- "Removal-side invalidation ownership: the operation that removes a contribution owns marking its coordinate indexes dirty, and the re-registration path clears only the indexes it re-registers — indexes that fell out of range keep the dirty flag and are re-aggregated by refreshIndex"
- "An empty Set is a registered value, not an absence: enemyStore.set(view, emptySet) keeps a later markEnemyDirty on the local path instead of falling back to refreshAll(); no early-return branch is added for the empty case (A5)"
- "Prune on delete instead of relying on a lazy skip: deleteEnemy removes the deleted view from point.affectedBy so refreshIndex never calls getDamageWithoutCheck on a dead view"
- "Read-time cleanup at the entry point: clearDynamics runs as the first line of loadState (not inside loadDynamics) so the three compression tiers AND the missing-dynamicBlocks case start from a clean slate by construction"
- "Snapshot-then-mutate iteration: [...iterateDynamicTiles()] snapshots the managed tiles before posTileMap is mutated during cleanup"
- "Fire-and-forget cleanup hooks: loadState is synchronous while onDeleteDynamic returns Promise<void>, so the read-time cleanup calls forEachHook without awaiting (the same call shape transferToStatic uses)"
- "Mutation-only code paths in the data layer carry no new comments: the plan authorized exactly one jsDoc (the new private method), and the pruning mirrors removeEnemyAffecting's existing shape"
key-files:
created: []
modified:
- 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
key-decisions:
- "Task 0 Q1=A (user, 2026-09-17): mark every removed index dirty AND register the empty view set; no early-return branch is added for the empty case (A5)"
- "Task 0 Q2=A: setMapRef/getMapRef keep the existing 'expire old object + replace' contract — zero code change and zero jsDoc change (user long-term rule: never touch jsDoc unprompted); the contract text lives in the <record> and this SUMMARY"
- "Task 0 Q3=A1: fully clear existing dynamic tiles before loading, reusing deleteDynamic semantics and FIRING onDeleteDynamic; non-awaited because loadState is synchronous (A6)"
- "Task 0 Q4=A: include IN-01 in this plan (two cleanups); sourcedDamage empty point entries are explicitly a non-goal (A4)"
- "deleteMapDamage's empty-point removal has NO publicly observable difference (A7) — registered honestly; no private-field reflection or chained as-assertion witness was manufactured"
- "Only one jsDoc was authored in this plan: the new private clearDynamics (dev.md:73 requires a jsDoc; dev.md:118 requires private methods before their caller). Task 1 additionally added three plain // why-comments inside mapDamage.ts (AGENTS.md permits new comments; dev.md:83 requires a why, not a what) — so the plan wording 'no jsDoc/comment change' holds as 'no jsDoc added beyond clearDynamics and no existing comment/jsDoc modified'"
patterns-established:
- "Empty set is truthy: markEnemyDirty/deleteEnemy need no new branch for a registered empty view set"
- "Delete-time pruning with a getDamageWithoutCheck spy as the dead-view witness"
requirements-completed: [FIX-01]
coverage:
- id: D1
description: "CR-02 (Q1=A): a source enemy's aura/special disappearing or its range shrinking no longer leaves ghost map damage; the empty view set is registered so a later markEnemyDirty stays local"
requirement: FIX-01
verification:
- kind: unit
ref: "packages-user/data-system/src/combat/mapDamage.test.ts#MapDamage sourced conversion and reduction (drops ghost damage when the enemy view set becomes empty / drops stale damage on indexes that fall out of a shrunken range / keeps refreshing locally after an empty view set is registered / registers an empty view set through refreshAll as well); existing deleteEnemy and code-102/103/104 and multi-source cases kept verbatim"
status: pass
human_judgment: false
- id: D2
description: "IN-01 (Q4=A) deleteEnemy: the deleted view is pruned from point.affectedBy and its point.damages entry is dropped, so the lazy refreshIndex never calls getDamageWithoutCheck on a dead view"
requirement: FIX-01
verification:
- kind: unit
ref: "packages-user/data-system/src/combat/mapDamage.test.ts#MapDamage sourced conversion and reduction (does not rebuild damage from a deleted view after pruning); reverse-verified: reverting the pruning fails the case with getDamageWithoutCheck called once"
status: pass
human_judgment: false
- id: D3
description: "#06-17-8 (Q3=A1): loadState never accumulates dynamic tiles — the loaded set equals the save, repeated loads do not grow it, old instances stop being managed, and the cleanup fires onDeleteDynamic synchronously"
requirement: FIX-01
verification:
- kind: unit
ref: "packages-user/data-base/src/map/mapLayer.test.ts#MapLayer dynamic conversion (clears existing dynamic tiles before loading); packages-user/data-base/src/map/saveLoad.test.ts#MapLayer save and load round trips (does not accumulate dynamic tiles when loading fewer than present); reverse-verified: disabling the clearDynamics() call fails them with 5-vs-2 and 4-vs-1"
status: pass
human_judgment: false
- id: D4
description: "Q2 = A registered: setMapRef/getMapRef keep 'expire the old mapData object + whole-object replacement' with zero code change and zero jsDoc change"
requirement: FIX-01
verification:
- kind: other
ref: "git diff 674b325..HEAD -- packages-user/data-base/src/map/mapLayer.ts packages-user/data-system/src/combat/types.ts => mapLayer.ts +14/-0 with no setMapRef/getMapRef/resize/resize2/expired line; combat/types.ts empty"
status: pass
human_judgment: false
- id: D5
description: "IN-01 (Q4=A) deleteMapDamage: the sourcelessDamage IPointInfo is removed once both damages and affectedBy are empty (bookkeeping/memory only)"
requirement: FIX-01
verification:
- kind: unit
ref: "packages-user/data-system/src/combat/mapDamage.test.ts#MapDamage sourceless damage (prunes the sourceless point once every damage is deleted) — public contract only"
status: pass
human_judgment: true
rationale: "A7: this branch has NO publicly observable difference — getReducedDamage/getSeparatedDamage return identical values before and after the fix. Its landing is confirmed by line-by-line conformance to the Task 0 <record> plus the public-contract regression above; a private-field-reflection or chained-as witness was explicitly forbidden and was not manufactured. Accepting the bookkeeping fix therefore requires reading the code/record comparison, not a test signal."
# Metrics
duration: 56min
completed: 2026-09-17
status: complete
---
# Phase 7 Plan 13: Map invalidation boundaries (CR-02 ghost damage / IN-01 post-delete residue / `#06-17-8` dynamic-tile accumulation) Summary
**`MapDamage` 的幽灵伤害CR-02与删除后点残留IN-01闭合、`MapLayer.loadState` 读档前全清既有动态块(`#06-17-8``setMapRef`/`getMapRef` 按用户裁决保持「标旧对象 `expired` + 整对象替换」的现契约,**零代码改动、零 jsDoc 改动**;新增 4 条公开可观测回归2 条 CR-02 之外的 IN-01 + 2 条读档不累积),全量套件 66 文件 / 737 通过 / 0 失败 / 1 跳过。**
## Performance
- **Duration:** ~56 min plan-level本地 16:36 → 17:32本次接力会话Tasks 24**14 min**(本地 17:18 → 17:32
- **Started:** 2026-09-17T08:36:49Z本地 16:36:49裁决锁提交 `b37c218`
- **Completed:** 2026-09-17T09:32Z本地 17:32
- **Tasks:** 5Task 0 已由用户于 2026-09-17 裁决并写入 `<record>`Task 1 由上一会话完成并提交 `cff4291`;本会话完成 Task 2/3/4
- **Files modified:** 5生产 2 + 测试 3无新建文件、无新依赖、无删除文件
- **Diff size:** +199 / 3`git diff --numstat b37c218..HEAD` 五个文件);实测 `actuals.tokens = 3502`diff 字符数 14007 / 4相对 `estimate.tokens = 54000` 偏低 15.4×(与 07-11、07-12 同量级偏高,供后续校准)
## Task Commits
`git rev-list --count b37c218..HEAD` = **6**Task 1/2/3 三条任务提交 + 本 SUMMARY 的元数据提交 + 2 条非本计划产出的提交:合并提交 `3da7863` 及其带入的 `954c3fc``plan_head_before` = `b37c218f2d4c4ac2158d478795e2738d4edfbd69`。全部为**普通提交、钩子生效**,未使用 `--no-verify`、未 amend 任何已推送提交:
1. **Task 1: CR-02Q1=A移除端标脏 + 空视图集登记**`cff4291` (fix)〔上一会话完成,本会话未改动其任何内容〕
2. **Task 2: IN-01Q4=A删除后点残留清理**`c1df1e0` (fix)
3. **Task 3: `#06-17-8`Q3=A1读档前全清既有动态块**`13234b7` (fix)
4. **Plan metadata:** 本 SUMMARY 的 `docs(07-13): …` 提交 (docs)
两条非任务提交的来历(如实登记,不影响计划产出):`3da7863` 是上一会话为同步 origin 而做的合并;`954c3fc` 是该合并从 origin 带入的 07-12 重复完成提交(与主线 `4917baa` 同内容。二者均未触碰本计划五个文件下方「Issues Encountered 3」
**提交文件清单**`git log --name-only`):三条任务提交共 5 个文件全部落在 frontmatter `files_modified` 内,**零计划外文件**`git log --name-only b37c218..HEAD` 中不含 `packages-user/client-base/**`、`packages-user/client-modules/**`、`AGENTS.md`(并发 WIP 未被暂存、未被提交)。
## Files Created/Modified
- `packages-user/data-system/src/combat/mapDamage.ts` — (Task 1) `removeEnemyAffecting` 收集被移除贡献的坐标索引并在遍历结束后统一 `markDirtyIndex``refreshEnemyAndClearCache`/`refreshEnemy` 换序为「先 `enemyStore.set(view, set)``if (set.size === 0) return;`」,并各加一条 why-commentTask 1 落笔)。(Task 2) `deleteEnemy` 内层循环补「取 `point``point.affectedBy.delete(viewItem)` + `point.damages.delete(dam)`」;`deleteMapDamage` 在 `damages`/`affectedBy` 皆空时移除该 `sourcelessDamage` 条目。全计划 +17 / 2
- `packages-user/data-system/src/combat/mapDamage.test.ts` — Task 1 新增 4 条(空视图集来源消失 / 范围收缩 / 空集后局部刷新 / `refreshAll` 同样登记空集Task 2 新增 2 条(`deleteEnemy` 死视图见证 / `deleteMapDamage` 两条臂);文件头覆盖说明按 A1 追加「CR-02 空视图集/范围收缩、IN-01 删除后点残留」。既有用例逐字保留。全计划 +125 / 1
- `packages-user/data-base/src/map/mapLayer.ts` — 新增私有方法 `clearDynamics()`(置于 `loadDynamics` 之后、其调用者 `loadState` 之前;本计划**唯一授权**的 jsDoc 落笔),`loadState` 首行调用它;`setMapRef`/`getMapRef`/`resize`/`resize2`/`loadDynamics` 逐字未改。+14 / 0
- `packages-user/data-base/src/map/mapLayer.test.ts` — 新增 1 条实例级回归(清空 + 钩子触发 + 旧实例不再被管理),新增 `SaveCompression` 导入(既有导出,无新依赖)。+25 / 0
- `packages-user/data-base/src/map/saveLoad.test.ts` — 新增 1 条三档往返回归(动态块多于存档 → 读档后等于存档;连续两次读档不累积),放在 `MapLayer save and load round trips` describe 内。+18 / 0
## Decisions Made
Task 0 的四条用户裁决(`07-13-PLAN.md` 的 `<record>`,锁于 `b37c218`)逐条落地,无自行改选:
| 裁决 | 落地位置 | 可观测证据 |
| --- | --- | --- |
| **Q1=A 标脏 + 空集登记** | `mapDamage.ts``removeEnemyAffecting` / `refreshEnemyAndClearCache` / `refreshEnemy``cff4291` | `mapDamage.test.ts` 的 4 条 CR-02 用例(含 `iterateEnemy` 未被调用 + `converter.calls` 恰增 1 的双重断言) |
| **Q2=A 保留替换 + `expired`,零代码零 jsDoc** | **无改动**`cff4291`/`c1df1e0`/`13234b7` 均未触碰) | `git diff 674b325..HEAD -- mapLayer.ts combat/types.ts``mapLayer.ts` +14/0 且无 `setMapRef`/`getMapRef`/`resize`/`resize2`/`expired` 行;`combat/types.ts` 输出为空 |
| **Q3=A1 读档前全清 + 触发钩子** | `mapLayer.ts``clearDynamics` + `loadState` 首行(`13234b7` | `mapLayer.test.ts` 的钩子触发与旧实例断言;`saveLoad.test.ts` 三档重复读档块数恒等于存档 |
| **Q4=A 一并修 IN-01** | `mapDamage.ts``deleteEnemy` 剪除 + `deleteMapDamage` 空点移除(`c1df1e0` | `deleteEnemy` 有自动化见证(死视图不再被 `refreshIndex` 触达);`deleteMapDamage` 为簿记修复A7见下 |
**「空集不是未登记」口径A5**`markEnemyDirty``if (!store)`)与 `deleteEnemy``if (!store) return`**未新增任何分支**——空 `Set` 为真值,已登记的空集合天然走局部路径;该口径由 `keeps refreshing locally after an empty view set is registered``registers an empty view set through refreshAll as well` 两条用例固定。
**「触发式、不等待」口径A6**`clearDynamics` 为同步方法(`loadState` 与 `MapState.loadState` 均同步),`onDeleteDynamic` 返回 `Promise<void>`,故按 `forEachHook(hook => hook.onDeleteDynamic?.(tile))` 触发而**不** `await`(与 `transferToStatic` 的调用形状一致,与可等待的 `deleteDynamic` 有意区分)。
## Findings ↔ Ledger / Registry 对照缺口登记Task 4(4)
| 条目 | 结论(引用裁决) | 锚点 |
| --- | --- | --- |
| **CR-02** `MapDamage` 幽灵伤害(来源消失 / 范围收缩后缓存残留) | **已闭合Q1=A**:移除端逐个 `markDirtyIndex`,两条路径都经既有 `refreshIndex` 按剩余贡献重新聚合;空视图集登记后 `markEnemyDirty` 保持局部刷新(`refreshAll()` 不再被级联触发。反向验证Task 1 用例在当前工作树为绿,且 07-12 基线中它们不存在 | `cff4291` |
| **IN-01** 删除后的点残留 | **已闭合Q4=A两处清理**`deleteEnemy` 剪除 `point.affectedBy``point.damages`**有自动化见证**:删除后惰性 `refreshIndex``spy(getDamageWithoutCheck)` 计数为 0反向验证关闭剪除后该用例确定性失败「Number of calls: 1」`deleteMapDamage` 在两集合皆空时移除该 `sourcelessDamage` 条目。**如实登记**`deleteMapDamage` 的空点移除为**簿记/内存修复,无公开可观测差异**`getReducedDamage`/`getSeparatedDamage` 前后一致),其见证仅为公开契约回归 + 与 `<record>` 的逐行对照,**未**伪造私有字段反射 / 连续 `as` 见证A7 | `c1df1e0` |
| **`#06-17-8`** 读档旧动态块累积(审计 G | **已闭合Q3=A1**`loadState` 首行 `clearDynamics()` 全清本层既有动态块(逐块 `syncStaticEvent(tile,false)` + `removeTile` + **触发** `onDeleteDynamic`,不等待),三档与「存档不含 `dynamicBlocks`」情形**按构造**一致反向验证关闭该调用后两条新用例确定性失败5-vs-2、4-vs-1 | `13234b7` |
| **Q2** `setMapRef` 失效契约 | **保留现契约、无代码改动、无 jsDoc 改动**`setMapRef`(标旧 `mapData` `expired = true` + 整对象替换)与 `getMapRef` 逐字保留,与 `resize`/`resize2` 的 `:625`/`:668` 口径一致。契约文本按用户长期规则**不进源码注释**,改由 Task 0 `<record>` 与本 SUMMARY 承载T-7-41 仍为 accept / low | 无提交(零改动) |
| **`sourcedDamage` 空点条目** | **已知惰性残留Q4=A 非目标)**:本计划**不删除** `sourcedDamage` 中的空 `IPointInfo`;该残留不产生错误结果(`refreshIndex` 对无 `viewStore` 的视图不再重建),登记为设计边界 | 未修(按裁决) |
| **IN-04 / IN-05** | **保留在册,本轮不修**`07-REVIEW.md` 的 Info 级条目:`DamageContext.getDamageInfo` 冗余判空、`EnemyManager.reusePrefab` 未注册来源静默 no-op——不在本轮清单只登记不修 | 未修 |
| `WINDOWS.md` | **未被本计划改动、不新建条目、未执行 `fixed`/`waive`**D-14CR-02 / IN-01 / `#06-17-8` 均无入账 id实测 `git status --short -- .planning/WINDOWS.md` 为空,文件仍为 11 open / 0 waived / 17 fixed / 28 total | 无 |
### jsDoc / 注释审计(用户长期规则)
| 项 | 结论 | 证据 |
| --- | --- | --- |
| 新增 jsDoc | **恰好 1 处**`clearDynamics`(本计划唯一授权落笔) | `git diff 674b325..HEAD -- "*.ts" \| Select-String "^\+.*/\*\*"` 仅 1 命中,位于 `mapLayer.ts` |
| 既有 jsDoc / 注释被修改 | **0 处**(生产文件) | `git diff 674b325..HEAD -- "*.ts" \| Select-String "^-\s*(//\|/\*\|\*)"` 唯一命中是 `mapDamage.test.ts:1` 的**测试文件头**——按 A1 与 dev.md:85 的既定要求授权同步(只追加、不删除信息) |
| 非 jsDoc 新增注释 | Task 1 在 `mapDamage.ts` 新增 3 条单行 why-comment`收集被移除贡献的坐标索引…`、`空集也需要登记…` ×2AGENTS.md 明许「新增注释随便新增」且 dev.md:83 要求注释说明「为什么」Task 1 的验收口径为「零 jsDoc 改动」,已满足。**Task 2/3 未新增任何注释**(含 jsDoc | `git diff 674b325..HEAD -- mapDamage.ts` 的注释行均为 Task 1 的 `+` 行 |
| 计划 Task 4 表述校准 | `<objective>` 第 6 条「无任何 jsDoc/注释改动」按「除 `clearDynamics` 的 jsDoc 外无**新增 jsDoc**、且无**既有** jsDoc/注释被修改」成立Task 1 的 3 条非 jsDoc 注释不违禁令 | 见上两行 |
### 断言变更清单A1 唯一授权;逐条说明)
- **既有断言无一条被改写**`git diff 674b325..HEAD` 中被删除的断言行仅为 `mapDamage.test.ts:1` 的文件头注释;`mapDamage.test.ts` 的既有 CR-02 之外的用例、102/103/104、`deleteEnemy`/`markEnemyDirty`、多来源叠加用例,以及 `map/saveLoad.test.ts` 的三档往返与码 124 用例**逐字保留且全部通过**。
- **未新增 `it.skip` / `test.skip`**`git diff 674b325..HEAD -- "*.test.ts" | Select-String "^\+.*(it|test)\.skip"` 为空);跳过项仍仅 1 条(码 147D-06
- **无既有用例被删除**`git diff --diff-filter=D --name-only 674b325..HEAD` 为空。
## D-44 文件级门禁结果
| 门禁 | 命令 | 结果 |
| --- | --- | --- |
| ESLint5 个改动文件) | `pnpm exec eslint --fix <file>``pnpm exec eslint <file>`Task 2/3 后各一次) | **0 errors**5/5 文件 `--fix` 与检查退出码均为 0`--fix` 后工作树无新增改动,说明提交版本即已合规) |
| 类型(文件级) | `pnpm exec vue-tsc --noEmit` → 过滤 5 个改动文件相对路径(**未**以整仓退出码判定,按 06-RESEARCH Pitfall 4 | **0 命中**(整仓 27 条诊断全部属并发 WIP 的 `client-*` / legacy 渲染端,与 5 个改动文件无关;`TSC_EXIT=2` 不作为判据) |
| 全量套件 | `pnpm test:ci` | 66 文件;**737 通过 / 0 失败 / 1 跳过** |
| 聚焦套件 | `pnpm exec vitest run packages-user/data-system/src/combat/mapDamage.test.ts` | **21 通过 / 0 失败** |
| 聚焦套件 | `pnpm exec vitest run packages-user/data-base/src/map/` | **125 通过 / 0 失败**11 文件,含 `mapLayer.test.ts` 36、`saveLoad.test.ts` 10 |
| Q2 零改动 | `git diff 674b325..HEAD -- packages-user/data-base/src/map/mapLayer.ts packages-user/data-system/src/combat/types.ts` | `mapLayer.ts` **+14/0** 且**无** `setMapRef`/`getMapRef`/`resize`/`resize2`/`expired` 相关行;`combat/types.ts` **输出为空**(以 `b37c218..HEAD` 复核结果相同) |
**跳过项实测位置**`packages-user/data-base/src/hero/equipment.test.ts:353``it.skip('warns code 147 when no equipment slot is available', …)`,码 147D-06。计划与接力说明记载的「`:306`」为**陈旧行号**(文件未被本计划触碰,`git diff` 输出为空);**跳过数与码均未变化**(全仓 `it.skip` 计数 = 1性质不受影响。
## Reverse-Verification用例有效性证据
本次接力会话对 Task 2/3 的新用例各做一次反向实验,实验后均精确恢复(`git diff` 复核改动与提交版本逐字一致):
| 反向实验 | 期望 | 实测 |
| --- | --- | --- |
| 临时移除 `deleteEnemy` 内的 `point.affectedBy.delete` / `point.damages.delete`Task 2 剪除) | `does not rebuild damage from a deleted view after pruning` 失败 | **1 failed**`expect(spy).not.toHaveBeenCalled()` 失败,`getDamageWithoutCheck` 被调用 1 次(`{x:1,y:0}` |
| 临时移除 `loadState` 首行的 `this.clearDynamics();`Task 3 入口清理) | 两条 `#06-17-8` 用例失败 | **`mapLayer.test.ts` 1 failed**`expected […] to have a length of 2 but got 5`**`saveLoad.test.ts` 1 failed**`expected […] to have a length of 1 but got 4` |
恢复后:`pnpm exec vitest run packages-user/data-base/src/map/` = **125 通过**`pnpm test:ci` = **737 通过 / 0 失败 / 1 跳过**。**未**执行任何 `--no-verify`、未使用 `git add -A/-a`、未使用 `git stash`/`git clean`/`git reset --hard`。
## Deviations from Plan
### 计划口径落地偏差
**1. [如实登记 - 计划表述校准] Task 4 的「无任何 jsDoc/注释改动」按「无新增 jsDoc除授权项+ 无既有注释被修改」口径成立**
- **Found during:** Task 4 的 jsDoc 审计
- **Issue:** 计划 `<objective>` 第 6 条与 `<action>`(3) 的表述措辞为「本计划无任何 jsDoc/注释改动」。实测 `git diff 674b325..HEAD` 显示 Task 1 在 `mapDamage.ts` 新增了 3 条**非 jsDoc** 的单行 `//` why-comment空集登记说明与移除端收集说明。AGENTS.md 明许「新增注释随便新增」(且 dev.md:83 要求注释说明「为什么」Task 1 的验收口径亦仅为「零 jsDoc 改动」;故这不构成禁令违反,但使计划那句总括表述不严谨。
- **处置:** Task 1 已提交且按硬约束**不得回改**,故不改代码、不改注释;在 SUMMARY 的「jsDoc / 注释审计」表中如实列明,并把门禁判据明确为「新增 jsDoc 仅 `clearDynamics` 1 处、既有 jsDoc/注释 0 处被修改」。Task 2/3 未新增任何注释(含 jsDoc本计划的唯一授权 jsDoc 落笔保持唯一。
- **Files affected:** 表述面SUMMARY**未修改任何源码/注释**
- **Committed in:** 本 SUMMARY
### 计划未落地的声明项
- **`WINDOWS.md` 未改动**D-14 + Task 4 验收项:不新建、不 `fixed`/`waive`)。
- **未新增依赖、未新建源码文件、未使用 `import type`、未使用连续 `as` 断言**Task 3 仅新增一个既有导出 `SaveCompression` 到测试文件的普通导入。
- **`combat/types.ts`、`mapDamage.ts` 之外的 `context.ts`、`gameMap.ts`、`mapState.ts`、`dynamicTile.ts` 零改动**(范围守卫 A4
- **`sourcedDamage` 空条目按裁决保留**(非目标)。
## Issues Encountered
**1. 全量套件基线重录D-12 要求)**
| 时点 | 文件 | 通过 | 失败 | 跳过 |
| --- | --- | --- | --- | --- |
| 07-12 收尾(计划 objective 记载) | 66 | 729 | 0 | 1 |
| 07-13 Task 1 收尾(接力说明记载) | 66 | 733 | 0 | 1 |
| 本次接力会话执行前(起点) | — | 733 | 0 | 1 |
| 本次接力会话执行后(本计划完成态) | 66 | **737** | **0** | **1** |
- 通过数 +4 = 本轮新增 4 条用例Task 2 计 2、Task 3 计 2Task 1 的 4 条已在 733 内);**失败数 0 不变**,跳过仍 1 条(`equipment.test.ts` 码 147
**2. 计划/接力说明记载的跳过行号 `equipment.test.ts:306` 已陈旧**
- **现象:** 实测唯一 `it.skip` 位于 `packages-user/data-base/src/hero/equipment.test.ts:353`(码 147`warns code 147 when no equipment slot is available`),全仓 `it.skip` 计数 = 1。
- **处置:** 该文件不在本计划 `files_modified` 内、`git diff` 为空,故不改动;按范围守卫只作**事实登记**(非缺陷、非本计划引入),门禁判据「跳过项恰 1 条且为码 147」已满足。**未**将其写入 `WINDOWS.md` 或任何登记列表。
**3. 计划头与 HEAD 之间存在一条合并提交与其带入的重复 07-12 完成提交**
- **现象:** `git rev-list --count b37c218..HEAD` = 6其中 `3da7863`(上一会话同步 origin 的合并)与 `954c3fc`(该合并带入的 07-12 重复完成提交,与主线 `4917baa` 同内容)不是本计划产出;三条任务提交 + 本 SUMMARY 的元数据提交 = 4。
- **处置:**#3968`plan_head_before``b37c218`**实测**计数并如实登记构成见「Task Commits」两条非任务提交未触碰本计划 5 个文件(`git log --name-only` 复核)。**未**回退/改写任何提交(`refactor/data` 已推送,禁止 amend
**4. 上一会话遗留的未跟踪接力文件**
- `.planning/phases/07-data-fixes/.continue-here.md``.planning/milestone.lock` 为未跟踪文件,非本计划 `files_modified` 声明项,故**未**暂存、**未**提交(避免超出计划声明的提交范围);如需入库请由编排器统一处置。
## User Setup Required
None - no external service configuration required.
## Known Stubs
None —— 本计划未引入硬编码空值、占位文案、未接线数据源或新增 `TODO`/`FIXME`/`it.skip``git diff 674b325..HEAD` 复核)。唯一「无公开可观测差异」的簿记改动(`deleteMapDamage` 空点移除)已按 A7 以 coverage 条目 D5`human_judgment: true`)如实登记,不为它制造伪见证。
## Threat Model Disposition
无新增威胁面(无新端点、无鉴权路径、无文件访问、无依赖安装)。计划 `<threat_model>` 的缓解项逐条落地:
| Threat ID | 处置 | 缓解落地 | 证据 |
| --- | --- | --- | --- |
| T-7-37 `removeEnemyAffecting` 不标脏high / mitigate | Q1=A | 收集被移除索引并逐个 `markDirtyIndex`,旧坐标经 `refreshIndex` 重新聚合 | `cff4291`CR-02 两条路径用例 |
| T-7-38 空视图集早退high / mitigate | Q1=A | 两处均改为「先 `enemyStore.set(view, set)``if (set.size === 0) return;`」,避免缓存残留与 `refreshAll()` 级联 | `cff4291`;两入口各 `iterateEnemy` spy + `converter.calls` 断言 |
| T-7-39 空集被误读为未登记low / mitigate | A5 | **不新增分支**(空 `Set` 为真值),以两条用例固定口径 | `cff4291``keeps refreshing locally…` / `registers an empty view set through refreshAll as well` |
| T-7-40 `loadDynamics` 不清旧动态块medium / mitigate | Q3=A1 | `loadState` 入口全清(`clearDynamics`,复用 `deleteDynamic` 语义),三档与缺字段按构造一致 | `13234b7`;重复读档 / 旧实例不再被管理断言 + 反向验证 |
| T-7-41 `expired` 契约无源码文本low / accept | Q2=A | **保留现契约、零代码、零 jsDoc**;契约文本落在 `<record>` 与 SUMMARY | 无提交;`git diff` 证明零改动 |
| T-7-42 读档全清触发大量钩子low / mitigate | A6 | 钩子**触发式、不等待**,读档不被渲染端异步钩子阻塞;`syncStaticEvent(tile,false)` 的写入随后被矩阵读档覆盖 | `13234b7``loadState` 保持同步 |
| T-7-43 删除后 `affectedBy`/`damages` 残留low / mitigate | Q4=A | `deleteEnemy` 剪除死视图与其伤害(有 `getDamageWithoutCheck` 不被调用的见证);`deleteMapDamage` 在两集合皆空时移除该点 | `c1df1e0`反向验证「Number of calls: 1」 |
| T-7-SC 依赖安装high / mitigate | — | 未新增依赖、未改 `package.json`/`pnpm-lock.yaml` | `git status --short -- package.json pnpm-lock.yaml` 为空 |
## Phase 7 验证失效与重跑要求
**`07-VERIFICATION.md``verified: 2026-09-16T06:04:11Z``status: passed``score: 15/15`)已失效**:其结论不覆盖 07-09 之后追加的 07-10..07-13以及尚未执行的 07-14。本计划执行完毕后**必须重跑 `/gsd-verify-work`(或 `gsd-verify-work 7`**重新出具验证结论,方可评估 Phase 7 收口。`ROADMAP.md` 的 Phase 7 计划计数与 Wave 列表已按 SUMMARY 数同步07-13 完成后为 13/14
## Next Phase Readiness
- 地图侧三条失效边界缺陷CR-02 幽灵伤害 / IN-01 删除后点残留 / `#06-17-8` 读档动态块累积)已闭合;`setMapRef` 按用户裁决零改动,契约文本登记完毕。`combat/types.ts` 与本计划范围外的 map 文件零改动。
- **待办(非本计划):** ① 重跑 `/gsd-verify-work 7` 重新出具 Phase 7 验证结论(现结论已失效);② 执行最后一个计划 **07-14**`data-fallback` hero 代理跟随活属性 / `#06-17-7`);③ `IN-04`/`IN-05` 保留在册、未修;④ `sourcedDamage` 空点条目为已知惰性残留Q4=A 非目标);⑤ 计划/接力说明中 `equipment.test.ts:306` 的陈旧行号可在后续文档维护中更新为 `:353`
---
## Self-Check: PASSED
- FOUND: `cff4291`Task 1上一会话未改动、`c1df1e0`Task 2、`13234b7`Task 3`git log --oneline b37c218..HEAD``git rev-list --count b37c218..HEAD` = 6
- FOUND: 5 个改动文件(`git diff --name-only b37c218..HEAD`),全部在 frontmatter `files_modified` 内;三条任务提交零计划外文件
- VERIFIED: `pnpm test:ci` = 66 文件 / **737 通过 / 0 失败 / 1 跳过**(仅 `equipment.test.ts:353` 码 147D-06
- VERIFIED: `pnpm exec eslint` 5 文件 0 错误;`vue-tsc --noEmit` 对 5 文件 0 命中(整仓 27 条均属并发 WIP
- VERIFIED: Q2 零改动——`git diff 674b325..HEAD -- mapLayer.ts combat/types.ts``mapLayer.ts` +14/0 且无 `setMapRef`/`getMapRef`/`resize`/`resize2`/`expired` 行,`combat/types.ts` 空
- VERIFIED: 新增 jsDoc 仅 `clearDynamics` 1 处;既有 jsDoc/注释 0 处被修改;未新增 `it.skip``git diff --diff-filter=D --name-only 674b325..HEAD` 为空
- VERIFIED: `git log --name-only b37c218..HEAD` 不含 `client-base`/`client-modules`/`AGENTS.md` WIP`WINDOWS.md` 未改动git status 为空)
- VERIFIED: 反向验证两条Task 2 死视图见证「Number of calls: 1」Task 3 关闭清理 5-vs-2 与 4-vs-1恢复后全绿
---
*Phase: 07-data-fixes*
*Completed: 2026-09-17*