From 3d2d6dd0b19409d61ad798e7c039e0e1a73f0f4b Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Fri, 18 Sep 2026 14:46:41 +0800 Subject: [PATCH] fix(04-03): revise plan per checker feedback (token-scoped rename gates, remove unused FaceDirection import, baseline-aware scope gate) --- .../phases/04-render-adaptation/04-03-PLAN.md | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/.planning/phases/04-render-adaptation/04-03-PLAN.md b/.planning/phases/04-render-adaptation/04-03-PLAN.md index 8ae465d..5a71c07 100644 --- a/.planning/phases/04-render-adaptation/04-03-PLAN.md +++ b/.planning/phases/04-render-adaptation/04-03-PLAN.md @@ -23,7 +23,7 @@ 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)" + - "三个在范围文件内不再出现**独立 token**(词边界判定)`IHeroMoveController`、`IHeroMoveControllerHooks`、`HeroAnimateDirection`,也不再出现旧钩子方法名 `onSetPosition` / `onStartMove` / `onMoveHero` / `onEndMove` / `onJumpHero` / `onTurnHero`;类上保留的内部实现方法名 `setHeroAnimateDirection` 因不构成独立 token 不在判定范围(静态门禁机器判定,见 Task 3)" - "钩子注册改为订阅**数据端现有钩子**(D-21):`hero.addHook(...)` 注册 `IHeroLocationHooks` 的订阅(`data-base/src/hero/types.ts:277`),`hero.mover.addHook(...)` 注册 `IObjectMoverHooks` 的订阅(`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 暂缓)" @@ -81,7 +81,7 @@ Phase 4 第三个增量(D-17 的适配半程)——**勇士本体适配: **大致解决方案:** - **绑定对象换成位置对象**:`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`)上各注册一次 `MapHeroHook`,用 `load()` / `unload()` 管理(原 `controller` 字段拆为两个控制器字段)。渲染端实现现有钩子成员名,旧名全部删除;`onStepEnd` 成为逐格渲染的驱动点(此时 `tile` 坐标已更新),按 `ObjectMoveType` 分派到既有渲染内部方法。 -- **被动化**(D-20):`IMapHeroRenderer` 去掉「外部驱动渲染状态」的成员(`startMove` / `waitMoveEnd` / `stopMove` / `move` / `jumpTo` / `setPosition` / `turn` / `setHeroAnimateDirection`;其中 `setHeroAnimateDirection` 因形参类型 `HeroAnimateDirection` 在数据端已不存在,在 Task 1 随类型改名一并删除,其余 7 个在 Task 2 删除);这些方法退为渲染端内部实现,仅由钩子回调调用。 +- **被动化**(D-20):`IMapHeroRenderer` 去掉「外部驱动渲染状态」的成员(`startMove` / `waitMoveEnd` / `stopMove` / `move` / `jumpTo` / `setPosition` / `turn` / `setHeroAnimateDirection`;其中 `setHeroAnimateDirection` 的**接口声明**因形参类型 `HeroAnimateDirection` 在数据端已不存在,在 Task 1 随类型改名一并删除(`MapHeroRenderer` 上的类内实现方法保留),其余 7 个的接口声明在 Task 2 删除);这些方法退为渲染端内部实现,仅由钩子回调调用。 - **范围硬边界**:贴图 / 不透明度(D-18)、跟随者(D-22)、`action/move.ts` 与移动控制(D-19)、`core.*`(D-12)、裸函数(D-11)一律不动。 **执行前置关卡(AGENTS.md,第一个 block 点):** 本计划第一个执行步骤是 Task 0 —— 先向用户汇报「要解决的问题 / 大致解决方案(含逐钩子映射表)/ 拟新增或改写的成员命名清单 / 范围边界与禁令 / D-18 编译桥接的取舍」,然后**停下等待**用户明确回复「可以执行」四个字。收到该回复前,任何任务都不得执行,也不得派发执行子代理;无论触发方式或配置模式如何均不得自动放行。故本计划 `autonomous: false`。 @@ -106,7 +106,7 @@ Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复 | 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 | `IMapHeroRenderer` 删除 8 个外部驱动成员(`setHeroAnimateDirection` 随 Task 1 的类型改名一并删除,其余 7 个由 Task 2 删除),改为钩子驱动 | +| CONTEXT | D-20 | 勇士渲染完全被动、旧驱动接口删除 | 04-03 | COVERED | `IMapHeroRenderer` 删除 8 个外部驱动成员的**接口声明**(`setHeroAnimateDirection` 随 Task 1 的类型改名一并删除,其余 7 个由 Task 2 删除;对应类内实现方法保留),改为钩子驱动 | | 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 增量 | @@ -160,7 +160,7 @@ Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复 ② **大致解决方案(三个文件)**:`manager.ts` 与 `types.ts` 的勇士键 / 参数类型由 `IHeroMoveController` 改为 `IHeroLocation`;`hero.ts` 构造器绑定 `IHeroLocation`(坐标 `x`/`y`、朝向 `getCurrentFaceDirection()`、动画方向 `ObjectAnimDirection` + `mover.currAnimDir`),在 `hero`(`IHeroLocationHooks`)与 `hero.mover`(`IObjectMoverHooks`)上各 `addHook` 一次,`MapHeroHook` 改为实现现有钩子成员名;`IMapHeroRenderer` 按 D-20 去掉 8 个外部驱动成员(`startMove` / `waitMoveEnd` / `stopMove` / `move` / `jumpTo` / `setPosition` / `turn` / `setHeroAnimateDirection`;其中 `setHeroAnimateDirection` 因形参类型 `HeroAnimateDirection` 已不存在,在 Task 1 连同其 jsDoc 一并删除,其余 7 个在 Task 2 删除)。**逐钩子映射表须逐行汇报**:数据端钩子 → 触发点(`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`)与 `moverController`(`IHookController>`)——取代原单一 `controller` 字段;**不新增任何类**(沿用既有 `MapHeroHook` 类名);**删除**旧钩子方法名 `onSetPosition` / `onStartMove` / `onMoveHero` / `onEndMove` / `onJumpHero` / `onTurnHero`(**Task 1 即删除,不留存为类上的多余方法**)与 `IMapHeroRenderer` 的 8 个驱动成员(连同其配套 jsDoc,属删除代码而非改写注释;其中 `setHeroAnimateDirection` 在 Task 1 删除,其余 7 个在 Task 2 删除);**新增**的钩子方法名 `onSetPos` / `onMoveStart` / `onMoveEnd` / `onStepEnd` / `onSetFaceDir` 逐字取自数据端现有钩子接口成员名(D-21,非自创命名),`onStepEnd` 的未使用首参按项目惯例命名为 `_code`(eslint `args: 'all'` + `argsIgnorePattern: '^_'`),一并请用户确认;**逐字保留** `setImage` / `setAlpha` / `setFollowerAlpha` / `onSetImage` / `onSetAlpha` / `onSetFollowerAlpha` 与跟随者族(D-18 / D-22)。备选命名(如用户不同意 `locationController` / `moverController`)请用户当场指定。 + ③ **成员命名清单(须用户同意,AGENTS.md 命名规则)**:**新增**渲染端字段 `locationController`(`IHookController`)与 `moverController`(`IHookController>`)——取代原单一 `controller` 字段;**不新增任何类**(沿用既有 `MapHeroHook` 类名);**删除**旧钩子方法名 `onSetPosition` / `onStartMove` / `onMoveHero` / `onEndMove` / `onJumpHero` / `onTurnHero`(**Task 1 即删除,不留存为类上的多余方法**)与 `IMapHeroRenderer` 的 8 个驱动成员的**接口声明**(连同其配套 jsDoc,属删除代码而非改写注释;其中 `setHeroAnimateDirection` 的接口声明在 Task 1 删除,其余 7 个的接口声明在 Task 2 删除;`MapHeroRenderer` 上对应的 8 个实现方法保留为类内内部实现,仍由钩子回调调用);**新增**的钩子方法名 `onSetPos` / `onMoveStart` / `onMoveEnd` / `onStepEnd` / `onSetFaceDir` 逐字取自数据端现有钩子接口成员名(D-21,非自创命名),`onStepEnd` 的未使用首参按项目惯例命名为 `_code`(eslint `args: 'all'` + `argsIgnorePattern: '^_'`),一并请用户确认;**逐字保留** `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,等用户先改数据端)。请用户确认该桥接,或指定其它处理方式。 @@ -228,10 +228,10 @@ Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复 (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`;`addHero(state: IHeroLocation, layer: IMapLayer)`;`removeHero(state: IHeroLocation)`。`addDoor` / `removeDoor` / `addText` / `removeText` / `destroy` 一字不改。 - (2) **`types.ts`(契约层:绑定类型 + 删除 `setHeroAnimateDirection` 声明)**:从 `@user/data-common` 的 import 中删除 `HeroAnimateDirection` 与 `IHeroMoveController`(连带其两行 `// @ts-expect-error 需要重构` 标记;删除后该 import 仅剩 `FaceDirection`,**不需要任何替代类型**),改为从 `@user/data-base` 导入 `IHeroLocation`(与既有 `IMapLayer` 合并);`IMapExtensionManager.heroMap: Map`、`addHero(state: IHeroLocation, layer: IMapLayer)`、`removeHero(state: IHeroLocation)`;**删除 `IMapHeroRenderer.setHeroAnimateDirection` 的成员声明及其配套 jsDoc**(原 `:143-147`;其形参类型 `HeroAnimateDirection` 在数据端已不存在,无法保留——若只删 import 而不删该成员,`types.ts` 会残留悬空引用并触发 TS2304):该成员本任务即删除,其实现方法 `MapHeroRenderer.setHeroAnimateDirection` 保留在类上(见 (3f));`IMapHeroRenderer` 的**其余 7 个驱动成员**(`setPosition` / `startMove` / `waitMoveEnd` / `stopMove` / `move` / `jumpTo` / `turn`)连同其 jsDoc 在 Task 2 删除,本任务不动。`Map<` / `addHero` / `removeHero` 的 jsDoc **不改写**(参数名与含义不变)。 + (2) **`types.ts`(契约层:绑定类型 + 删除 `setHeroAnimateDirection` 声明)**:从 `@user/data-common` 的 import 中删除 `HeroAnimateDirection` 与 `IHeroMoveController`(连带其两行 `// @ts-expect-error 需要重构` 标记;删除后该 import 仅剩 `FaceDirection`,**不需要任何替代类型**;该 `FaceDirection` 在本任务仍被 `IMapHeroRenderer.move` / `.turn` 使用,Task 2 删除这两个成员后会使其失去使用点,届时在 Task 2 步骤 (2) 一并删除该 import),改为从 `@user/data-base` 导入 `IHeroLocation`(与既有 `IMapLayer` 合并);`IMapExtensionManager.heroMap: Map`、`addHero(state: IHeroLocation, layer: IMapLayer)`、`removeHero(state: IHeroLocation)`;**删除 `IMapHeroRenderer.setHeroAnimateDirection` 的成员声明及其配套 jsDoc**(原 `:143-147`;其形参类型 `HeroAnimateDirection` 在数据端已不存在,无法保留——若只删 import 而不删该成员,`types.ts` 会残留悬空引用并触发 TS2304):该成员本任务即删除,其实现方法 `MapHeroRenderer.setHeroAnimateDirection` 保留在类上(见 (3f));`IMapHeroRenderer` 的**其余 7 个驱动成员**(`setPosition` / `startMove` / `waitMoveEnd` / `stopMove` / `move` / `jumpTo` / `turn`)连同其 jsDoc 在 Task 2 删除,本任务不动。`Map<` / `addHero` / `removeHero` 的 jsDoc **不改写**(参数名与含义不变)。 (3) **`hero.ts` import 与绑定(tracer 主路径)**: (3a) import 改造(**只在本任务内引入本任务用得到的名字**:项目 eslint 把 `@typescript-eslint/no-unused-vars` 设为 `error` 且 `args: 'all'`,未使用的 import 会直接报错,`--fix` 也不会删除未使用 import;反之 Task 2 也不得保留只在 Task 1 用过的名字):`@user/data-common` 侧删除 `HeroAnimateDirection` / `IHeroMoveController` / `IHeroMoveControllerHooks`(连带 3 行 `// @ts-expect-error 需要重构` 标记),新增 `ObjectAnimDirection`(本任务即消费:`:53` / `:97` / `:170` / `:372` 四处)与 `IObjectMoverHooks`(本任务即消费:`moverController` 字段类型);**不要**在本任务引入 `IObjectMover` / `ObjectMoveStep` / `ObjectMoveType`(本任务无使用点,留到 Task 2 首次使用时再引入);**不要**引入 `ObjectSpecialStep`(两个任务都不需要:`Special` 步的移动方向已由数据端 `ObjectMover.prepareStep` 写入 `mover.moveDirection`,渲染端无需读取 `step.direction`)。`@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`);两处实体初始化的默认值 `HeroAnimateDirection.Forward` → `ObjectAnimDirection.Forward`(`:97` 构造器、`:372` `addFollower`);`tick` 内的播放方向比较 `v.animateDirection === HeroAnimateDirection.Forward` → `ObjectAnimDirection.Forward`(`:170`)。**四处取值点必须全部改到**,否则改名门禁(三文件不得出现 `HeroAnimateDirection`)与 scoped 类型门禁(该名字已从 import 移除,任何残留即 TS2304)都会失败。 + (3b) 实体类型与**全部取值点**:`HeroRenderEntity.animateDirection` 由 `HeroAnimateDirection` 改为 `ObjectAnimDirection`(`:53`);两处实体初始化的默认值 `HeroAnimateDirection.Forward` → `ObjectAnimDirection.Forward`(`:97` 构造器、`:372` `addFollower`);`tick` 内的播放方向比较 `v.animateDirection === HeroAnimateDirection.Forward` → `ObjectAnimDirection.Forward`(`:170`)。**四处取值点必须全部改到**,否则改名门禁(三文件不得出现**独立 token** `HeroAnimateDirection`;保留的类内实现方法名 `setHeroAnimateDirection` 不属于被禁 token,见 (3f))与 scoped 类型门禁(该名字已从 import 移除,任何残留即 TS2304)都会失败。 (3c) 构造器:`readonly hero: IHeroMoveController` → `readonly hero: IHeroLocation`(`:79`);`direction: hero.direction` → `direction: hero.getCurrentFaceDirection()`(`:89`);钩子注册由单一 `controller` 改为**两个控制器字段**:`readonly locationController: IHookController` 与 `readonly moverController: IHookController>`(替换原 `readonly controller: IHookController` 字段,`: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:贴图语义不动)。 @@ -244,8 +244,8 @@ Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复 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)" non-zero exit, 或 stderr 含 "IN-SCOPE TYPE ERRORS"(三个在范围文件仍有类型错误)/ "CLIENT-MODULES ERROR SET CHANGED"(渲染端类型错误集相对记录基线发生增减,含连带打断它处) - 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')" - non-zero exit, 或 stderr 含 "LEGACY SYMBOLS REMAIN"(旧类型名 / 旧钩子方法名仍出现在三个在范围文件,含注释)/ "MISSING NEW BINDINGS"(绑定、动画方向或两个钩子控制器未落地) + 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 hasTok=(s,t)=>new RegExp('(?hasTok(src,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')" + non-zero exit, 或 stderr 含 "LEGACY SYMBOLS REMAIN"(旧类型名 / 旧钩子方法名仍以**独立 token**(词边界 lookbehind/lookahead)出现在三个在范围文件,含注释;保留的类内实现方法名 `setHeroAnimateDirection` 中的 `HeroAnimateDirection` 前接标识符字符 `t`,不构成独立 token,不触发本门禁)/ "MISSING NEW BINDINGS"(绑定、动画方向或两个钩子控制器未落地) 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 non-zero exit 或输出含 "error"(ESLint / Prettier 违规;CRLF 由 `endOfLine: crlf` 在 `--fix` 时统一) 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')" @@ -257,12 +257,12 @@ Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复 - `hero.ts` 的构造器参数为 `IHeroLocation`,构造期朝向取自 `hero.getCurrentFaceDirection()`;`HeroRenderEntity.animateDirection` 的声明(`:53`)、两处初始化默认值(`:97` / `:372`)与 `tick` 内的比较(`:170`)均为 `ObjectAnimDirection` - `hero.ts` 同时存在 `hero.addHook(` 与 `hero.mover.addHook(`,并存在 `locationController` / `moverController` 两个字段;`destroy()` 对两者都 `unload()` - `MapHeroHook` 声明为 `Partial>`,且已实现 `onSetPos`;类上**不再存在任何旧命名方法**(`onSetPosition` / `onStartMove` / `onMoveHero` / `onEndMove` / `onJumpHero` / `onTurnHero` 本任务即全部删除,不留存为多余方法;Task 2 再按映射表补上现有钩子成员名) - - 三个文件不再出现 `IHeroMoveController` / `IHeroMoveControllerHooks` / `HeroAnimateDirection` / `onSetPosition` / `onStartMove` / `onMoveHero` / `onEndMove` / `onJumpHero` / `onTurnHero` + - 三个文件不再出现**独立 token**(词边界判定)`IHeroMoveController` / `IHeroMoveControllerHooks` / `HeroAnimateDirection` / `onSetPosition` / `onStartMove` / `onMoveHero` / `onEndMove` / `onJumpHero` / `onTurnHero`;类上保留的内部实现方法名 `setHeroAnimateDirection`(形参类型已改为 `ObjectAnimDirection`)不是被禁 token,不在判定范围内 - `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 - 四条 `` 全部转绿:scoped 类型门禁(三个文件 0 条、client-modules 错误集 == 基线)、静态改名门禁、eslint 0 错误、CRLF 门禁 - 绑定层(`IHeroLocation`)、构造器两个 `addHook` 注册、`onSetPos` 一条通路与三文件的类型 / import / 成员名改造全部落地,四条机器门禁转绿,且三个在范围文件不再出现任何旧类型名与旧钩子方法名 + 绑定层(`IHeroLocation`)、构造器两个 `addHook` 注册、`onSetPos` 一条通路与三文件的类型 / import / 成员名改造全部落地,四条机器门禁转绿,且三个在范围文件不再出现任何旧类型名 token 与旧钩子方法名(保留的类内实现方法名 `setHeroAnimateDirection` 除外) @@ -288,7 +288,7 @@ Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复 - `onSetPos`(mover 级四参调用)与 location 级两参调用共用同一个两参方法:JS 会忽略多余实参,本方法的「移动中 return」守卫同时覆盖两条触发路径;该方法保持 `void` 返回(`IHeroLocationHooks.onSetPos` 与 `IObjectMoverHooks.onSetPos` 在数据端都是 `void`)。 - **不实现**(在 `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`)**:删除以下 **7 个**成员声明**及其配套 jsDoc**(属删除代码,Task 0 已获用户确认):`setPosition(x, y)`(`:92`)、`startMove()`(`:97`)、`waitMoveEnd(waitFollower)`(`:103`)、`stopMove()`(`:108`)、`move(direction, time)`(`:114`)、`jumpTo(x, y, time, waitFollower)`(`:123`)、`turn(direction?)`(`:153`)。D-20 的第 8 个成员 `setHeroAnimateDirection` 已在 Task 1 随类型改名删除(其声明连同 jsDoc 已不在 `types.ts`),**本任务不得为它重新引入任何声明**,故本任务结束时接口内的 8 个外部驱动成员全部不存在。保留 `setImage` / `addFollower` / `removeFollower` / `removeAllFollowers` / `setAlpha` / `setFollowerAlpha` / `destroy`(D-18 / D-22 成员,逐字不动)。不得新增任何成员。 + (2) **`IMapHeroRenderer` 按 D-20 裁剪收口(`types.ts`)**:删除以下 **7 个**成员声明**及其配套 jsDoc**(属删除代码,Task 0 已获用户确认):`setPosition(x, y)`(`:92`)、`startMove()`(`:97`)、`waitMoveEnd(waitFollower)`(`:103`)、`stopMove()`(`:108`)、`move(direction, time)`(`:114`)、`jumpTo(x, y, time, waitFollower)`(`:123`)、`turn(direction?)`(`:153`)。D-20 的第 8 个成员 `setHeroAnimateDirection` 已在 Task 1 随类型改名删除(其声明连同 jsDoc 已不在 `types.ts`),**本任务不得为它重新引入任何声明**,故本任务结束时接口内的 8 个外部驱动成员全部不存在。保留 `setImage` / `addFollower` / `removeFollower` / `removeAllFollowers` / `setAlpha` / `setFollowerAlpha` / `destroy`(D-18 / D-22 成员,逐字不动)。不得新增任何成员。**同步收口未使用 import**:`move`(`:114`)与 `turn`(`:153`)是 `types.ts` 内 `FaceDirection` 的全部使用点,删除后 Task 1 保留的 `import { FaceDirection } from '@user/data-common';` 会成为未使用 import(项目 eslint `@typescript-eslint/no-unused-vars` 为 error 且无 fixer,会让本任务与 Task 3 的 eslint 门禁失败)。故本步骤必须先用 `rg -n "FaceDirection" packages-user/client-modules/src/render/map/extension/types.ts` 复核确认删除 `move` / `turn` 后 `types.ts` 内已无 `FaceDirection` 引用,随后**删除该 import 语句**(删除后 `types.ts` 不再从 `@user/data-common` import 任何名字);若复核发现仍有其它使用点则保留该 import 并在 `04-03-SUMMARY.md` 说明。 (3) **`MapHeroRenderer` 侧配套**:上述 8 个驱动方法(含 Task 1 已从 `IMapHeroRenderer` 删除声明的 `setHeroAnimateDirection`)在类上**保留为内部实现**(不删除、不改名),仅由 `MapHeroHook` 的回调调用;其可见性/实现细节保持不变。`waitMoveEnd()` 的实现签名(无参)与接口原声明的参数落差(04-02「未能从阅读确定」第 6 条)随接口成员移除而消解,在 `04-03-SUMMARY.md` 记为「由 D-20 契约裁剪消解」。`MapHeroRenderer.setHeroAnimateDirection(direction: ObjectAnimDirection)` 已在 Task 1 把形参类型改为 `ObjectAnimDirection`、并保留为类上的内部实现方法(`IMapHeroRenderer` 侧的声明已删除),本任务不再动它。 (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 错误。 @@ -308,6 +308,7 @@ Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复 - 三个返回 `Promise` 的钩子(`onMoveStart` / `onMoveEnd` / `onStepEnd`)均以 `async` 实现,签名与数据端 `IObjectMoverHooks` 可赋值(无 TS2416「void is not assignable to Promise」);`onSetPos` / `onSetFaceDir` 保持 `void`;`onStepEnd` 的未使用首参命名为 `_code`(满足 eslint `args: 'all'` + `argsIgnorePattern: '^_'`),且 `hero.ts` 未引入任何本任务用不到的 import(尤其 `ObjectSpecialStep`) - `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` 逐字保留 + - `types.ts` 删除 `move` / `turn` 后无未使用 import:`import { FaceDirection } from '@user/data-common';` 已随之删除(已 grep 复核 `types.ts` 内无其余 `FaceDirection` 引用),`pnpm exec eslint` 对三个文件 0 错误 - `hero.ts` 仍含全部 D-18 / D-22 钩子方法与实现方法(含 `onSetImage` / `onSetAlpha` / `onSetFollowerAlpha` 与跟随者三方法),且其注释与实现体未被改写 - 四条 `` 全绿;未新增任何接口成员;三个文件已一个原子提交入库,且提交未包含用户并发改动 @@ -325,21 +326,21 @@ Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复 - .planning/STATE.md:217-236(Phase 04 既有决策记录格式) - (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/**` 均无本计划引入的新改动。若出现第四个生产文件的改动:停止、汇报、修订计划(不得自行扩面)。 + (1) **范围门禁(基线感知,先跑,防止越界改动)**:执行 `git status --porcelain`,把其中的**生产路径**(`^packages(-user)?/`)与 `04-03-SUMMARY.md`「执行前基线」段记录的 porcelain 条目做**差集**:只有**基线之外**、且不属于三个在范围文件(`render/map/extension/{hero.ts,types.ts,manager.ts}`)的生产改动才算越界。Task 1 记录的并发用户改动(`client-base/src/load/*`、`data-base/src/load/*`、`data-state/src/{core,types}.ts`、`packages/loader/src/*` 等 8 个生产文件)必须原样放行——它们同时受步骤 (2) 的基线摘要门禁约束。特别断言 `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 门禁。 + (3) **复跑全部机器门禁**:scoped 类型门禁(三个文件 0 条 + `client-modules` 错误集 == 两条记录基线)、静态改名门禁(`IHeroMoveController` / `IHeroMoveControllerHooks` / `HeroAnimateDirection` 与 6 个旧钩子方法名以**独立 token**(词边界)不复现,保留的类内实现方法名 `setHeroAnimateDirection` 不触发;`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`)。 - 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')" - non-zero exit, 或 stderr 含 "OUT-OF-SCOPE FILE MODIFIED"(出现 `files_modified` 之外的生产文件改动,含 D-19 的 `action/move.ts` 或任何数据端文件) + node -e "const fs=require('fs'),{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 t=fs.readFileSync('.planning/phases/04-render-adaptation/04-03-SUMMARY.md','utf8');const base=(t.match(/^[ MARCUD?!]{2} [^\r\n]*/gm)||[]).map(s=>s.replace(/\s+$/,'')).map(s=>s.slice(3).replace(/^\"|\"$/g,''));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&&base.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 (baseline-aware): out-of-scope extra 0; in-scope modified '+missing.length+'/3 (committed ones absent)')" + non-zero exit, 或 stderr 含 "OUT-OF-SCOPE FILE MODIFIED"(出现 `files_modified` 之外、且**不在 Task 1 记录的并发改动基线内**的生产文件改动,含 D-19 的 `action/move.ts`、任何数据端文件或 `packages/**` 的新改动;基线内的用户并发改动不触发本门禁)/ "GIT STATUS FAILED" 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)" non-zero exit, 或 stderr 含 "NO BASELINE DIGEST" / "BASELINE ENTRY SET CHANGED" / "BASELINE DIGEST CHANGED"(用户并发的未提交改动被本计划回滚 / 暂存 / 提交 / 修改 —— 违反并发改动禁令) - 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;','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;','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')" - 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" + 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 hasTok=(s,t)=>new RegExp('(?hasTok(src,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;','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;','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')" + non-zero exit, 或 stderr 含 "LEGACY SYMBOLS REMAIN"(旧类型名 / 旧钩子方法名仍以**独立 token**(词边界判定)出现;保留的类内实现方法名 `setHeroAnimateDirection` 不触发)/ "MISSING NEW BINDINGS" / "D20 DRIVE MEMBERS STILL EXPOSED" / "D18 D22 CONTRACT MEMBERS MISSING" / "D18 D22 OR HOOK MEMBERS MISSING" 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)" non-zero exit, 或 stderr 含 "IN-SCOPE TYPE ERRORS" / "CLIENT-MODULES ERROR SET CHANGED"(终验类型门禁未过) 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 @@ -350,7 +351,7 @@ Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复 non-zero exit, 或 stderr 含 "SUMMARY MISSING SECTIONS"(SUMMARY 缺少执行前核对 / 基线 / 人工复核 / 需求 Pending 声明 / 运行时不可得说明) - - 范围门禁通过:无 `files_modified` 之外的生产文件改动(`action/move.ts`、任何数据端包、`client-base`、`packages` 均无本计划引入的改动) + - 范围门禁(基线感知)通过:`git status --porcelain` 的生产路径与 Task 1 记录的基线条目差集为空——即除三个在范围文件与基线内的并发用户改动外,无其它生产文件改动(`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 不可得的明确说明 @@ -377,7 +378,7 @@ Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复 | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| -| T-04-03-01 | Tampering | `IMapHeroRenderer` 公共契约(`types.ts`) | high | mitigate | Task 1 删除 `setHeroAnimateDirection`(随 `HeroAnimateDirection` 类型改名,无悬空引用)、Task 2 删除其余 7 个外部驱动成员(合计 8 个,D-20),并由 Task 2 / Task 3 的 D-20 契约门禁断言其不再出现;prohibitions 禁止新增任何接口成员;D-18 / D-22 成员设独立保留门禁,防误删 | +| T-04-03-01 | Tampering | `IMapHeroRenderer` 公共契约(`types.ts`) | high | mitigate | Task 1 删除 `setHeroAnimateDirection` 的接口声明(随 `HeroAnimateDirection` 类型改名,无悬空引用;类内实现方法保留)、Task 2 删除其余 7 个外部驱动成员的接口声明(合计 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 范围门禁额外断言无范围外生产文件改动 | @@ -392,11 +393,11 @@ Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复 0. 执行前置关卡(Task 0,AGENTS.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`。 +3. 静态改名门禁(Task 1 / 2 / 3):三个文件不出现**独立 token**(词边界判定)`IHeroMoveController` / `IHeroMoveControllerHooks` / `HeroAnimateDirection` 与 `onSetPosition` / `onStartMove` / `onMoveHero` / `onEndMove` / `onJumpHero` / `onTurnHero`(保留的类内实现方法名 `setHeroAnimateDirection` 不构成独立 token,不触发);必须出现 `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` 均未被触碰)。 +7. 范围门禁(Task 3,基线感知):将当前 `git status --porcelain` 的生产路径(`^packages(-user)?/`)与 Task 1 记录的基线条目做差集;只有基线之外、且不属于三个在范围文件的生产改动才判越界。Task 1 记录的并发用户改动原样放行(同时受第 6 条基线摘要门禁约束);`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;用户并发改动未被纳入任何提交。 @@ -404,7 +405,7 @@ Output: 三个在范围文件的适配改动(原子提交)+ 会话内可复 - **执行前置关卡(AGENTS.md):** 本计划第一个 block 点是 Task 0 —— 向用户汇报计划内容、逐钩子映射表、成员命名清单与 D-18 编译桥接取舍后停下等待用户明确回复「可以执行」四个字;未收到前不执行任何任务、不派发执行子代理,任何触发方式与 `yolo` / `auto_advance` 模式均不得自动放行。 -- 渲染端勇士本体已绑定 `IHeroLocation`:`manager.ts` / `types.ts` 的键与参数类型、`hero.ts` 的构造器参数与朝向读取口径(`getCurrentFaceDirection()`)、动画方向(`ObjectAnimDirection` + `mover.currAnimDir`)全部对齐数据端现行接口;三个文件不再出现 `IHeroMoveController` / `IHeroMoveControllerHooks` / `HeroAnimateDirection`。 +- 渲染端勇士本体已绑定 `IHeroLocation`:`manager.ts` / `types.ts` 的键与参数类型、`hero.ts` 的构造器参数与朝向读取口径(`getCurrentFaceDirection()`)、动画方向(`ObjectAnimDirection` + `mover.currAnimDir`)全部对齐数据端现行接口;三个文件不再出现独立 token `IHeroMoveController` / `IHeroMoveControllerHooks` / `HeroAnimateDirection`(词边界判定;保留的类内实现方法名 `setHeroAnimateDirection` 不在判定范围)。 - 渲染端已改为**订阅数据端现有钩子**(D-21):`hero.addHook`(`IHeroLocationHooks`)与 `hero.mover.addHook`(`IObjectMoverHooks`)两处注册、两个控制器字段、`load()` / `unload()` 配套;`MapHeroHook` 实现现有钩子成员名,6 个旧钩子方法名全部消失;**未新增任何数据端公共接口,也未修改任何数据端文件**。 - 勇士渲染为**完全被动渲染**(D-20):`IMapHeroRenderer` 不再暴露 8 个外部驱动成员,渲染状态只由钩子回调推进;无任何新增公共接口成员。 - **D-18 / D-22 成员逐字保留**:贴图 / 不透明度成员与跟随者族成员均未被删除或改写语义;贴图别名的读取只做最小编译桥接(一处收敛 + 既有 `// @ts-expect-error 需要重构` 标注,分支与 warn 未变)。