diff --git a/.planning/phases/09-minimum-browser-runnable/09-35-PLAN.md b/.planning/phases/09-minimum-browser-runnable/09-35-PLAN.md new file mode 100644 index 0000000..3c44b16 --- /dev/null +++ b/.planning/phases/09-minimum-browser-runnable/09-35-PLAN.md @@ -0,0 +1,318 @@ +--- +phase: 09-minimum-browser-runnable +plan: 35 +type: execute +# 30:沿用本阶段既有计划的波次编号,使本计划排在 09-01..09-34 之后(D-01 增量派发,计划累积) +# 编号说明:派发方给出的编号是 09-27 / 波次 22,但仓库中 09-27..09-34 已被既有「已执行」计划占用 +#(09-27-PLAN.md / 09-27-SUMMARY.md 等均已提交,内容是 UI / 热键 / 设置等无关任务)。 +# 本计划是「新增的独立增量」,为不覆盖既有计划与摘要,改用下一个空闲编号 09-35 / 波次 30; +# 若用户坚持使用 09-27,必须先明确确认删除既有 09-27 计划与摘要(本计划未做、也不得擅自做)。 +# depends_on 为空:本计划改动的文件没有任何未执行计划触碰,不存在同波次并行写冲突,故结构上独立; +# 运行在最后的波次只是「排期在后」,不是「依赖前序计划」 +wave: 30 +depends_on: [] +files_modified: + - 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 + - .planning/phases/09-minimum-browser-runnable/09-35-PLAN.md + - .planning/phases/09-minimum-browser-runnable/09-35-SUMMARY.md +autonomous: false +requirements: [] +estimate: + # 未校准估算(confidence: low),SUMMARY 须声明这一点 + # 本计划只做一次小结构重构:在 types.ts 增加覆盖块抽象,moving.ts 加一个只读成员, + # vertex.ts 把私有绘制条目换成覆盖块联合类型,并同步闸门测试的 5 条既有断言 + 新增 7 条断言。 + # 不新增文件、不新增类、不改渲染行为;主要成本在读 4 个文件与运行四类门禁(含一次全仓 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(只新增);不复用任何未获批准的新命名;发现疑似问题只记录、不擅自修复。" + 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`;其余既有声明与注释逐字不动" + - path: packages-user/client-base/src/map/moving.ts + provides: "`MovingBlock` 增加 `readonly kind: OverlayBlockKind.Dynamic = OverlayBlockKind.Dynamic;`,并从 `./types` 引入 `OverlayBlockKind`;`index` / `x` / `y` / 动画逻辑零改动" + - 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)`);其余覆盖层与顶点数组逻辑零改动" + - path: packages-user/client-base/src/map/__test__/rendererGate.test.ts + provides: "4 条 `pendingAlpha` 断言对齐为 `alphaMap`、1 条排序断言改为 `getLayerIndex(a.layer)` 形态、新增 7 条覆盖块抽象断言;每个 `it` 前一行注释" + - path: .planning/phases/09-minimum-browser-runnable/09-35-PLAN.md + provides: "本计划文件本身" + - path: .planning/phases/09-minimum-browser-runnable/09-35-SUMMARY.md + 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>` 键 ↔ `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` 在本次执行中未被改动" + 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 条只做「断言与源码 / 新形态对齐」,不改任何行为。" + - "**`getLayerIndex` 是 O(1)**:`renderer.getLayerIndex(layer)` 的实现是 `this.layerIndexMap.get(layer) ?? -1`(`Map` 读取)。比较器每次比较调用两次,取值与旧的逐条目 `layerIndex` 字段完全相同;排序结果不变,只是把「排序前一次性计算」改成「比较时读取同一张表」。" + - "**无 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` 导出。" + - "不得改变渲染行为、绘制顺序、脏标记语义、`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 之外的疑似问题;发现新问题只记录、不修复、不擅自登记为缺陷。" + 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`)。" +--- + + +Phase 9 · 第 35 个计划(新增的独立小增量,**不是** 09-01..09-34 的 UI / 文档任务)—— **把「覆盖块」抽成一个概念,让 `MovingBlock` 成为覆盖块的一种**,从而去掉 `vertex.ts` 里专为排序缓存而设的私有绘制条目与私有序枚举。 + +**要解决的问题:** 现在 `vertex.ts` 的排序缓存条目 `IOverlayDrawEntry` 用可选字段(`mapIndex?` / `moving?`)同时表示静态与动态两种东西,并另有一个私有枚举 `OverlayEntryKind`;动态条目还要包一层 `{ moving }` 副本。这些结构只服务于 `sortOverlay()` / `assembleOverlay()` 的局部需要,却让「静态图块」与「移动图块」两种成员在同一位置有两种表示。把共同点(种类 / 图层 / 坐标)抽成接口后,静态条目只需额外携带地图索引,动态条目就是 `IMovingBlock` 本身。 + +**做法(锁定设计,最小改动):** +- `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、不提交任何代码**。 + +**约束:** 只动 `files_modified` 的 6 个路径;不改其它文件;不改既有注释与 jsDoc;不新增依赖;不改配置;**所有新增命名待批准(OI-1)**。 + +**修改方式声明(AGENTS.md):** 本计划的全部文件写入(改 3 个源码、改 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 验证不适用。 + + +## 本计划产出物(Artifacts this plan produces) + +| 类型 | 名称 / 路径 | 说明 | +|------|-------------|------| +| 新符号(导出) | `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 条 | +| 交付 | `.planning/phases/09-minimum-browser-runnable/09-35-SUMMARY.md` | 执行摘要、门禁结果、UAT 跳过声明 | + +## 覆盖审计(Multi-Source Coverage Audit) + +| 源 | 条目 | 覆盖位置 | +|----|------|----------| +| GOAL(ROADMAP §Phase 9) | 「可在浏览器中运行并验证的最小版本」伞目标 | **本增量不新增覆盖**:只做结构抽象;浏览器可运行性仍未验证,SUMMARY 如实记录 | +| REQ | 本阶段无 requirement ID(`requirements: []`) | Nyquist 不适用 | +| RESEARCH | 本 run 无 RESEARCH.md | 不适用 | +| CONTEXT D-01 | 增量派发,一个任务一个计划 | 本计划即该派发的唯一计划,不拆分、不预规划后续 | +| CONTEXT D-02 | 计划可累积追加 | plan 35,排在 09-01..09-34 之后(编号说明见 frontmatter) | +| 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 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 | + + +@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md +@.planning/phases/09-minimum-browser-runnable/09-26-PLAN.md +@.planning/phases/09-minimum-browser-runnable/09-26-SUMMARY.md +@AGENTS.md +@dev.md +@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 + + + + + + Task 0: 预执行汇报关卡 —— 汇报覆盖块抽象(前后形态、逐文件符号、不改清单、待批准命名、漂移处置),等待「可以执行」 + 工作树为 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'.`,本计划所有类型闸门精确豁免该条一条。 + 无(本任务只读,不写任何文件;汇报后停下等待「可以执行」) + + - .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) + + + 一份待批准的执行计划(`09-35-PLAN.md`):把「覆盖块」抽成 `IOverlayBlock` / `OverlayBlockKind`,让 `MovingBlock` 成为覆盖块的一种,并用覆盖块联合类型替换 `vertex.ts` 的私有绘制条目;不改变任何渲染行为。 + + + **停下并向用户汇报,等待其恰好回复「可以执行」后才继续;本任务不写任何文件、不改任何源码 / 测试 / 计划,不派发执行子代理。** + + ① **要做什么 / 为什么:** 现在 `vertex.ts` 的排序缓存用私有 `IOverlayDrawEntry` 表示两种成员——静态图块与动态图块——靠可选字段 `mapIndex?` / `moving?` 区分,另配私有枚举 `OverlayEntryKind`;动态成员还被包成 `{ moving }` 副本。这些结构只服务于 `sortOverlay()` / `assembleOverlay()` 的局部需要,却让同一种「覆盖块」有两种表示。本次把共同点(种类、图层、坐标)抽成接口 `IOverlayBlock`,静态条目只需额外带地图索引,动态条目就是 `IMovingBlock` 本身——即「`MovingBlock` 是覆盖块的一种」。 + + ② **精确的前后形态(逐符号):** + - `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`(`OverlayBlockKind`、`IOverlayBlock`、`IMovingBlock` 的 `kind` 与新继承);`moving.ts`(`MovingBlock.kind`、引入 `OverlayBlockKind`);`vertex.ts`(删 `IOverlayDrawEntry` / `OverlayEntryKind`,增 `IOverlayStaticBlock` / `OverlayBlock`,改 `overlayOrdered` / `sortOverlay` / `assembleOverlay`);`rendererGate.test.ts`(见⑦)。**不新增方法、不新增类、不新增文件、不新增其它导出。** + + ④ **明确不改动的部分:** `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`;其它包与依赖配置。 + + ⑤ **待批准命名(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 禁止把对象函数赋给临时变量)也不引入缓存变量。 + + ⑥ **无行为变化主张与等价性论证:** 排序键顺序不变(层序 → 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` 的语义与旧枚举一致。**不改任何渲染行为。** + + ⑦ **两处既有闸门漂移及处置(必须如实报备):** ① `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 条全部通过。 + + ⑧ **验证方式(命令均为本机 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。** + + ⑨ **范围:** 只写 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` 一律零改动。 + + ⑩ **编号说明(必须报备):** 派发方给出的输出路径是 `09-27-PLAN.md` / 波次 22,但仓库中 `09-27`..`09-34` 已被既有已执行计划占用(其计划与摘要均已提交,内容与本次无关)。为不覆盖任何既有记录,本计划落在 `09-35-PLAN.md` / 波次 30。**若坚持使用 09-27,必须先明确确认删除既有 09-27 计划与摘要**——本计划未做、也不会擅自做。 + + ⑪ **两条铁律:** **不做浏览器 UAT、不宣称任何 WebGL E2E**(游戏当前不可运行,排序 / 组装 / 混合的视觉结果未验证,源码闸门通过不等于渲染结果已验证);**不提交任何代码**(禁止 `git add` / `commit` / `stash` / `checkout` / `restore` / `worktree` / `reset`),改动全部留在工作树供用户审查。 + + ⑫ **已知限制:** 覆盖层组装仍对每条静态记录做一次 `getBlockByDataLoc` + `getLayerInstanced` 查找;同图层同行的不透明与覆盖层图块 z 相同,先绘制者胜出;动态块移动期间每帧重排;`addPostEffect` 无调用方;估算未校准(`confidence: low`)。 + + 用户恰好回复「可以执行」四个字;在此之前不执行 Task 1 / Task 2 的任何一步,也不派发执行子代理。 + 已完整汇报 ①–⑫(含前后形态、逐文件符号、不改清单、待批准命名、无行为变化论证、两处闸门漂移处置、编号说明、两条铁律),并停下等待用户回复「可以执行」。 + + 逐条核对 ①–⑫ 是否与你的意图一致,特别是:② 的精确前后形态(`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、不提交任何代码)。 + + 仅当完全同意时回复恰好四个字「可以执行」;任何修改意见(尤其是 ② 的结构形态、⑤ 的命名、⑦ 的闸门漂移处置、⑩ 的编号)请直接说明,本计划会据此改写后再来一次汇报。 + + + + Task 1: 增加覆盖块抽象(types.ts / moving.ts / vertex.ts) + packages-user/client-base/src/map/types.ts, packages-user/client-base/src/map/moving.ts, packages-user/client-base/src/map/vertex.ts + + 仅在收到用户恰好回复「可以执行」之后执行;开始改代码前先运行下面的哈希采集命令,把 `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 退出或文件缺失必须停下上报,不得跳过哈希闸门。 + + 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)。其余声明与既有注释逐字不动。 + + 2. `moving.ts`:从 `./types` 的既有 import 追加 `OverlayBlockKind`(保持普通引入,禁止 `import type`);在 `MovingBlock` 的 `readonly layer: IMapLayer;` 之后新增 `readonly kind: OverlayBlockKind.Dynamic = OverlayBlockKind.Dynamic;`(显式类型注解;不重复写 jsDoc,继承的说明已足够)。不修改 `index` / `x` / `y` / 动画逻辑与其它既有内容。 + + 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[] = []`(不改则类型检查失败)。 + + 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` 元素本身,不再包 `{ 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 保持不变。 + + 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 保持不变。 + + 6. **不得改动** `flushPendingAlpha`、`setStaticAlpha` / `getStaticAlpha` / 帧数方法、`alphaMap` / `overlayStatic` / 两个脏标记的其它赋值点、`getVertexArray` / `getOverlayArray` / `getOverlayCount`、`checkRebuild` / `setBlockSize` / `updateMoving` / `deleteMoving`、`IOverlayStaticRecord` 与记录读写方法;不得改 `renderer.ts` / `status.ts` / `shader/map.frag` / 其它任何文件;不得改任何既有注释或 jsDoc;代码文件保持 CRLF。 + + + 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)" + 任一形状检查为假(例如仍残留 `IOverlayDrawEntry` / `OverlayEntryKind`、`IMovingBlock` 未继承 `IOverlayBlock`、比较器仍用 `a.layerIndex`、`sortOverlay()` 仍留逐图层 `const layerIndex`、动态分支仍在包 `{ moving }`、`assembleOverlay()` 缺少显式 `else if (... Static)` 分支),退出码非 0。 + + 三个源码文件改完:`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`;其余文件零改动。 + + + + Task 2: 同步闸门测试、跑四类门禁并写 SUMMARY + packages-user/client-base/src/map/__test__/rendererGate.test.ts, .planning/phases/09-minimum-browser-runnable/09-35-SUMMARY.md + + 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`(只改断言字符串,不改任何既有注释)。 + + 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`。 + + 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` 源码字符串,不导入任何运行期模块。 + + 4. 依次运行四类门禁并记录真实输出(命令见 `` 与下方):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` 未被本计划改动)。任何一项失败:停下并如实报告,不得放宽断言、不得扩大豁免、不得改配置文件。 + + 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 要求只针对代码文件。 + + + node node_modules/vitest/vitest.mjs run packages-user/client-base/src/map/__test__/rendererGate.test.ts + 任一用例失败(例如覆盖块抽象断言为假、既有漂移未对齐、排序断言仍是旧形态),退出码非 0。 + 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 + 任一 error / warning,退出码非 0。 + 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)" + `map/` 下出现任何非基线诊断,或基线豁免不再是恰好 1 条(豁免被扩大 / 被静默吞掉),退出码非 0。 + 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)" + `map/` 下出现白名单之外的变更(新增 / 删除 / 重命名文件),或某条既有状态消失,退出码非 0。 + 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)" + `status.ts` / `renderer.ts` / `shader/map.frag` 的哈希与 Task 1 采集值不一致(说明被本计划越界改动),或采集文件缺失 / 首行无空格 / 路径非相对 `map/`,退出码非 0。 + + 闸门测试 37 条全绿;eslint 4 个文件 0 报错;类型闸门恰好 1 条既有基线豁免且其它 0 条;范围闸门恰好 7 条既有状态;哈希闸门三个文件逐字相等;`09-35-SUMMARY.md` 写完并包含全部门禁真实结果、漂移处置、无 UAT / 无 WebGL E2E、估算未校准、0 次提交与编号说明。 + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| 排序缓存 ↔ 组装 | `sortOverlay()` 产出的覆盖块顺序是 `assembleOverlay()` 唯一的成员来源;判别式写错会让动态块走进静态分支(读错实例) | +| `MovingBlock` ↔ `IMovingBlock` ↔ 渲染器 | 新增的 `kind` 是只读判别式;渲染器持有的活动集合仍以 `index` 为身份,不得被本计划改变 | +| 私有类型 ↔ 导出类型 | `IOverlayBlock` / `OverlayBlockKind` 导出;`IOverlayStaticBlock` / `OverlayBlock` 必须保持私有,不得扩大公开面 | +| 测试源码字符串 ↔ 真实源码 | 闸门通过 `?raw` 断言源码形状;断言漂移会变成恒红或恒绿,失去验证意义 | +| 执行器 ↔ 用户工作树 | 只允许改 6 个路径;`status.ts` 用户维护、`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`);若执行期出现安装需求必须停下上报 | + + + +- 源码形状闸门(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 采集值逐字相等。 +- 明确不覆盖:任何浏览器 / WebGL 运行期行为(无 E2E、无 UAT)。 + + + +- `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 次代码提交。 + + + +Create `.planning/phases/09-minimum-browser-runnable/09-35-SUMMARY.md` when done +