docs(04-03): plan hero body render adaptation

This commit is contained in:
unanmed 2026-09-18 14:14:12 +08:00
parent 90d14ef1c1
commit d7f2e277d9
2 changed files with 428 additions and 1 deletions

View File

@ -181,7 +181,7 @@ Plans:
3. 移动端(窄屏)布局下,同一场景正常显示且可操作
4. 数据端与渲染端保持双端分离——数据端无 DOM仍可在 Node 环境跑回放验证
**Plans**: 2/2 plans executed第一步·只读对账已完成第二步·勇士移动接口探索只读已规划 — 04-CONTEXT D-01/D-02/D-13/D-17/D-18后续适配实施与双布局待探索结果与缺失候选裁决后另行规划)
**Plans**: 3 plans第一步·只读对账已完成第二步·勇士移动接口探索只读已完成第三步·勇士本体适配渲染端被动重接已规划待执行 — 04-CONTEXT D-01/D-02/D-13/D-17/D-18/D-19/D-20/D-21/D-22其余渲染适配与移动端/桌面端双布局待后续增量另行规划)
Plans:
**Wave 1**
@ -192,6 +192,10 @@ Plans:
- [x] 04-02-PLAN.md — 勇士移动接口探索只读D-17产出 `04-HERO-MOVER-INTERFACE.md`(数据端现行接口 / 渲染端现状 / 逐成员三态对账 / 缺失接口候选 / 未确定 / 处置;排除 `HeroRendering`D-18`core.*`D-12不改任何代码适配04-03+)未规划)
**Wave 3** *(blocked on Wave 2 completion)*
- [ ] 04-03-PLAN.md — 勇士本体适配(渲染端被动重接):`render/map/extension/{hero,types,manager}.ts` 绑定 `IHeroLocation` / `IHeroMover`,改接现有钩子 `IHeroLocationHooks` / `IObjectMoverHooks`D-21按 D-20 裁剪 `IMapHeroRenderer` 的外部驱动成员;排除贴图/不透明度D-18、跟随者D-22、移动控制D-19、`core.*`D-12与裸函数D-11不新增数据端接口
**UI hint**: yes
### Phase 5: Legacy 移植

View File

