docs(09-35): revise plan to class-based OverlayBlock

This commit is contained in:
AncTe committed 2026-11-01 21:34:10 +08:00
1 parent 14e57e90b3
commit 49302b4754
1 file changed
+173 -140
@@ -13,6 +13,7 @@ wave: 30
depends_on: []
files_modified:
- packages-user/client-base/src/map/types.ts
- packages-user/client-base/src/map/status.ts
- packages-user/client-base/src/map/moving.ts
- packages-user/client-base/src/map/vertex.ts
- packages-user/client-base/src/map/__test__/rendererGate.test.ts
@@ -22,82 +23,97 @@ autonomous: false
requirements: []
estimate:
# 未校准估算(confidence: low),SUMMARY 须声明这一点
# 本计划只做一次小结构重构:在 types.ts 增加覆盖块抽象,moving.ts 加一个只读成员,
# vertex.ts 把私有绘制条目换成覆盖块联合类型,并同步闸门测试的 5 条既有断言 + 新增 7 条断言。
# 不新增文件、不新增类、不改渲染行为;主要成本在读 4 个文件与运行四类门禁(含一次全仓 vue-tsc)
# 本计划只做一次小结构重构:types.ts 去掉种类枚举、加覆盖块接口与优先级;status.ts 加三个普通类
#(其中一个基类、一个静态块类、改写动态块状态的继承);moving.ts 删除种类与重复成员;
# vertex.ts 把私有联合类型换成接口数组 + instanceof 判别;并同步闸门测试的既有断言与新增类断言。
# 不新增源码文件、不改渲染行为;主要成本在读 5 个文件与运行五类门禁(含一次全仓 vue-tsc)
tokens: 30000
raw_tokens: 30000
tasks: 3
confidence: low
must_haves:
truths:
- "**本计划第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(D-03 / D-04)**:先汇报要做什么 / 为什么 / 大致怎么做 / 影响范围,逐条列明「精确的前后形态、逐文件逐符号改动、明确不改的部分、待批准命名清单、无行为变化主张与等价性论证、无浏览器 UAT / 无 WebGL E2E、不提交任何代码」以及「两处既有闸门漂移(4 条 `pendingAlpha` 断言、1 条 `a.layerIndex` 断言)的处置」和「计划编号 09-27 已被占用、本计划改用 09-35 的说明」,然后停下等待用户恰好回复「可以执行」。"
- "**覆盖块抽象只加在 `types.ts`**:新增导出 `OverlayBlockKind`(`Static` / `Dynamic`,顺序与现在的 `OverlayEntryKind` 一致)与导出接口 `IOverlayBlock`(`kind` / `layer` / `x` / `y`);`IMovingBlock` 改为 `extends IBlockStatus, IOverlayBlock` 并声明 `readonly kind: OverlayBlockKind.Dynamic`;`MovingBlock` 增加 `readonly kind: OverlayBlockKind.Dynamic = OverlayBlockKind.Dynamic;`。不新增任何其它导出类型、类或常量。"
- "**`vertex.ts` 的私有绘制条目被覆盖块联合类型取代**:删除私有 `IOverlayDrawEntry` 与私有 `OverlayEntryKind`;新增私有 `IOverlayStaticBlock extends IOverlayBlock`(`readonly kind: OverlayBlockKind.Static` + `readonly mapIndex: number`)与私有类型别名 `type OverlayBlock = IOverlayStaticBlock | IMovingBlock`;`overlayOrdered` 类型改为 `OverlayBlock[]`。"
- "**排序与组装的语义逐字保持**:静态条目 push 对象字面量 `{ kind: Static, layer, x, y, mapIndex }`;动态条目 push `renderer.getMovingBlock()` 的 `IMovingBlock` 本对象(不再包一层 `{ moving }`);排序键仍是 `(layerIndex, y, x, kind)`,其中 `layerIndex` 在比较器里用 `renderer.getLayerIndex(a.layer)` / `getLayerIndex(b.layer)` 现取(该方法是 `Map.get(...) ?? -1` 的 O(1) 读取,取值与旧的预计算 `layerIndex` 完全相同),`kind` 仍最后;`sortOverlay()` 内逐图层的 `const layerIndex` 局部随之删除、局部 `ordered` 的类型改为 `OverlayBlock[]`;`sortOverlay()` 结尾仍置 `overlayValueDirty = true`。`assembleOverlay()` 按 `kind` **显式两分支**收窄(`if (block.kind === OverlayBlockKind.Dynamic)` / `else if (block.kind === OverlayBlockKind.Static)`):动态用 `block.index` + `dynamicInstancedArray`,静态用 `block.mapIndex` + `block.layer`;16 float 拷贝、前四个 rect 浮点覆盖、`overlayCount`、清两个脏标记全部不变。"
- "**其余一切不变**:`flushPendingAlpha`、`setStaticAlpha` / `getStaticAlpha`、`alphaMap` / `overlayStatic`、`overlayOrderDirty` / `overlayValueDirty`、`getVertexArray()` 的 flush 落点、`getOverlayArray()` 的 flush → sort → assemble 顺序、`getOverlayCount()` 契约、`checkRebuild` / `setBlockSize` / `updateMoving` / `deleteMoving` 的脏标记、renderer 两遍式 / FBO / 深度附件 / ping-pong / postEffect / `blendFuncSeparate` / `map.frag` 的 `0.004`、分块张量布局、动态索引身份与 `status.ts` 全部零改动。"
- "**闸门测试同步到位且真实通过**:`rendererGate.test.ts` 里 4 条把源码符号写成 `pendingAlpha` 的既有断言(源码实际是 `alphaMap`)对齐为 `alphaMap`;1 条 `a.layerIndex - b.layerIndex` 断言改为新的比较器形态;新增 7 条覆盖块抽象的形状断言;每个 `it` 前一行注释;该文件全部用例(37 条)通过。"
- "**验证形态(无浏览器 UAT)**:`node node_modules/vitest/vitest.mjs run <闸门文件>`、`node node_modules/eslint/bin/eslint.js <4 个文件>`、`node -e` 包裹 `vue-tsc --noEmit` 且只对 `packages-user/client-base/src/map/` 负责(精确豁免用户裁定的 `getLayerByAlias` 一条并打印原文)、`git status --porcelain` 范围闸门、`git hash-object` 证明 `status.ts` / `renderer.ts` / `shader/map.frag` 未变;**不宣称任何 WebGL E2E**,SUMMARY 如实记录浏览器 UAT 因游戏尚不可运行而跳过。**代码一律不提交**:不得运行 `git add` / `commit` / `stash` / `checkout` / `restore` / `worktree` / `reset`。"
- "**范围与纪律**:只写 `files_modified` 的 6 个路径;不改 `status.ts` / `renderer.ts` / `shader/map.frag` / 其它 `map/` 文件 / 其它包 / 依赖与配置 / 09-01..09-34 的计划与摘要 / `09-CONTEXT.md`;不修改任何既有注释或 jsDoc(只新增);不复用任何未获批准的新命名;发现疑似问题只记录、不擅自修复。"
- "**本计划第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(D-03 / D-04 / AGENTS.md)**:先汇报要做什么 / 为什么 / 大致怎么做 / 影响范围,逐条列明「精确的类层次、优先级取值、`instanceof` 用法、逐文件逐符号改动、明确不改的部分、待批准命名清单(含备选)、无行为变化主张与等价性论证、无浏览器 UAT / 无 WebGL E2E、不提交任何代码」「两处既有闸门漂移(4 条 `pendingAlpha`、1 条 `a.layerIndex`)的处置」「类型闸门基线(用户自有的 `TimingFn` 诊断)」「09-27 已被占用、本计划改用 09-35 的说明」「既有 `sortOverlay` 注释仍写 `kind`、按 AGENTS.md 不得擅自修改」然后停下等待用户恰好回复「可以执行」。"
- "**覆盖块抽象只写在 `types.ts`,且不含种类枚举**:删除导出枚举 `OverlayBlockKind`(上一版设计)并清零所有引用;`IOverlayBlock` 的成员恰为 `readonly layer: IMapLayer` / `x: number` / `y: number` / `readonly priority: number`(不再有 `kind`);新增导出接口 `IOverlayStaticBlock extends IOverlayBlock`(追加 `readonly mapIndex: number`);`IMovingBlock` 改为 `extends IBlockStatus, IOverlayBlock` 且**不再声明** `kind`。不新增其它导出类型、类或常量。"
- "**`status.ts` 新增三个普通类(非接口、非抽象)**:① `export class OverlayBlock implements IOverlayBlock`,成员 `readonly layer: IMapLayer`、`x: number = 0`、`y: number = 0`、`readonly priority: number`,构造器 `(layer: IMapLayer, priority: number)` 赋值两个只读成员;② `DynamicBlockStatus` 改为 `extends OverlayBlock implements IBlockStatus`,构造器首句 `super(layer, 1)`(动态优先级 1),`vertex` / `index` 字段与全部方法逐字保留,`layer` 形参不再加 `readonly`(改由基类持有);③ 新增 `StaticOverlayBlock`(`extends OverlayBlock`、`implements IOverlayStaticBlock`;完整声明 85 列,prettier 必然折成三行且 `{` 独占一行,属期望形态),成员 `readonly mapIndex: number`,构造器 `(layer: IMapLayer, mapIndex: number, x: number, y: number)` 首句 `super(layer, 0)`(静态优先级 0)并赋值 `mapIndex` / `x` / `y`。`StaticBlockStatus` 及其 `setAlpha` 的参数顺序逐字不动。"
- "**`moving.ts` 去掉种类与重复成员**:删除 `OverlayBlockKind` 的引入与 `readonly kind` 字段;删除 `readonly layer: IMapLayer;` 声明与 `this.layer = layer;` 赋值(`layer` 由 `OverlayBlock` 持有且只读);删除重复的 `x` / `y` 字段声明(改由基类初始化);`MovingBlock` 仍是 `extends DynamicBlockStatus implements IMovingBlock`,`index` 身份与全部动画逻辑逐字保留;用户既有的 `TimingFn` → `ExcitationCurve` / `ExcitationCurve2D` 改写逐字保留。"
- "**`vertex.ts` 用接口数组 + `instanceof` 取代私有联合类型**:删除私有 `IOverlayStaticBlock` 与私有 `type OverlayBlock`;`overlayOrdered: IOverlayBlock[]`,局部 `const ordered: IOverlayBlock[]`;静态记录 `ordered.push(new StaticOverlayBlock(layer, mapIndex, mapIndex % this.mapWidth, Math.floor(mapIndex / this.mapWidth)))`;动态记录 `ordered.push(moving)`(`Readonly<IMovingBlock>` 本对象,不复制、不包壳);比较器 `this.renderer.getLayerIndex(a.layer) - this.renderer.getLayerIndex(b.layer) || a.y - b.y || a.x - b.x || a.priority - b.priority`;结尾仍置 `overlayOrdered = ordered`、`overlayOrderDirty = false`、`overlayValueDirty = true`。`assembleOverlay()` 用 `if (block instanceof MovingBlock)` / `else if (block instanceof StaticOverlayBlock)` 显式收窄:动态用 `block.index` + `dynamicInstancedArray`,静态用 `block.mapIndex` / `block.layer`;16 float 拷贝、前四个 rect 浮点覆盖、`overlayCount`、清两个脏标记全部逐字不变。新增 `./moving` 与 `./status` 的普通引入(禁止 `import type`),不产生循环引用(`moving.ts` / `status.ts` 从不引入 `vertex.ts`)。"
- "**无行为变化**:排序键顺序仍是「层序 → y → x → 同位置优先级」,静态优先级 0 先于动态优先级 1,与旧 `kind` 的 `Static = 0` / `Dynamic = 1` 数值与语义一致,排序结果逐条相同;`getLayerIndex(layer)` 是 `layerIndexMap.get(layer) ?? -1` 的 O(1) 读取,取值与旧的逐条目 `layerIndex` 字段完全相同;静态块携带 `layer` / `x` / `y` / `mapIndex`,动态块就是同一个 `MovingBlock` 实例(`instanceof` 成立),二者仍指向同一 `index` 槽位;组装拷贝与 rect 覆盖不变;`flushPendingAlpha`、惰性 setter、`alphaMap` / `overlayStatic`、两个脏标记、`getVertexArray()` / `getOverlayArray()` / `getOverlayCount()` 契约、`checkRebuild` / `setBlockSize` / `updateMoving` / `deleteMoving`、renderer 两遍式 / FBO / 深度附件 / ping-pong / postEffect / `blendFuncSeparate` / `map.frag` 的 `0.004`、分块张量布局、动态索引身份全部零改动。"
- "**闸门测试同步到位且真实通过**:`rendererGate.test.ts` 删除全部 `OverlayBlockKind` / `kind` / `type OverlayBlock` 断言,改为断言类层次(`OverlayBlock` 是普通类且 `implements IOverlayBlock`、`DynamicBlockStatus extends OverlayBlock` + `super(layer, 1)`、`StaticOverlayBlock extends OverlayBlock` + `super(layer, 0)`、`MovingBlock` 继承它、`IOverlayBlock` 含 `readonly priority`、无 `OverlayBlockKind`)、排序用 `a.priority - b.priority`、组装用 `instanceof MovingBlock` / `StaticOverlayBlock`;保留既有的 4 条 `pendingAlpha` → `alphaMap` 对齐与 1 条 `a.layerIndex` → `getLayerIndex(a.layer)` 对齐(测试侧漂移修正);每个 `it` 前一行 `//` 注释(测试范围变化时同步改写,AGENTS.md 对测试文件豁免注释禁令);**所有源码形状断言必须用拆分 `toContain`(`export class X` / `extends Y` / `implements Z`)或空白容忍正则,禁止整串单行比对**——`StaticOverlayBlock` 的完整类声明为 85 列、超过 `printWidth: 80`,prettier 会折成三行、`{` 独占一行;该文件全部用例通过(实际条数执行时如实写入 SUMMARY)。"
- "**验证形态(无浏览器 UAT)**:`node node_modules/vitest/vitest.mjs run <闸门文件>`、`node node_modules/eslint/bin/eslint.js <5 个文件>`、`node -e` 包裹 `vue-tsc --noEmit` 且**只对本计划的 4 个源码文件负责**(先校验 `vue-tsc` 真正完成:`r.error` / `r.signal` / `status===null` / 非 0 退出且无输出 一律判失败,禁止空 `scoped` 空过;精确豁免用户自有的 `types.ts` `TS2305 TimingFn` 一条并打印原文,允许 0 或 1 条,其它任何诊断失败;`extension/hero.ts` 由过滤自然排除)、修正后的范围闸门(不再 `.trim()`,保留 ` M` 的行首空格)、`git hash-object` 证明 `renderer.ts` / `shader/map.frag` 未变;**不宣称任何 WebGL E2E**,SUMMARY 如实记录浏览器 UAT 因游戏尚不可运行而跳过。**代码一律不提交**:不得运行 `git add` / `commit` / `stash` / `checkout` / `restore` / `worktree` / `reset`。"
- "**范围与纪律**:只写 `files_modified` 的 7 个路径;`status.ts` 只允许「围绕 `DynamicBlockStatus` 加基类、新增 `OverlayBlock` 与 `StaticOverlayBlock`」,`StaticBlockStatus`(含用户已改好的 `setAlpha` 参数顺序)逐字不动;不改 `renderer.ts` / `viewport.ts` / `block.ts` / `element.ts` / `constant.ts` / `map.frag` / 其它 `map/` 文件 / 其它包 / 依赖与配置 / 09-01..09-34 的计划与摘要 / `09-CONTEXT.md`;不修改**非测试文件**的任何既有注释或 jsDoc(只新增;`sortOverlay` 那句仍写 `kind` 的旧注释保持不动并报备);**测试文件例外**:`rendererGate.test.ts` 每个 `it` 前的一行说明注释按 dev.md 随测试范围同步改写(AGENTS.md 将测试文件豁免于注释禁令);不复用任何未获批准的新命名;发现疑似问题只记录、不擅自修复。"
artifacts:
- path: packages-user/client-base/src/map/types.ts
provides: "导出 `OverlayBlockKind`(`Static` / `Dynamic`)与导出接口 `IOverlayBlock`(`kind` / `layer` / `x` / `y`);`IMovingBlock` 改为 `extends IBlockStatus, IOverlayBlock` 并声明 `readonly kind: OverlayBlockKind.Dynamic`;其余既有声明与注释逐字不动"
provides: "删除 `OverlayBlockKind`;导出 `IOverlayBlock`(`layer` / `x` / `y` / `priority`)与 `IOverlayStaticBlock extends IOverlayBlock`(`mapIndex`);`IMovingBlock extends IBlockStatus, IOverlayBlock` 且无 `kind`;其余既有声明与注释逐字不动(含用户自有的 `TimingFn` 引入)"
- path: packages-user/client-base/src/map/status.ts
provides: "新增普通类 `OverlayBlock implements IOverlayBlock`(`layer` / `x` / `y` / `priority`);`DynamicBlockStatus extends OverlayBlock implements IBlockStatus` 且构造器 `super(layer, 1)`;新增 `StaticOverlayBlock extends OverlayBlock implements IOverlayStaticBlock`(`mapIndex`,构造器 `super(layer, 0)`);`StaticBlockStatus` 逐字不动"
- path: packages-user/client-base/src/map/moving.ts
provides: "`MovingBlock` 增加 `readonly kind: OverlayBlockKind.Dynamic = OverlayBlockKind.Dynamic;`,并从 `./types` 引入 `OverlayBlockKind`;`index` / `x` / `y` / 动画逻辑零改动"
provides: "`MovingBlock` 去掉 `kind` / 重复的 `layer` / `x` / `y` 与 `this.layer = layer`,完全继承 `DynamicBlockStatus` / `OverlayBlock`;`index` 身份、动画逻辑与用户的曲线类型改写逐字保留"
- path: packages-user/client-base/src/map/vertex.ts
provides: "私有 `IOverlayStaticBlock extends IOverlayBlock` 与私有 `type OverlayBlock`;`overlayOrdered: OverlayBlock[]` 与局部 `const ordered: OverlayBlock[]`;`sortOverlay()` 直接产生静态覆盖块并原样收集动态块、比较器改用 `getLayerIndex(a.layer)`、删除不再使用的逐图层 `const layerIndex`;`assembleOverlay()` 按 `kind` 显式两分支收窄(`if (... Dynamic)` / `else if (... Static)`);其余覆盖层与顶点数组逻辑零改动"
provides: "删除私有 `IOverlayStaticBlock` / `type OverlayBlock`;`overlayOrdered: IOverlayBlock[]`;`sortOverlay()` push `new StaticOverlayBlock(...)` 与 `moving` 本对象、比较器用 `a.priority`;`assembleOverlay()` 用 `instanceof` 两分支;其余覆盖层与顶点数组逻辑零改动"
- path: packages-user/client-base/src/map/__test__/rendererGate.test.ts
provides: "4 条 `pendingAlpha` 断言对齐为 `alphaMap`、1 条排序断言改为 `getLayerIndex(a.layer)` 形态、新增 7 条覆盖块抽象断言;每个 `it` 前一行注释"
provides: "删除种类枚举断言并新增类层次 / 优先级 / `instanceof` 断言;保留 4 条 `alphaMap` 与 1 条 `getLayerIndex(a.layer)` 的对齐;每个 `it` 前一行注释"
- path: .planning/phases/09-minimum-browser-runnable/09-35-PLAN.md
provides: "本计划文件本身"
provides: "本计划文件本身(本次修订版)"
- path: .planning/phases/09-minimum-browser-runnable/09-35-SUMMARY.md
provides: "执行摘要:前后形态、逐文件改动、未改动清单、四类门禁的真实结果(含豁免原文)、既有闸门漂移的处置、无浏览器 UAT / 无 WebGL E2E 声明、估算未校准声明、0 次代码提交声明、计划编号 09-35 的说明"
provides: "执行摘要:前后形态、逐文件改动、未改动清单、五类门禁的真实结果(含豁免原文)、既有闸门漂移的处置、无浏览器 UAT / 无 WebGL E2E 声明、估算未校准声明、0 次代码提交声明、计划编号 09-35 的说明"
key_links:
- "`MovingBlock.kind`(运行时值 `OverlayBlockKind.Dynamic`)↔ `IMovingBlock.kind`(字面量类型)↔ `type OverlayBlock` 的判别式 ↔ `assembleOverlay()` 的 `block.kind === OverlayBlockKind.Dynamic` 收窄 —— 三者必须指向同一个枚举成员,否则联合类型收窄失效(类型检查失败)或动态块走进静态分支(运行期取错数据)"
- "`sortOverlay()` push 的 `IMovingBlock` 本对象 ↔ `assembleOverlay()` 的 `block.index * INSTANCED_COUNT` ↔ `dynamicInstancedArray` —— 动态块不复制、不缓存,`index` 身份必须仍指向同一槽位;包一层副本会重新引入「销毁后残影 / 旧 layer」问题"
- "比较器 `getLayerIndex(a.layer) - getLayerIndex(b.layer)` ↔ 渲染器的 `layerIndexMap`(`Map.get(layer) ?? -1`)↔ 旧的逐条目 `layerIndex` 字段 —— 取值必须完全相同,否则绘制顺序变化(静态与动态在跨图层时的先后)"
- "`IOverlayStaticBlock.mapIndex` ↔ `overlayStatic` 的 `Map<IMapLayer, Map<number, IOverlayStaticRecord>>` 键 ↔ `getOverlayStaticRecord(block.layer, block.mapIndex)` —— 组装必须仍能从记录取回进入覆盖层前的 rect"
- "闸门测试的 `?raw` 源码字符串 ↔ 真实源码符号 —— 断言里的符号名必须与源码一致(本次修正 4 条 `pendingAlpha` 与 1 条 `a.layerIndex` 漂移),否则闸门恒红或恒绿而失去意义"
- "Task 1 的 `git hash-object` 采集 ↔ Task 2 的逐字比较 —— 证明 `status.ts` / `renderer.ts` / `shader/map.frag` 在本次执行中未被改动"
- "`StaticOverlayBlock` 的 `priority = 0`(`super(layer, 0)`)↔ `DynamicBlockStatus` 的 `priority = 1`(`super(layer, 1)`)↔ 比较器 `a.priority - b.priority` ↔ 旧 `kind` 的 `Static = 0` / `Dynamic = 1` —— 两个数值必须恰为 0 与 1,否则同层同坐标的静态与动态块绘制顺序反转"
- "`ordered.push(moving)`(运行期是 `MovingBlock` 实例)↔ `assembleOverlay()` 的 `block instanceof MovingBlock` ↔ `block.index * INSTANCED_COUNT` + `dynamicInstancedArray` —— 若动态分支推入副本或非 `MovingBlock` 对象,`instanceof` 不成立,该块会静默丢失(既不进动态分支也不进静态分支)"
- "`new StaticOverlayBlock(...)`(构造器写入 `mapIndex` / `x` / `y`)↔ `block instanceof StaticOverlayBlock` ↔ `getOverlayStaticRecord(block.layer, block.mapIndex)` 与 `getIndexInBlock(block.layer, block.x, block.y)` —— 必须是真的类实例;用同形状对象字面量会绕过 `instanceof`,组装时被跳过"
- "`IOverlayBlock.priority` / `OverlayBlock.priority` / `IMovingBlock`(经由继承)↔ 比较器读取 `a.priority` / `b.priority` —— 若某个成员缺 `priority`,`a.priority - b.priority` 得到 `NaN`,比较器把它当 0,顺序会静默退化"
- "比较器 `getLayerIndex(a.layer) - getLayerIndex(b.layer)` ↔ 渲染器的 `layerIndexMap`(`Map.get(layer) ?? -1`)↔ 旧的逐条目 `layerIndex` 字段 —— 取值必须完全相同,否则跨图层的静态 / 动态先后会变化"
- "`MovingBlock` 去掉自身 `layer` / `x` / `y` 声明 ↔ `DynamicBlockStatus` → `OverlayBlock` 的字段 ↔ `IMovingBlock` 的可读属性 —— 必须删干净(含 `this.layer = layer`),否则对基类只读属性赋值会触发类型错误,而字段重复初始化会掩盖基类赋值"
- "`vertex.ts` 新增 `import { MovingBlock } from './moving'` / `import { StaticOverlayBlock } from './status'` ↔ 模块图 `vertex → moving → status → types` —— `moving.ts` / `status.ts` 从不引入 `vertex.ts`,不得为了「方便」反向引入,否则形成循环引用(dev.md 禁止)"
- "闸门测试的 `?raw` 源码字符串 ↔ 真实源码符号 —— 断言里的符号名必须与源码一致(本次同时对齐 4 条 `pendingAlpha` 与 1 条 `a.layerIndex` 的既有漂移),否则闸门恒红或恒绿而失去意义"
- "Task 1 的 `git hash-object` 采集(`renderer.ts` / `shader/map.frag`)↔ Task 2 的逐字比较 —— 证明这两个已有未提交改动的文件在本次执行中未被改动;`status.ts` 由本计划合法修改,故不进哈希闸门"
assumptions:
- "**工作树基线(规划日实测)**:`map/` 下 09-26 的未提交实现仍在——`vertex.ts` 含私有 `IOverlayDrawEntry` / 私有 `OverlayEntryKind` / `alphaMap` / `overlayOrdered` / `flushPendingAlpha` / `sortOverlay` / `assembleOverlay`;`rendererGate.test.ts` 未被跟踪(`??`)且其中 4 条断言仍写 `pendingAlpha`。`moving.ts` 另有其它计划留下的未提交改动(`mutate-animate` → `@motajs/animate` 的曲线类型替换),本计划只在其上新增 `kind`,不改这些改动。"
- "**编号冲突(规划日实测)**:`.planning/phases/09-minimum-browser-runnable/` 下已存在 `09-27-PLAN.md`..`09-34-PLAN.md` 与对应 SUMMARY,且均已提交;因此本计划使用 09-35 / 波次 30,不覆盖任何既有计划或摘要。"
- "**已知类型基线(用户裁定,不在修复范围)**:`vue-tsc --noEmit` 对 `packages-user/client-base/src/map/renderer.ts` 报 `error TS2339: Property 'getLayerByAlias' does not exist on type 'IGameMap'.`;类型闸门精确豁免恰好一条并打印原文,其它任何 `map/` 诊断一律失败。"
- "**工具链现状(规划日实测)**:`pnpm` 在本树已损坏(`packages-user/client-modules/package.json` 引用了已删除的 `@motajs/legacy-ui`),本计划不改任何 workspace / package 文件;所有门禁直连 `node_modules` 里的二进制:`node_modules/vitest/vitest.mjs`、`node_modules/eslint/bin/eslint.js`、`node_modules/vue-tsc/bin/vue-tsc.js` 均存在,`node` 为 v24。"
- "**既有闸门漂移(规划日静态核对)**:`rendererGate.test.ts` 的 4 条断言(`makes setStaticAlpha fully lazy` / `reads pending alpha before the instance array` / `covers pending, order and value in the overlay dirty flag` / `clears overlay members when the arrays are rebuilt`)断言的关键字是 `pendingAlpha`,而源码用的是 `alphaMap`,当前恒红;`sorts by layer, y, x and kind ...` 断言 `a.layerIndex - b.layerIndex`,本次重构会移除该字段。上述 5 条只做「断言与源码 / 新形态对齐」,不改任何行为。"
- "**工作树基线(规划日实测,必须按此执行)**:上一版「接口 + 种类枚举」设计已经被写进工作树但**未提交**——`types.ts` 含 `export const enum OverlayBlockKind`、`IOverlayBlock`(含 `readonly kind`)、`IMovingBlock extends IBlockStatus, IOverlayBlock` 与 `readonly kind: OverlayBlockKind.Dynamic;`;`moving.ts` 引入 `OverlayBlockKind` 并含 `readonly kind`;`vertex.ts` 含私有 `IOverlayStaticBlock` / 私有 `type OverlayBlock` 与 `block.kind === OverlayBlockKind.*` 分支;`rendererGate.test.ts` 含 6 条种类相关 `it` 用例(另有两处排序用例使用 `a.kind - b.kind`)。`status.ts` 的 ` M` 是用户自己的 `setAlpha` 参数顺序修正。**本计划把上述代码视为「被用户纠正后要整体替换的上一版」:Task 1 删除种类枚举与私有联合类型、改写为普通类 + `priority` + `instanceof`;不提交、不回滚、不 stash。**"
- "**`map/` 执行前的真实 git 状态(规划日 `git status --porcelain -uall -- packages-user/client-base/src/map` 实测)**:` M moving.ts` / ` M renderer.ts` / ` M shader/map.frag` / ` M status.ts` / ` M types.ts` / ` M vertex.ts` / `?? __test__/rendererGate.test.ts`。执行后该集合必须**逐条保持这 7 条**(本计划不在 `map/` 下新增或删除任何文件);`status.ts` 与 `renderer.ts` / `map.frag` 的 ` M` 在执行前已存在,故范围闸门只能证明「没有第 8 条变化」,`renderer.ts` / `map.frag` 是否被越界改动由哈希闸门证明。"
- "**类型闸门基线(用户自有,规划日已裁定,不在修复范围)**:`vue-tsc --noEmit` 对 `packages-user/client-base/src/map/types.ts` 报 `error TS2305: Module '@motajs/animate' has no exported member 'TimingFn'.`(用户未提交的 `@motajs/animate` 导入改写留下的,第 14 行),另有 `packages-user/client-base/src/extension/hero.ts` 的 `TS2305 state`(完全在本计划范围外)。本计划**不修复**这两条;类型闸门只取 `packages-user/client-base/src/map/` 下 `types.ts` / `status.ts` / `moving.ts` / `vertex.ts` 四个文件的诊断,把 `types.ts` 的那条 `TS2305 TimingFn` **精确豁免并打印原文**,允许 0 或 1 条(用户若在执行前先行修好则为 0),其它任何诊断一律失败;不得扩大成「忽略 `types.ts` 的全部错误」。"
- "**`ExcitationCurve` / `ExcitationCurve2D` 确实存在**:`packages/animate/src/types.ts` 导出这两个类型别名,`packages/animate/src/utils.ts` 导出 `linear()` 等函数;因此 `moving.ts` 的用户改写只会在 `types.ts` 的 `TimingFn` 上留下诊断,`moving.ts` 本身应为 0 条。"
- "**工具链现状(规划日实测)**:`pnpm` 在本树已损坏(`packages-user/client-modules/package.json` 引用了已删除的 `@motajs/legacy-ui`),本计划不改任何 workspace / package 文件;所有门禁直连 `node_modules` 里的二进制:`node_modules/vitest/vitest.mjs`、`node_modules/eslint/bin/eslint.js`、`node_modules/vue-tsc/bin/vue-tsc.js`、`node_modules/prettier/bin/prettier.cjs` 均存在,`node` 为 v24.21.0。"
- "**格式约束**:`.prettierrc` 为 `printWidth: 80` / `tabWidth: 4` / `singleQuote: true` / `arrowParens: avoid` / `trailingComma: none` / `endOfLine: crlf`,且 eslint 配置启用了 prettier 插件(格式错误即 error)。代码文件必须 CRLF;若 eslint 报纯格式问题,只允许对 `files_modified` 里的 5 个代码文件运行 `prettier --write`(仅换行与排版,语义与断言不变),并在 SUMMARY 记录。**已实测**:`export class StaticOverlayBlock extends OverlayBlock implements IOverlayStaticBlock {` 为 85 列,prettier 会折成 `export class StaticOverlayBlock` / `extends OverlayBlock` / `implements IOverlayStaticBlock` 三行、`{` 独占一行;因此任何源码形状断言都不得用整串单行比对(短于 80 列的单行声明如 `export class OverlayBlock implements IOverlayBlock {` 除外)。"
- "**既有闸门漂移(规划日静态核对)**:`rendererGate.test.ts` 里 4 条断言(`makes setStaticAlpha fully lazy` / `reads pending alpha before the instance array` / `covers pending, order and value in the overlay dirty flag` / `clears overlay members when the arrays are rebuilt`)当前已写 `alphaMap`(与源码一致),1 条 `a.layerIndex` 已改为 `getLayerIndex(a.layer)`;本次修订**保持这两处对齐**,不得回退成 `pendingAlpha` / `a.layerIndex`。种类相关断言(`declares the overlay block kind...` / `makes the moving block a dynamic overlay block` / `declares the moving block kind as dynamic` / `replaces the private draw entry...` / `pushes static blocks as literals...` / `narrows the assembled blocks by kind explicitly`,以及两条排序用例里的 `a.kind - b.kind`)必须改写为类 / 优先级 / `instanceof` 形态。"
- "**`getLayerIndex` 是 O(1)**:`renderer.getLayerIndex(layer)` 的实现是 `this.layerIndexMap.get(layer) ?? -1`(`Map` 读取)。比较器每次比较调用两次,取值与旧的逐条目 `layerIndex` 字段完全相同;排序结果不变,只是把「排序前一次性计算」改成「比较时读取同一张表」。"
- "**无循环引用**:`moving.ts` 只引入 `./status`、`./types` 与外部包;`status.ts` 只引入 `./types` 与 `@user/data-base`;`vertex.ts` 新增引入 `./moving` / `./status` 后,模块图为 `vertex → moving → status → types`,无回边。"
- "**测试源码读取方式**:沿用 09-25 / 09-26 的结论,用 Vite 的 `*?raw` 导入(`../status.ts?raw` 等)读取源码文本,不用 `node:fs`(仓库 TS program 未包含 Node 类型,改配置被禁);测试在仓库根运行,vitest 默认收集 `**/*.test.ts`。"
- "**既有注释不修改**:`vertex.ts` 的 `sortOverlay()` jsDoc 与函数内两行注释仍写着 `kind` / `种类`,与新的 `priority` 语义不再逐字一致;按 AGENTS.md「不得擅自修改任何已有注释」,本计划**保持它们逐字不动**,只在 Task 0 报备并列入 OI-3,等用户批准后再单独修订。**测试文件例外**:`rendererGate.test.ts` 的 `it` 说明注释不受此禁(AGENTS.md 明确豁免测试文件;dev.md 要求测试范围变化时同步更新),其中 `sorts by layer, y, x and kind ...` 上方那行「条目种类」注释必须随改写同步为 `priority` 语义。"
- "**无 RESEARCH / 无 VALIDATION**:本 run 不存在 `09-35-RESEARCH.md` 与 `09-35-VALIDATION.md`,本阶段无 requirement ID,Nyquist 验证不适用。spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)。"
prohibitions:
- "不得在用户恰好回复「可以执行」之前执行任何任务或派发执行子代理(D-03)。"
- "**不得运行任何 git 写命令**:`git add` / `commit` / `stash` / `checkout` / `restore` / `worktree` / `reset` 一律禁止;代码改动留在工作树,只有非代码产物(本计划与 SUMMARY)由编排器另行处理。"
- "不得新增任何类、文件、导出类型或导出常量(`IOverlayBlock` 与 `OverlayBlockKind` 除外);不得把私有 `IOverlayStaticBlock` / `OverlayBlock` 导出。"
- "不得在用户恰好回复「可以执行」之前执行任何任务或派发执行子代理(D-03 / AGENTS.md)。"
- "**不得运行任何 git 写命令**:`git add` / `commit` / `stash` / `checkout` / `restore` / `worktree` / `reset` 一律禁止;**本计划全程 0 次代码提交**,代码改动留在工作树供用户审查。执行器按 `<output>` 写 `09-35-SUMMARY.md`(Task 2 第 6 步,属允许的非代码产物);`.planning/` 下若有提交,由编排器在流程外处理,执行器不得自行提交。"
- "**不得新增 `OverlayBlockKind` 或任何种类枚举 / 字符串标识**;不得把 `OverlayBlock` 写成 `interface` 或 `abstract class`;不得把 `IOverlayBlock` / `IOverlayStaticBlock` 写成类。"
- "不得新增任何**源码文件**、导出类型或导出常量(`IOverlayBlock` / `IOverlayStaticBlock` / `OverlayBlock` / `StaticOverlayBlock` 除外);`.planning/phases/09-minimum-browser-runnable/09-35-PLAN.md` 与 `09-35-SUMMARY.md` 是本计划允许的**非代码产物**,不属「新增源码文件」;不得把 `OverlayBlock` / `StaticOverlayBlock` / `IOverlayStaticBlock` 之外的符号导出到 `status.ts` 之外。"
- "不得改变渲染行为、绘制顺序、脏标记语义、`flushPendingAlpha` 的落点、`getOverlayArray()` 的调用顺序、分块张量、动态索引身份、renderer 两遍式 / FBO / postEffect / `map.frag`。"
- "不得改 `status.ts`(用户维护)、`renderer.ts`、`shader/map.frag` 或任何其它 `map/` 文件;不得改依赖、`package.json`、`tsconfig`、eslint 配置;不得新增依赖或执行安装命令。"
- "不得修改任何既有注释或 jsDoc(AGENTS.md);只允许新增,新增遵循 dev.md(单行 `//`、`//` 后留空格、不解释下一行代码、说明「为什么」;新增成员 jsDoc 用不换行风格)。"
- "不得为了「让闸门变绿」删除用例、放宽断言、忽略整个文件或整类诊断;不得把「源码形状通过」写成「渲染结果已验证」。"
- "不得修复已知类型基线 `renderer.ts` 的 `TS2339 getLayerByAlias`,不得改 `MapRenderer.getLayer` / `IGameMap` / `GameMap`,不得把豁免扩大成「忽略 `renderer.ts` 的全部诊断」。"
- "不得擅自处理 OI-2 之外的疑似问题;发现新问题只记录、不修复、不擅自登记为缺陷。"
- "不得改 `StaticBlockStatus`(含用户已改好的 `setAlpha` 参数顺序)、`renderer.ts`、`viewport.ts`、`block.ts`、`element.ts`、`constant.ts`、`shader/map.frag` 或任何其它 `map/` 文件;不得改依赖、`package.json`、`tsconfig`、eslint / prettier 配置;不得新增依赖或执行安装命令。"
- "不得修改**非测试文件**的任何既有注释或 jsDoc(AGENTS.md);只允许新增,新增遵循 dev.md(单行 `//`、`//` 后留空格、不解释下一行代码、说明「为什么」;成员 jsDoc 不换行风格、方法 jsDoc 换行风格、不写构造器注释)。`sortOverlay` 的旧注释即使与 `priority` 不一致也不得擅自改。**测试文件例外**:`rendererGate.test.ts` 的既有 `it` 说明注释必须随用例改名 / 范围变化同步更新(dev.md 要求;AGENTS.md 已豁免测试文件),不得以「不修改既有注释」为由留下错注释。"
- "不得为了「让闸门变绿」删除用例、放宽断言、忽略整个文件或整类诊断;不得把「源码形状通过」写成「渲染结果已验证」;不得把范围闸门的 `.trim()` 缺陷「绕过」(例如手工改期望字符串去迁就丢失的行首空格)。"
- "不得修复用户自有的 `types.ts` `TS2305 TimingFn` 诊断,也不得修复 `renderer.ts` 的 `TS2339 getLayerByAlias`;不得把豁免扩大到「忽略 `types.ts` 或 `map/` 的全部诊断」。"
- "不得擅自处理 OI-2 / OI-3 之外的疑似问题;发现新问题只记录、不修复、不擅自登记为缺陷。"
open_items:
- "**OI-1(命名待批准,未获批前不得落码;含备选):** ① 导出枚举 `OverlayBlockKind`(备选 `OverlayKind` / `OverlayBlockType`),成员 `Static`(备选 `Tile`)/ `Dynamic`(备选 `Moving`);② 导出接口 `IOverlayBlock`(备选 `IOverlayItem` / `IOverlayMember`),成员 `kind`(备选 `blockKind`)/ `layer`(备选 `mapLayer`)/ `x`(备选 `mapX`)/ `y`(备选 `mapY`);③ `IMovingBlock.kind` 与 `MovingBlock.kind`(字段名同 ②);④ 私有接口 `IOverlayStaticBlock`(备选 `IOverlayStaticEntry` / `IStaticOverlayBlock`);⑤ 私有类型别名 `OverlayBlock`(备选 `OverlayEntry` / `OverlayBlockEntry`)。**本计划不新增任何其它标识符**(比较器直接用既有的 `getLayerIndex`,不引入局部函数或缓存变量)。"
- "**OI-2(已知限制,只记录、不修复):** ① 覆盖层组装对每条静态记录仍做 `getBlockByDataLoc` + `getLayerInstanced` 查找(既有成本);② 同图层同行的不透明与覆盖层图块 z 相同,先绘制者胜出(既有深度式排序边界);③ 动态块移动期间每帧重排(既有);④ 游戏尚不可运行,浏览器 UAT 全程跳过,**无 WebGL E2E**;⑤ 估算未校准(`confidence: low`)。"
- "**OI-1(命名待批准,未获批前不得落码;含备选):** ① 导出类 `OverlayBlock`(备选 `OverlayBlockBase` / `BaseOverlayBlock`);② 导出接口 `IOverlayBlock`(备选 `IOverlayItem` / `IOverlayMember`),成员 `layer`(备选 `mapLayer`)/ `x` / `y` / `priority`(备选 `drawPriority` / `order`);③ 导出接口 `IOverlayStaticBlock`(备选 `IOverlayStaticItem` / `IStaticOverlayBlock`),成员 `mapIndex`(备选 `dataIndex`);④ 导出类 `StaticOverlayBlock`(备选 `StaticOverlay` / `StaticBlockOverlay`);⑤ `DynamicBlockStatus extends OverlayBlock` 与 `super(layer, 1)`;⑥ `MovingBlock` 删除 `kind` / `layer` / `x` / `y` 后的继承形态。**本计划不新增任何其它标识符**(比较器直接用既有的 `getLayerIndex`,不引入局部函数或缓存变量;`instanceof` 直接用两个类)。"
- "**OI-2(已知限制,只记录、不修复):** ① 覆盖层组装对每条静态记录仍做 `getBlockByDataLoc` + `getLayerInstanced` 查找(既有成本);② 同图层同行的不透明与覆盖层图块 z 相同,先绘制者胜出(既有深度式排序边界);③ 动态块移动期间每帧重排(既有);④ 每次排序会为每条静态记录新建一个 `StaticOverlayBlock` 实例(上一版是对象字面量,成本同阶,未做池化);⑤ 游戏尚不可运行,浏览器 UAT 全程跳过,**无 WebGL E2E**;⑥ 估算未校准(`confidence: low`);⑦ `types.ts` 的用户自有 `TS2305 TimingFn` 诊断仍在,本计划不修。"
- "**OI-3(注释与实现不一致,只记录、待批准):** `vertex.ts` 的 `sortOverlay()` jsDoc 仍写「按 `(layerIndex, y, x, kind)` 重建覆盖层的绘制顺序」,函数内两行注释仍写「最后用种类保证静态先于动态」;重构后语义由 `priority` 承担。按 AGENTS.md 不得擅自修改既有注释,本计划保持原文;是否改写由用户决定,另行处理。"
---
<objective>
Phase 9 · 第 35 个计划(新增的独立小增量,**不是** 09-01..09-34 的 UI / 文档任务)—— **把「覆盖块」抽成一个概念,让 `MovingBlock` 成为覆盖块的一种**,从而去掉 `vertex.ts` 里专为排序缓存而设的私有绘制条目与私有序枚举。
Phase 9 · 第 35 个计划(新增的独立小增量,**不是** 09-01..09-34 的 UI / 文档任务)—— **把「覆盖块」做成一个具体的普通类体系,让 `MovingBlock` 通过继承成为覆盖块的一种,并用只读 `priority` 取代上一版的种类枚举**,从而去掉 `vertex.ts` 里专为排序缓存而设的私有联合类型。
**要解决的问题:** 现在 `vertex.ts` 的排序缓存条目 `IOverlayDrawEntry` 用可选字段(`mapIndex?` / `moving?`)同时表示静态与动态两种东西,并另有一个私有枚举 `OverlayEntryKind`;动态条目还要包一层 `{ moving }` 副本。这些结构只服务于 `sortOverlay()` / `assembleOverlay()` 的局部需要,却让「静态图块」与「移动图块」两种成员在同一位置有两种表示。把共同点(种类 / 图层 / 坐标)抽成接口后,静态条目只需额外携带地图索引,动态条目就是 `IMovingBlock` 本身。
**要解决的问题:** 上一版设计(已被用户纠正)用 `OverlayBlockKind` 枚举 + 接口判别式区分静态与动态覆盖块,并在 `vertex.ts` 里保留私有 `IOverlayStaticBlock` 与私有 `type OverlayBlock = IOverlayStaticBlock | IMovingBlock`。用户裁定:`OverlayBlock` 必须是**普通类**(不是接口、不是抽象类),**不得新增种类枚举**(未来还会有别的覆盖块类型,枚举会逼着每加一种就改枚举),排序稳定性改用 `readonly priority`;「最小改动」是指不做顺手重构、不碰无关内容,但类 + 优先级这件事是明确要做的,不能以「最小」为由省掉。
**做法(锁定设计,最小改动):**
- `types.ts` 增加导出 `OverlayBlockKind`(`Static` / `Dynamic`)与导出接口 `IOverlayBlock`(`kind` / `layer` / `x` / `y`);`IMovingBlock extends IBlockStatus, IOverlayBlock` 并声明 `readonly kind: OverlayBlockKind.Dynamic`。
- `moving.ts` 的 `MovingBlock` 增加 `readonly kind: OverlayBlockKind.Dynamic = OverlayBlockKind.Dynamic;`(`MovingBlock` 因此本身就是一个覆盖块)。
- `vertex.ts` 删除私有 `IOverlayDrawEntry` / 私有 `OverlayEntryKind`,改为私有 `IOverlayStaticBlock extends IOverlayBlock`(额外 `mapIndex`)与私有 `type OverlayBlock = IOverlayStaticBlock | IMovingBlock`;`overlayOrdered: OverlayBlock[]`;静态 push 对象字面量、动态 push `getMovingBlock()` 的本对象;比较器改用 `getLayerIndex(a.layer)` 并删除逐图层 `const layerIndex` 局部;组装以 `if (block.kind === OverlayBlockKind.Dynamic)` / `else if (block.kind === OverlayBlockKind.Static)` 显式收窄。
- 不改渲染行为、不改变排序结果、不改 flush / 脏标记 / 组装拷贝 / 渲染器两遍式 / 分块张量 / 动态索引身份 / `status.ts`。
- 同步闸门测试:修正 4 条 `pendingAlpha` 与 1 条 `a.layerIndex` 的既有漂移,新增 7 条覆盖块抽象断言。
- 验证:vitest 闸门 + eslint + 按 `map/` 过滤的 `vue-tsc`(精确豁免一条并打印)+ 范围闸门 + 受保护文件哈希;**不做浏览器 UAT、不宣称 WebGL E2E、不提交任何代码**。
**做法(锁定设计):**
- `types.ts`:删除 `OverlayBlockKind`;`IOverlayBlock` 只保留 `readonly layer` / `x` / `y` / `readonly priority`;新增 `IOverlayStaticBlock extends IOverlayBlock`(`readonly mapIndex`);`IMovingBlock extends IBlockStatus, IOverlayBlock` 且不再声明 `kind`。
- `status.ts`:新增普通类 `OverlayBlock implements IOverlayBlock`(`layer` / `x = 0` / `y = 0` / `priority`,构造器 `(layer, priority)`);`DynamicBlockStatus extends OverlayBlock implements IBlockStatus`,构造器 `super(layer, 1)`;新增 `StaticOverlayBlock extends OverlayBlock implements IOverlayStaticBlock`(`mapIndex`,构造器 `(layer, mapIndex, x, y)` 且 `super(layer, 0)`)。`StaticBlockStatus` 逐字不动。
- `moving.ts`:删除 `kind` 与重复的 `layer` / `x` / `y` 声明及 `this.layer = layer`,完全继承基类;保留 `index` 身份、动画逻辑与用户的曲线类型改写。
- `vertex.ts`:删除私有 `IOverlayStaticBlock` / `type OverlayBlock`;`overlayOrdered: IOverlayBlock[]`;静态 push `new StaticOverlayBlock(...)`、动态 push `moving` 本对象;比较器末位改用 `a.priority - b.priority`;组装改用 `instanceof MovingBlock` / `else if (block instanceof StaticOverlayBlock)`。
- 不改渲染行为、不改变排序结果(静态 0 先于动态 1)、不改 flush / 脏标记 / 组装拷贝 / 渲染器两遍式 / 分块张量 / 动态索引身份 / `StaticBlockStatus`。
- 同步闸门测试:删除种类断言、新增类层次 / 优先级 / `instanceof` 断言,保留既有的 `alphaMap` 与 `getLayerIndex(a.layer)` 对齐。
- 验证:vitest 闸门 + eslint(5 文件)+ 只对本计划 4 个源码文件负责的 `vue-tsc`(精确豁免并打印用户自有的 `TimingFn` 一条,允许 0/1)+ 修正后的范围闸门(不再 `.trim()`)+ `renderer.ts` / `map.frag` 哈希闸门;**不做浏览器 UAT、不宣称 WebGL E2E、不提交任何代码**。
**约束:** 只动 `files_modified` 的 6 个路径;不改其它文件;不改既有注释与 jsDoc;不新增依赖;不改配置;**所有新增命名待批准(OI-1)**。
**约束:** 只动 `files_modified` 的 7 个路径;`status.ts` 仅限上述三个类的改动;不改既有注释与 jsDoc;不新增依赖;不改配置;**所有新增命名待批准(OI-1)**。
**修改方式声明(AGENTS.md):** 本计划的全部文件写入(改 3 个源码、改 1 个测试、写 1 个 SUMMARY)由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 外无任何写入;代码一律不提交。
**修改方式声明(AGENTS.md):** 本计划的全部文件写入(改 4 个源码、改 1 个测试、写 1 个 SUMMARY)由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 外无任何写入;代码一律不提交。
**可见跳过:** spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)。本阶段无 requirement ID,不创建 RESEARCH.md / VALIDATION.md,Nyquist 验证不适用。
</objective>
@@ -106,12 +122,13 @@ Phase 9 · 第 35 个计划(新增的独立小增量,**不是** 09-01..09-34
| 类型 | 名称 / 路径 | 说明 |
|------|-------------|------|
| 新符号(导出) | `OverlayBlockKind`(`Static` / `Dynamic`)、`IOverlayBlock` | `types.ts`;命名待批准(OI-1) |
| 新符号(成员) | `IMovingBlock.kind`、`MovingBlock.kind` | 固定为 `OverlayBlockKind.Dynamic` |
| 新符号(私有) | `IOverlayStaticBlock`、`type OverlayBlock` | `vertex.ts`;不导出 |
| 删除(私有) | `IOverlayDrawEntry`、`OverlayEntryKind` | `vertex.ts` 内部替换 |
| 修改 | `types.ts`、`moving.ts`、`vertex.ts` | 仅上述结构改动 |
| 修改 | `map/__test__/rendererGate.test.ts` | 修 5 条既有断言 + 新增 7 条 |
| 删除(导出) | `OverlayBlockKind` | `types.ts`;上一版设计的种类枚举,整体移除 |
| 新符号(导出接口) | `IOverlayBlock`(`layer` / `x` / `y` / `priority`)、`IOverlayStaticBlock`(`mapIndex`) | `types.ts`;命名待批准(OI-1) |
| 新符号(导出类) | `OverlayBlock`(普通类)、`StaticOverlayBlock` | `status.ts`;命名待批准(OI-1) |
| 新符号(继承) | `DynamicBlockStatus extends OverlayBlock`、`MovingBlock` 继承链 | `status.ts` / `moving.ts` |
| 删除(私有) | 私有 `IOverlayStaticBlock`、私有 `type OverlayBlock`、`MovingBlock.kind` | `vertex.ts` / `moving.ts` |
| 修改 | `types.ts`、`status.ts`、`moving.ts`、`vertex.ts` | 仅上述结构改动 |
| 修改 | `map/__test__/rendererGate.test.ts` | 删除种类断言 + 新增类 / 优先级 / `instanceof` 断言 |
| 交付 | `.planning/phases/09-minimum-browser-runnable/09-35-SUMMARY.md` | 执行摘要、门禁结果、UAT 跳过声明 |
## 覆盖审计(Multi-Source Coverage Audit)
@@ -126,12 +143,16 @@ Phase 9 · 第 35 个计划(新增的独立小增量,**不是** 09-01..09-34
| CONTEXT D-03 | 执行前必须先汇报并等待「可以执行」 | Task 0(`blocking-human`),`autonomous: false` |
| CONTEXT D-04 | 新命名必须先反馈获批 | Task 0 逐条报备 OI-1 |
| CONTEXT D-05 | 默认不逐阶段验证 | 本计划仍做自动化门禁,但**明确不做浏览器 UAT** |
| CONTEXT D-06 | 范围由用户界定,不擅自扩大 | `files_modified` 6 项 + 范围闸门 + 哈希闸门 |
| CONTEXT D-06 | 范围由用户界定,不擅自扩大 | `files_modified` 7 项 + 范围闸门 + 哈希闸门 |
| CONTEXT Deferred | 不做可见性裁剪、不做桶排序、不做性能优化 | 比较器保持一次性层序取值语义,不引入桶 / 缓存 / 裁剪 |
| 用户指令(本次派发) | 抽象 `OverlayBlock`、`MovingBlock` 成为其一种、最小改动 | Task 1(`types.ts` / `moving.ts` / `vertex.ts`)+ Task 2(闸门) |
| 用户指令(本次派发) | 不改渲染行为 / flush / 分块 / 动态身份 / postEffect | Task 1 的「其余不变」+ Task 2 的既有断言 |
| 用户指令(本次派发) | Task 0 必须先汇报并等待「可以执行」 | Task 0 |
| 用户指令(本次派发) | 不提交代码、不做 UAT | Task 0 报备 + Task 2 SUMMARY |
| 用户纠正 1 | `OverlayBlock` 必须是具体普通类,不是接口也不是抽象类 | Task 1(`status.ts`)+ Task 2(类断言) |
| 用户纠正 2 | 不得新增 `OverlayBlockKind`(未来还会有别的覆盖块类型) | Task 1(`types.ts` 删除枚举)+ Task 2(无枚举断言) |
| 用户纠正 3 | 用 `readonly priority` 代替种类枚举保证排序稳定 | Task 1(`priority` 0 / 1 + 比较器)+ Task 2(优先级断言) |
| 用户纠正 4 | 「最小改动」不等于把类 + 优先级改没了 | Task 1 / Task 2 全量覆盖,不做无关重构 |
| 用户纠正 5 | `status.ts` 进入范围(`DynamicBlockStatus` 继承 + `StaticOverlayBlock` 落点) | `files_modified` + Task 1 + Task 2 |
| 用户纠正 6 | 类型闸门要豁免用户自有的 `TimingFn` 诊断并打印,不许放宽整类 | Task 2 类型闸门 + Task 0 ⑫ |
| 用户纠正 7 | 上一版范围闸门的 `.trim()` 缺陷必须修正 | Task 2 范围闸门 |
| 用户纠正 8 | 不做浏览器 UAT / 无 WebGL E2E / 不提交代码 | Task 0 报备 + Task 2 SUMMARY |
<execution_context>
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
@@ -148,6 +169,7 @@ Phase 9 · 第 35 个计划(新增的独立小增量,**不是** 09-01..09-34
@AGENTS.md
@dev.md
@packages-user/client-base/src/map/types.ts
@packages-user/client-base/src/map/status.ts
@packages-user/client-base/src/map/moving.ts
@packages-user/client-base/src/map/vertex.ts
@packages-user/client-base/src/map/__test__/rendererGate.test.ts
@@ -156,118 +178,124 @@ Phase 9 · 第 35 个计划(新增的独立小增量,**不是** 09-01..09-34
<tasks>
<task type="checkpoint:human-verify" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报覆盖块抽象(前后形态、逐文件符号、不改清单、待批准命名、漂移处置),等待「可以执行」</name>
<precondition>工作树为 09-26 的未提交实现:`vertex.ts` 含私有 `IOverlayDrawEntry` / 私有 `OverlayEntryKind` / `alphaMap` / `overlayOrdered` / `flushPendingAlpha` / `sortOverlay` / `assembleOverlay`,且 `sortOverlay` 的比较器使用 `a.layerIndex - b.layerIndex`;`types.ts` 的 `IMovingBlock` 仍是 `extends IBlockStatus {`;`moving.ts` 的 `MovingBlock implements IMovingBlock` 且无 `kind` 成员;`map/__test__/rendererGate.test.ts` 存在(未跟踪)、含 4 条 `pendingAlpha` 断言与 1 条 `a.layerIndex` 断言;`node_modules/vitest/vitest.mjs`、`node_modules/eslint/bin/eslint.js`、`node_modules/vue-tsc/bin/vue-tsc.js` 均存在。**已知类型基线(用户已裁定,不在修复范围)**:`vue-tsc --noEmit` 对 `renderer.ts` 报 `error TS2339: Property 'getLayerByAlias' does not exist on type 'IGameMap'.`,本计划所有类型闸门精确豁免该条一条。</precondition>
<name>Task 0: 预执行汇报关卡 —— 汇报普通类覆盖块体系(类层次、优先级、instanceof、逐文件符号、不改清单、待批准命名、类型基线),等待「可以执行」</name>
<precondition>工作树含上一版「接口 + 种类枚举」设计的未提交实现:`types.ts` 有 `export const enum OverlayBlockKind`、`IOverlayBlock`(含 `readonly kind`)与 `IMovingBlock` 的 `readonly kind: OverlayBlockKind.Dynamic;`;`moving.ts` 引入 `OverlayBlockKind` 且 `MovingBlock` 含 `readonly kind`、`readonly layer`、`x` / `y`;`vertex.ts` 含私有 `IOverlayStaticBlock` / 私有 `type OverlayBlock` 与 `block.kind === OverlayBlockKind.*`;`rendererGate.test.ts` 含 6 条种类相关断言与两条排序用例中的 `a.kind - b.kind`;`status.ts` 仅含 `StaticBlockStatus` / `DynamicBlockStatus`(`DynamicBlockStatus implements IBlockStatus`,未继承任何类)。工具链:`node_modules/vitest/vitest.mjs`、`node_modules/eslint/bin/eslint.js`、`node_modules/vue-tsc/bin/vue-tsc.js`、`node_modules/prettier/bin/prettier.cjs` 均存在,`node` v24.21.0。**类型基线(用户自有,不在修复范围)**:`types.ts(14,10)` 的 `TS2305 Module '@motajs/animate' has no exported member 'TimingFn'`(允许 0 或 1 条并打印),`extension/hero.ts` 的 `TS2305 state` 由过滤排除。</precondition>
<files>无(本任务只读,不写任何文件;汇报后停下等待「可以执行」)</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-26-PLAN.md(被本计划延续的覆盖层设计:惰性 setter、flush 落点、排序键、两个脏标记、门禁纪律)
- .planning/phases/09-minimum-browser-runnable/09-26-SUMMARY.md(09-26 的实际落点、保留项、门禁口径与偏差)
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-01 增量派发、D-03 汇报关卡、D-04 命名批准、Deferred 性能条目)
- packages-user/client-base/src/map/types.ts(`IBlockStatus`、`IMovingBlock`、`IMapVertexGenerator`)
- packages-user/client-base/src/map/moving.ts(`MovingBlock` 的成员与 `index` 身份)
- packages-user/client-base/src/map/vertex.ts(`IOverlayStaticRecord`、`IOverlayDrawEntry`、`OverlayEntryKind`、`overlayOrdered`、`sortOverlay`、`assembleOverlay`、`getIndexInBlock`)
- packages-user/client-base/src/map/__test__/rendererGate.test.ts(现有的 30 条断言与 `getMethodBody` 辅助)
- AGENTS.md(执行前须获批、命名先反馈、既有注释不得修改、疑似问题须先确认)、dev.md(`!` / `isNil`、不用 `?.`、不把函数赋给临时变量、新增成员 jsDoc、CRLF)
- packages-user/client-base/src/map/types.ts(`IBlockStatus`、`IOverlayBlock`、`IMovingBlock`、`IMapVertexGenerator`)
- packages-user/client-base/src/map/status.ts(`StaticBlockStatus`、`DynamicBlockStatus` 与用户已改好的 `setAlpha` 参数顺序)
- packages-user/client-base/src/map/moving.ts(`MovingBlock` 的继承链、成员与 `index` 身份)
- packages-user/client-base/src/map/vertex.ts(`IOverlayStaticRecord`、`overlayOrdered`、`sortOverlay`、`assembleOverlay`、`getIndexInBlock`)
- packages-user/client-base/src/map/__test__/rendererGate.test.ts(现有的 37 条断言与 `getMethodBody` 辅助)
- AGENTS.md(执行前须获批、命名先反馈、既有注释不得修改、疑似问题须先确认)、dev.md(类必须先接口后 `implements`、`!` / `isNil`、不用 `?.`、不把函数赋给临时变量、不用字符串作键、新增成员 jsDoc、CRLF)
</read_first>
<what-built>
一份待批准的执行计划(`09-35-PLAN.md`):把「覆盖块」抽成 `IOverlayBlock` / `OverlayBlockKind`,让 `MovingBlock` 成为覆盖块的一种,并用覆盖块联合类型替换 `vertex.ts` 的私有绘制条目;不改变任何渲染行为。
一份待批准的执行计划(`09-35-PLAN.md`,本次修订版):把覆盖块做成「普通类基类 + 静态块类 + 动态块状态继承」,用只读 `priority` 取代上一版的 `OverlayBlockKind`,并让 `vertex.ts` 用 `instanceof` 判别两种覆盖块;不改变任何渲染行为。
</what-built>
<action>
**停下并向用户汇报,等待其恰好回复「可以执行」后才继续;本任务不写任何文件、不改任何源码 / 测试 / 计划,不派发执行子代理。**
① **要做什么 / 为什么:** 现在 `vertex.ts` 的排序缓存用私有 `IOverlayDrawEntry` 表示两种成员——静态图块与动态图块——靠可选字段 `mapIndex?` / `moving?` 区分,另配私有枚举 `OverlayEntryKind`;动态成员还被包成 `{ moving }` 副本。这些结构只服务于 `sortOverlay()` / `assembleOverlay()` 的局部需要,却让同一种「覆盖块」有两种表示。本次把共同点(种类、图层、坐标)抽成接口 `IOverlayBlock`,静态条目只需额外带地图索引,动态条目就是 `IMovingBlock` 本身——即「`MovingBlock` 是覆盖块的一种」。
① **要做什么 / 为什么(含「替换上一版」):** 当前工作树里留着上一版设计——`OverlayBlockKind` 枚举 + `IOverlayBlock.kind` 接口判别式 + `vertex.ts` 的私有 `IOverlayStaticBlock` 与私有 `type OverlayBlock`。用户已明确纠正:`OverlayBlock` 必须是**具体普通类**;**不得新增 `OverlayBlockKind`**(只假设静态与动态是错的,将来还会有别的覆盖块类型,枚举会逼着每加一种就改枚举);排序稳定性改用 `readonly priority`;「最小改动」指不做顺手重构、不碰无关内容,但类 + 优先级这件事明确要做,不能以「最小」为由省掉。因此本计划把上一版实现整体替换为「普通类 + `priority` + `instanceof`」。
② **精确的前后形态(逐符号):**
- `types.ts` 的 `//#region 枚举值` 末尾新增 `export const enum OverlayBlockKind { Static, Dynamic }`,两个成员的注释沿用现有 `OverlayEntryKind` 的语义(静态先于动态)。
- `types.ts` 在 `IBlockStatus` 之后、`IMovingBlock` 之前新增 `export interface IOverlayBlock`,成员为 `readonly kind: OverlayBlockKind` / `readonly layer: IMapLayer` / `readonly x: number` / `readonly y: number`(每项一行 jsDoc)。
- `IMovingBlock` 的声明由 `extends IBlockStatus {` 改为 `extends IBlockStatus, IOverlayBlock {`,并新增 `readonly kind: OverlayBlockKind.Dynamic;`(继承来源已有说明,不重复写 jsDoc)。
- `moving.ts` 的 `MovingBlock` 在 `readonly layer: IMapLayer;` 之后新增 `readonly kind: OverlayBlockKind.Dynamic = OverlayBlockKind.Dynamic;`,并从 `./types` 追加引入 `OverlayBlockKind`(`import type` 禁止)。
- `vertex.ts` 删除私有 `IOverlayDrawEntry` 与私有 `OverlayEntryKind`;改为私有 `interface IOverlayStaticBlock extends IOverlayBlock`,追加 `readonly kind: OverlayBlockKind.Static` 与 `readonly mapIndex: number`(各带 jsDoc);并在其后新增私有 `type OverlayBlock = IOverlayStaticBlock | IMovingBlock`(不写头注释)。
- `overlayOrdered` 的类型由 `IOverlayDrawEntry[]` 改为 `OverlayBlock[]`。
- `sortOverlay()` 的静态分支 push `{ kind: OverlayBlockKind.Static, layer, x: mapIndex % this.mapWidth, y: Math.floor(mapIndex / this.mapWidth), mapIndex }`;动态分支只写 `ordered.push(moving);`(不再包 `{ moving }`、不再逐条算 `layerIndex`);局部 `const ordered: IOverlayDrawEntry[]` 改为 `const ordered: OverlayBlock[]`,并删除 `this.overlayStatic.forEach((records, layer) => {` 内部的 `const layerIndex = this.renderer.getLayerIndex(layer);`(比较器改为现取后该局部已无使用者,留下会被 eslint 判为未使用变量)。
- 比较器由 `a.layerIndex - b.layerIndex || a.y - b.y || a.x - b.x || a.kind - b.kind` 改为 `this.renderer.getLayerIndex(a.layer) - this.renderer.getLayerIndex(b.layer) || a.y - b.y || a.x - b.x || a.kind - b.kind`;结尾仍置 `overlayOrderDirty = false` 与 `overlayValueDirty = true`。
- `assembleOverlay()` 的循环参数由 `entry` 改为 `block`,并写成两个显式判别分支:`if (block.kind === OverlayBlockKind.Dynamic)` 时用 `block.index * INSTANCED_COUNT` 从 `dynamicInstancedArray` 拷贝;`else if (block.kind === OverlayBlockKind.Static)` 时用 `getOverlayStaticRecord(block.layer, block.mapIndex)` 与 `getIndexInBlock(block.layer, block.x, block.y)`(静态分支不得省略成 `else`,闸门要求源码同时含两个枚举成员);16 float 拷贝、前四个 rect 浮点覆盖、`overlayCount`、清两个脏标记全部不变;原先 `isNil(mapIndex)` 的守卫随可选字段一起移除(类型收窄后 `mapIndex` 必为数字)。
② **精确的类层次(逐符号,普通类、非接口、非抽象):**
- `types.ts`:删除整个 `export const enum OverlayBlockKind`(含 `Static` / `Dynamic` 两个成员与其注释);`IOverlayBlock` 只保留 `readonly layer: IMapLayer` / `x: number` / `y: number` / **新增** `readonly priority: number`,删除 `readonly kind` 与其注释;新增 `export interface IOverlayStaticBlock extends IOverlayBlock`,追加 `readonly mapIndex: number`;`IMovingBlock` 的声明保持 `extends IBlockStatus, IOverlayBlock`,删除 `readonly kind: OverlayBlockKind.Dynamic;`,其余成员(含 `readonly x` / `readonly y`)不动。
- `status.ts`:新增 `export class OverlayBlock implements IOverlayBlock`,字段 `readonly layer: IMapLayer`、`x: number = 0`、`y: number = 0`、`readonly priority: number`,构造器 `(layer: IMapLayer, priority: number)` 逐项赋值;`DynamicBlockStatus` 改为 `export class DynamicBlockStatus extends OverlayBlock implements IBlockStatus`,构造器形参 `readonly layer: IMapLayer` 去掉 `readonly` 修饰(改由基类持有),构造器首句 `super(layer, 1)`,`vertex` / `index` 与全部方法逐字保留;新增 `export class StaticOverlayBlock extends OverlayBlock implements IOverlayStaticBlock`,字段 `readonly mapIndex: number`,构造器 `(layer: IMapLayer, mapIndex: number, x: number, y: number)` 首句 `super(layer, 0)` 并赋值 `mapIndex` / `x` / `y`;`StaticBlockStatus`(含用户已改好的 `setAlpha` 参数顺序)逐字不动。
- `moving.ts`:`MovingBlock` 仍为 `export class MovingBlock extends DynamicBlockStatus implements IMovingBlock`;删除 `readonly kind` 字段与 `OverlayBlockKind` 引入;删除 `readonly layer: IMapLayer;` 声明与构造器里的 `this.layer = layer;`;删除重复的 `x: number = 0;` / `y: number = 0;` 声明(改由 `OverlayBlock` 初始化);保留 `index: number;` 与 `this.index = index;`、全部动画逻辑,以及用户的 `TimingFn` → `ExcitationCurve` / `ExcitationCurve2D` 改写。
- `vertex.ts`:删除私有 `IOverlayStaticBlock` 与私有 `type OverlayBlock`;`overlayOrdered: IOverlayBlock[]`;`sortOverlay()` 的静态分支 push `new StaticOverlayBlock(layer, mapIndex, mapIndex % this.mapWidth, Math.floor(mapIndex / this.mapWidth))`,动态分支 `ordered.push(moving)`(`Readonly<IMovingBlock>` 本对象);`assembleOverlay()` 用 `if (block instanceof MovingBlock)` / `else if (block instanceof StaticOverlayBlock)` 两个显式分支。
③ **逐文件符号清单:** `types.ts`(`OverlayBlockKind`、`IOverlayBlock`、`IMovingBlock` 的 `kind` 与新继承);`moving.ts`(`MovingBlock.kind`、引入 `OverlayBlockKind`);`vertex.ts`(删 `IOverlayDrawEntry` / `OverlayEntryKind`,增 `IOverlayStaticBlock` / `OverlayBlock`,改 `overlayOrdered` / `sortOverlay` / `assembleOverlay`);`rendererGate.test.ts`(见⑦)。**不新增方法、不新增类、不新增文件、不新增其它导出。**
③ **`instanceof` 用法与被判别的运行期对象:** `getMovingBlock()` 的元素运行期就是 `MovingBlock` 实例(构造器 `new MovingBlock(...)`),所以 `block instanceof MovingBlock` 成立并收窄出 `index`;静态记录由本计划 `new StaticOverlayBlock(...)` 现造,所以 `block instanceof StaticOverlayBlock` 成立并收窄出 `mapIndex`。两个分支都必须写全(`if` / `else if`),不得写成「只判动态 + 落空静态」,否则组件无法证明两种类都被覆盖。
④ **明确不改动的部分:** `flushPendingAlpha()` 整段;`setStaticAlpha` / `getStaticAlpha` / `setStaticFrame` / `getStaticFrame` / `setDynamicAlpha` / `setDynamicFrame`;`alphaMap` / `overlayStatic` / `overlayOrderDirty` / `overlayValueDirty` / `overlayInstancedArray` / `overlayCount`;`getVertexArray()` 的 flush 落点;`getOverlayArray()` 的 flush → sort → assemble 顺序;`getOverlayCount()` 契约;`checkRebuild` / `setBlockSize` / `updateMoving` / `deleteMoving` 的脏标记;`IOverlayStaticRecord` 与记录读写方法;`renderer.ts`(两遍式、`depthMask`、`blendFuncSeparate`、FBO / 深度附件 / ping-pong、postEffect、覆盖层上传与绘制、`needUpdate`);`shader/map.frag` 的 `0.004`;分块张量 `[chunk][layer][row][col]`;`MovingBlock.index` 身份与 `movingIndexMap`;`status.ts`;其它包与依赖配置。
④ **逐文件符号清单:** `types.ts`(删 `OverlayBlockKind`;改 `IOverlayBlock` 加 `priority`;增 `IOverlayStaticBlock`;改 `IMovingBlock` 去 `kind`);`status.ts`(增 `OverlayBlock`、`StaticOverlayBlock`;改 `DynamicBlockStatus` 继承与 `super(layer, 1)`);`moving.ts`(删 `kind` / 重复 `layer` / `x` / `y` / `this.layer = layer`,改引入);`vertex.ts`(删私有联合类型,改 `overlayOrdered` / `sortOverlay` / `assembleOverlay`,加 `./moving` 与 `./status` 引入);`rendererGate.test.ts`(见⑨)。**不新增方法、不新增文件、不新增其它导出。**
⑤ **待批准命名(OI-1,未获批前不得落码;每项含备选):** `OverlayBlockKind`(`OverlayKind` / `OverlayBlockType`),成员 `Static`(`Tile`)/ `Dynamic`(`Moving`);`IOverlayBlock`(`IOverlayItem` / `IOverlayMember`),成员 `kind`(`blockKind`)/ `layer`(`mapLayer`)/ `x`(`mapX`)/ `y`(`mapY`);`IMovingBlock.kind` 与 `MovingBlock.kind`(同 ②③);私有 `IOverlayStaticBlock`(`IOverlayStaticEntry` / `IStaticOverlayBlock`);私有 `type OverlayBlock`(`OverlayEntry` / `OverlayBlockEntry`)。**没有其它新标识符**——比较器直接用既有的 `getLayerIndex`,不引入局部函数(dev.md 禁止把对象函数赋给临时变量)也不引入缓存变量。
⑤ **明确不改动的部分:** `StaticBlockStatus` 整段(含 `setAlpha(this.layer, this.x, this.y, alpha)` 的参数顺序);`flushPendingAlpha()` 整段;`setStaticAlpha` / `getStaticAlpha` / `setStaticFrame` / `getStaticFrame` / `setDynamicAlpha` / `setDynamicFrame`;`alphaMap` / `overlayStatic` / `overlayOrderDirty` / `overlayValueDirty` / `overlayInstancedArray` / `overlayCount`;`getVertexArray()` 的 flush 落点;`getOverlayArray()` 的 flush → sort → assemble 顺序;`getOverlayCount()` 契约;`checkRebuild` / `setBlockSize` / `updateMoving` / `deleteMoving` 的脏标记;`IOverlayStaticRecord` 与记录读写方法;`MovingBlock.index` 身份与 `movingIndexMap`;`renderer.ts`(两遍式、`depthMask`、`blendFuncSeparate`、FBO / 深度附件 / ping-pong、postEffect、覆盖层上传与绘制、`needUpdate`);`shader/map.frag` 的 `0.004`;分块张量 `[chunk][layer][row][col]`;`viewport.ts` / `block.ts` / `element.ts` / `constant.ts`;其它包与依赖配置。
⑥ **无行为变化主张与等价性论证:** 排序键顺序不变(层序 → y → x → 种类);`getLayerIndex(layer)` 的实现是 `layerIndexMap.get(layer) ?? -1` 的 O(1) 读取,逐次调用与旧的逐条目 `layerIndex` 字段取值完全相同,因此排序结果逐条相同;静态条目原样携带 `layer` / `x` / `y` / `mapIndex`,动态条目从 `{ moving }` 变成对象本身,二者都仍指向同一 `IMovingBlock` 与同一 `index` 槽位;组装逻辑的拷贝、rect 覆盖与计数不变;`kind` 的数值仍是 `Static = 0` / `Dynamic = 1`,比较器 `a.kind - b.kind` 的语义与旧枚举一致。**不改任何渲染行为。**
⑥ **无行为变化主张与等价性论证:** 排序键顺序不变(层序 → y → x → 同位置优先级);静态 `priority = 0`、动态 `priority = 1`,与旧 `kind` 的 `Static = 0` / `Dynamic = 1` 数值与语义一致,`a.priority - b.priority` 与 `a.kind - b.kind` 的比较结果逐条相同;`getLayerIndex(layer)` 是 `layerIndexMap.get(layer) ?? -1` 的 O(1) 读取,逐次调用与旧的逐条目 `layerIndex` 字段取值完全相同;静态块原样携带 `layer` / `x` / `y` / `mapIndex`,动态块从「包一层 / 打标记」变成「就是同一个 `MovingBlock` 实例」,二者都仍指向同一 `index` 槽位;组装逻辑的 16 float 拷贝、rect 覆盖与计数不变;`alphaMap` / 两个脏标记 / flush 落点不变。**不改任何渲染行为。**
⑦ **两处既有闸门漂移及处置(必须如实报备):** ① `rendererGate.test.ts` 的 4 条断言(`makes setStaticAlpha fully lazy`、`reads pending alpha before the instance array`、`covers pending, order and value in the overlay dirty flag`、`clears overlay members when the arrays are rebuilt`)断言的关键字是 `pendingAlpha`,而源码实际是 `alphaMap`,**当前恒红**;本计划把这 4 条断言的关键字对齐为 `alphaMap`(只改断言字符串,不改任何行为)。② `sorts by layer, y, x and kind and always reassembles afterwards` 断言 `a.layerIndex - b.layerIndex`,本次重构会移除该字段,故改为断言 `getLayerIndex(a.layer)`(其余 `a.y` / `a.x` / `a.kind` / `overlayValueDirty = true` 保留)。此外**新增 7 条**覆盖块抽象断言(`types.ts` 的枚举与接口、`IMovingBlock` 的继承与字面量类型、`MovingBlock.kind`、`vertex.ts` 的私有类型与联合、静态 push 与动态原样入列、比较器层序取值、组装以 `if` / `else if` 显式按 `kind` 收窄),每个 `it` 前一行注释。改完该文件应 37 条全部通过。
⑦ **两处既有闸门漂移及处置(必须如实报备):** ① `rendererGate.test.ts` 的 4 条断言(`makes setStaticAlpha fully lazy`、`reads pending alpha before the instance array`、`covers pending, order and value in the overlay dirty flag`、`clears overlay members when the arrays are rebuilt`)当前已对齐为 `alphaMap`,本次只**保持**对齐、不回退;② `sorts by layer, y, x and kind ...` 与 `reads the layer index live ...` 已对齐为 `getLayerIndex(a.layer)`,本次保留该形态。此外,上一版新增的 6 条种类相关断言(`declares the overlay block kind and interface in types`、`makes the moving block a dynamic overlay block`、`declares the moving block kind as dynamic`、`replaces the private draw entry with the overlay block union`、`pushes static blocks as literals and dynamic blocks themselves`、`narrows the assembled blocks by kind explicitly`)与两条用例里的 `a.kind - b.kind` 都要**改写**为类 / 优先级 / `instanceof` 断言,并新增状态类断言(`OverlayBlock` 是普通类、`super(layer, 1)`、`super(layer, 0)`、`readonly mapIndex`、无 `OverlayBlockKind`、无 `abstract`),每个 `it` 前一行注释。**全部类声明断言拆成 `export class X` / `extends Y` / `implements Z` 三条 `toContain`**(`StaticOverlayBlock` 的 85 列声明经 prettier 必然折行、`{` 独占一行,整串单行断言会与 eslint 冲突);`sorts by layer, y, x and kind ...` 上方那行「条目种类」注释同步改为 `priority` 语义(测试文件注释豁免)。
⑧ **验证方式(命令均为本机 PowerShell 5.1 可执行;`pnpm` 在本树已损坏,故直连二进制):** ① `node node_modules/vitest/vitest.mjs run packages-user/client-base/src/map/__test__/rendererGate.test.ts`;② `node node_modules/eslint/bin/eslint.js <4 个文件>`;③ `node -e` 包裹 `node_modules/vue-tsc/bin/vue-tsc.js --noEmit`,只取 `packages-user/client-base/src/map/` 诊断,**精确豁免** `renderer.ts` 的 `TS2339 getLayerByAlias` 一条并打印原文,其它任何 `map/` 诊断失败;④ 范围闸门:`git status --porcelain -uall -- packages-user/client-base/src/map` 的变更集必须**恰好**为 7 条既有状态(` M moving.ts` / ` M renderer.ts` / ` M shader/map.frag` / ` M status.ts` / ` M types.ts` / ` M vertex.ts` / `?? __test__/rendererGate.test.ts`);⑤ 哈希闸门:Task 1 在改代码前用 `node -e` 调 `git hash-object`,把「相对 `map/` 的路径 + 一个空格 + 哈希」逐行以 UTF-8(无 BOM)写入 `%TEMP%\gsd-09-35-hashes.txt`(`shader/map.frag` 保留相对路径,不是裸文件名),Task 2 按首个空格切分,用同一相对路径重算并逐字比较。**不做浏览器 UAT、不宣称 WebGL E2E。**
⑧ **验证方式(命令均为本机 PowerShell 5.1 可执行;`pnpm` 在本树已损坏,故直连二进制):** ① `node node_modules/vitest/vitest.mjs run packages-user/client-base/src/map/__test__/rendererGate.test.ts`;② `node node_modules/eslint/bin/eslint.js` 对 5 个代码文件;③ `node -e` 包裹 `node_modules/vue-tsc/bin/vue-tsc.js --noEmit`,先校验**闸门真正完成**(`r.error` / `r.signal` / `status===null` / 非 0 退出且无输出 一律判失败,禁止 `scoped=[]` 空过),再只取 `packages-user/client-base/src/map/` 下 `types.ts` / `status.ts` / `moving.ts` / `vertex.ts` 的诊断,**精确豁免** `types.ts` 的 `TS2305 TimingFn` 一条并打印原文(允许 0 或 1 条),其它任何诊断失败;`extension/hero.ts` 由过滤排除;④ 范围闸门:`git status --porcelain -uall -- packages-user/client-base/src/map` 的变更集必须**恰好**为 7 条既有状态(` M moving.ts` / ` M renderer.ts` / ` M shader/map.frag` / ` M status.ts` / ` M types.ts` / ` M vertex.ts` / `?? __test__/rendererGate.test.ts`),且**不得用 `.trim()` 削掉行首空格**;⑤ 哈希闸门:Task 1 改代码前用 `node -e` 调 `git hash-object`,把 `renderer.ts` 与 `shader/map.frag` 的「相对 `map/` 的路径 + 一个空格 + 哈希」逐行以 UTF-8(无 BOM)写入 `%TEMP%\gsd-09-35-hashes.txt`,Task 2 用同一相对路径重算并逐字比较(`status.ts` 由本计划合法修改,故不进哈希闸门)。**不做浏览器 UAT、不宣称 WebGL E2E。**
⑨ **范围:** 只写 6 个路径——`types.ts`、`moving.ts`、`vertex.ts`、`__test__/rendererGate.test.ts`、本计划、`09-35-SUMMARY.md`。`status.ts`(用户维护)、`renderer.ts`、`shader/map.frag`、其它 `map/` 文件、其它包、依赖与配置、09-01..09-34 的计划与摘要、`09-CONTEXT.md` 一律零改动。
⑨ **测试改动清单:** 删除 `OverlayBlockKind` / `kind` / `type OverlayBlock` / `IOverlayDrawEntry` / `OverlayEntryKind` 相关断言;改为断言 `types.ts` 的 `IOverlayBlock` 四成员(含 `readonly priority: number;`)与 `IOverlayStaticBlock` 的 `readonly mapIndex: number;`、`IMovingBlock` 的继承与「无 `readonly kind`」;断言 `status.ts` 的 `export class OverlayBlock implements IOverlayBlock`(含 `x: number = 0;` / `y: number = 0;` / `readonly priority: number;`)、`DynamicBlockStatus extends OverlayBlock implements IBlockStatus` + `super(layer, 1);`、`StaticOverlayBlock extends OverlayBlock implements IOverlayStaticBlock` + `super(layer, 0);` + `readonly mapIndex: number;`、无 `abstract`;断言 `moving.ts` 无 `OverlayBlockKind` / `readonly kind` / 重复 `layer` / `x` / `y` 与 `this.layer = layer`;断言 `vertex.ts` 的 `overlayOrdered: IOverlayBlock[]`、`new StaticOverlayBlock(`、`ordered.push(moving)`、`a.priority - b.priority`、`block instanceof MovingBlock`、`else if (block instanceof StaticOverlayBlock)`、`block.mapIndex` / `block.layer` / `block.index`,且方法体内无 `block.kind` / `entry.moving`。每个 `it` 前一行 `//` 注释(随范围同步改写)。所有类声明断言写成分离的 `toContain`(`export class StaticOverlayBlock` / `extends OverlayBlock` / `implements IOverlayStaticBlock`),**不得整串单行**——该声明 85 列,超过 `printWidth: 80`,prettier 实测会折成三行、`{` 独占一行。
⑩ **编号说明(必须报备):** 派发方给出的输出路径是 `09-27-PLAN.md` / 波次 22,但仓库中 `09-27`..`09-34` 已被既有已执行计划占用(其计划与摘要均已提交,内容与本次无关)。为不覆盖任何既有记录,本计划落在 `09-35-PLAN.md` / 波次 30。**若坚持使用 09-27,必须先明确确认删除既有 09-27 计划与摘要**——本计划未做、也不会擅自做。
⑩ **范围:** 只写 7 个路径——`types.ts`、`status.ts`、`moving.ts`、`vertex.ts`、`__test__/rendererGate.test.ts`、本计划、`09-35-SUMMARY.md`。`StaticBlockStatus` 以外的 `status.ts` 内容、`renderer.ts`、`shader/map.frag`、`viewport.ts` / `block.ts` / `element.ts` / `constant.ts`、其它包、依赖与配置、09-01..09-34 的计划与摘要、`09-CONTEXT.md` 一律零改动。
⑪ **两条铁律:** **不做浏览器 UAT、不宣称任何 WebGL E2E**(游戏当前不可运行,排序 / 组装 / 混合的视觉结果未验证,源码闸门通过不等于渲染结果已验证);**不提交任何代码**(禁止 `git add` / `commit` / `stash` / `checkout` / `restore` / `worktree` / `reset`),改动全部留在工作树供用户审查。
⑪ **编号说明(必须报备):** 派发方给出的输出路径是 `09-27-PLAN.md` / 波次 22,但仓库中 `09-27`..`09-34` 已被既有已执行计划占用(其计划与摘要均已提交,内容与本次无关)。为不覆盖任何既有记录,本计划落在 `09-35-PLAN.md` / 波次 30。**若坚持使用 09-27,必须先明确确认删除既有 09-27 计划与摘要**——本计划未做、也不会擅自做。
⑫ **已知限制:** 覆盖层组装仍对每条静态记录做一次 `getBlockByDataLoc` + `getLayerInstanced` 查找;同图层同行的不透明与覆盖层图块 z 相同,先绘制者胜出;动态块移动期间每帧重排;`addPostEffect` 无调用方;估算未校准(`confidence: low`)。
⑫ **两条铁律 + 类型基线 + 注释现状:** **不做浏览器 UAT、不宣称任何 WebGL E2E**(游戏当前不可运行,排序 / 组装 / 混合的视觉结果未验证,源码闸门通过不等于渲染结果已验证);**不提交任何代码**(禁止 `git add` / `commit` / `stash` / `checkout` / `restore` / `worktree` / `reset`),改动全部留在工作树供用户审查。类型闸门**不修复**用户自有的 `types.ts(14,10) TS2305 TimingFn`(打印并允许 0/1 条),也不修复 `renderer.ts` 的 `TS2339 getLayerByAlias`;`sortOverlay()` 的旧 jsDoc 与两行注释仍写 `kind` / `种类`,按 AGENTS.md 保持不动,是否改写由用户另行决定(OI-3)。**测试文件例外**:`rendererGate.test.ts` 的 `it` 说明注释随范围同步更新(AGENTS.md 豁免测试文件)。
⑬ **已知限制:** 覆盖层组装仍对每条静态记录做一次 `getBlockByDataLoc` + `getLayerInstanced` 查找;同图层同行的不透明与覆盖层图块 z 相同,先绘制者胜出;动态块移动期间每帧重排;每次排序为每条静态记录新建一个 `StaticOverlayBlock` 实例(与对象字面量同阶);`addPostEffect` 无调用方;估算未校准(`confidence: low`)。
</action>
<verify>用户恰好回复「可以执行」四个字;在此之前不执行 Task 1 / Task 2 的任何一步,也不派发执行子代理。</verify>
<done>已完整汇报 ①–⑫(含前后形态、逐文件符号、不改清单、待批准命名、无行为变化论证、两处闸门漂移处置、编号说明、两条铁律),并停下等待用户回复「可以执行」。</done>
<done>已完整汇报 ①–⑬(含类层次、优先级 0/1、`instanceof` 用法、逐文件符号、不改清单、待批准命名、无行为变化论证、测试改动清单、类型基线、两处漂移处置、编号说明、两条铁律、注释现状),并停下等待用户回复「可以执行」。</done>
<how-to-verify>
逐条核对 ①–⑫ 是否与你的意图一致,特别是:② 的精确前后形态(`OverlayBlockKind` / `IOverlayBlock` / `IMovingBlock` 的新继承与字面量种类、`MovingBlock.kind`、私有 `IOverlayStaticBlock` 与 `type OverlayBlock`、`overlayOrdered` 类型、静态 push 字面量 / 动态原样入列、比较器改用 `getLayerIndex(a.layer)`、组装以 `if` / `else if` 显式按 `kind` 收窄);④ 的「明确不改」清单(flush / 惰性 setter / 脏标记 / 组装拷贝 / renderer 两遍式与后处理 / 分块张量 / 动态索引身份 / `status.ts` / `map.frag`);⑤ 的命名清单与备选;⑥ 的「无行为变化」等价性论证;⑦ 两处既有闸门漂移的处置以及新增 7 条断言;⑩ 的编号说明(09-27..09-34 已占用,本计划为 09-35 / 波次 30);⑪ 的两条铁律(不做浏览器 UAT、不宣称 WebGL E2E、不提交任何代码)。
逐条核对 ①–⑬ 是否与你的意图一致,特别是:② 的精确类层次(`OverlayBlock` 是普通类、`DynamicBlockStatus extends OverlayBlock` + `super(layer, 1)`、`StaticOverlayBlock extends OverlayBlock` + `super(layer, 0)`、`IOverlayBlock.priority`、删除 `OverlayBlockKind`);③ 的 `instanceof` 两分支;⑤ 的「明确不改」清单(`StaticBlockStatus` / flush / 惰性 setter / 脏标记 / 组装拷贝 / renderer 两遍式与后处理 / 分块张量 / 动态索引身份 / `map.frag`);⑥ 的「无行为变化」等价性论证;⑨ 的测试改动清单;⑫ 的类型基线豁免、两条铁律与注释现状;⑪ 的编号说明(09-27..09-34 已占用,本计划为 09-35 / 波次 30)。
</how-to-verify>
<resume-signal>仅当完全同意时回复恰好四个字「可以执行」;任何修改意见(尤其是 ② 的结构形态、⑤ 的命名、⑦ 的闸门漂移处置、⑩ 的编号)请直接说明,本计划会据此改写后再来一次汇报。</resume-signal>
<resume-signal>仅当完全同意时回复恰好四个字「可以执行」;任何修改意见(尤其是 ② 的类层次与优先级取值、⑤ 的命名、⑦ 的漂移处置、⑫ 的类型基线口径、⑪ 的编号)请直接说明,本计划会据此改写后再来一次汇报。</resume-signal>
</task>
<task type="auto">
<name>Task 1: 增加覆盖块抽象(types.ts / moving.ts / vertex.ts)</name>
<files>packages-user/client-base/src/map/types.ts, packages-user/client-base/src/map/moving.ts, packages-user/client-base/src/map/vertex.ts</files>
<name>Task 1: 用普通类 + priority 实现覆盖块体系(types.ts / status.ts / moving.ts / vertex.ts)</name>
<files>packages-user/client-base/src/map/types.ts, packages-user/client-base/src/map/status.ts, packages-user/client-base/src/map/moving.ts, packages-user/client-base/src/map/vertex.ts</files>
<action>
仅在收到用户恰好回复「可以执行」之后执行;开始改代码前先运行下面的哈希采集命令,把 `status.ts` / `renderer.ts` / `shader/map.frag` 的当前 `git hash-object` 写入 `%TEMP%\gsd-09-35-hashes.txt` 供 Task 2 逐字比较。该命令可直接在 PowerShell 5.1 执行,输出为 UTF-8 无 BOM,每行「相对 `map/` 的路径 + 一个空格 + 哈希」(`shader/map.frag` 保留相对路径而不是裸文件名,因为 Task 2 的读取端会统一加 `packages-user/client-base/src/map/` 前缀):`node -e "const{execSync}=require('child_process'),fs=require('fs');const dir='packages-user/client-base/src/map';const files=['status.ts','renderer.ts','shader/map.frag'];const lines=files.map(f=>f+' '+execSync('git hash-object '+dir+'/'+f,{encoding:'utf8'}).trim());const p=(process.env.TEMP||'.')+'/gsd-09-35-hashes.txt';fs.writeFileSync(p,lines.join('\n')+'\n','utf8');console.log(p);console.log(lines.join('\n'))"`。采集非 0 退出或文件缺失必须停下上报,不得跳过哈希闸门。
仅在收到用户恰好回复「可以执行」之后执行;开始改代码前先运行下面的哈希采集命令,把 `renderer.ts` / `shader/map.frag` 的当前 `git hash-object` 写入 `%TEMP%\gsd-09-35-hashes.txt` 供 Task 2 逐字比较。该命令可直接在 PowerShell 5.1 执行,输出为 UTF-8 无 BOM,每行「相对 `map/` 的路径 + 一个空格 + 哈希」:`node -e "const{execSync}=require('child_process'),fs=require('fs');const dir='packages-user/client-base/src/map';const files=['renderer.ts','shader/map.frag'];const lines=files.map(f=>f+' '+execSync('git hash-object '+dir+'/'+f,{encoding:'utf8'}).trim());const p=(process.env.TEMP||'.')+'/gsd-09-35-hashes.txt';fs.writeFileSync(p,lines.join('\n')+'\n','utf8');console.log(p);console.log(lines.join('\n'))"`。采集非 0 退出或文件缺失必须停下上报,不得跳过哈希闸门。`status.ts` 由本计划合法修改,不进哈希闸门。
1. `types.ts`:在 `//#region 枚举值` 末尾(`MapTileAlign` 之后、`//#endregion` 之前)新增 `export const enum OverlayBlockKind`,成员 `Static`(注释:静态图块,排序时先于动态图块)与 `Dynamic`(注释:动态图块)。在 `IBlockStatus` 之后、`IMovingBlock` 之前新增 `export interface IOverlayBlock`,成员 `readonly kind: OverlayBlockKind`(覆盖块种类)、`readonly layer: IMapLayer`(所属图层)、`readonly x: number`(横坐标,动态块可为小数)、`readonly y: number`(纵坐标,动态块可为小数),各一行 jsDoc。把 `IMovingBlock` 的声明改为 `export interface IMovingBlock extends IBlockStatus, IOverlayBlock {`,并在 `readonly index: number;` 之后新增 `readonly kind: OverlayBlockKind.Dynamic;`(不重复写 jsDoc)。其余声明与既有注释逐字不动。
1. `types.ts`:删除整个 `//#region 枚举值` 内的 `export const enum OverlayBlockKind`(含 `Static` / `Dynamic` 两个成员及其两行成员注释),`//#region 枚举值` 与 `//#endregion` 的其余内容与注释逐字不动。把 `IOverlayBlock` 改为只有四个成员:`readonly layer: IMapLayer`(注释「所属图层」)、`x: number`(注释「横坐标,动态块可为小数」)、`y: number`(注释「纵坐标,动态块可为小数」)、**新增** `readonly priority: number`(注释「同一排序位置时的绘制优先级,数值小的先绘制」);删除 `readonly kind` 成员及其注释,`layer` / `x` / `y` 的既有注释逐字保留。在 `IOverlayBlock` 之后新增 `export interface IOverlayStaticBlock extends IOverlayBlock`,只追加一个成员 `readonly mapIndex: number`(注释「静态图块在地图中的索引」)。`IMovingBlock` 的声明保持 `export interface IMovingBlock extends IBlockStatus, IOverlayBlock {`,删除 `readonly kind: OverlayBlockKind.Dynamic;` 一行,其余成员(含 `readonly x: number;` / `readonly y: number;`)与全部方法、注释逐字不动。不得改第 14 行的 `TimingFn` 引入(用户自有)。
2. `moving.ts`:从 `./types` 的既有 import 追加 `OverlayBlockKind`(保持普通引入,禁止 `import type`);在 `MovingBlock` 的 `readonly layer: IMapLayer;` 之后新增 `readonly kind: OverlayBlockKind.Dynamic = OverlayBlockKind.Dynamic;`(显式类型注解;不重复写 jsDoc,继承的说明已足够)。不修改 `index` / `x` / `y` / 动画逻辑与其它既有内容。
2. `status.ts`:把 `./types` 的既有引入扩展为同时引入 `IOverlayBlock` 与 `IOverlayStaticBlock`(普通引入,禁止 `import type`)。在 `StaticBlockStatus` 之前新增 `export class OverlayBlock implements IOverlayBlock`,成员依次为:`readonly layer: IMapLayer;`(成员 jsDoc「所属图层」)、`x: number = 0;`(「覆盖块横坐标」)、`y: number = 0;`(「覆盖块纵坐标」)、`readonly priority: number;`(「同一排序位置时的绘制优先级,数值小的先绘制」);构造器 `(layer: IMapLayer, priority: number)` 内只做 `this.layer = layer;` 与 `this.priority = priority;`,**不给构造器写注释**(成员已单独声明并各自带 jsDoc)。不得使用 `abstract`,不得写成 `interface`。
3. `vertex.ts`:从 `./types` 的既有 import 追加 `IOverlayBlock` 与 `OverlayBlockKind`。删除私有接口 `IOverlayDrawEntry` 与私有 `const enum OverlayEntryKind`;在原 `IOverlayDrawEntry` 位置新增私有 `interface IOverlayStaticBlock extends IOverlayBlock`,成员为 `readonly kind: OverlayBlockKind.Static`(覆盖块种类,静态图块固定为静态)与 `readonly mapIndex: number`(静态图块在地图中的索引),各带 jsDoc;其后新增私有 `type OverlayBlock = IOverlayStaticBlock | IMovingBlock;`(不加头注释)。把 `overlayOrdered` 的类型改为 `OverlayBlock[]`,并把 `sortOverlay()` 内的局部声明 `const ordered: IOverlayDrawEntry[] = []` 改为 `const ordered: OverlayBlock[] = []`(不改则类型检查失败)。
3. `status.ts`:把 `DynamicBlockStatus` 的声明改为 `export class DynamicBlockStatus extends OverlayBlock implements IBlockStatus {`;构造器第一个形参由 `readonly layer: IMapLayer` 改为 `layer: IMapLayer`(去掉 `readonly`,`layer` 改由基类持有),构造器体首句加 `super(layer, 1);`(动态覆盖块优先级 1),`readonly vertex` / `readonly index` 两个形参与其属性声明保持不变、`super` 之后不再写其它语句;构造器上方那段 `@param layer` / `@param vertex` / `@param index` 的既有 jsDoc 逐字保留;`setAlpha` / `getAlpha` / `useGlobalFrame` / `useSpecifiedFrame` / `getFrame` 全部逐字不动。该文件下方的 `StaticBlockStatus`(含用户已改好的 `setAlpha` 参数顺序)一律不改,且 `OverlayBlock` 必须声明在 `DynamicBlockStatus` 与 `StaticOverlayBlock` 之前。
4. `vertex.ts` 的 `sortOverlay()`:静态分支改为 push 对象字面量 `{ kind: OverlayBlockKind.Static, layer, x: mapIndex % this.mapWidth, y: Math.floor(mapIndex / this.mapWidth), mapIndex }`;动态分支改为 `ordered.push(moving);`(使用 `this.renderer.getMovingBlock()` 的 `Readonly<IMovingBlock>` 元素本身,不再包 `{ moving }`,不再逐条取 `layerIndex`);同时删除 `this.overlayStatic.forEach((records, layer) => {` 内部那行 `const layerIndex = this.renderer.getLayerIndex(layer);`——比较器改为现取后它已无使用者,保留会被 eslint 判为未使用变量(`layer` 形参仍被字面量使用,保持不变)。比较器改为 `this.renderer.getLayerIndex(a.layer) - this.renderer.getLayerIndex(b.layer) || a.y - b.y || a.x - b.x || a.kind - b.kind`;保留「静态先于动态 / x 只是 CPU 侧稳定次级键」的既有两行注释与结尾的 `overlayOrdered = ordered`、`overlayOrderDirty = false`、`overlayValueDirty = true`。该函数的 jsDoc 保持不变。
4. `status.ts`:在 `DynamicBlockStatus` 之后新增 `export class StaticOverlayBlock extends OverlayBlock implements IOverlayStaticBlock`,成员 `readonly mapIndex: number;`(成员 jsDoc「静态图块在地图中的索引」);构造器 `(layer: IMapLayer, mapIndex: number, x: number, y: number)` 内依次执行 `super(layer, 0);`(静态覆盖块优先级 0)、`this.mapIndex = mapIndex;`、`this.x = x;`、`this.y = y;`。**不给构造器写注释**;不得使用 `abstract`。该声明的单行为 85 列,`prettier --write` 会折成 `export class StaticOverlayBlock` / `extends OverlayBlock` / `implements IOverlayStaticBlock` 三行并把 `{` 放在单独一行——这是**期望形态**,不得加 `// prettier-ignore`,也不得为凑单行而缩短命名。
5. `vertex.ts` 的 `assembleOverlay()`:循环变量由 `entry` 改为 `block`,并写成**两个显式判别分支**:`if (block.kind === OverlayBlockKind.Dynamic)` 分支用 `block.index * INSTANCED_COUNT` 与 `dynamicInstancedArray` 拷贝;`else if (block.kind === OverlayBlockKind.Static)` 分支移除 `const mapIndex = entry.mapIndex;` 与 `isNil(mapIndex)` 守卫,直接 `const record = this.getOverlayStaticRecord(block.layer, block.mapIndex);`、`const index = this.getIndexInBlock(block.layer, block.x, block.y);`。**静态分支不得省略成 `else`、也不得写成裸落空分支**:Task 2 的闸门要求 `assembleOverlay()` 源码同时出现 `OverlayBlockKind.Dynamic` 与 `OverlayBlockKind.Static`,两个显式判别(`if` 与 `else if`)都是该联合类型的合法收窄,行为与「动态判定 + 落空静态」完全等价。16 float 拷贝、前四个 rect 浮点覆盖、`overlayCount`、清两个脏标记全部逐字保持。该函数的 jsDoc 保持不变。
5. `moving.ts`:从 `./types` 的引入列表删除 `OverlayBlockKind`(其余引入项与顺序不动)。删除 `MovingBlock` 的 `readonly layer: IMapLayer;` 声明、`readonly kind: OverlayBlockKind.Dynamic = OverlayBlockKind.Dynamic;` 声明、`x: number = 0;` 与 `y: number = 0;` 声明(四者都改由 `DynamicBlockStatus` / `OverlayBlock` 继承);删除构造器里的 `this.layer = layer;`(`layer` 在基类是只读的,再赋值会触发类型错误),保留 `super(layer, renderer.vertex, index);`、`this.renderer = renderer;`、`this.index = index;` 与其余赋值;保留 `index: number;` 声明(子类可写覆盖基类只读)。保留 `IMapLayer` 引入(构造器形参仍用它)。**保留用户的 `TimingFn` → `ExcitationCurve` / `ExcitationCurve2D` 改写逐字不动**,不得把 `lineTo` / `moveAs` / `moveRelative` 的 `timing` 参数改回 `TimingFn`,不得改任何动画逻辑。
6. **不得改动** `flushPendingAlpha`、`setStaticAlpha` / `getStaticAlpha` / 帧数方法、`alphaMap` / `overlayStatic` / 两个脏标记的其它赋值点、`getVertexArray` / `getOverlayArray` / `getOverlayCount`、`checkRebuild` / `setBlockSize` / `updateMoving` / `deleteMoving`、`IOverlayStaticRecord` 与记录读写方法;不得改 `renderer.ts` / `status.ts` / `shader/map.frag` / 其它任何文件;不得改任何既有注释或 jsDoc;代码文件保持 CRLF。
6. `vertex.ts`:把 `./types` 的引入列表中的 `OverlayBlockKind` 删除、保留 `IOverlayBlock`;新增两条普通引入 `import { MovingBlock } from './moving';` 与 `import { StaticOverlayBlock } from './status';`,放在其它相对引入之间,**所有 import 之间不得插入注释或空行**(dev.md)。删除私有 `interface IOverlayStaticBlock extends IOverlayBlock { ... }` 与私有 `type OverlayBlock = IOverlayStaticBlock | IMovingBlock;`(连同它们各自的成员注释)。把 `overlayOrdered` 的类型改为 `IOverlayBlock[]`(保留其既有成员注释)。
7. `vertex.ts` 的 `sortOverlay()`:局部声明改为 `const ordered: IOverlayBlock[] = [];`;静态分支改为 `ordered.push(new StaticOverlayBlock(layer, mapIndex, mapIndex % this.mapWidth, Math.floor(mapIndex / this.mapWidth)));`(按 prettier 折行到 80 列内,`new StaticOverlayBlock(` 必须是完整连续的字符序列);动态分支改为 `ordered.push(moving);`(`this.renderer.getMovingBlock()` 的元素本身,不复制、不包 `{ moving }`);比较器末位由 `a.kind - b.kind` 改为 `a.priority - b.priority`,其余三项仍是 `this.renderer.getLayerIndex(a.layer) - this.renderer.getLayerIndex(b.layer)` / `a.y - b.y` / `a.x - b.x`;保留函数末尾的 `this.overlayOrdered = ordered;`、`this.overlayOrderDirty = false;`、`this.overlayValueDirty = true;`。**该函数既有的 jsDoc 与函数内两行 `//` 注释(仍写 `kind` / `种类`)逐字保留,不得修改**(OI-3)。
8. `vertex.ts` 的 `assembleOverlay()`:循环变量保持 `block`,写成两个显式判别分支——`if (block instanceof MovingBlock) { ... count++; return; }` 分支用 `const source = block.index * INSTANCED_COUNT;` 与 `this.dynamicInstancedArray.subarray(source, source + INSTANCED_COUNT)` 拷贝到 `offset`;`else if (block instanceof StaticOverlayBlock) { ... }` 分支用 `const record = this.getOverlayStaticRecord(block.layer, block.mapIndex);` 与 `const index = this.getIndexInBlock(block.layer, block.x, block.y);`,随后仍是 16 float 拷贝、前四个浮点覆盖为 `record.rectX` / `record.rectY` / `record.rectWidth` / `record.rectHeight`、`count++`。**静态分支不得省略成 `else`、不得写成裸落空分支**(Task 2 的闸门要求方法体内同时出现两种 `instanceof`)。`this.overlayCount = count;`、`this.overlayValueDirty = false;`、`this.overlayOrderDirty = false;` 与扩容逻辑逐字保持;该函数既有 jsDoc 保持不变。
9. **不得改动** `StaticBlockStatus`(含 `setAlpha` 参数顺序)、`flushPendingAlpha`、`setStaticAlpha` / `getStaticAlpha` / 帧数方法、`alphaMap` / `overlayStatic` / 两个脏标记的其它赋值点、`getVertexArray` / `getOverlayArray` / `getOverlayCount`、`checkRebuild` / `setBlockSize` / `updateMoving` / `deleteMoving`、`IOverlayStaticRecord` 与记录读写方法、`IMapDataGetter` / `MapVertexBlock` 等文件内其它结构;不得改 `renderer.ts` / `shader/map.frag` / `viewport.ts` / `block.ts` / `element.ts` / `constant.ts` / 其它任何文件;不得改任何既有注释或 jsDoc(只允许新增);不得新增文件、类(除 `OverlayBlock` / `StaticOverlayBlock` 两个类)或其它导出;代码文件保持 CRLF,必要时可对 4 个源码文件运行 `node node_modules/prettier/bin/prettier.cjs --write` 统一格式(仅排版)。
</action>
<verify>
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('packages-user/client-base/src/map/types.ts','utf8');const v=fs.readFileSync('packages-user/client-base/src/map/vertex.ts','utf8');const m=fs.readFileSync('packages-user/client-base/src/map/moving.ts','utf8');const s=v.slice(v.indexOf('private sortOverlay('),v.indexOf('private assembleOverlay('));const a=v.slice(v.indexOf('private assembleOverlay('));const checks=[/export const enum OverlayBlockKind/.test(t),/export interface IOverlayBlock\s*\{/.test(t),/export interface IMovingBlock\s+extends\s+IBlockStatus\s*,\s*IOverlayBlock\s*\{/.test(t),/readonly kind: OverlayBlockKind\.Dynamic;/.test(t),/readonly kind: OverlayBlockKind\.Dynamic\s*=/.test(m),!v.includes('IOverlayDrawEntry'),!v.includes('OverlayEntryKind'),v.includes('interface IOverlayStaticBlock extends IOverlayBlock'),v.includes('type OverlayBlock = IOverlayStaticBlock | IMovingBlock'),v.includes('overlayOrdered: OverlayBlock[]'),v.includes('const ordered: OverlayBlock[]'),v.includes('ordered.push(moving)'),v.includes('getLayerIndex(a.layer)'),!s.includes('const layerIndex'),a.includes('block.kind === OverlayBlockKind.Dynamic'),a.includes('else if (block.kind === OverlayBlockKind.Static)'),a.includes('block.mapIndex'),a.includes('block.index'),a.includes('block.layer')];const bad=checks.filter(c=>!c).length;console.log('shape checks',checks.length,'failed',bad);process.exit(bad?1:0)"</automated>
<fails_when>任一形状检查为假(例如仍残留 `IOverlayDrawEntry` / `OverlayEntryKind`、`IMovingBlock` 未继承 `IOverlayBlock`、比较器仍用 `a.layerIndex`、`sortOverlay()` 仍留逐图层 `const layerIndex`、动态分支仍在包 `{ moving }`、`assembleOverlay()` 缺少显式 `else if (... Static)` 分支),退出码非 0。</fails_when>
<automated>node -e "const fs=require('fs');const r=f=>fs.readFileSync('packages-user/client-base/src/map/'+f,'utf8');const t=r('types.ts'),s=r('status.ts'),m=r('moving.ts'),v=r('vertex.ts');const sortBody=v.slice(v.indexOf('private sortOverlay('),v.indexOf('private assembleOverlay('));const asmBody=v.slice(v.indexOf('private assembleOverlay('));const dyn=s.slice(s.indexOf('export class DynamicBlockStatus'));const sta=s.slice(s.indexOf('export class StaticOverlayBlock'));const checks=[[/export interface IOverlayBlock\s*\{/.test(t),'IOverlayBlock'],[/export interface IOverlayStaticBlock\s+extends\s+IOverlayBlock\s*\{/.test(t),'IOverlayStaticBlock'],[/export interface IMovingBlock\s+extends\s+IBlockStatus\s*,\s*IOverlayBlock\s*\{/.test(t),'IMovingBlock extends'],[/readonly priority: number;/.test(t),'IOverlayBlock.priority'],[/readonly mapIndex: number;/.test(t),'IOverlayStaticBlock.mapIndex'],[!t.includes('OverlayBlockKind'),'no OverlayBlockKind in types'],[s.includes('export class OverlayBlock implements IOverlayBlock {'),'OverlayBlock class'],[s.includes('readonly priority: number;'),'priority field'],[s.includes('x: number = 0;')&&s.includes('y: number = 0;'),'x/y fields'],[!s.includes('abstract'),'not abstract'],[s.includes('export class DynamicBlockStatus')&&dyn.includes('extends OverlayBlock')&&dyn.includes('implements IBlockStatus'),'DynamicBlockStatus extends'],[dyn.includes('super(layer, 1);'),'dynamic priority 1'],[s.includes('export class StaticOverlayBlock')&&sta.includes('extends OverlayBlock')&&sta.includes('implements IOverlayStaticBlock'),'StaticOverlayBlock extends'],[sta.includes('super(layer, 0);'),'static priority 0'],[sta.includes('readonly mapIndex: number;'),'static mapIndex'],[m.includes('export class MovingBlock')&&m.includes('extends DynamicBlockStatus')&&m.includes('implements IMovingBlock'),'MovingBlock extends'],[!m.includes('OverlayBlockKind')&&!m.includes('readonly kind'),'no kind in moving'],[!m.includes('this.layer = layer;'),'no layer assign'],[!m.includes('readonly layer: IMapLayer;'),'no duplicate layer'],[!m.includes('x: number = 0;')&&!m.includes('y: number = 0;'),'no duplicate x/y'],[v.includes('overlayOrdered: IOverlayBlock[]'),'overlayOrdered type'],[v.includes('const ordered: IOverlayBlock[]'),'ordered type'],[v.includes('new StaticOverlayBlock('),'static push'],[sortBody.includes('ordered.push(moving)'),'dynamic push itself'],[sortBody.includes('a.priority - b.priority'),'priority comparator'],[!sortBody.includes('block.kind'),'no kind comparator'],[!v.includes('IOverlayDrawEntry')&&!v.includes('OverlayEntryKind')&&!v.includes('OverlayBlockKind'),'no legacy types'],[!v.includes('type OverlayBlock ='),'no private union'],[asmBody.includes('block instanceof MovingBlock'),'instanceof moving'],[asmBody.includes('else if (block instanceof StaticOverlayBlock)'),'instanceof static'],[asmBody.includes('block.mapIndex')&&asmBody.includes('block.layer')&&asmBody.includes('block.index'),'field access']];const bad=checks.filter(c=>!c[0]);console.log('shape checks',checks.length,'failed',bad.length);bad.forEach(c=>console.log('FAIL:',c[1]));process.exit(bad.length?1:0)"</automated>
<fails_when>任一形状检查为假(例如仍残留 `OverlayBlockKind` / `type OverlayBlock`、`IMovingBlock` 未继承 `IOverlayBlock`、`OverlayBlock` 被写成 `abstract`、`DynamicBlockStatus` 未 `super(layer, 1)`、`StaticOverlayBlock` 未 `super(layer, 0)`、比较器仍用 `a.kind`、动态分支仍在包 `{ moving }`、组装缺少显式 `else if (block instanceof StaticOverlayBlock)`),退出码非 0。类声明检查已拆分为 `export class X` / `extends Y` / `implements Z` 三条独立判定,prettier 把 85 列的 `StaticOverlayBlock` 声明折行不影响结果。</fails_when>
</verify>
<done>三个源码文件改完:`types.ts` 有 `OverlayBlockKind` / `IOverlayBlock` / `IMovingBlock.kind`;`moving.ts` 的 `MovingBlock` 有固定为动态的 `kind`;`vertex.ts` 用 `IOverlayStaticBlock` + `type OverlayBlock` 取代绘制条目与条目枚举,`overlayOrdered` 与局部 `ordered` 均为 `OverlayBlock[]`,静态 push 字面量、动态原样 push、比较器用 `getLayerIndex(a.layer)` 且删除逐图层 `const layerIndex`、组装以 `if (... Dynamic)` / `else if (... Static)` 显式收窄;哈希采集(UTF-8 无 BOM、路径相对 `map/`)已写入 `%TEMP%\gsd-09-35-hashes.txt`;其余文件零改动。</done>
<done>四个源码文件改完:`types.ts` 无 `OverlayBlockKind`,`IOverlayBlock` 含 `priority` 且新增 `IOverlayStaticBlock`,`IMovingBlock` 无 `kind`;`status.ts` 有普通类 `OverlayBlock`、`DynamicBlockStatus extends OverlayBlock` + `super(layer, 1)`、`StaticOverlayBlock extends OverlayBlock` + `super(layer, 0)`,`StaticBlockStatus` 未变;`moving.ts` 无 `kind` 与重复的 `layer` / `x` / `y`,曲线类型改写保留;`vertex.ts` 无私有联合类型,`overlayOrdered: IOverlayBlock[]`,静态 push `new StaticOverlayBlock(...)`、动态 push `moving` 本身、比较器用 `a.priority`、组装用 `instanceof` 两分支;哈希采集(UTF-8 无 BOM、路径相对 `map/`,仅 `renderer.ts` / `shader/map.frag`)已写入 `%TEMP%\gsd-09-35-hashes.txt`;其余文件零改动。</done>
</task>
<task type="auto">
<name>Task 2: 同步闸门测试、跑四类门禁并写 SUMMARY</name>
<name>Task 2: 同步闸门测试、跑五类门禁并写 SUMMARY</name>
<files>packages-user/client-base/src/map/__test__/rendererGate.test.ts, .planning/phases/09-minimum-browser-runnable/09-35-SUMMARY.md</files>
<action>
1. 修改 `rendererGate.test.ts` 中 4 条既有断言的关键字:`makes setStaticAlpha fully lazy`、`reads pending alpha before the instance array`、`covers pending, order and value in the overlay dirty flag`(`pendingAlpha.size`)、`clears overlay members when the arrays are rebuilt`(`pendingAlpha.clear()`)——把断言字符串里的 `pendingAlpha` 对齐为源码实际使用的 `alphaMap`(只改断言字符串,不改任何既有注释)。
1. 在 `rendererGate.test.ts` 的 `?raw` 引入区新增 `import statusSource from '../status.ts?raw';`(与其他 `?raw` 引入连续书写,中间不插注释与空行)。**保留**既有的 4 条 `alphaMap` 断言(`makes setStaticAlpha fully lazy` / `reads pending alpha before the instance array` / `covers pending, order and value in the overlay dirty flag` / `clears overlay members when the arrays are rebuilt`)与 1 条 `getLayerIndex(a.layer)` 对齐,不得回退成 `pendingAlpha` / `a.layerIndex`。**测试文件注释例外**:AGENTS.md 将测试文件豁免于注释禁令,dev.md 要求测试范围变化时同步更新说明;因此必须重写第 141 行(以 `sorts by layer, y, x and kind ...` 用例为准,行号会因新增用例而移动)上方那行「排序键依次为图层、纵坐标、横坐标与条目种类」为 `priority` 语义(如「排序键依次为图层、纵坐标、横坐标与绘制优先级」),其余被改名用例的 `it` 注释也同步改写。除测试文件外,任何既有注释与 jsDoc 仍不得修改。
2. 修改 `sorts by layer, y, x and kind and always reassembles afterwards`:把 `expect(body).toContain('a.layerIndex - b.layerIndex')` 改为断言 `getLayerIndex(a.layer)` 与 `getLayerIndex(b.layer)`,保留 `a.y - b.y`、`a.x - b.x`、`a.kind - b.kind`、`overlayValueDirty = true`,并断言不再含 `a.layerIndex`。
2. 改写 6 条种类相关用例(每条仍是一行 `//` 注释 + 一个 `it`):① `declares the overlay block kind and interface in types` → `declares the overlay block interfaces with a priority`:取 `typesSource` 中 `export interface IOverlayBlock` 到其后第一个 `\n}` 的片段,断言含 `readonly layer: IMapLayer;` / `x: number;` / `y: number;` / `readonly priority: number;`,且该片段不含 `kind`、整份 `typesSource` 不含 `OverlayBlockKind`;再断言 `export interface IOverlayStaticBlock` 片段含 `readonly mapIndex: number;`。② `makes the moving block a dynamic overlay block` → `makes the moving block an overlay block`:正则断言 `/export interface IMovingBlock\s+extends\s+IBlockStatus\s*,\s*IOverlayBlock\s*\{/`,且 `typesSource` 不含 `readonly kind:`。③ `declares the moving block kind as dynamic` → `removes the moving block kind in favour of inheritance`:断言 `movingSource` 分别含 `export class MovingBlock`、`extends DynamicBlockStatus`、`implements IMovingBlock`(三条独立 `toContain`,不得整串单行),不含 `OverlayBlockKind` / `readonly kind` / `this.layer = layer;`。④ `replaces the private draw entry with the overlay block union` → `removes the private draw entry and uses the overlay block interface`:断言 `vertexSource` 不含 `IOverlayDrawEntry` / `OverlayEntryKind` / `OverlayBlockKind` / `type OverlayBlock =`,含 `overlayOrdered: IOverlayBlock[]`、`from './moving'`、`from './status'`。⑤ `pushes static blocks as literals and dynamic blocks themselves` → `pushes static overlay blocks and dynamic blocks themselves`:对 `private sortOverlay(` 方法体断言含 `new StaticOverlayBlock(` 与 `ordered.push(moving)`,不含 `kind:` 与 `moving:`。⑥ `narrows the assembled blocks by kind explicitly` → `narrows the assembled blocks by class`:对 `private assembleOverlay(` 方法体断言含 `block instanceof MovingBlock` / `else if (block instanceof StaticOverlayBlock)` / `block.index` / `block.mapIndex` / `block.layer`,不含 `block.kind` 与 `entry.moving`。
3. 追加 7 条新用例(每个 `it` 前一行中文注释):① `types.ts` 的 `OverlayBlockKind`(`Static` / `Dynamic`)与 `IOverlayBlock` 的四个成员;② `IMovingBlock` 以 `extends IBlockStatus, IOverlayBlock` 声明并含 `readonly kind: OverlayBlockKind.Dynamic;`;③ `moving.ts` 引入 `OverlayBlockKind` 且 `MovingBlock` 含 `readonly kind: OverlayBlockKind.Dynamic =`;④ `vertex.ts` 不再含 `IOverlayDrawEntry` / `OverlayEntryKind`,且含 `interface IOverlayStaticBlock extends IOverlayBlock`、`type OverlayBlock = IOverlayStaticBlock | IMovingBlock`、`overlayOrdered: OverlayBlock[]`;⑤ `sortOverlay` 含 `kind: OverlayBlockKind.Static` 与 `ordered.push(moving)`,且不含 `moving:`;⑥ `sortOverlay` 的比较器用 `getLayerIndex(a.layer)` / `getLayerIndex(b.layer)`(含 `a.y - b.y` / `a.x - b.x` / `a.kind - b.kind` / `overlayValueDirty = true`);⑦ 用 `getMethodBody(vertexSource, 'private assembleOverlay(')` 取出该方法体,断言其中含 `OverlayBlockKind.Dynamic`、`else if (block.kind === OverlayBlockKind.Static)`、`block.index`、`block.mapIndex`、`block.layer`,且不含 `entry.moving`(必须取方法体而不是整份源码,否则 `OverlayBlockKind.Static` 可能来自 `sortOverlay()` 的静态 push 而失去意义)。断言全部基于 `?raw` 源码字符串,不导入任何运行期模块。
3. 改写两条排序用例:`sorts by layer, y, x and kind and always reassembles afterwards` 改名为 `sorts by layer, y, x and priority and always reassembles afterwards`,把 `a.kind - b.kind` 断言换成 `a.priority - b.priority`(其余 `getLayerIndex(a.layer)` / `getLayerIndex(b.layer)` / `a.y - b.y` / `a.x - b.x` / `overlayValueDirty = true` / 不含 `a.layerIndex` 全部保留);`reads the layer index live from the renderer when sorting` 同样把 `a.kind - b.kind` 换成 `a.priority - b.priority`。
4. 依次运行四类门禁并记录真实输出(命令见 `<verify>` 与下方):vitest 闸门文件必须 37 条全绿;eslint 对 4 个文件 0 error / 0 warning;类型闸门只对 `packages-user/client-base/src/map/` 负责,必须恰好打印 1 条 `renderer.ts` 的 `TS2339 getLayerByAlias` 豁免原文且其它 0 条;范围闸门必须恰好 7 条既有状态;哈希闸门读取 `%TEMP%\gsd-09-35-hashes.txt`(UTF-8 无 BOM,每行「相对 `map/` 的路径 + 空格 + 哈希」),按首个空格切分后用同一相对路径重算并逐字相等(`status.ts` / `renderer.ts` / `shader/map.frag` 未被本计划改动)。任何一项失败:停下并如实报告,不得放宽断言、不得扩大豁免、不得改配置文件。
4. 新增 3 条状态类用例(每个 `it` 前一行中文 `//` 注释)。**全部类声明断言一律拆成独立 `toContain`(`export class X` / `extends Y` / `implements Z`),禁止整串单行比对**——`StaticOverlayBlock` 的完整声明为 85 列,prettier 实测会折成三行、`{` 独占一行,整串断言会与 eslint 的 prettier 规则直接冲突(其一必红)。① `declares the overlay block as a concrete class with a priority`——断言 `statusSource` 分别含 `export class OverlayBlock`、`implements IOverlayBlock`,以及 `readonly layer: IMapLayer;`、`x: number = 0;`、`y: number = 0;`、`readonly priority: number;`,且不含 `abstract class`。② `makes the dynamic block status an overlay block of priority one`——断言分别含 `export class DynamicBlockStatus`、`extends OverlayBlock`、`implements IBlockStatus`,且从 `statusSource.indexOf('export class DynamicBlockStatus')` 起切片的片段含 `super(layer, 1);`。③ `declares the static overlay block of priority zero`——断言分别含 `export class StaticOverlayBlock`、`extends OverlayBlock`、`implements IOverlayStaticBlock`,且从 `statusSource.indexOf('export class StaticOverlayBlock')` 起切片的片段含 `super(layer, 0);` 与 `readonly mapIndex: number;`。断言全部基于 `?raw` 源码字符串,不导入任何运行期模块;每个 `it` 前必须有一行 `//` 注释。
5. 写 `09-35-SUMMARY.md`:逐文件说明前后形态与新符号;列出「未改动清单」(flush / 惰性 setter / 脏标记 / 组装拷贝 / renderer 两遍式与后处理 / 分块张量 / 动态索引身份 / `status.ts` / `map.frag`);记录四类门禁的真实数字与豁免原文;说明修正了 4 条 `pendingAlpha` 与 1 条 `a.layerIndex` 的既有闸门漂移(测试断言对齐,无行为变化);声明**不做浏览器 UAT、无 WebGL E2E**、估算未校准(`confidence: low`)、**0 次代码提交**、未更新 `STATE.md` / `ROADMAP.md`;记录计划编号 09-35 的原因(09-27..09-34 已占用)。SUMMARY 使用 LF、CRLF 要求只针对代码文件。
5. 依次运行五类门禁并记录真实输出(命令见 `<verify>`):vitest 闸门文件全部用例通过(条数以实际运行为准,如实写入 SUMMARY,不得为了让数字好看而删用例);eslint 5 个文件 0 error / 0 warning(若只报 prettier 格式,可对这 5 个文件运行 `node node_modules/prettier/bin/prettier.cjs --write` 后重跑,仅排版);类型闸门只对 `types.ts` / `status.ts` / `moving.ts` / `vertex.ts` 负责(先确认 `vue-tsc` 真正完成,否则按 `INCOMPLETE` 失败,不允许空 `scoped` 空过),恰好 0 或 1 条 `types.ts` 的 `TS2305 TimingFn` 豁免并打印原文,其它 0 条;范围闸门恰好 7 条既有状态且行首空格完整(**命令中不得出现 `.trim()`**);哈希闸门读取 `%TEMP%\gsd-09-35-hashes.txt`(恰好 2 行,UTF-8 无 BOM,每行「相对 `map/` 的路径 + 空格 + 哈希」),按首个空格切分后用同一相对路径重算并逐字相等。任何一项失败:停下并如实报告,不得放宽断言、不得扩大豁免、不得改配置文件、不得删用例。
6. 写 `09-35-SUMMARY.md`:逐文件说明前后形态与新符号(含上一版 `OverlayBlockKind` 被整体移除);列出「未改动清单」(`StaticBlockStatus` / flush / 惰性 setter / 脏标记 / 组装拷贝 / renderer 两遍式与后处理 / 分块张量 / 动态索引身份 / `map.frag`);记录五类门禁的真实数字与豁免原文;记录实际用例条数与相对上一版的增减;说明保留了 4 条 `alphaMap` 与 1 条 `getLayerIndex(a.layer)` 的对齐(测试断言对齐,无行为变化);声明**不做浏览器 UAT、无 WebGL E2E**、估算未校准(`confidence: low`)、**0 次代码提交**、未更新 `STATE.md` / `ROADMAP.md`;记录计划编号 09-35 的原因(09-27..09-34 已占用);记录 OI-3(`sortOverlay` 旧注释仍写 `kind`,按 AGENTS.md 未改)与 `types.ts` 用户自有 `TS2305 TimingFn` 基线仍在;记录测试文件的 `it` 说明注释已随范围同步改写(AGENTS.md 测试文件豁免),以及类型闸门含完成性守卫(`r.error` / `r.signal` / `status===null` / 非 0 退出且无输出 均判失败,不会因 `vue-tsc` 崩溃而空过)。SUMMARY 使用 LF,CRLF 要求只针对代码文件。
</action>
<verify>
<automated>node node_modules/vitest/vitest.mjs run packages-user/client-base/src/map/__test__/rendererGate.test.ts</automated>
<fails_when>任一用例失败(例如覆盖块抽象断言为假、既有漂移未对齐、排序断言仍是旧形态),退出码非 0。</fails_when>
<automated>node node_modules/eslint/bin/eslint.js packages-user/client-base/src/map/types.ts packages-user/client-base/src/map/moving.ts packages-user/client-base/src/map/vertex.ts packages-user/client-base/src/map/__test__/rendererGate.test.ts</automated>
<fails_when>任一 error / warning,退出码非 0。</fails_when>
<automated>node -e "const{spawnSync}=require('child_process');const r=spawnSync(process.execPath,['node_modules/vue-tsc/bin/vue-tsc.js','--noEmit'],{encoding:'utf8',maxBuffer:1024*1024*256});const out=(r.stdout||'')+(r.stderr||'');const base=/error TS2339: Property 'getLayerByAlias' does not exist on type 'IGameMap'\./;const lines=out.split(/\r?\n/).filter(l=>l.includes('packages-user/client-base/src/map/'));const exempt=lines.filter(l=>base.test(l));const bad=lines.filter(l=>!base.test(l));console.log('map diagnostics',lines.length,'exempt',exempt.length,'unexpected',bad.length);exempt.forEach(l=>console.log('EXEMPT:',l));bad.forEach(l=>console.log('UNEXPECTED:',l));process.exit(bad.length===0&&exempt.length===1?0:1)"</automated>
<fails_when>`map/` 下出现任何非基线诊断,或基线豁免不再是恰好 1 条(豁免被扩大 / 被静默吞掉),退出码非 0。</fails_when>
<automated>node -e "const{execSync}=require('child_process');const want=[' M packages-user/client-base/src/map/moving.ts',' M packages-user/client-base/src/map/renderer.ts',' M packages-user/client-base/src/map/shader/map.frag',' M packages-user/client-base/src/map/status.ts',' M packages-user/client-base/src/map/types.ts',' M packages-user/client-base/src/map/vertex.ts','?? packages-user/client-base/src/map/__test__/rendererGate.test.ts'];const lines=execSync('git status --porcelain -uall -- packages-user/client-base/src/map',{encoding:'utf8'}).trim().split(/\r?\n/).filter(Boolean);const ok=lines.length===want.length&&want.every(w=>lines.includes(w));console.log(lines.join('\n'));process.exit(ok?0:1)"</automated>
<fails_when>`map/` 下出现白名单之外的变更(新增 / 删除 / 重命名文件),或某条既有状态消失,退出码非 0。</fails_when>
<automated>node -e "const{execSync}=require('child_process'),fs=require('fs');const p=(process.env.TEMP||'.')+'/gsd-09-35-hashes.txt';const lines=fs.readFileSync(p,'utf8').trim().split(/\r?\n/);const out=lines.map(l=>{const i=l.indexOf(' ');const f=l.slice(0,i);const h=execSync('git hash-object packages-user/client-base/src/map/'+f,{encoding:'utf8'}).trim();return f+' '+h});const ok=out.length===lines.length&&out.every((v,i)=>v===lines[i]);console.log(out.join('\n'));process.exit(ok?0:1)"</automated>
<fails_when>`status.ts` / `renderer.ts` / `shader/map.frag` 的哈希与 Task 1 采集值不一致(说明被本计划越界改动),或采集文件缺失 / 首行无空格 / 路径非相对 `map/`,退出码非 0。</fails_when>
<fails_when>任一用例失败(例如类层次断言为假、种类断言残留、排序断言仍是 `a.kind`、`instanceof` 断言缺失),退出码非 0。</fails_when>
<automated>node node_modules/eslint/bin/eslint.js packages-user/client-base/src/map/types.ts packages-user/client-base/src/map/status.ts packages-user/client-base/src/map/moving.ts packages-user/client-base/src/map/vertex.ts packages-user/client-base/src/map/__test__/rendererGate.test.ts</automated>
<fails_when>任一 error / warning(含 prettier 格式),退出码非 0。</fails_when>
<automated>node -e "const{spawnSync}=require('child_process');const r=spawnSync(process.execPath,['node_modules/vue-tsc/bin/vue-tsc.js','--noEmit'],{encoding:'utf8',maxBuffer:1024*1024*256});const out=(r.stdout||'')+(r.stderr||'');const done=!r.error&&r.status!==null&&!r.signal&&(r.status===0||(out.trim().length>0&&out.includes('error TS')));if(!done){console.log('INCOMPLETE','error',String(r.error),'status',r.status,'signal',r.signal,'bytes',out.length);process.exit(1);}const scope=/((?:packages-user[\\/]client-base[\\/]src[\\/]map[\\/])(?:types|status|moving|vertex)\.ts)\((\d+),(\d+)\): error (TS\d+): (.*)/;const base=/^Module .+ has no exported member 'TimingFn'\./;const scoped=[];out.split(/\r?\n/).forEach(line=>{const m=scope.exec(line);if(m)scoped.push(m)});const isExempt=m=>m[4]==='TS2305'&&m[1].endsWith('map/types.ts')&&base.test(m[5]);const exempt=scoped.filter(isExempt);const bad=scoped.filter(m=>!isExempt(m));console.log('status',r.status,'scoped',scoped.length,'exempt',exempt.length,'unexpected',bad.length);exempt.forEach(m=>console.log('EXEMPT:',m[0]));bad.forEach(m=>console.log('UNEXPECTED:',m[0]));process.exit(bad.length===0&&exempt.length<=1?0:1)"</automated>
<fails_when>`vue-tsc` 未能真正完成——`r.error` 非空(启动失败 / 找不到二进制)、`r.signal` 非空(被杀)、`r.status===null`,或非 0 退出但**输出为空**(崩溃),或非 0 退出且输出里没有任何 `error TS` 诊断行(例如 node 抛 `Cannot find module`,说明 `vue-tsc` 根本没跑起来)——此时打印 `INCOMPLETE` 并失败,绝不允许 `scoped=[]` 空过;四个源码文件出现任何非基线诊断,或 `types.ts` 的 `TS2305 TimingFn` 豁免超过 1 条(豁免被扩大 / 被重复吞掉),退出码非 0;基线诊断导致的非 0 退出且输出含 `error TS` 属正常,仍按豁免判定;`extension/hero.ts` 等范围外文件由过滤排除,不参与判定。</fails_when>
<automated>node -e "const{execSync}=require('child_process');const want=[' M packages-user/client-base/src/map/moving.ts',' M packages-user/client-base/src/map/renderer.ts',' M packages-user/client-base/src/map/shader/map.frag',' M packages-user/client-base/src/map/status.ts',' M packages-user/client-base/src/map/types.ts',' M packages-user/client-base/src/map/vertex.ts','?? packages-user/client-base/src/map/__test__/rendererGate.test.ts'];const raw=execSync('git status --porcelain -uall -- packages-user/client-base/src/map',{encoding:'utf8'});const lines=raw.split(/\r?\n/).filter(l=>l.length>0);const ok=lines.length===want.length&&want.every(w=>lines.includes(w));console.log(lines.join('\n'));process.exit(ok?0:1)"</automated>
<fails_when>`map/` 下出现白名单之外的变更(新增 / 删除 / 重命名文件),或某条既有状态消失,退出码非 0。该命令**刻意不对输出做 `.trim()`**(上一版缺陷:`git status --porcelain` 的 ` M` 行以一个空格开头,`trim()` 会削掉首行空格导致恒红),不得改回。</fails_when>
<automated>node -e "const{execSync}=require('child_process'),fs=require('fs');const p=(process.env.TEMP||'.')+'/gsd-09-35-hashes.txt';const raw=fs.readFileSync(p,'utf8');const lines=raw.split(/\r?\n/).filter(l=>l.length>0);let ok=lines.length===2;const out=lines.map(l=>{const i=l.indexOf(' ');if(i<1){ok=false;return l}const f=l.slice(0,i);const h=execSync('git hash-object packages-user/client-base/src/map/'+f,{encoding:'utf8'}).trim();const v=f+' '+h;if(v!==l)ok=false;return v});console.log(out.join('\n'));process.exit(ok?0:1)"</automated>
<fails_when>采集文件不是恰好 2 行、某行首个空格前为空、相对路径不在 `map/` 下,或 `renderer.ts` / `shader/map.frag` 的哈希与 Task 1 采集值不一致(说明被本计划越界改动),退出码非 0。</fails_when>
</verify>
<done>闸门测试 37 条全绿;eslint 4 个文件 0 报错;类型闸门恰好 1 条既有基线豁免且其它 0 条;范围闸门恰好 7 条既有状态;哈希闸门三个文件逐字相等;`09-35-SUMMARY.md` 写完并包含全部门禁真实结果、漂移处置、无 UAT / 无 WebGL E2E、估算未校准、0 次提交与编号说明。</done>
<done>闸门测试全部用例通过;eslint 5 个文件 0 报错;类型闸门 0 或 1 条 `TS2305 TimingFn` 豁免且其它 0 条(`vue-tsc` 完成性守卫生效);范围闸门恰好 7 条既有状态(行首空格完整);哈希闸门 `renderer.ts` / `shader/map.frag` 逐字相等;测试文件的 `it` 说明注释已随范围同步改写;`09-35-SUMMARY.md` 写完并包含全部五类门禁真实结果、实际用例条数、既有对齐说明、无 UAT / 无 WebGL E2E、估算未校准、0 次提交与编号说明。</done>
</task>
</tasks>
@@ -277,40 +305,45 @@ Phase 9 · 第 35 个计划(新增的独立小增量,**不是** 09-01..09-34
| Boundary | Description |
|----------|-------------|
| 排序缓存 ↔ 组装 | `sortOverlay()` 产出的覆盖块顺序是 `assembleOverlay()` 唯一的成员来源;判别式写错会让动态块走进静态分支(读错实例) |
| `MovingBlock` ↔ `IMovingBlock` ↔ 渲染器 | 新增的 `kind` 是只读判别式;渲染器持有的活动集合仍以 `index` 为身份,不得被本计划改变 |
| 私有类型 ↔ 导出类型 | `IOverlayBlock` / `OverlayBlockKind` 导出;`IOverlayStaticBlock` / `OverlayBlock` 必须保持私有,不得扩大公开面 |
| 排序缓存 ↔ 组装 | `sortOverlay()` 产出的覆盖块顺序是 `assembleOverlay()` 唯一的成员来源;`instanceof` 判别写错会让动态块静默丢失(两个分支都不进)或读错实例 |
| `MovingBlock` ↔ `IMovingBlock` ↔ 渲染器 | `MovingBlock` 的成员改由基类提供;渲染器持有的活动集合仍以 `index` 为身份,不得被本计划改变 |
| 类层次 ↔ 接口 | `types.ts` 的 `IOverlayBlock` / `IOverlayStaticBlock` 是契约,`status.ts` 的两个类是唯一实现;类改成接口 / 抽象类会让 `new` 与 `instanceof` 双双失效 |
| 优先级 ↔ 绘制顺序 | `priority` 是新的稳定次级键,静态必须 0、动态必须 1,否则同层同坐标的先后反转且无类型错误提示 |
| 私有类型 ↔ 导出类型 | `IOverlayBlock` / `IOverlayStaticBlock` / `OverlayBlock` / `StaticOverlayBlock` 导出;不得再引入种类枚举 |
| 测试源码字符串 ↔ 真实源码 | 闸门通过 `?raw` 断言源码形状;断言漂移会变成恒红或恒绿,失去验证意义 |
| 执行器 ↔ 用户工作树 | 只允许改 6 个路径;`status.ts` 用户维护、`renderer.ts` / `map.frag` 已存在未提交改动,必须零触碰 |
| 执行器 ↔ 用户工作树 | 只允许改 7 个路径;`StaticBlockStatus` 用户维护、`renderer.ts` / `map.frag` 已存在未提交改动,必须零触碰 |
| 依赖安装 | 本计划不新增依赖、不执行安装命令(`pnpm` 在本树已损坏) |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-35-01 | Tampering | `type OverlayBlock` 的判别式与 `assembleOverlay()` 收窄 | high | mitigate | 新增 2 条源码闸门断言(`IMovingBlock` 的 `readonly kind: OverlayBlockKind.Dynamic;`、组装含 `OverlayBlockKind.Dynamic` / `Static` 且用 `block.index` / `block.mapIndex` / `block.layer`);Task 1 形状闸门同时检查联合类型别名 |
| T-09-35-02 | Tampering | 排序结果变化(层序取值方式改变) | high | mitigate | 比较器改用 `getLayerIndex(a.layer)` / `(b.layer)`(O(1) `Map` 读取,取值与旧字段相同);Task 1 形状闸门断言该调用存在;Task 2 断言 `a.y` / `a.x` / `a.kind` 与 `overlayValueDirty = true` 仍在 |
| T-09-35-03 | Tampering | 借「抽象」之名改动渲染 / 脏标记 / 组装拷贝 | high | mitigate | 逐条禁止清单 + 范围闸门(恰好 7 条既有状态)+ 哈希闸门(`renderer.ts` / `status.ts` / `map.frag` 未变)+ Task 1 明确只改 3 个文件 |
| T-09-35-04 | Repudiation | 闸门恒红却写成「已通过」 | high | mitigate | 4 条 `pendingAlpha` 漂移与 1 条 `a.layerIndex` 漂移在 Task 0 报备、Task 2 对齐;vitest 必须 37 条全绿;SUMMARY 记录真实数字与豁免原文 |
| T-09-35-05 | Elevation of Privilege | 为让类型检查变绿而改 `tsconfig` / 依赖 / 加 `@ts-expect-error` | high | mitigate | 禁止改配置与依赖;类型闸门只豁免恰好 1 条既有基线并打印,其它任何 `map/` 诊断失败;遇到新诊断必须停下上报 |
| T-09-35-06 | Repudiation | 声称「渲染已验证」但未做浏览器 UAT | medium | mitigate | Task 0 报备 + SUMMARY 明确「不做浏览器 UAT、无 WebGL E2E、源码闸门通过不等于渲染结果已验证」 |
| T-09-35-SC | Tampering | npm / pnpm 安装 | high | mitigate | 本计划**不新增任何依赖、不执行安装命令、不改 workspace / `package.json`**;门禁只用 `node_modules` 里既有的直连二进制(`vitest` / `eslint` / `vue-tsc`);若执行期出现安装需求必须停下上报 |
| T-09-35-01 | Tampering | `instanceof` 判别与 `assembleOverlay()` 收窄 | high | mitigate | 新增源码闸门断言(`block instanceof MovingBlock`、`else if (block instanceof StaticOverlayBlock)`、`block.index` / `block.mapIndex` / `block.layer`、无 `block.kind`);Task 1 形状闸门同时检查两个类与接口 |
| T-09-35-02 | Tampering | `priority` 取值错误导致绘制顺序变化 | high | mitigate | 静态 `super(layer, 0)` / 动态 `super(layer, 1)` 由 Task 1 形状闸门与 Task 2 的 `super(layer, N)` 断言双重固定;比较器末位必须是 `a.priority - b.priority` |
| T-09-35-03 | Tampering | 排序结果变化(层序取值方式改变) | high | mitigate | 比较器保持 `getLayerIndex(a.layer)` / `(b.layer)`(O(1) `Map` 读取,取值与旧字段相同);Task 1 / Task 2 均断言该调用存在,且断言 `a.y` / `a.x` 与 `overlayValueDirty = true` 仍在 |
| T-09-35-04 | Tampering | 借「抽象」之名改动渲染 / 脏标记 / 组装拷贝 / `StaticBlockStatus` | high | mitigate | 逐条禁止清单 + 范围闸门(恰好 7 条既有状态)+ 哈希闸门(`renderer.ts` / `map.frag` 未变)+ Task 1 明确只改 4 个文件且 `StaticBlockStatus` 逐字不动 |
| T-09-35-05 | Repudiation | 闸门恒红却写成「已通过」 | high | mitigate | vitest 全绿(条数如实记录);4 条 `alphaMap` 与 1 条 `getLayerIndex(a.layer)` 对齐保留;SUMMARY 记录真实数字、豁免原文与实际用例数 |
| T-09-35-06 | Elevation of Privilege | 为让类型检查变绿而改 `tsconfig` / 依赖 / 加 `@ts-expect-error` / 修用户基线 | high | mitigate | 禁止改配置与依赖;类型闸门只豁免恰好 0/1 条用户自有的 `types.ts TS2305 TimingFn` 并打印,其它任何诊断失败;遇到新诊断必须停下上报 |
| T-09-35-07 | Tampering | 范围闸门被「绕过」(用 `.trim()` 削掉行首空格后改期望值迁就) | medium | mitigate | Task 2 的门禁命令不含 `.trim()`,`<fails_when>` 明确禁止改回,并要求 7 条期望字符串保留前导空格 |
| T-09-35-08 | Repudiation | 声称「渲染已验证」但未做浏览器 UAT | medium | mitigate | Task 0 报备 + SUMMARY 明确「不做浏览器 UAT、无 WebGL E2E、源码闸门通过不等于渲染结果已验证」 |
| T-09-35-SC | Tampering | npm / pnpm 安装 | high | mitigate | 本计划**不新增任何依赖、不执行安装命令、不改 workspace / `package.json`**;门禁只用 `node_modules` 里既有的直连二进制(`vitest` / `eslint` / `vue-tsc` / `prettier`);若执行期出现安装需求必须停下上报 |
</threat_model>
<verification>
- 源码形状闸门(vitest `?raw`):37 条用例全绿,含既有两遍式 / 混合 / 深度附件 / 配对 / 效果链路 / 裁剪 / `dataX`-`dataY` 等保留性断言。
- `eslint`:4 个文件 0 error / 0 warning。
- 类型闸门:仅 `packages-user/client-base/src/map/`,恰好 1 条既有 `TS2339 getLayerByAlias` 豁免并打印原文,其它 0 条。
- 范围闸门:`map/` 变更集恰好 7 条既有状态;无白名单外路径。
- 哈希闸门:`status.ts` / `renderer.ts` / `shader/map.frag` 与 Task 1 采集值逐字相等。
- 源码形状闸门(vitest `?raw`):全部用例通过,含 `OverlayBlock` 普通类、`DynamicBlockStatus` / `StaticOverlayBlock` 继承与优先级、`IOverlayBlock.priority`、无 `OverlayBlockKind`、排序用 `a.priority`、组装用 `instanceof`,以及既有两遍式 / 混合 / 深度附件 / 配对 / 效果链路 / 裁剪 / `dataX`-`dataY` 等保留性断言;所有类声明断言为拆分 `toContain` 或空白容忍正则,prettier 把 85 列的 `StaticOverlayBlock` 声明折行不影响判定。
- `eslint`:5 个文件 0 error / 0 warning。
- 类型闸门:仅 `types.ts` / `status.ts` / `moving.ts` / `vertex.ts`;先验证 `vue-tsc` 真正跑完(无 `r.error` / 无 `r.signal` / `status` 非 `null` / 非 0 退出时输出必须含 `error TS` 诊断行),再判定恰好 0 或 1 条用户自有的 `TS2305 TimingFn` 豁免并打印原文,其它 0 条。
- 范围闸门:`map/` 变更集恰好 7 条既有状态(命令不含 `.trim()`);无白名单外路径。
- 哈希闸门:`renderer.ts` / `shader/map.frag` 与 Task 1 采集值逐字相等。
- 明确不覆盖:任何浏览器 / WebGL 运行期行为(无 E2E、无 UAT)。
</verification>
<success_criteria>
- `OverlayBlockKind` / `IOverlayBlock` 只出现在 `types.ts`,`IMovingBlock` 继承二者并固定为动态种类,`MovingBlock` 本身是覆盖块。
- `vertex.ts` 不再有 `IOverlayDrawEntry` / `OverlayEntryKind`;排序缓存为 `OverlayBlock[]`(字段与局部一致),静态 push 字面量、动态原样入列,比较器用 `getLayerIndex` 且无残留逐图层 `layerIndex` 局部,组装以 `if (... Dynamic)` / `else if (... Static)` 显式收窄。
- 渲染行为、flush / 惰性 setter / 脏标记 / 组装拷贝 / 分块张量 / 动态索引身份 / renderer / `map.frag` / `status.ts` 零改动。
- 四类门禁 + 哈希闸门全部通过;SUMMARY 如实记录结果、漂移处置与未验证项;0 次代码提交。
- `OverlayBlock` 是 `status.ts` 中的具体普通类(`implements IOverlayBlock`),`DynamicBlockStatus extends OverlayBlock`(`super(layer, 1)`)、`StaticOverlayBlock extends OverlayBlock`(`super(layer, 0)`),`MovingBlock` 经由 `DynamicBlockStatus` 成为覆盖块。
- `types.ts` 无 `OverlayBlockKind`;`IOverlayBlock` 含 `readonly priority` / `layer` / `x` / `y`;`IOverlayStaticBlock` 含 `mapIndex`;`IMovingBlock` 继承覆盖块但无 `kind`。
- `vertex.ts` 不再有私有 `IOverlayDrawEntry` / `OverlayEntryKind` / `IOverlayStaticBlock` / `type OverlayBlock`;`overlayOrdered: IOverlayBlock[]`;静态 push `new StaticOverlayBlock(...)`、动态原样入列,比较器末尾为 `a.priority - b.priority`,组装以 `instanceof` 两分支收窄。
- 渲染行为、flush / 惰性 setter / 脏标记 / 组装拷贝 / 分块张量 / 动态索引身份 / renderer / `map.frag` / `StaticBlockStatus` 零改动。
- 五类门禁全部通过;SUMMARY 如实记录结果、实际用例数、对齐说明与未验证项;0 次代码提交。
</success_criteria>
<output>