@ -0,0 +1,423 @@
---
phase: 04-render-adaptation
plan: 3
type: execute
wave: 3
depends_on:
- 04-01
- 04-02
files_modified:
- packages-user/client-modules/src/render/map/extension/hero.ts
- packages-user/client-modules/src/render/map/extension/types.ts
- packages-user/client-modules/src/render/map/extension/manager.ts
autonomous: false
requirements:
- REND-01
- REND-02
estimate:
tokens: 75000
raw_tokens: 75000
tasks: 4
confidence: low
must_haves:
truths:
- "本计划的**第一个 block 点**是 Task 0 的 `blocking-human` 汇报关卡:先向用户汇报本计划要解决的问题、解决方案(含逐钩子映射表与拟新增/变更的成员命名清单)、范围边界与禁令,然后必须**停下等待**用户明确回复「可以执行」四个字;收到该回复前不得执行任何任务、不得派发任何执行子代理。无论触发方式(`/gsd-execute-phase`、`/gsd-manager`、`/gsd-autonomous`、子代理/执行器派发)或配置模式(`.planning/config.json` 的 `yolo`、`workflow.auto_advance`如何均不得自动放行AGENTS.md"
- "渲染端勇士本体改为**绑定勇士位置对象**`MapHeroRenderer` 的勇士引用类型为 `IHeroLocation``packages-user/data-base/src/hero/types.ts:303``MapExtensionManager.heroMap` / `addHero` / `removeHero` 的键与参数类型同为 `IHeroLocation`;坐标经 `IObjectMovable.x` / `.y``data-common/src/common/mover.ts:48-51`),朝向经 `IHeroLocation.getCurrentFaceDirection()``data-base/src/hero/location.ts:65`)与 `IHeroMover.faceDirection` / `.moveDirection``mover.ts:243,245`),动画播放方向经 `IHeroMover.currAnimDir``ObjectAnimDirection``mover.ts:40,247`"
- "三个在范围文件内不再出现 `IHeroMoveController`、`IHeroMoveControllerHooks`、`HeroAnimateDirection`,也不再出现旧钩子方法名 `onSetPosition` / `onStartMove` / `onMoveHero` / `onEndMove` / `onJumpHero` / `onTurnHero`(静态门禁机器判定,见 Task 3"
- "钩子注册改为订阅**数据端现有钩子**D-21`hero.addHook(...)` 注册 `IHeroLocationHooks` 的订阅(`data-base/src/hero/types.ts:277``hero.mover.addHook(...)` 注册 `IObjectMoverHooks<IHeroLocation>` 的订阅(`data-common/src/common/mover.ts:166`);注册对象实现的是**现有钩子成员名**`onSetPos` / `onMoveStart` / `onMoveEnd` / `onStepStart` / `onStepEnd` / `onSetFaceDir` / `onSetMoveDir`**不新增任何数据端公共接口**D-21也不修改任何数据端文件"
- "勇士渲染为**完全被动渲染**D-20`IMapHeroRenderer` 契约不再暴露允许外部驱动渲染状态的成员(`startMove` / `waitMoveEnd` / `stopMove` / `move` / `jumpTo` / `setPosition` / `turn` / `setHeroAnimateDirection`);渲染状态只由数据端现有钩子推进,渲染端自身仍可修改它自己的内容(不与 D-18 / D-22 冲突)"
- "**D-18 / D-22 成员逐字保留**`setImage` / `setAlpha` / `setFollowerAlpha` 与钩子方法中的 `onSetImage` / `onSetAlpha` / `onSetFollowerAlpha`,跟随者族的 `addFollower` / `removeFollower` / `removeAllFollowers` / `onAddFollower` / `onRemoveFollower` / `onRemoveAllFollowers` 均不被删除、不被改写语义(贴图 / 不透明度按 D-18 待用户先改数据端;跟随者按 D-22 暂缓)"
- "**范围外零改动**`packages-user/client-modules/src/action/move.ts` 与全部移动控制 API 未改动D-19`core.*` / legacy 适配D-12`getHeroStatusOn` 类裸函数处理D-11无数据端文件改动`data-common` / `data-base` / `data-state` / `data-system` / `packages` / `packages-user/client-base` 均不在 `files_modified`"
- "**验证可运行且不臆造通过**:改动前先记录 `pnpm exec vue-tsc --noEmit``client-modules` 基线错误集(规划时为 exactly 2 条、与勇士移动无关);改动后该错误集必须与基线**完全一致**(不多不少),且三个在范围文件命中 0 条类型错误;`pnpm exec eslint` 对三个文件 0 错误静态改名门禁、D-18/D-22 保留门禁、并发改动基线门禁与 CRLF 门禁全部转绿。渲染运行时无测试设施、且 `MapHeroExtension.addHero` 当前**未被接线**`client.ts:148-152` 已注释),故本计划的实质验证为「编译 + 契约 + 静态」三类机器门禁,另附人工 UAT 代码复核;**不得**以任何无法运行的门禁冒充通过"
- "用户并发的未提交改动(`packages-user/client-base/src/load/{loader,types}.ts`、`packages-user/data-base/src/load/{loader,types}.ts`、`packages-user/data-state/src/{core,types}.ts`、`packages/loader/src/{progress,types}.ts`、`.planning/phases/04-render-adaptation/04-CONTEXT.md`)在 Task 1 起始记录基线porcelain 条目 + 各文件 sha256Task 3 复验逐条与内容未变;**不回滚 / 不暂存 / 不提交 / 不修改**它们"
- "REND-01 / REND-02 在本 run 后仍保持 Pending本计划只完成「勇士本体适配渲染端被动重接」一个子切片未实施其余渲染适配与移动端/桌面端双布局(见 assumptions 的两条 FLAGGED ASSUMPTION"
artifacts:
- path: packages-user/client-modules/src/render/map/extension/hero.ts
provides: "勇士渲染拓展以 `IHeroLocation` 为绑定对象;经 `hero.addHook` / `hero.mover.addHook` 订阅数据端现有钩子;旧控制器类型与旧钩子方法名清除;动画方向改用 `ObjectAnimDirection` + `mover.currAnimDir`"
- path: packages-user/client-modules/src/render/map/extension/types.ts
provides: "`IMapExtensionManager` 的勇士键/参数类型改为 `IHeroLocation``IMapHeroRenderer` 按 D-20 去掉外部驱动成员,保留 D-18 / D-22 成员"
- path: packages-user/client-modules/src/render/map/extension/manager.ts
provides: "`heroMap` / `addHero` / `removeHero``IHeroLocation` 为键与参数,且不再带 `@ts-expect-error` 重构标记"
key_links:
- "`manager.ts` 的 `addHero(state: IHeroLocation, layer)``new MapHeroRenderer(renderer, layer, state)``hero.ts` 构造器 `readonly hero: IHeroLocation``data-base/src/hero/types.ts:303`"
- "`hero.addHook(new MapHeroHook(this))` ↔ `IHeroLocation extends IHookable<IHeroLocationHooks>``data-base/src/hero/types.ts:303-307``IHeroLocationHooks.onSetPos(x, y)``:283`)由 `HeroLocation.setPos` 触发(`data-base/src/hero/location.ts:59-63`"
- "`hero.mover.addHook(new MapHeroHook(this))` ↔ `IHeroMover<IHeroLocation> extends IObjectMover<IHeroLocation> extends IHookable<IObjectMoverHooks<IHeroLocation>>``data-base/src/hero/types.ts:399-400`、`data-common/src/common/mover.ts:166,235`);钩子在 `ObjectMover.moveProgress` 的固定时序被调用(`mover.ts:651-686``onMoveStart` → 每步 `onStepStart`(坐标未变)→ `tile.setPos``onStepEnd`(坐标已更新)→ `onMoveEnd`"
- "`hero.mover` 的 `currAnimDir``data-common/src/common/mover.ts:247`)↔ 渲染端 `HeroRenderEntity.animateDirection``ObjectAnimDirection``mover.ts:40`"
- "`IHeroState.location: IHeroLocation``data-base/src/hero/types.ts:889,894`)↔ 渲染端接线点(`client.ts:148-152` 已注释的 `addHero(...)`):本计划不取消注释、不改 `client.ts`,但把 `addHero` 的入参类型固定为 `IHeroLocation`(注释中的 `this.hero.mover` 形参落差登记为后续接线增量事项)"
- "`pnpm exec vue-tsc --noEmit` 的 `client-modules` 错误集基线(规划时 2 条:`client.ts(66,9): error TS2554`、`render/ui/load.tsx(77,31): error TS2504`)↔ Task 1 起始复核与 Task 3 终验的一致断言"
assumptions:
- "**FLAGGED ASSUMPTIONspecless fallback— REND-01 edge unresolved** 未提供 REND-01渲染端适配新数据层接口的验收边界本计划**不发明**阈值,只交付勇士本体(渲染端被动重接)这一个子切片,验收判据取自 04-02 对账账本 `#04-02-R-01..12` 的「渲染端需改」项与用户裁定 D-19..D-22。用户须在推进其余渲染适配前补齐 REND-01 的边界判据。"
- "**FLAGGED ASSUMPTIONspecless fallback— REND-02 edge unresolved** 未提供 REND-02移动端与桌面端双布局的验收边界本计划不涉及双布局实现也不发明其验收阈值。"
- "**执行前必须复核(未确定项,不猜):** 04-02 账本「未能从阅读确定(未猜测)」第 2 条(`state.hero.location` 可达性)与第 7 条(`stepEnd` 桥接在本计划被消费。Task 1 步骤 (0) 必须在动手前打开当日 HEAD 的数据端文件逐条核对:`IHeroState.location` 存在且类型为 `IHeroLocation``IHeroLocation.mover` 为 `IHeroMover<this>``IHeroLocationHooks.onSetPos` / `IObjectMoverHooks` 七个成员名与参数形态未变;`ObjectAnimDirection` / `ObjectMoveType` / `ObjectSpecialStep` 仍由 `@user/data-common` 导出;`HeroLocation.setPos` 触发 `onSetPos`、`ObjectMover.moveProgress` 的 `onStepEnd``tile.setPos` 之后。任一不同即**停下汇报并修订本计划**,不得按记忆改代码。"
- "**D-18 编译耦合的显式处理:** 把 `hero` 由旧控制器类型改为 `IHeroLocation` 后,`addHeroMoving` 中读取贴图别名(`hero.image`)的表达式不再有数据端对位。本计划**只做最小编译桥接**(把该读取收敛为一处并用项目既有 `// @ts-expect-error 需要重构` 惯用法标注,语义与分支保持不变),**不重新设计贴图来源**——贴图子系统属 D-18由用户先改数据端后再处理。若用户对该桥接有其它裁决在 Task 0 汇报时给出AI 不自行改道。"
- "**运行时 UAT 不可得(诚实降级):** `MapExtensionManager.addHero` 目前无任何调用点(`client.ts:148-152` 被注释),故勇士渲染拓展在运行时并未启用;本计划不接线 `client.ts`。因此本计划的运行结果无法在渲染环境观察验收依赖「scoped 类型门禁 + 契约/静态门禁 + 人工代码复核」,运行时渲染 UAT 顺延到启用接线的增量。"
- "**数据端实现被用户并发修改D-05** 接口签名视为稳定,实现可变;本计划只读引用数据端接口,不修改任何数据端文件。若执行当日数据端接口签名与规划基线不一致,按 Task 1 步骤 (0) 停下汇报,不按旧形状适配。"
- "**成员命名需用户同意:** 本计划引入的渲染端新成员名(`locationController`、`moverController`)与将被删除/改写的既有成员名,须在 Task 0 汇报中获得用户同意后才可落地AGENTS.md「任何公共、受保护、私有方法、字段等内容的命名必须先向我反馈」用户未同意即改名为用户指定的写法或停下修订计划。"
prohibitions:
- "不得在用户明确回复「可以执行」四个字之前执行本计划的任何任务、或派发任何执行子代理AGENTS.md 的第一个 block 点约束;不因触发方式或 `yolo` / `auto_advance` 模式而放宽)。"
- "不得修改任何数据端文件(`packages-user/data-common` / `data-base` / `data-system` / `data-state`也不得新增数据端公共接口D-21本计划只从数据端 import 现有类型与钩子。"
- "不得改动 `packages-user/client-modules/src/action/move.ts` 及任何移动控制 APID-19`HeroKeyMover` / `IMoveController` 队列 / `oneStep` 由用户手动处理;`#04-02-G-02` 不在本计划)。"
- "不得改写贴图 / 不透明度相关代码的语义:`setImage` / `setAlpha` / `hero.image`(贴图别名读取)/ `IHeroRendering` / `onSetImage` / `onSetAlpha` / `onSetFollowerAlpha` 一律保留D-18本计划对该处的唯一允许动作是任务规定的「读取收敛 + 既有 `// @ts-expect-error 需要重构` 惯用法标注」编译桥接。"
- "不得适配跟随者:`IHeroFollower` / `IHeroFollowersController` / 跟随者钩子与渲染方法(`addFollower` / `removeFollower` / `removeAllFollowers` / `setFollowerAlpha` / `onAddFollower` / `onRemoveFollower` / `onRemoveAllFollowers`保持原样D-22 暂缓,本次不纳入、不单列计划)。"
- "不得引入 `core.*` / `client.*` / legacy 全局适配D-12属第五阶段不得处理 `getHeroStatusOn` 类裸函数或其它 `// @ts-expect-error 需要重构` 的 UI / 属性项D-11。"
- "不得新增任何渲染端公共接口成员(除 D-20 规定的删除外,`IMapHeroRenderer` / `IMapExtensionManager` 的成员集合只减不增);不得让渲染端出现允许外部驱动渲染状态的新方法。"
- "不得修改任何既有 jsDoc 注释的内容;被 D-20 删除的接口成员连同其配套 jsDoc 一并删除属「删除代码」而非「改写注释」,须在 Task 0 汇报中经用户确认。"
- "不得改动 `files_modified` 之外的源码;若编译链强制要求触碰第四个文件,必须停下汇报并修订本计划(不得自行扩面)。"
- "不得回滚 / 暂存 / 提交用户并发的未提交改动;不得把用户既有改动纳入本计划的提交。"
- "不得新增依赖、不得修改 `package.json` / `pnpm-lock.yaml`;出现安装需求即暂停并要求用户确认包合法性。"
- "不得以「无测试设施」为由弱化验证:每条 `<automated>` 必须有 `<fails_when>`;确实无法运行的门禁必须显式声明为不可得并降级为静态 + 人工,不得伪造绿。"
---
<objective>
Phase 4 第三个增量D-17 的适配半程)——**勇士本体适配:渲染端被动重接**。按 04-02 的逐成员对账(`04-HERO-MOVER-INTERFACE.md` 的 `#04-02-R-01..12`)与用户裁定 D-19..D-22把渲染端勇士本体从已被删除的 `IHeroMoveController` 模型换成**绑定勇士位置对象** `IHeroLocation` / `IHeroMover`,并改为**订阅数据端现有钩子**`IHeroLocationHooks` / `IObjectMoverHooks`),不新增数据端接口、不做主动驱动。
**本计划是代码改动计划**(不同于 04-01 / 04-02 的只读清点):改动面严格限定为三个文件 —— `render/map/extension/hero.ts`(勇士渲染拓展本体)、`.../extension/types.ts`(拓展契约)、`.../extension/manager.ts`(拓展管理器)。三者构成 `IMapExtensionManager.addHero → MapHeroRenderer(hero: IHeroLocation) → hero.addHook / hero.mover.addHook → 钩子回调驱动渲染` 这一条最小闭环。
**要解决的问题(三条,全部来自 04-02 对账的「渲染端需改」):**
1. **类型名整体失配**`#04-02-R-03/R-04/R-12`):渲染端 import 的 `HeroAnimateDirection`、`IHeroMoveController`、`IHeroMoveControllerHooks` 在数据端**不存在**(当前靠 `// @ts-expect-error 需要重构` 压着);`heroMap` / `addHero` / `removeHero` 的键类型是已消失的控制器。
2. **成员形状失配**`#04-02-R-05/R-06``hero.direction` 在现行接口不存在(朝向为 `faceDirection` / `moveDirection` / `getCurrentFaceDirection()``hero.image` 无数据端对位D-18 待用户处理)。
3. **钩子族整体断链**`#04-02-R-01` 与缺失候选 `#04-02-G-01`):渲染端 `MapHeroHook` 实现的 `onSetPosition` / `onStartMove` / `onMoveHero` / `onEndMove` / `onJumpHero` / `onTurnHero` 在数据端没有同名对位须改接现有钩子D-21 裁决:不新增数据端接口)。
**大致解决方案:**
- **绑定对象换成位置对象**`MapHeroRenderer` 持有 `IHeroLocation`;坐标读 `IObjectMovable.x/.y`;朝向读 `getCurrentFaceDirection()`(构造期初始化)与 `mover.faceDirection`;移动方向读 `mover.moveDirection`;动画播放方向读 `mover.currAnimDir` 并改用 `ObjectAnimDirection`R-03/R-05/R-06
- **钩子改为订阅现有钩子**D-21`hero``IHeroLocationHooks`)与 `hero.mover``IObjectMoverHooks<IHeroLocation>`)上各注册一次 `MapHeroHook`,用 `load()` / `unload()` 管理(原 `controller` 字段拆为两个控制器字段)。渲染端实现现有钩子成员名,旧名全部删除;`onStepEnd` 成为逐格渲染的驱动点(此时 `tile` 坐标已更新),按 `ObjectMoveType` 分派到既有渲染内部方法。
- **被动化**D-20`IMapHeroRenderer` 去掉「外部驱动渲染状态」的成员(`startMove` / `waitMoveEnd` / `stopMove` / `move` / `jumpTo` / `setPosition` / `turn` / `setHeroAnimateDirection`);这些方法退为渲染端内部实现,仅由钩子回调调用。
- **范围硬边界**:贴图 / 不透明度D-18、跟随者D-22、`action/move.ts` 与移动控制D-19、`core.*`D-12、裸函数D-11一律不动。
**执行前置关卡AGENTS.md第一个 block 点):** 本计划第一个执行步骤是 Task 0 —— 先向用户汇报「要解决的问题 / 大致解决方案(含逐钩子映射表)/ 拟新增或改写的成员命名清单 / 范围边界与禁令 / D-18 编译桥接的取舍」,然后**停下等待**用户明确回复「可以执行」四个字。收到该回复前,任何任务都不得执行,也不得派发执行子代理;无论触发方式或配置模式如何均不得自动放行。故本计划 `autonomous: false`
**后续增量(显式延后,本计划不涉及、不实施):** 04-02 的两条缺失候选(`#04-02-G-01` 移动语义钩子族、`#04-02-G-02` `oneStep` / 控制器队列——G-01 已由 D-21 裁决为「改接现有钩子」本计划即其落地G-02 由 D-19 交由用户手动处理,**不在本计划**。`client.ts` 的 `addHero` 接线启用、贴图与不透明度来源D-18、跟随者位置/动画归属D-22、其余 ① 错配(`#04-01-M-06..09`)与 42 条 ③ 多余旧路径、移动端/桌面端双布局REND-02均不在本 run。
Purpose: 勇士移动是 04-01 / 04-02 锁定的主要失配点D-17。这一子切片把渲染端勇士本体从「按旧形状读已删除的控制器」改为「绑定位置对象 + 订阅现有钩子」,是后续渲染适配与双布局能建立在编译通过、契约一致的地基上的前提。
Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复跑的 scoped 类型门禁、静态改名门禁、D-18/D-22 保留门禁、并发改动基线门禁与 CRLF 门禁结果 + `.planning/phases/04-render-adaptation/04-03-SUMMARY.md`。Task 0 为只读汇报关卡,不写任何文件。
## Source Audit多源覆盖审计
| SOURCE | ID | Feature/Requirement | Plan | Status | Notes |
|--------|----|---------------------|------|--------|-------|
| GOAL | — | Phase 4「渲染端通过新数据层接口驱动并同时支持移动端与桌面端布局」 | 04-03 | COVERED增量·勇士本体子切片 | D-17 的适配半程:本 run 只做勇士本体的被动重接其余渲染适配与双布局属后续增量REND-02 见 assumptions |
| REQ | REND-01 | 渲染端适配新数据层接口 | 04-03 | COVERED子切片需求保持 Pending | 落定 `#04-02-R-01..R-08`、`#04-02-R-12` 的「渲染端需改」项;验收边界未提供 → 见 assumptions 的 FLAGGED ASSUMPTION |
| REQ | REND-02 | 渲染端同时支持移动端与桌面端布局 | 04-03 | COVERED不涉及 | 本 run 范围不含双布局REND-02 验收边界未提供 → 见 assumptions 的 FLAGGED ASSUMPTION |
| CONTEXT | D-05 | 以接口为准,不受数据端实现变动影响 | 04-03 | COVERED | 只读引用数据端现行接口;签名若变则 Task 1 步骤 (0) 停下汇报 |
| CONTEXT | D-11 | `getHeroStatusOn` 类裸函数多为待删除 | 04-03 | COVERED边界 | 属 UI / 属性读取非勇士本体prohibitions 明列不处理 |
| CONTEXT | D-12 | `core.*` 属旧引擎适配,归第五阶段 | 04-03 | COVERED排除 | 三个在范围文件本就无 `core.*`prohibitions 明列不得引入 |
| CONTEXT | D-13 | 缺失接口待改时逐步确认 | 04-03 | COVERED边界 | 04-02 的候选裁决已由 D-19 / D-21 给出,本计划不做新的缺失接口裁定 |
| CONTEXT | D-17 | 下一步=勇士移动适配(先探索再适配) | 04-03 | COVERED | 04-02 完成探索;本计划即其适配实施(勇士本体部分) |
| CONTEXT | D-18 | `HeroRendering` 暂不处理,用户先改数据端 | 04-03 | COVERED排除 + 编译桥接) | 贴图 / 不透明度成员逐字保留;`hero.image` 只做最小编译桥接不重设来源Task 0 汇报该取舍 |
| CONTEXT | D-19 | 移动控制由用户手动修改 | 04-03 | COVERED排除 | `action/move.ts` 与移动控制 API 不在 `files_modified`prohibitions 明列 |
| CONTEXT | D-20 | 勇士渲染完全被动、旧驱动接口删除 | 04-03 | COVERED | Task 2 从 `IMapHeroRenderer` 删除 8 个外部驱动成员,改为钩子驱动 |
| CONTEXT | D-21 | 走现有 `IObjectMoverHooks` / `IHeroLocationHooks`,不新增数据端接口 | 04-03 | COVERED | Task 1/2 的两次 `addHook` 注册与逐钩子映射即本条的落地 |
| CONTEXT | D-22 | 跟随者暂缓 | 04-03 | COVERED排除 | 跟随者成员与钩子方法逐字保留、不删除不改写 |
| CONTEXT | D-10 | `HeroRendering` 删除、渲染状态存储放渲染端 | 04-03 | COVERED一致性依据 | D-20 的被动化与 D-18 的保留范围保持一致;贴图来源的最终归属留给 D-18 增量 |
| RESEARCH | — | (本阶段无 RESEARCH.md | — | N/A | Phase 4 未产出 RESEARCH.md本阶段的等价输入为 04-01 对账账本与 04-02 探索账本 |
## Estimate Basis估算依据
`estimate-calibration` 返回 `factor: 1`、`sample_count: 0`、`confidence: low`(项目无历史样本),故 `tokens == raw_tokens`,并如实标注 `confidence: low`。按「执行者实际触及量」估算(`estimate.tasks: 4` = Task 0 汇报关卡 + 三个工作任务Task 0 只读汇报并等待用户确认(约 3k不写文件Task 1tracer起始复核数据端现行接口 + 三文件类型 / import / 绑定 / 一次 `addHook` × 2 + `onSetPos` 一条通路 + scoped 类型门禁(约 30k`hero.ts` 517 行与数据端基准的定向读取Task 2 逐钩子映射实现 + D-20 契约裁剪 + manager / types 收口(约 27kTask 3 五类门禁 + 人工 UAT 复核 + SUMMARY约 11k固定开销约 4k合计约 75k。
</objective>
<execution_context>
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/04-render-adaptation/04-CONTEXT.md
@.planning/phases/04-render-adaptation/04-01-SUMMARY.md
@.planning/phases/04-render-adaptation/04-02-SUMMARY.md
@.planning/phases/04-render-adaptation/04-HERO-MOVER-INTERFACE.md
@.planning/phases/04-render-adaptation/04-RENDER-INTERFACE-AUDIT.md
@.planning/phases/04-render-adaptation/04-PATTERNS.md
@AGENTS.md
@dev.md
</context>
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报本计划的解决方案、逐钩子映射表与成员命名清单,等待用户回复「可以执行」</name>
<files>(只读汇报,不修改任何文件)</files>
<read_first>
- AGENTS.md「任何计划的执行其第一个 block 点必须是向我汇报计划内容并等待我确认」;只有明确回复「可以执行」四个字之后才允许开始执行;「任何公共、受保护、私有方法、字段等内容的命名必须先向我反馈,我同意后才可以使用」)
- .planning/phases/04-render-adaptation/04-CONTEXT.mdD-11 / D-12 / D-17 / D-18 / D-19 / D-20 / D-21 / D-22
- .planning/phases/04-render-adaptation/04-HERO-MOVER-INTERFACE.md:152-207本计划的输入逐成员三态对账 `#04-02-R-01..12`、缺失候选 `#04-02-G-01/02`、7 条未确定)
- .planning/phases/04-render-adaptation/04-03-PLAN.md本计划全文objective / tasks / 映射表 / 禁令 / 门禁)
- packages-user/client-modules/src/render/map/extension/hero.ts:1-21,76-102,110-132,437-517现状 import 与 `@ts-expect-error`、构造器、`addHeroMoving` 的 `hero.image`、`MapHeroHook`
- packages-user/client-modules/src/render/map/extension/types.ts:13-40,63-159待改的 `IMapExtensionManager` 与待裁剪的 `IMapHeroRenderer`
- packages-user/data-common/src/common/mover.ts:166-233,235-351`IObjectMoverHooks` / `IObjectMover`
- packages-user/data-base/src/hero/types.ts:277-321,399-417,889-909`IHeroLocationHooks` / `IHeroLocation` / `IHeroMover` / `IHeroState.location`
</read_first>
<action>
**停下并向用户汇报,等待明确回复后才继续;汇报本身只读,不写任何文件、不改任何生产代码、不派发子代理。**
**要解决的问题**:渲染端勇士本体仍按已删除的 `IHeroMoveController` 模型编写(`hero.direction` / `hero.image` / `IHeroMoveControllerHooks``onSetPosition` 等),数据端现行接口无这些成员,全靠 `// @ts-expect-error 需要重构` 压着(`#04-02-R-01/R-03/R-04/R-05/R-06/R-12`)。不改就会一直停在「编译靠压制、契约对不上」。
**大致解决方案(三个文件)**`manager.ts` 与 `types.ts` 的勇士键 / 参数类型由 `IHeroMoveController` 改为 `IHeroLocation``hero.ts` 构造器绑定 `IHeroLocation`(坐标 `x`/`y`、朝向 `getCurrentFaceDirection()`、动画方向 `ObjectAnimDirection` + `mover.currAnimDir`),在 `hero``IHeroLocationHooks`)与 `hero.mover``IObjectMoverHooks<IHeroLocation>`)上各 `addHook` 一次,`MapHeroHook` 改为实现现有钩子成员名;`IMapHeroRenderer` 按 D-20 去掉 8 个外部驱动成员(`startMove` / `waitMoveEnd` / `stopMove` / `move` / `jumpTo` / `setPosition` / `turn` / `setHeroAnimateDirection`)。**逐钩子映射表须逐行汇报**:数据端钩子 → 触发点(`file:line`)→ 渲染端动作。核心几条:`IHeroLocationHooks.onSetPos(x, y)``data-base/src/hero/location.ts:59-63`)→ 非移动中时 `setPosition(x, y)`,移动中交给 `onStepEnd``IObjectMoverHooks.onMoveStart` → `startMove()``onMoveEnd` → `await waitMoveEnd()``onStepEnd(code, step, tile, mover)`(坐标已更新,`data-common/src/common/mover.ts:674-678`)→ 按 `step.type``ObjectMoveType`)分派:`Dir`/`DirFace` → `move(step.move, mover.currentSpeed)``DirFace` 另 `turn(step.face)`)、`Special` → `move(mover.moveDirection, mover.currentSpeed)`、`Face` → `turn(step.value)`、`Teleport` → `setPosition(tile.x, tile.y)`、`Jump` → `jumpTo(tile.x, tile.y, mover.currentSpeed, false)`、`AnimDir`/`Speed` → 不处理;动画方向统一在 `onStepEnd``mover.currAnimDir``onSetFaceDir` → `turn(dir)``onSetPos`mover 级)→ `setPosition(x, y)``onSetMoveDir` / `onStepStart` → 不处理(并说明理由)。
**成员命名清单须用户同意AGENTS.md 命名规则)****新增**渲染端字段 `locationController``IHookController<IHeroLocationHooks>`)与 `moverController``IHookController<IObjectMoverHooks<IHeroLocation>>`)——取代原单一 `controller` 字段;**不新增任何类**(沿用既有 `MapHeroHook` 类名);**删除**旧钩子方法名 `onSetPosition` / `onStartMove` / `onMoveHero` / `onEndMove` / `onJumpHero` / `onTurnHero``IMapHeroRenderer` 的 8 个驱动成员(连同其配套 jsDoc属删除代码而非改写注释**逐字保留** `setImage` / `setAlpha` / `setFollowerAlpha` / `onSetImage` / `onSetAlpha` / `onSetFollowerAlpha` 与跟随者族D-18 / D-22。备选命名如用户不同意 `locationController` / `moverController`)请用户当场指定。
**D-18 编译桥接的取舍(须用户裁定)**`hero` 类型改为 `IHeroLocation` 后,`addHeroMoving` 内读取贴图别名的 `hero.image``hero.ts:115,119,121`)失去数据端对位。本计划主张**只做最小编译桥接**:把该读取收敛为一处局部变量、用项目既有 `// @ts-expect-error 需要重构` 惯用法标注,运行分支与语义保持不变(`undefined` → 既有 `logger.warn(88)` 分支),**不重新设计贴图来源**(贴图来源与 `IHeroRendering` 属 D-18等用户先改数据端。请用户确认该桥接或指定其它处理方式。
**边界与禁令**:只改三个文件(`hero.ts` / `types.ts` / `manager.ts`不新增数据端公共接口、不改任何数据端文件D-21不动 `action/move.ts` 与移动控制 APID-19不动贴图 / 不透明度D-18、跟随者D-22、`core.*`D-12、`getHeroStatusOn` 类裸函数D-11不接线 `client.ts``addHero` 仍未启用);不回滚 / 不暂存 / 不提交用户并发未提交改动。
**验证方式与诚实降级(须原样告知)**:渲染端无测试设施,且 `MapExtensionManager.addHero` 当前无调用点(`client.ts:148-152` 被注释),故运行时渲染无法观察。本计划的运行结果 = **scoped 类型门禁**`pnpm exec vue-tsc --noEmit` 过滤到三个文件为 0 条,且 `client-modules` 错误集与记录基线完全一致)+ **静态改名门禁** + **D-18/D-22 保留门禁** + **D-20 契约门禁** + **eslint / CRLF 门禁** + **并发改动基线门禁**;另附**人工代码复核**。**不冒充任何无法运行的门禁。**
**关卡性质(必须原样告知用户)**:无论触发方式(`/gsd-execute-phase`、`/gsd-manager`、`/gsd-autonomous`、子代理/执行器派发)或配置模式(`.planning/config.json` 的 `yolo`、`workflow.auto_advance`)如何,**执行者/子代理都必须在此停下并等待**用户明确回复「可以执行」四个字;未收到这四个字前不得执行任何任务、不得派发执行子代理,也不得以「自动化模式」为由自动放行。
**失败语义(等同硬门禁,非自动命令)**:用户未回复「可以执行」即继续,或回复的是其它任何内容(含修订意见、沉默、`yolo` / `auto_advance` 模式下的自动推进、或子代理/执行器的自行放行),一律视为**未通过本关卡**:必须立即停止执行,不派发任何执行子代理,保持只读、不写任何文件,直到用户在对话中明确回复「可以执行」四个字后方可继续。本条失败语义只以本 `<action>``<acceptance_criteria>` 的形式表达,不写成 XML 失败声明元素(人工关卡没有可运行的自动命令可与之绑定)。
**若用户给出修订意见**(含不同意命名、不同意 D-18 桥接、或要求改映射):退出本计划、修订 `04-03-PLAN.md` 后重新汇报,不得自行改道执行。
</action>
<decision>是否按本计划(三文件勇士本体被动重接:绑定 `IHeroLocation`、订阅现有钩子、按 D-20 裁剪 `IMapHeroRenderer`)开始执行;并确认 ③ 的成员命名清单与 ④ 的 D-18 编译桥接</decision>
<context>
AGENTS.md 要求任何计划的第一个 block 点必须是向用户汇报计划内容并等待用户确认,且任何新增 / 改写的成员命名必须先经用户同意。本计划是 Phase 4 的首个**代码改动**增量04-01 / 04-02 均为只读),改的是渲染端勇士契约面与钩子接线,属于「接口设计归用户」的边界;`.planning/config.json` 为 `mode: yolo`,因此该关卡必须显式声明为 `blocking-human` 并置 `autonomous: false`,否则计划层面不提供任何 block 点。D-18 的编译耦合若不先裁决,执行者会在「不触碰贴图」与「必须编译通过」之间自行猜测,故必须在汇报中给出取舍。
</context>
<options>
<option id="execute">
<name>可以执行</name>
<pros>按本计划三个文件落地:绑定 `IHeroLocation`、订阅现有钩子、D-20 裁剪驱动成员、D-18 最小编译桥接;命名与映射均按 ②/③/④ 汇报的内容执行</pros>
<cons>D-18 贴图别名的读取在桥接后仍为 `undefined`(保持既有 warn 分支),需等用户改完数据端后再定来源</cons>
</option>
<option id="execute-renamed">
<name>可以执行,但改名</name>
<pros>按用户指定的字段名 / 方法名执行,避免命名返工</pros>
<cons></cons>
</option>
<option id="revise">
<name>修订方案 / 命名 / 桥接后再执行</name>
<pros>避免带着错误映射或错误命名落地(本计划改的是渲染端契约面,返工成本高)</pros>
<cons>退出本计划并修订 PLAN.md 后重新汇报,增加一轮往返</cons>
</option>
</options>
<resume-signal>回复「可以执行」四个字(如需改名或改桥接,请在回复中一并给出),或给出修订意见</resume-signal>
<verify>
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对用户回复原文;本关卡为人工关卡,无自动命令,失败语义写在 action ⑦/⑧ 与 acceptance_criteria 中)</human-check>
<fails_when>用户回复的是修订意见、改名要求未获确认、或未回复即继续 —— 必须停止执行,不派发任何执行子代理</fails_when>
</verify>
<acceptance_criteria>
- 用户回复中明确包含「可以执行」四个字
- 汇报内容涵盖:要解决的问题(三条失配)、逐钩子映射表(数据端钩子 → 触发点 file:line → 渲染端动作)、成员命名清单(新增 `locationController` / `moverController`;删除的旧钩子方法名与 8 个 D-20 驱动成员;逐字保留的 D-18 / D-22 成员、D-18 编译桥接取舍、范围边界与禁令、验证方式与运行时不可得的诚实降级
- 汇报中已原样告知关卡性质:执行者/子代理必须停下等待,不因触发方式或 yolo / auto 模式自动放行
</acceptance_criteria>
<done>用户明确回复「可以执行」(如需改名或改桥接,已在回复中给出);在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
</task>
<task type="tracer">
<name>Task 1tracer勇士本体端到端重接最小切片 —— manager 绑定 `IHeroLocation` → 构造器注册两个现有钩子 → `onSetPos` 一条通路贯通,三文件类型 / import / 成员名改到位</name>
<files>packages-user/client-modules/src/render/map/extension/hero.ts, packages-user/client-modules/src/render/map/extension/types.ts, packages-user/client-modules/src/render/map/extension/manager.ts, .planning/phases/04-render-adaptation/04-03-SUMMARY.md</files>
<read_first>
- AGENTS.md命名规则 / 注释规则 / 第一个 block 点)与 dev.md双端分离注释规范换行 CRLF
- .planning/phases/04-render-adaptation/04-HERO-MOVER-INTERFACE.md:152-197`#04-02-R-01..12` 的对账结论与 `#04-02-G-01` 裁决背景)
- .planning/phases/04-render-adaptation/04-03-PLAN.mdobjective 的映射表与禁令)
- packages-user/client-modules/src/render/map/extension/hero.ts整文件 517 行import :1-21、构造器 :76-102、`addHeroMoving` :110-132、`setImage` :191-196、`setHeroAnimateDirection` :437-439、`MapHeroHook` :457-517
- packages-user/client-modules/src/render/map/extension/types.ts:1-40,63-159 与 manager.ts:1-44
- packages-user/data-common/src/common/mover.ts:40-64,142-164,166-233,235-351,522-541,646-731`ObjectAnimDirection` / `IMoverController` / `IObjectMoverHooks` / `IObjectMover` / `setPos` / `moveProgress` 的钩子时序)
- packages-user/data-base/src/hero/types.ts:277-321,399-417,889-909 与 packages-user/data-base/src/hero/location.ts:53-67`onSetPos` / `onSetFloor` 触发点与 `getCurrentFaceDirection`
- packages-user/data-common/src/index.ts、packages-user/data-common/src/common/index.ts、packages-user/data-base/src/index.ts、packages-user/data-base/src/hero/index.tsbarrel 实际导出核对)
- packages-user/client-modules/src/action/move.ts:1-6**只读**:确认 D-19 范围外,不得改动,仅用于核对 `@ts-expect-error` 清单口径)
</read_first>
<action>
(0) **执行前核对与基线(只读前置,必须先做)**
(0a) **数据端现行接口复核(消费 04-02 的未确定项,不猜)**:打开当日 HEAD 的数据端文件,逐条确认并在 `04-03-SUMMARY.md` 的「执行前核对」段记录结论(每条一行:结论 + `file:line`):① `IHeroState.location: IHeroLocation` 存在(`data-base/src/hero/types.ts:894`);② `IHeroLocation extends ISaveableContent<IHeroLocationSave>, IObjectMovable, IHookable<IHeroLocationHooks>, IDataCommonExtended` 且含 `readonly mover: IHeroMover<this>``:303-314`);③ `IHeroLocationHooks.onSetPos?(x, y)``onSetFloor?(map)``:283,289`);④ `IObjectMoverHooks``onMoveStart` / `onMoveEnd` / `onStepStart` / `onStepEnd` / `onSetPos` / `onSetFaceDir` / `onSetMoveDir` 七个成员名与参数形态(`data-common/src/common/mover.ts:166-233`);⑤ `IObjectMover``moving` / `faceDirection` / `moveDirection` / `currAnimDir` / `currentSpeed``:239-249`)与 `getCurrentFaceDirection()``data-common/src/common/mover.ts:63`、`data-base/src/hero/location.ts:65`);⑥ `ObjectAnimDirection``mover.ts:40`)、`ObjectMoveType``:14`)、`ObjectSpecialStep``:33`)仍由 `@user/data-common` 导出(`data-common/src/common/index.ts:4`);⑦ `HeroLocation.setPos` 触发 `onSetPos``location.ts:59-63`)、`ObjectMover.moveProgress` 的 `onStepEnd``tile.setPos` 之后(`mover.ts:669-678`)。**任一不同:立即停止,不写任何代码,向用户汇报差异并修订本计划。**
(0b) **并发改动基线**:执行 `node -e "const fs=require('fs'),c=require('crypto'),{execSync}=require('child_process');const keep=l=>!/packages-user\/client-modules\/src\/render\/map\/extension\//.test(l)&&!/\.planning\/phases\/04-render-adaptation\//.test(l);const out=execSync('git status --porcelain',{encoding:'utf8'}).replace(/\s+$/,'');const cur=(out?out.split(/\r?\n/).map(s=>s.replace(/\s+$/,'')):[]).filter(keep);const digest=c.createHash('sha256').update(cur.slice().sort().map(l=>{let h='MISSING';try{h=c.createHash('sha256').update(fs.readFileSync(l.slice(3))).digest('hex')}catch(e){}return l+' '+h}).join('\n')).digest('hex');console.log(cur.join('\n'));console.log('基线摘要 sha256='+digest)"`,把输出**原样**写入 `04-03-SUMMARY.md` 的「执行前基线」段porcelain 条目逐行放进围栏 `text` 块,随后一行 `基线摘要 sha256=<64hex>`)。规划时该集合为用户并发的未提交改动:` M packages-user/client-base/src/load/loader.ts`、` M packages-user/client-base/src/load/types.ts`、` M packages-user/data-base/src/load/loader.ts`、` M packages-user/data-base/src/load/types.ts`、` M packages-user/data-state/src/core.ts`、` M packages-user/data-state/src/types.ts`、` M packages/loader/src/progress.ts`、` M packages/loader/src/types.ts`、` M .planning/phases/04-render-adaptation/04-CONTEXT.md`、`?? .planning/milestone.lock`。**不得回滚 / 暂存 / 提交 / 修改**这些改动。
(0c) **类型门禁基线**:执行 `pnpm exec vue-tsc --noEmit`(整仓会以非 0 退出属预期——规划时有 122 条诊断)并记录 `client-modules` 的命中行;规划时恰为 2 条、与勇士移动无关:`packages-user/client-modules/src/client.ts(66,9): error TS2554: Expected 1 arguments, but got 0.` 与 `packages-user/client-modules/src/render/ui/load.tsx(77,31): error TS2504: Type 'ITaskManager' must have a 'Symbol.asyncIterator]()' method that returns an async iterator.`。同时确认三个在范围文件**当前 0 条诊断**。把这两条原样抄入 `04-03-SUMMARY.md` 的「类型门禁基线」段;**若与记录不一致,停止并汇报**(说明用户并发改动改变了基线,不得静默调整门禁)。
(0d) 建立 `04-03-SUMMARY.md` 骨架(含上面三段 + 后续任务要填的空节),保持 CRLF。
(1) **`manager.ts`(绑定层)**:删除首行 `// @ts-expect-error 需要重构`,把 `import { IHeroMoveController } from '@user/data-base'` 改为 `import { IHeroLocation, IMapLayer } from '@user/data-base'`(合并第三行的 `IMapLayer` import避免重复 import 语句;若 lint 要求分行则按 lint 结果排布);`readonly heroMap: Map<IHeroLocation, IMapHeroRenderer>``addHero(state: IHeroLocation, layer: IMapLayer)``removeHero(state: IHeroLocation)`。`addDoor` / `removeDoor` / `addText` / `removeText` / `destroy` 一字不改。
(2) **`types.ts`(契约层,本任务只改绑定类型,不动 D-20 裁剪)**:从 `@user/data-common` 的 import 中删除 `HeroAnimateDirection``IHeroMoveController`(连带其两行 `// @ts-expect-error 需要重构` 标记),改为从 `@user/data-base` 导入 `IHeroLocation`(与既有 `IMapLayer` 合并);`IMapExtensionManager.heroMap: Map<IHeroLocation, IMapHeroRenderer>`、`addHero(state: IHeroLocation, layer: IMapLayer)`、`removeHero(state: IHeroLocation)``Map<` / `addHero` / `removeHero` 的 jsDoc **不改写**(参数名与含义不变)。本任务**不动** `IMapHeroRenderer`(其 8 个驱动成员的删除在 Task 2
(3) **`hero.ts` import 与绑定tracer 主路径)**
(3a) import 改造:`@user/data-common` 侧删除 `HeroAnimateDirection` / `IHeroMoveController` / `IHeroMoveControllerHooks`(连带 3 行 `// @ts-expect-error 需要重构` 标记),新增 `ObjectAnimDirection`、`IObjectMover`、`IObjectMoverHooks`、`ObjectMoveStep`、`ObjectMoveType`、`ObjectSpecialStep``@user/data-base` 侧把 `import { IMapLayer }` 扩为 `import { IHeroLocation, IHeroLocationHooks, IMapLayer }``IHeroLocation` / `IHeroLocationHooks` / `IHeroMover` 均可经该 barrel 取得,见 `data-base/src/hero/index.ts:10``data-base/src/index.ts:3`)。
(3b) 实体类型:`HeroRenderEntity.animateDirection` 由 `HeroAnimateDirection` 改为 `ObjectAnimDirection``:53``new MapHeroRenderer(...)` 内的默认值 `HeroAnimateDirection.Forward``ObjectAnimDirection.Forward``:97`)。
(3c) 构造器:`readonly hero: IHeroMoveController` → `readonly hero: IHeroLocation``:79``direction: hero.direction` → `direction: hero.getCurrentFaceDirection()``:89`);钩子注册由单一 `controller` 改为**两个控制器字段**`readonly locationController: IHookController<IHeroLocationHooks>` 与 `readonly moverController: IHookController<IObjectMoverHooks<IHeroLocation>>`(替换原 `readonly controller: IHookController<IHeroMoveControllerHooks>` 字段,`:61`);构造体内 `const hook = new MapHeroHook(this); this.locationController = hero.addHook(hook); this.locationController.load(); this.moverController = hero.mover.addHook(hook); this.moverController.load();`(同一 hook 实例注册到两个 hookable`Hookable.addHook` 按 hookable 实例分别存储,见 `packages/common/src/hook.ts:24-31``destroy()``:452-454`)改为 `this.locationController.unload(); this.moverController.unload();`
(3d) `addHeroMoving``:110-132`):参数 `hero: IHeroMoveController``IHeroLocation``degradeFace(hero.direction)` → `degradeFace(hero.getCurrentFaceDirection())``:125``hero.image` 的三处读取(`:115,119,121`)按 Task 0 确认的 **D-18 最小编译桥接**处理——收敛为一处局部读取(如 `const imageAlias = hero.image;`)并在其正上方加一行**新增**注释 `// @ts-expect-error 需要重构(贴图别名来源属 D-18待用户先改数据端后处理`,其后三处改用 `imageAlias`;分支结构与既有 `logger.warn(88/89)` 行为保持不变;**不得**改写既有注释、不得重设贴图来源。
(3e) `setImage``:191-196`):只把内部的 `degradeFace(this.hero.direction)` 改为 `degradeFace(this.hero.getCurrentFaceDirection())`同一朝向读取口径的编译修复其余一字不改D-18贴图语义不动
(3f) `setHeroAnimateDirection(direction: HeroAnimateDirection)``:437-439`)→ 形参类型改 `ObjectAnimDirection`;本任务保留其在 `IMapHeroRenderer` 中的声明Task 2 再按 D-20 删除)。
(3g) **`MapHeroHook` 重接tracer 只贯通一条通路)**:类声明由 `implements Partial<IHeroMoveControllerHooks>` 改为 `implements Partial<IHeroLocationHooks & IObjectMoverHooks<IHeroLocation>>``:457`);把 `onSetPosition(x, y)``:469-471`)改为 `onSetPos(x: number, y: number): void`,实现「移动之外的直接定位才立刻同步、移动中的逐格定位交给 `onStepEnd`」:当 `this.hero.hero.mover.moving``true` 时直接 return否则 `this.hero.setPosition(x, y)`**其余旧命名方法本任务先保留为类上的多余方法(不影响实现 `Partial<...>` 的可赋值性Task 2 再整体替换**——但不得新增任何旧命名方法。
(3h) 全程保持文件 CRLF改完执行 `pnpm exec eslint --fix` 对三个文件,再 `pnpm exec eslint` 必须 0 错误。
(4) **提交**:本任务改动是整体适配的中间态,**不单独提交**Task 2 完成后再一次性原子提交(见 Task 2 步骤 (5))。若执行环境强制每任务提交,则提交信息为 `refactor(04-03): rebind hero renderer to IHeroLocation`,且必须与 Task 2 的提交在本次执行内连续完成。
</action>
<reversibility rating="costly">本任务改变的是渲染端勇士拓展的**契约面**`IMapExtensionManager` 的键/参数类型、`IMapHeroRenderer` 后续要删成员),一旦后续增量或用户代码按新契约接线,回退需要同时回退调用方;但改动本身局限在三个文件内、无数据端影响,故为 `costly` 而非 `one-way`</reversibility>
<verify>
<automated>node -e "const {execSync}=require('child_process');let out='';try{out=execSync('pnpm exec vue-tsc --noEmit',{encoding:'utf8',maxBuffer:1<<26,stdio:['ignore','pipe','pipe']})}catch(e){out=String(e.stdout||'')+String(e.stderr||'')}const lines=out.split(/\r?\n/).filter(l=>/: error TS\d+/.test(l));const scope=lines.filter(l=>/render[\\/]map[\\/]extension[\\/](hero|types|manager)\.ts\(\d+,\d+\)/.test(l));if(scope.length){console.error('IN-SCOPE TYPE ERRORS '+scope.length+'\n'+scope.join('\n'));process.exit(1)}const cm=lines.filter(l=>l.includes('client-modules')).map(l=>l.replace(/^.*client-modules[\\/]src[\\/]/,''));const base=['client.ts(66,9): error TS2554','render/ui/load.tsx(77,31): error TS2504'];const missing=base.filter(b=>!cm.some(l=>l.startsWith(b)));const extra=cm.filter(l=>!base.some(b=>l.startsWith(b)));if(missing.length||extra.length){console.error('CLIENT-MODULES ERROR SET CHANGED missing=['+missing.join(' ; ')+'] extra=['+extra.join(' ; ')+']');process.exit(1)}console.log('OK in-scope type errors 0; client-modules baseline '+cm.length)"</automated>
<fails_when>non-zero exit, 或 stderr 含 "IN-SCOPE TYPE ERRORS"(三个在范围文件仍有类型错误)/ "CLIENT-MODULES ERROR SET CHANGED"(渲染端类型错误集相对记录基线发生增减,含连带打断它处)</fails_when>
<automated>node -e "const fs=require('fs');const files=['packages-user/client-modules/src/render/map/extension/hero.ts','packages-user/client-modules/src/render/map/extension/types.ts','packages-user/client-modules/src/render/map/extension/manager.ts'];const src=files.map(f=>fs.readFileSync(f,'utf8')).join('\n');const banned=['IHeroMoveControllerHooks','IHeroMoveController','HeroAnimateDirection','onSetPosition','onStartMove','onMoveHero','onEndMove','onJumpHero','onTurnHero'];const hit=banned.filter(t=>src.includes(t));if(hit.length){console.error('LEGACY SYMBOLS REMAIN '+hit.join(','));process.exit(1)}const need=['IHeroLocation','ObjectAnimDirection','getCurrentFaceDirection','mover.addHook','locationController','moverController'];const miss=need.filter(t=>!src.includes(t));if(miss.length){console.error('MISSING NEW BINDINGS '+miss.join(','));process.exit(1)}console.log('OK static rename gate')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "LEGACY SYMBOLS REMAIN"(旧类型名 / 旧钩子方法名仍出现在三个在范围文件,含注释)/ "MISSING NEW BINDINGS"(绑定、动画方向或两个钩子控制器未落地)</fails_when>
<automated>pnpm exec eslint packages-user/client-modules/src/render/map/extension/hero.ts packages-user/client-modules/src/render/map/extension/types.ts packages-user/client-modules/src/render/map/extension/manager.ts</automated>
<fails_when>non-zero exit 或输出含 "error"ESLint / Prettier 违规CRLF 由 `endOfLine: crlf``--fix` 时统一)</fails_when>
<automated>node -e "const fs=require('fs');const files=['packages-user/client-modules/src/render/map/extension/hero.ts','packages-user/client-modules/src/render/map/extension/types.ts','packages-user/client-modules/src/render/map/extension/manager.ts'];for(const f of files){const s=fs.readFileSync(f,'utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;if(lf!==crlf){console.error('NON-CRLF LINE ENDINGS '+f);process.exit(1)}}console.log('OK CRLF')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "NON-CRLF LINE ENDINGS"(出现孤立 LF违反 dev.md「换行使用 CRLF 格式」)</fails_when>
</verify>
<acceptance_criteria>
- `manager.ts``types.ts``heroMap` / `addHero` / `removeHero` 均为 `IHeroLocation``manager.ts` 首行的 `// @ts-expect-error 需要重构` 已删除,且不再 import `IHeroMoveController`
- `hero.ts` 的构造器参数为 `IHeroLocation`,构造期朝向取自 `hero.getCurrentFaceDirection()``HeroRenderEntity.animateDirection` 与默认值为 `ObjectAnimDirection`
- `hero.ts` 同时存在 `hero.addHook(``hero.mover.addHook(`,并存在 `locationController` / `moverController` 两个字段;`destroy()` 对两者都 `unload()`
- `MapHeroHook` 声明为 `Partial<IHeroLocationHooks & IObjectMoverHooks<IHeroLocation>>`,且已实现 `onSetPos`(旧 `onSetPosition` 已不存在)
- 三个文件不再出现 `IHeroMoveController` / `IHeroMoveControllerHooks` / `HeroAnimateDirection` / `onSetPosition` / `onStartMove` / `onMoveHero` / `onEndMove` / `onJumpHero` / `onTurnHero`
- `hero.ts``hero.image` 的读取已收敛为一处(带新增的 `// @ts-expect-error 需要重构…D-18…` 标注),既有 `logger.warn(88/89/90)` 分支结构未变;`setImage` / `setAlpha` / 跟随者族成员未被删除
- `04-03-SUMMARY.md` 含「执行前核对」(步骤 0a 七条,每条带 `file:line`、「执行前基线」porcelain 条目 + `基线摘要 sha256`)与「类型门禁基线」(两条 client-modules 诊断原样)三段,全文 CRLF
- 四条 `<automated>` 全部转绿scoped 类型门禁(三个文件 0 条、client-modules 错误集 == 基线、静态改名门禁、eslint 0 错误、CRLF 门禁
</acceptance_criteria>
<done>绑定层(`IHeroLocation`)、构造器两个 `addHook` 注册、`onSetPos` 一条通路与三文件的类型 / import / 成员名改造全部落地,四条机器门禁转绿,且三个在范围文件不再出现任何旧类型名与旧钩子方法名</done>
</task>
<task type="auto">
<name>Task 2逐钩子映射落地 + D-20 契约裁剪 —— 旧钩子方法名整体替换为现有钩子成员名;`IMapHeroRenderer` 去掉 8 个外部驱动成员D-18 / D-22 成员逐字保留)</name>
<files>packages-user/client-modules/src/render/map/extension/hero.ts, packages-user/client-modules/src/render/map/extension/types.ts</files>
<read_first>
- packages-user/client-modules/src/render/map/extension/hero.tsTask 1 产出的现状全文;重点 `MapHeroHook``IMapHeroRenderer` 的实现方法)
- packages-user/client-modules/src/render/map/extension/types.ts:63-159`IMapHeroRenderer` 当前 17 个成员)
- packages-user/data-common/src/common/mover.ts:166-233钩子参数形态、:641-687`moveProgress` 的钩子时序:`onMoveStart` → 每步 `prepareStep``onStepStart`(坐标未变)→ `tile.setPos``onStepEnd`(坐标已更新)→ `onMoveEnd`
- packages-user/data-common/src/common/mover.ts:40-45,14-31,33-38`ObjectAnimDirection` / `ObjectMoveType` / `ObjectSpecialStep` 成员名)
- packages-user/data-base/src/hero/mover.ts:147-280`HeroMover` 的 `onStepStart` / `onStepEnd``HeroMoveCode`,用于确认渲染侧读到的 `code` / `step` 语义)
- .planning/phases/04-render-adaptation/04-CONTEXT.mdD-18 / D-19 / D-20 / D-21 / D-22
</read_first>
<action>
(1) **`MapHeroHook` 逐钩子映射落地D-21只接现有钩子旧名全删**。把 Task 1 保留的旧命名方法整体替换为现有钩子成员名,按下列映射实现(每条都要能对上数据端触发点):
- `onSetPos(x: number, y: number): void`**移动之外的直接定位**才立刻同步——当 `this.hero.hero.mover.moving``true` 时直接 return移动中的逐格 `tile.setPos` 会触发本条,交给 `onStepEnd` 做动画),否则 `this.hero.setPosition(x, y)`。触发点:`HeroLocation.setPos``data-base/src/hero/location.ts:59-63`);移动中的逐格调用见 `data-common/src/common/mover.ts:671-673`
- `onMoveStart(): void``this.hero.startMove()`;触发点 `mover.ts:651-656`
- `onMoveEnd(): Promise<void>``return this.hero.waitMoveEnd()`;触发点 `mover.ts:682-686`
- `onStepEnd(code: number, step: Readonly<ObjectMoveStep>, tile: IHeroLocation, mover: IObjectMover<IHeroLocation>): Promise<void>`**逐格渲染的唯一驱动点**(此时 `tile` 坐标已更新,见 `mover.ts:669-678`)。先 `this.hero.setHeroAnimateDirection(mover.currAnimDir)`(动画播放方向统一取自数据端 `currAnimDir`,即 R-03 的 `ObjectAnimDirection` 落地),再按 `step.type` 分派:`ObjectMoveType.Dir` → `return this.hero.move(step.move, mover.currentSpeed)``ObjectMoveType.DirFace` → `this.hero.turn(step.face); return this.hero.move(step.move, mover.currentSpeed)``ObjectMoveType.Special` → `return this.hero.move(mover.moveDirection, mover.currentSpeed)`(前进/后退的移动方向已由 `ObjectMover.prepareStep` 写入 `moveDirection`,见 `mover.ts:500-512`,无需自行取反);`ObjectMoveType.Face` → `this.hero.turn(step.value)``ObjectMoveType.Teleport` → `this.hero.setPosition(tile.x, tile.y)``ObjectMoveType.Jump` → `return this.hero.jumpTo(tile.x, tile.y, mover.currentSpeed, false)``ObjectMoveType.AnimDir` / `ObjectMoveType.Speed` → 不处理(动画方向已在 `currAnimDir` 中体现,速度由数据端消费)。
- `onSetFaceDir(dir: FaceDirection): void``this.hero.turn(dir)`;触发点 `mover.ts:529-534`
- `onSetPos`mover 级四参调用)与 location 级两参调用共用同一个两参方法JS 会忽略多余实参,本方法的「移动中 return」守卫同时覆盖两条触发路径。
- **不实现**(在 `04-03-SUMMARY.md` 写明理由):`onStepStart`(渲染只在新坐标就绪后推进,避免重复驱动)、`onSetMoveDir`(渲染朝向只随 `onSetFaceDir` 变化;`moveDirection` 已由 `onStepEnd` 读取)、`IHeroLocationHooks.onSetFloor`(楼层切换需要重绑图层的接线路径,本增量不接线 `client.ts`;登记为后续增量事项,不在本计划实现)。
- **逐字保留D-18 / D-22不得删除、改写语义或改名**`onSetImage(image: ImageIds)`、`onSetAlpha(alpha: number)`、`onSetFollowerAlpha(identifier: string, alpha: number)`、`onAddFollower(follower: number, identifier: string)`、`onRemoveFollower(identifier: string, animate: boolean)`、`onRemoveAllFollowers()`——它们是本类上的多余方法(不再属于任何现有钩子接口的成员),保持原样并**不注册到任何数据端对象**;其现有 jsDoc / 注释一字不改。是否在后续增量D-18 / D-22 解开时)迁到数据端对象上,留待用户裁定。
(2) **`IMapHeroRenderer` 按 D-20 裁剪(`types.ts`**:删除以下 8 个成员声明**及其配套 jsDoc**属删除代码Task 0 已获用户确认):`setPosition(x, y)``:92`)、`startMove()``:97`)、`waitMoveEnd(waitFollower)``:103`)、`stopMove()``:108`)、`move(direction, time)``:114`)、`jumpTo(x, y, time, waitFollower)``:123`)、`setHeroAnimateDirection(direction)``:147`)、`turn(direction?)``:153`)。保留 `setImage` / `addFollower` / `removeFollower` / `removeAllFollowers` / `setAlpha` / `setFollowerAlpha` / `destroy`D-18 / D-22 成员,逐字不动)。不得新增任何成员。
(3) **`MapHeroRenderer` 侧配套**:上述 8 个方法在类上**保留为内部实现**(不删除、不改名),仅由 `MapHeroHook` 的回调调用;其可见性/实现细节保持不变。`waitMoveEnd()` 的实现签名无参与接口原声明的参数落差04-02「未能从阅读确定」第 6 条)随接口成员移除而消解,在 `04-03-SUMMARY.md` 记为「由 D-20 契约裁剪消解」。`setHeroAnimateDirection(direction: ObjectAnimDirection)` 的形参类型在 Task 1 已改,本任务不再动它。
(4) **不改的事**:不改 `MapHeroRenderer` 的动画算法(`tick` / `moveEntity` / `jumpEntity` / `generateJumpFn`)、不改 `addHeroMoving` 的贴图分支结构、不改 `setImage` / `setAlpha` / 跟随者族实现体、不接线 `client.ts`、不动 `action/move.ts`。既有注释一字不改(新增注释除外)。
(5) **原子提交**`refactor(04-03): rebind hero renderer to IHeroLocation and existing hooks`(或执行环境约定的等价信息),提交内容仅含 `hero.ts` / `types.ts` / `manager.ts` 三个文件;**不得**把用户的并发未提交改动纳入提交。随后执行 `pnpm exec eslint --fix` 对三个文件,再 `pnpm exec eslint` 必须 0 错误。
</action>
<verify>
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('packages-user/client-modules/src/render/map/extension/types.ts','utf8');const i=t.indexOf('export interface IMapHeroRenderer');if(i<0){console.error('NO IMapHeroRenderer');process.exit(1)}const j=t.indexOf('export interface ',i+1);const seg=t.slice(i,j<0?t.length:j);const banned=['startMove(): void;','waitMoveEnd(','stopMove(): void;','move(direction: FaceDirection, time: number): Promise<void>;','jumpTo(','setPosition(x: number, y: number): void;','turn(direction?: FaceDirection): void;','setHeroAnimateDirection('];const hit=banned.filter(x=>seg.includes(x));if(hit.length){console.error('D20 DRIVE MEMBERS STILL EXPOSED '+hit.join(' , '));process.exit(1)}const need=['setImage(image: ITexture): void;','addFollower(image: number, id: string): void;','removeFollower(follower: string, animate: boolean): Promise<void>;','removeAllFollowers(): void;','setAlpha(alpha: number): void;','setFollowerAlpha(identifier: string, alpha: number): void;','destroy(): void;'];const miss=need.filter(x=>!seg.includes(x));if(miss.length){console.error('D18 D22 CONTRACT MEMBERS MISSING '+miss.join(' , '));process.exit(1)}console.log('OK D20 passive contract / D18 D22 preserved')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "D20 DRIVE MEMBERS STILL EXPOSED"`IMapHeroRenderer` 仍暴露外部驱动成员,违反 D-20/ "D18 D22 CONTRACT MEMBERS MISSING"(贴图 / 不透明度 / 跟随者成员被误删,违反 D-18 / D-22</fails_when>
<automated>node -e "const fs=require('fs');const h=fs.readFileSync('packages-user/client-modules/src/render/map/extension/hero.ts','utf8');const need=['onSetImage(','onSetAlpha(','onSetFollowerAlpha(','onAddFollower(','onRemoveFollower(','onRemoveAllFollowers(','setImage(','setAlpha(','setFollowerAlpha(','addFollower(','removeFollower(','removeAllFollowers('];const miss=need.filter(x=>!h.includes(x));if(miss.length){console.error('D18 D22 MEMBERS REMOVED FROM hero.ts '+miss.join(' , '));process.exit(1)}const hook=['onSetPos(','onMoveStart(','onMoveEnd(','onStepEnd(','onSetFaceDir('];const hMiss=hook.filter(x=>!h.includes(x));if(hMiss.length){console.error('EXISTING HOOK HANDLERS MISSING '+hMiss.join(' , '));process.exit(1)}const legacy=['onSetPosition','onStartMove','onMoveHero','onEndMove','onJumpHero','onTurnHero'];const lHit=legacy.filter(x=>h.includes(x));if(lHit.length){console.error('LEGACY HOOK NAMES REMAIN '+lHit.join(','));process.exit(1)}console.log('OK mapping + D18 D22 hook members')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "D18 D22 MEMBERS REMOVED FROM hero.ts"D-18 / D-22 钩子方法被误删)/ "EXISTING HOOK HANDLERS MISSING"(现有钩子成员未落地)/ "LEGACY HOOK NAMES REMAIN"(旧钩子方法名残留)</fails_when>
<automated>node -e "const {execSync}=require('child_process');let out='';try{out=execSync('pnpm exec vue-tsc --noEmit',{encoding:'utf8',maxBuffer:1<<26,stdio:['ignore','pipe','pipe']})}catch(e){out=String(e.stdout||'')+String(e.stderr||'')}const lines=out.split(/\r?\n/).filter(l=>/: error TS\d+/.test(l));const scope=lines.filter(l=>/render[\\/]map[\\/]extension[\\/](hero|types|manager)\.ts\(\d+,\d+\)/.test(l));if(scope.length){console.error('IN-SCOPE TYPE ERRORS '+scope.length+'\n'+scope.join('\n'));process.exit(1)}const cm=lines.filter(l=>l.includes('client-modules')).map(l=>l.replace(/^.*client-modules[\\/]src[\\/]/,''));const base=['client.ts(66,9): error TS2554','render/ui/load.tsx(77,31): error TS2504'];const extra=cm.filter(l=>!base.some(b=>l.startsWith(b)));const missing=base.filter(b=>!cm.some(l=>l.startsWith(b)));if(extra.length||missing.length){console.error('CLIENT-MODULES ERROR SET CHANGED missing=['+missing.join(' ; ')+'] extra=['+extra.join(' ; ')+']');process.exit(1)}console.log('OK in-scope type errors 0; client-modules baseline '+cm.length)"</automated>
<fails_when>non-zero exit, 或 stderr 含 "IN-SCOPE TYPE ERRORS" / "CLIENT-MODULES ERROR SET CHANGED"D-20 裁剪或钩子替换引入新诊断)</fails_when>
<automated>pnpm exec eslint packages-user/client-modules/src/render/map/extension/hero.ts packages-user/client-modules/src/render/map/extension/types.ts packages-user/client-modules/src/render/map/extension/manager.ts</automated>
<fails_when>non-zero exit 或输出含 "error"(含未使用变量 / Prettier 违规)</fails_when>
</verify>
<acceptance_criteria>
- `MapHeroHook` 实现 `onSetPos` / `onMoveStart` / `onMoveEnd` / `onStepStart`(可不实现)/ `onStepEnd` / `onSetFaceDir` / `onSetMoveDir`(可不实现)中的映射子集,且**不含**任何旧命名方法
- `onStepEnd``ObjectMoveType` 分派到 `move` / `turn` / `setPosition` / `jumpTo`,并在开头以 `mover.currAnimDir` 设置动画方向;`onMoveStart` / `onMoveEnd` 分别驱动 `startMove` / `waitMoveEnd`
- `IMapHeroRenderer` 不再声明 `setPosition` / `startMove` / `waitMoveEnd` / `stopMove` / `move` / `jumpTo` / `setHeroAnimateDirection` / `turn`,且 `setImage` / `addFollower` / `removeFollower` / `removeAllFollowers` / `setAlpha` / `setFollowerAlpha` / `destroy` 逐字保留
- `hero.ts` 仍含全部 D-18 / D-22 钩子方法与实现方法(含 `onSetImage` / `onSetAlpha` / `onSetFollowerAlpha` 与跟随者三方法),且其注释与实现体未被改写
- 四条 `<automated>` 全绿;未新增任何接口成员;三个文件已一个原子提交入库,且提交未包含用户并发改动
</acceptance_criteria>
<done>逐钩子映射与 D-20 契约裁剪落地:旧钩子名全清、现有钩子成员全接、`IMapHeroRenderer` 只剩被动契约、D-18 / D-22 成员逐字保留,类型 / 契约 / 保留 / lint 四条门禁转绿并原子提交</done>
</task>
<task type="auto">
<name>Task 3终验门禁与人工复核 —— 并发改动基线、范围门禁、CRLF、全量门禁复跑 + `04-03-SUMMARY.md` 定稿</name>
<files>.planning/phases/04-render-adaptation/04-03-SUMMARY.md</files>
<read_first>
- .planning/phases/04-render-adaptation/04-03-SUMMARY.mdTask 1/2 的「执行前核对」「执行前基线」「类型门禁基线」三段)
- .planning/phases/04-render-adaptation/04-03-PLAN.md本任务的全部门禁定义与成功标准
- .planning/phases/04-render-adaptation/04-HERO-MOVER-INTERFACE.md:152-197对账结论 → 本计划覆盖对照)
- .planning/phases/04-render-adaptation/04-CONTEXT.mdD-19 / D-20 / D-21 / D-22 的裁定原文,用于 SUMMARY 的决策段)
- .planning/STATE.md:217-236Phase 04 既有决策记录格式)
</read_first>
<action>
(1) **范围门禁(先跑,防止越界改动)**:执行 `git status --porcelain`,确认**生产代码**相对 Task 1 记录的基线只多出 `packages-user/client-modules/src/render/map/extension/{hero.ts,types.ts,manager.ts}` 三条改动;特别断言 `packages-user/client-modules/src/action/move.ts`D-19、`packages-user/client-base/**`、`packages-user/data-common/**`、`packages-user/data-base/**`、`packages-user/data-system/**`、`packages-user/data-state/**`、`packages/**` 均无本计划引入的新改动。若出现第四个生产文件的改动:停止、汇报、修订计划(不得自行扩面)。
(2) **并发改动基线门禁**:用与 Task 1 步骤 (0b) 完全相同的固定命令重算(排除 `packages-user/client-modules/src/render/map/extension/``.planning/phases/04-render-adaptation/` 两个前缀),断言 porcelain 条目集合与内容摘要 `sha256` 均与 `04-03-SUMMARY.md`「执行前基线」段记录一致——即用户的并发未提交改动**未被回滚 / 暂存 / 提交 / 修改**。
(3) **复跑全部机器门禁**scoped 类型门禁(三个文件 0 条 + `client-modules` 错误集 == 两条记录基线)、静态改名门禁(`IHeroMoveController` / `IHeroMoveControllerHooks` / `HeroAnimateDirection` 与 6 个旧钩子方法名不复现;`IHeroLocation` / `ObjectAnimDirection` / `getCurrentFaceDirection` / `hero.addHook` / `hero.mover.addHook` / `locationController` / `moverController` 均存在、D-20 契约门禁、D-18 / D-22 保留门禁、`pnpm exec eslint` 三文件 0 错误、CRLF 门禁。
(4) **人工复核(写进 SUMMARY 的人工 UAT 段,逐条给结论)**:① 逐钩子映射是否与 Task 0 汇报并经用户确认的映射表一致(列出每条数据端触发点 `file:line` ↔ 渲染端动作);② `onSetPos` 的「移动中 return」守卫是否正确移动中的逐格定位不应瞬时吸附而应由 `onStepEnd` 动画推进);③ `onStepEnd``ObjectMoveType` 分派是否覆盖 `Dir` / `DirFace` / `Special` / `Face` / `Teleport` / `Jump``AnimDir` / `Speed` 是否确无渲染动作;④ D-18 编译桥接是否只收敛了一处 `hero.image` 读取、既有 warn 分支是否未变;⑤ D-18 / D-22 成员是否**逐字**保留(对照 Task 2 门禁输出);⑥ 是否有任何新公共接口成员被引入(应为零);⑦ 运行时 UAT 不可得的理由是否已写明(`addHero` 未被接线,`client.ts:148-152` 仍为注释)。
(5) **SUMMARY 定稿**:补齐 `04-04` 无(不存在)——写 `.planning/phases/04-render-adaptation/04-03-SUMMARY.md`,含:改动三文件的清单与职责、`#04-02-R-01..12` 的对账结论 → 本计划处置对照表(哪几条被本计划覆盖、哪几条属 D-19 / D-18 / D-22 / 后续增量)、执行前核对七条结论、五类门禁的实际输出摘要(含 `client-modules` 基线两条原样、scoped 0 条)、人工复核七条、未实现项清单(`onStepStart` / `onSetMoveDir` / `onSetFloor` / `client.ts` 接线 / `hero.image` 来源)、以及 REND-01 / REND-02 仍为 Pending 的明确声明。全文 CRLF。
(6) **提交**`docs(04-03): record execution state`,仅含 `04-03-SUMMARY.md`(三个生产文件的改动已在 Task 2 提交ROADMAP / STATE 的元数据更新由 orchestrator 的元数据提交处理,本任务不代劳)。
(7) **不做的事**:不接线 `client.ts`、不改 `action/move.ts`、不新增测试文件(渲染端无测试设施)、不改任何数据端文件、不修任何 ③ 多余旧路径或其它 ① 错配(`#04-01-M-06..09`)。
</action>
<verify>
<automated>node -e "const {execSync}=require('child_process');let out;try{out=execSync('git status --porcelain',{encoding:'utf8'})}catch(e){console.error('GIT STATUS FAILED '+e.message);process.exit(1)}const allow=['packages-user/client-modules/src/render/map/extension/hero.ts','packages-user/client-modules/src/render/map/extension/types.ts','packages-user/client-modules/src/render/map/extension/manager.ts'];const lines=out.split(/\r?\n/).filter(l=>l.trim().length>0);const prod=lines.map(l=>l.slice(3).replace(/^\"|\"$/g,'')).filter(p=>/^packages(-user)?\//.test(p)).filter(p=>!/\.planning\//.test(p));const extra=prod.filter(p=>allow.indexOf(p)<0);const missing=allow.filter(p=>prod.indexOf(p)<0);if(extra.length){console.error('OUT-OF-SCOPE FILE MODIFIED '+extra.join(' ; '));process.exit(1)}console.log('OK scope: in-scope modified '+missing.length+'/3 (committed ones absent) extra 0')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "OUT-OF-SCOPE FILE MODIFIED"(出现 `files_modified` 之外的生产文件改动,含 D-19 的 `action/move.ts` 或任何数据端文件)</fails_when>
<automated>node -e "const fs=require('fs'),c=require('crypto'),{execSync}=require('child_process');const keep=l=>!/packages-user\/client-modules\/src\/render\/map\/extension\//.test(l)&&!/\.planning\/phases\/04-render-adaptation\//.test(l);const out=execSync('git status --porcelain',{encoding:'utf8'}).replace(/\s+$/,'');const cur=(out?out.split(/\r?\n/).map(s=>s.replace(/\s+$/,'')):[]).filter(keep);const digest=c.createHash('sha256').update(cur.slice().sort().map(l=>{let h='MISSING';try{h=c.createHash('sha256').update(fs.readFileSync(l.slice(3))).digest('hex')}catch(e){}return l+' '+h}).join('\n')).digest('hex');const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-03-SUMMARY.md','utf8');const rec=(t.match(/^[ MARCUD?!]{2} [^\r\n]*/gm)||[]).map(s=>s.replace(/\s+$/,'')).filter(keep);const dm=t.match(/基线摘要[^\r\n]*?sha256=([0-9a-f]{64})/);if(!dm){console.error('NO BASELINE DIGEST');process.exit(1)}const extra=cur.filter(l=>rec.indexOf(l)<0);const gone=rec.filter(l=>cur.indexOf(l)<0);if(extra.length||gone.length){console.error('BASELINE ENTRY SET CHANGED extra=['+extra.join(' ; ')+'] missing=['+gone.join(' ; ')+']');process.exit(1)}if(dm[1]!==digest){console.error('BASELINE DIGEST CHANGED '+dm[1]+' vs '+digest);process.exit(1)}console.log('OK user concurrent edits unchanged entries '+cur.length)"</automated>
<fails_when>non-zero exit, 或 stderr 含 "NO BASELINE DIGEST" / "BASELINE ENTRY SET CHANGED" / "BASELINE DIGEST CHANGED"(用户并发的未提交改动被本计划回滚 / 暂存 / 提交 / 修改 —— 违反并发改动禁令)</fails_when>
<automated>node -e "const fs=require('fs');const S='packages-user/client-modules/src/render/map/extension/';const src=['hero.ts','types.ts','manager.ts'].map(f=>fs.readFileSync(S+f,'utf8')).join('\n');const banned=['IHeroMoveControllerHooks','IHeroMoveController','HeroAnimateDirection','onSetPosition','onStartMove','onMoveHero','onEndMove','onJumpHero','onTurnHero'];const hit=banned.filter(t=>src.includes(t));if(hit.length){console.error('LEGACY SYMBOLS REMAIN '+hit.join(','));process.exit(1)}const need=['IHeroLocation','ObjectAnimDirection','getCurrentFaceDirection','mover.addHook','hero.addHook','locationController','moverController'];const miss=need.filter(t=>!src.includes(t));if(miss.length){console.error('MISSING NEW BINDINGS '+miss.join(','));process.exit(1)}const t=fs.readFileSync(S+'types.ts','utf8');const i=t.indexOf('export interface IMapHeroRenderer');const j=t.indexOf('export interface ',i+1);const seg=t.slice(i,j<0?t.length:j);const d20=['startMove(): void;','waitMoveEnd(','stopMove(): void;','move(direction: FaceDirection, time: number): Promise<void>;','jumpTo(','setPosition(x: number, y: number): void;','turn(direction?: FaceDirection): void;','setHeroAnimateDirection('];const dHit=d20.filter(x=>seg.includes(x));if(dHit.length){console.error('D20 DRIVE MEMBERS STILL EXPOSED '+dHit.join(' , '));process.exit(1)}const d18=['setImage(image: ITexture): void;','addFollower(image: number, id: string): void;','removeFollower(follower: string, animate: boolean): Promise<void>;','removeAllFollowers(): void;','setAlpha(alpha: number): void;','setFollowerAlpha(identifier: string, alpha: number): void;','destroy(): void;'];const d18Miss=d18.filter(x=>!seg.includes(x));if(d18Miss.length){console.error('D18 D22 CONTRACT MEMBERS MISSING '+d18Miss.join(' , '));process.exit(1)}const h=fs.readFileSync(S+'hero.ts','utf8');const hNeed=['onSetImage(','onSetAlpha(','onSetFollowerAlpha(','onAddFollower(','onRemoveFollower(','onRemoveAllFollowers(','onSetPos(','onMoveStart(','onMoveEnd(','onStepEnd(','onSetFaceDir('];const hMiss=hNeed.filter(x=>!h.includes(x));if(hMiss.length){console.error('D18 D22 OR HOOK MEMBERS MISSING '+hMiss.join(' , '));process.exit(1)}console.log('OK final static gate: rename + D20 + D18/D22')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "LEGACY SYMBOLS REMAIN" / "MISSING NEW BINDINGS" / "D20 DRIVE MEMBERS STILL EXPOSED" / "D18 D22 CONTRACT MEMBERS MISSING" / "D18 D22 OR HOOK MEMBERS MISSING"</fails_when>
<automated>node -e "const {execSync}=require('child_process');let out='';try{out=execSync('pnpm exec vue-tsc --noEmit',{encoding:'utf8',maxBuffer:1<<26,stdio:['ignore','pipe','pipe']})}catch(e){out=String(e.stdout||'')+String(e.stderr||'')}const lines=out.split(/\r?\n/).filter(l=>/: error TS\d+/.test(l));const scope=lines.filter(l=>/render[\\/]map[\\/]extension[\\/](hero|types|manager)\.ts\(\d+,\d+\)/.test(l));if(scope.length){console.error('IN-SCOPE TYPE ERRORS '+scope.length+'\n'+scope.join('\n'));process.exit(1)}const cm=lines.filter(l=>l.includes('client-modules')).map(l=>l.replace(/^.*client-modules[\\/]src[\\/]/,''));const base=['client.ts(66,9): error TS2554','render/ui/load.tsx(77,31): error TS2504'];const extra=cm.filter(l=>!base.some(b=>l.startsWith(b)));const missing=base.filter(b=>!cm.some(l=>l.startsWith(b)));if(extra.length||missing.length){console.error('CLIENT-MODULES ERROR SET CHANGED missing=['+missing.join(' ; ')+'] extra=['+extra.join(' ; ')+']');process.exit(1)}console.log('OK final type gate: in-scope 0; client-modules baseline '+cm.length)"</automated>
<fails_when>non-zero exit, 或 stderr 含 "IN-SCOPE TYPE ERRORS" / "CLIENT-MODULES ERROR SET CHANGED"(终验类型门禁未过)</fails_when>
<automated>pnpm exec eslint packages-user/client-modules/src/render/map/extension/hero.ts packages-user/client-modules/src/render/map/extension/types.ts packages-user/client-modules/src/render/map/extension/manager.ts</automated>
<fails_when>non-zero exit 或输出含 "error"</fails_when>
<automated>node -e "const fs=require('fs');const S='packages-user/client-modules/src/render/map/extension/';for(const f of ['hero.ts','types.ts','manager.ts']){const s=fs.readFileSync(S+f,'utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;if(lf!==crlf){console.error('NON-CRLF LINE ENDINGS '+f);process.exit(1)}}const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-03-SUMMARY.md','utf8');const lf=(t.match(/\n/g)||[]).length;const crlf=(t.match(/\r\n/g)||[]).length;if(lf!==crlf){console.error('NON-CRLF SUMMARY');process.exit(1)}console.log('OK CRLF all four files')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "NON-CRLF LINE ENDINGS" / "NON-CRLF SUMMARY"</fails_when>
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-03-SUMMARY.md','utf8');const need=['执行前核对','执行前基线','类型门禁基线','人工复核','REND-01','REND-02','Pending','addHero','client.ts:148'];const miss=need.filter(x=>!t.includes(x));if(miss.length){console.error('SUMMARY MISSING SECTIONS '+miss.join(','));process.exit(1)}console.log('OK summary completeness')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "SUMMARY MISSING SECTIONS"SUMMARY 缺少执行前核对 / 基线 / 人工复核 / 需求 Pending 声明 / 运行时不可得说明)</fails_when>
</verify>
<acceptance_criteria>
- 范围门禁通过:无 `files_modified` 之外的生产文件改动(`action/move.ts`、任何数据端包、`client-base`、`packages` 均无本计划引入的改动)
- 并发改动基线门禁通过:用户并发未提交改动的 porcelain 条目集合与内容 `sha256` 均与记录的基线一致
- 静态改名门禁、D-20 契约门禁、D-18 / D-22 保留门禁、scoped 类型门禁(三文件 0 条、`client-modules` 错误集 == 两条基线、eslint 0 错误、CRLF 门禁(三个源文件 + SUMMARY全部转绿
- `04-03-SUMMARY.md` 含「执行前核对」七条(带 `file:line`、「执行前基线」porcelain + `sha256`)、「类型门禁基线」(两条原样)、`#04-02-R-01..12` 对账 → 本计划处置对照、五类门禁输出摘要、人工复核七条、未实现项清单、REND-01 / REND-02 仍为 Pending 与运行时 UAT 不可得的明确说明
- SUMMARY 已以 `docs(04-03)` 提交,且提交仅含该 SUMMARY
</acceptance_criteria>
<done>七条机器门禁全绿、用户并发改动零变化、范围无越界、SUMMARY 定稿并提交;用户可据此继续裁决后续渲染适配增量(贴图 D-18、跟随者 D-22、接线启用、双布局 REND-02</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 渲染端契约面 → 下游调用方 | `IMapExtensionManager` / `IMapHeroRenderer``client-base` / `client-modules` 其它模块与用户代码消费的公共契约D-20 的裁剪会改变契约面,误删 D-18 / D-22 成员会直接扩大破坏面 |
| 数据端接口 → 渲染端绑定 | 渲染端只读 import `data-common` / `data-base` 的接口与 const enum本计划不改数据端接口一旦被用户并发改动适配即按错误形状落地 |
| 数据端钩子回调 → 渲染状态 | 现有钩子是唯一允许推进渲染状态的入口D-20 / D-21把驱动能力留在公共契约上就等于开放外部改写渲染状态 |
| 执行者/子代理 → 执行前置关卡 | AGENTS.md 要求第一个 block 点先汇报并等待用户回复「可以执行」;`yolo` / `auto_advance` 或子代理派发都不是放行理由Task 0 为 `blocking-human`、`autonomous: false` |
| 执行者 → 仓库工作树 | 用户正在并发手工重构数据端(`client-base/src/load/**`、`data-base/src/load/**`、`data-state/src/{core,types}.ts`、`packages/loader/src/**`);本计划只允许改三个渲染端文件,且不得触碰这些改动 |
| 未接线状态 → 验收结论 | `addHero` 无调用点,运行时渲染不可观察;若不显式声明,机器门禁全绿会被误读为「勇士渲染已可用」 |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-04-03-01 | Tampering | `IMapHeroRenderer` 公共契约(`types.ts` | high | mitigate | Task 2 删除 8 个外部驱动成员D-20并由 Task 2 / Task 3 的 D-20 契约门禁断言其不再出现prohibitions 禁止新增任何接口成员D-18 / D-22 成员设独立保留门禁,防误删 |
| T-04-03-02 | Repudiation | 钩子映射与旧名清除的可追溯性 | medium | mitigate | 逐钩子映射在 Task 0 汇报并经用户确认Task 2 / Task 3 的静态改名门禁断言 6 个旧钩子方法名与 3 个旧类型名不复现、5 个现有钩子成员落地;映射关系写入 SUMMARY 人工复核段 |
| T-04-03-03 | Elevation of Privilege | 执行前置关卡AGENTS.md 要求的第一处 block 点) | high | mitigate | Task 0 为 `checkpoint:decision` + `gate="blocking-human"` 且本计划 `autonomous: false`Task 0 的 action ⑦/⑧ 与 acceptance_criteria 明示未收到「可以执行」四个字即必须停止、不派发执行子代理 |
| T-04-03-04 | Tampering | 用户并发的未提交改动 | high | mitigate | Task 1 步骤 (0b) 记录 porcelain 条目 + 各文件内容 `sha256` 摘要Task 3 用同一固定命令复算并断言集合与摘要未变prohibitions 明确不得回滚 / 暂存 / 提交 / 修改Task 3 范围门禁额外断言无范围外生产文件改动 |
| T-04-03-05 | Spoofing | 「编译通过」被当作「运行时可用」 | medium | mitigate | 运行时 UAT 不可得(`addHero` 未被接线)作为 truths/assumptions 明写SUMMARY 必须含运行时不可得说明Task 3 门禁断言 `addHero``client.ts:148` 字样);严禁以不可运行的门禁冒充通过 |
| T-04-03-06 | Elevation of Privilege | 范围蔓延(贴图 D-18 / 跟随者 D-22 / 移动控制 D-19 / legacy D-12 / 裸函数 D-11 | medium | mitigate | 范围门禁断言只有三个文件被改D-18 / D-22 保留门禁prohibitions 逐条列明;需要第四个文件时强制停下汇报并修订计划 |
| T-04-03-07 | Information Disclosure | 改动引用的接口名、文件行号与类型签名 | low | accept | 改动只含渲染端源码引用,不含凭据 / 密钥 / 用户数据;无需额外缓解 |
| T-04-03-SC | Tampering | npm / pnpm 依赖安装 | high | accept | 本计划零依赖变更、不修改 `package.json` / `pnpm-lock.yaml`Package Legitimacy Gate 不触发;若执行中出现安装需求,立即停止并汇报 |
</threat_model>
<verification>
<!-- plan-criteria-allow: R4 - 本计划判据区内的 `|` 全部是 node -e 脚本里 JS 正则的交替(如 (hero|types|manager)),不是 shell 管道;脚本内 git 调用由 execSync 的 try/catch 捕获并以非零退出失败,不会把失败读成通过 -->
0. 执行前置关卡Task 0AGENTS.md 第一个 block 点):向用户汇报「要解决的问题 / 大致解决方案(含逐钩子映射表)/ 成员命名清单 / D-18 编译桥接取舍 / 范围边界 / 验证方式的诚实降级」后**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行任何任务、不得派发执行子代理,任何触发方式与 `yolo` / `auto_advance` 模式均不得自动放行(人工核对:用户回复原文)。
1. 执行前核对与基线Task 1 步骤 0七条数据端现行接口复核结论`file:line`)写入 SUMMARY并发改动基线porcelain 条目 + `sha256`)与类型门禁基线(两条 `client-modules` 诊断原样、三文件 0 条)写入 SUMMARY任一与规划不一致即停止并修订计划。
2. scoped 类型门禁Task 1 / 2 / 3`pnpm exec vue-tsc --noEmit` 过滤到 `render/map/extension/{hero,types,manager}.ts` 必须 0 条诊断;`client-modules` 错误集必须与记录基线完全一致(规划时 `client.ts(66,9): error TS2554`、`render/ui/load.tsx(77,31): error TS2504`);整仓其余既有诊断不属本计划,不得以「整仓非 0 退出」判定失败,也不得漏判范围文件的新诊断。
3. 静态改名门禁Task 1 / 2 / 3三个文件不出现 `IHeroMoveController` / `IHeroMoveControllerHooks` / `HeroAnimateDirection``onSetPosition` / `onStartMove` / `onMoveHero` / `onEndMove` / `onJumpHero` / `onTurnHero`;必须出现 `IHeroLocation` / `ObjectAnimDirection` / `getCurrentFaceDirection` / `hero.addHook` / `hero.mover.addHook` / `locationController` / `moverController`
4. D-20 契约门禁Task 2 / 3`IMapHeroRenderer` 不再声明 `setPosition` / `startMove` / `waitMoveEnd` / `stopMove` / `move` / `jumpTo` / `setHeroAnimateDirection` / `turn`,且无任何新增接口成员。
5. D-18 / D-22 保留门禁Task 2 / 3`IMapHeroRenderer` 的 `setImage` / `addFollower` / `removeFollower` / `removeAllFollowers` / `setAlpha` / `setFollowerAlpha` / `destroy``hero.ts``onSetImage` / `onSetAlpha` / `onSetFollowerAlpha` / 跟随者三方法逐字保留。
6. 并发改动基线门禁Task 3排除三个在范围文件与 `.planning/phases/04-render-adaptation/`porcelain 条目集合与内容 `sha256` 与 SUMMARY 记录一致(用户改动未被回滚 / 暂存 / 提交 / 修改)。
7. 范围门禁Task 3`git status --porcelain` 中除三个在范围文件外的生产代码无本计划引入的改动(`action/move.ts`、数据端各包、`client-base`、`packages` 均未被触碰)。
8. ESLint / CRLF 门禁Task 1 / 2 / 3`pnpm exec eslint` 对三个文件 0 错误;三个源文件与 SUMMARY 无孤立 LF`.prettierrc` 的 `endOfLine: crlf`)。
9. 人工复核Task 3逐钩子映射与用户确认的映射表一致`onSetPos` 的「移动中不吸附」守卫正确;`onStepEnd` 的 `ObjectMoveType` 分派完整且 `AnimDir` / `Speed` 无渲染动作D-18 桥接只收敛一处 `hero.image` 读取且 warn 分支未变;无新增公共接口成员;运行时 UAT 不可得的理由已写明。
10. 提交Task 2 的 `refactor(04-03)` 仅含三个源文件Task 3 的 `docs(04-03)` 仅含 SUMMARY用户并发改动未被纳入任何提交。
</verification>
<success_criteria>
- **执行前置关卡AGENTS.md** 本计划第一个 block 点是 Task 0 —— 向用户汇报计划内容、逐钩子映射表、成员命名清单与 D-18 编译桥接取舍后停下等待用户明确回复「可以执行」四个字;未收到前不执行任何任务、不派发执行子代理,任何触发方式与 `yolo` / `auto_advance` 模式均不得自动放行。
- 渲染端勇士本体已绑定 `IHeroLocation``manager.ts` / `types.ts` 的键与参数类型、`hero.ts` 的构造器参数与朝向读取口径(`getCurrentFaceDirection()`)、动画方向(`ObjectAnimDirection` + `mover.currAnimDir`)全部对齐数据端现行接口;三个文件不再出现 `IHeroMoveController` / `IHeroMoveControllerHooks` / `HeroAnimateDirection`
- 渲染端已改为**订阅数据端现有钩子**D-21`hero.addHook``IHeroLocationHooks`)与 `hero.mover.addHook``IObjectMoverHooks<IHeroLocation>`)两处注册、两个控制器字段、`load()` / `unload()` 配套;`MapHeroHook` 实现现有钩子成员名6 个旧钩子方法名全部消失;**未新增任何数据端公共接口,也未修改任何数据端文件**。
- 勇士渲染为**完全被动渲染**D-20`IMapHeroRenderer` 不再暴露 8 个外部驱动成员,渲染状态只由钩子回调推进;无任何新增公共接口成员。
- **D-18 / D-22 成员逐字保留**:贴图 / 不透明度成员与跟随者族成员均未被删除或改写语义;贴图别名的读取只做最小编译桥接(一处收敛 + 既有 `// @ts-expect-error 需要重构` 标注,分支与 warn 未变)。
- **范围外零改动**`action/move.ts` 与移动控制 APID-19、`core.*`D-12、`getHeroStatusOn` 类裸函数D-11、任何数据端文件、`client.ts` 接线均未被触碰;用户并发的未提交改动未被回滚 / 暂存 / 提交 / 修改。
- **验证诚实且可运行**scoped 类型门禁(三文件 0 条、`client-modules` 错误集 == 基线、静态改名门禁、D-20 契约门禁、D-18 / D-22 保留门禁、eslint / CRLF 门禁、范围门禁、并发改动基线门禁全部转绿且有实际输出;运行时渲染 UAT 不可得的原因(`addHero` 未被接线)在摘要中明写,未以任何不可运行的门禁冒充通过。
- REND-01 / REND-02 保持 Pending两条 FLAGGED ASSUMPTION用户可据此继续裁决后续增量贴图 D-18、跟随者 D-22、接线启用、双布局 REND-02、其余 ① / ③ 项)。
## Artifacts this phase produces
| 类型 | 路径 | 内容 |
|------|------|------|
| 生产源码(修改) | `packages-user/client-modules/src/render/map/extension/hero.ts` | 绑定 `IHeroLocation`;两次 `addHook``IHeroLocationHooks` / `IObjectMoverHooks<IHeroLocation>`+ 两个控制器字段;`MapHeroHook` 改实现现有钩子成员名(含 `onStepEnd``ObjectMoveType` 分派与 `mover.currAnimDir` 动画方向);旧类型名与旧钩子名清除;`hero.image` 收敛为一处 D-18 编译桥接D-18 / D-22 成员逐字保留 |
| 生产源码(修改) | `packages-user/client-modules/src/render/map/extension/types.ts` | `IMapExtensionManager` 勇士键 / 参数 → `IHeroLocation``IMapHeroRenderer` 按 D-20 删除 8 个外部驱动成员,保留 D-18 / D-22 成员 |
| 生产源码(修改) | `packages-user/client-modules/src/render/map/extension/manager.ts` | `heroMap` / `addHero` / `removeHero``IHeroLocation` 为键与参数;移除 `@ts-expect-error 需要重构` 标记 |
| 摘要 | `.planning/phases/04-render-adaptation/04-03-SUMMARY.md` | 执行前核对七条(带 `file:line`、执行前基线porcelain + `sha256`)、类型门禁基线(两条原样)、`#04-02-R-01..12` → 本计划处置对照、五类机器门禁输出摘要、人工复核七条、未实现项清单、REND-01 / REND-02 Pending 声明与运行时 UAT 不可得说明 |
<output>
Create `.planning/phases/04-render-adaptation/04-03-SUMMARY.md` when done
</output>