mirror of
https://github.com/motajs/template.git
synced 2026-10-29 12:20:17 +08:00
docs(09-30): add inline JSX refactor plan and execution summary (code uncommitted)
This commit is contained in:
parent
e21c31d4e5
commit
ff0f729cfc
@ -0,0 +1,48 @@
|
||||
{
|
||||
"head": "e2415a7a48d9a9e9d2f886e00c10ca51e91340d1",
|
||||
"hashes": {
|
||||
"packages-user/client-base/src/components/scroll.tsx": "74fb21f5c9d9c7329e99b8345ba6d7ee7b1eae48",
|
||||
"packages-user/client-base/src/components/textbox.tsx": "b17e988d77c3a81fe3ce3250940e31a9774f8dfb",
|
||||
"src/client/ui/settings.tsx": "c5cca14cde8ba813ac4ef5e624e8b359470c472d",
|
||||
"src/client/ui/index.ts": "4e3950598b1beee23753a5556d8e8c82eb21bc57",
|
||||
"src/client/misc/hotkey.ts": "6e884ab4f36cdff958a3490697406a00f67d2e7f",
|
||||
"src/client/ui/inputBox.tsx": "3eb246200ae3300f9f221a8e0e9142e5a612a696",
|
||||
"src/client/ui/waitBox.tsx": "3d5ad7f304e1db0e1b1e75fe76801ec37b917c9f"
|
||||
},
|
||||
"dirty": [
|
||||
"AGENTS.md",
|
||||
"docs/advanced/iii/anon-tokyo.md",
|
||||
"docs/advanced/iii/core-state.md",
|
||||
"docs/advanced/iii/event-system.md",
|
||||
"docs/advanced/iii/index.md",
|
||||
"docs/advanced/iii/loading-system.md",
|
||||
"docs/advanced/iii/passability-and-pathfinding.md",
|
||||
"docs/advanced/iii/plugin-internals.md",
|
||||
"docs/advanced/iii/replay-internals.md",
|
||||
"packages-user/client-base/src/index.ts",
|
||||
"packages-user/client-base/src/map/__test__/rendererGate.test.ts",
|
||||
"packages-user/client-base/src/map/renderer.ts",
|
||||
"packages-user/client-base/src/map/shader/map.frag",
|
||||
"packages-user/client-base/src/map/status.ts",
|
||||
"packages-user/client-base/src/map/types.ts",
|
||||
"packages-user/client-base/src/map/vertex.ts",
|
||||
"packages-user/client-base/src/setting/index.ts",
|
||||
"packages-user/client-base/src/setting/types.ts",
|
||||
"packages-user/data-state/src/loader/types.ts",
|
||||
"src/client/ui/__test__/uiContainerGate.test.ts",
|
||||
"src/client/ui/book.tsx",
|
||||
"src/client/ui/bookDetail.tsx",
|
||||
"src/client/ui/equipbox.tsx",
|
||||
"src/client/ui/hotkey.tsx",
|
||||
"src/client/ui/toolbox.tsx"
|
||||
],
|
||||
"eslint": [],
|
||||
"typecheck": [
|
||||
"src/client/ui/book.tsx error TS2322: Type 'number' is not assignable to type 'AllNumbers | keyof IdToNumber'.",
|
||||
"src/client/ui/equipbox.tsx error TS2322: Type 'number' is not assignable to type 'AllNumbers | keyof IdToNumber'.",
|
||||
"src/client/ui/equipbox.tsx error TS2322: Type 'number' is not assignable to type 'AllNumbers | keyof IdToNumber'.",
|
||||
"src/client/ui/equipbox.tsx error TS2322: Type 'number' is not assignable to type 'AllNumbers | keyof IdToNumber'.",
|
||||
"src/client/ui/toolbox.tsx error TS2322: Type 'number' is not assignable to type 'AllNumbers | keyof IdToNumber'.",
|
||||
"src/client/ui/toolbox.tsx error TS2322: Type 'number' is not assignable to type 'AllNumbers | keyof IdToNumber'."
|
||||
]
|
||||
}
|
||||
491
.planning/phases/09-minimum-browser-runnable/09-30-PLAN.md
Normal file
491
.planning/phases/09-minimum-browser-runnable/09-30-PLAN.md
Normal file
@ -0,0 +1,491 @@
|
||||
---
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 30
|
||||
type: execute
|
||||
# 25:沿用本阶段既有计划的波次编号,使本计划排在 09-01..09-29 之后(D-01 增量派发,计划累积)
|
||||
# depends_on 为空:本计划触碰的 5 个 UI 文件与 1 个既有闸门测试没有任何并发计划在本波次触碰,故结构上独立;
|
||||
# 运行在最后的波次只是「排期在后」,不是「依赖前序计划」
|
||||
wave: 25
|
||||
depends_on: []
|
||||
# 09-30-BASELINE.json 由 Task 1 在收到「可以执行」之后、编辑任何代码之前写出(Task 0 只汇报、不写文件)
|
||||
files_modified:
|
||||
- src/client/ui/book.tsx
|
||||
- src/client/ui/bookDetail.tsx
|
||||
- src/client/ui/equipbox.tsx
|
||||
- src/client/ui/toolbox.tsx
|
||||
- src/client/ui/hotkey.tsx
|
||||
- src/client/ui/__test__/uiContainerGate.test.ts
|
||||
- .planning/phases/09-minimum-browser-runnable/09-30-PLAN.md
|
||||
- .planning/phases/09-minimum-browser-runnable/09-30-BASELINE.json
|
||||
- .planning/phases/09-minimum-browser-runnable/09-30-SUMMARY.md
|
||||
autonomous: false
|
||||
requirements: []
|
||||
estimate:
|
||||
# 未校准估算(confidence: low),SUMMARY 须声明这一点。
|
||||
# 本计划只做渲染写法的重构:把 09-29 顶部预建的 VNode[] 列表改回内联 JSX `.map()`,
|
||||
# 不新增行为、不改数据逻辑、不改按键注册、不改 Scroll / TextContent 实现。
|
||||
# 主要成本在 5 个文件逐个改写渲染函数、更新既有闸门断言、跑两轮 eslint 与一次整仓 vue-tsc。
|
||||
# tokens 取 raw 的 2 倍,与 09-19..09-29 的换算口径一致;仍属未校准。
|
||||
# tasks 只计 4 个 auto 执行任务(Task 1–4);Task 0 是 blocking-human 汇报关卡,不写文件、不编辑代码,不计入估算。
|
||||
tokens: 60000
|
||||
raw_tokens: 30000
|
||||
tasks: 4
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求 / D-03)**:先汇报「把 09-29 的预建 `VNode[]` 列表改回项目既有的内联 JSX `.map()` 风格」的要做什么 / 为什么 / 大致怎么做 / 影响范围,逐条说明**只是写法变化、行为不变**、逐文件删除的数组与新增的内联映射、注释与循环变量规则、验证方式,以及「不做浏览器 UAT / 执行器不提交任何代码」两条铁律,然后停下等待用户恰好回复「可以执行」"
|
||||
- "**只动 `files_modified` 的 9 个路径**:5 个 UI 文件、1 个既有闸门测试、3 个 `.planning` 产物;不改 `packages-user/client-base/src/components/scroll.tsx` / `textbox.tsx`、不改 `src/client/ui/settings.tsx` / `src/client/ui/index.ts` / `src/client/misc/hotkey.ts`、**不改 `src/client/ui/inputBox.tsx` / `waitBox.tsx`**、不改任何其它 UI / 依赖 / 配置"
|
||||
- "**列表一律改为内联 JSX `.map()`,不再有顶部预建数组**:`book` 的 `rows`、`bookDetail` 的 `specialNodes`、`equipbox` 的 `statusNodes` / `slotNodes` / `ownedNodes`、`toolbox` 的 `permNodes` / `consNodes`、`hotkey` 的 `groupNodes` / `commandNodes` 全部消失;五个文件中不出现 `VNode` 标识符(含 `import`、`VNode[]`、`: VNode[]`),也不出现上述数组名的 `.push(` 调用;`book` 不再有 `rows.push(`"
|
||||
- "**一行展开为多个同级 `Scroll` 子项时返回数组,不包容器**:`book` 用 `enemies.value.map((view, i) => { …; return [<g-rect …/>, <icon …/>, …十一个 <text …/>]; })`;`bookDetail` 用 `specialRows.value.map(row => [<text …/>, <TextContent …/>])`;`equipbox` 用 `props.attributes.map((attr, i) => …)` / `hero.equip.slots.map((name, i) => …)` / `owned.value.map((item, i) => …)` 各返回三元组;`toolbox` 用 `permItems.value.map(…)` / `consItems.value.map(…)` 各返回四元组;`hotkey` 用 `groups.map(([id, group], i) => [<g-rectr …/>, <text …/>])` 与 `commandRows.value.map((row, i) => [<text …/>, ...(combinationLists.value[i] ?? []).map((keys, j) => [<g-rectr …/>, <text …/>])])`;`Scroll` 内仍无 `container`、无 `hidden=`、无包裹层"
|
||||
- "**`bookDetail` 的高度逻辑原样保留**:`textHeights` / `updateTextHeight` / `watch(specials)` / `specialRows` 计算、`autoHeight` / `onUpdateHeight`、`TextContent` 的 `key`(`code` + `textTop` + `textHeight`)、`SPECIAL_MIN_HEIGHT` 回退全部不变;只把 `for` + `push` 改为 `specialRows.value.map(row => [...])`;`TextContent` 只在 setup 复制 `props.loc` 的两行既有 `//` 注释**原样留在渲染函数体、`return (` 之前**(当前位置不变),**绝不进入 `<Scroll>` 的 JSX 子元素位置**"
|
||||
- "**`hotkey` 的预计算数据原样保留**:`ICommandRow` / `commandRows` 仍是一次遍历给出的 `{ command, top }`,`combinationLists` 快照与 `refreshCombinations` 不变;两栏都改为内联 `.map()`;`selectKey` 的函数签名不变,调用点改为 `selectKey(row.command, j)`,按键框与文本的 `key` 改为 `box-${i}-${j}` / `text-${i}-${j}`"
|
||||
- "**循环临时变量统一缩短、`for (` 不换行**:`groupIndex` → `i`、`commandIndex` → `i`、内层 `index` → `j`;`hotkey` / `toolbox` / `equipbox` / `book` 的列表渲染不再有 `for`;`bookDetail` 的 `specialRows` / `watch` 与 `hotkey` 的 `refreshCombinations` / `commandRows` 中剩余的 `for (const x of y)` 必须单行书写(`for (` 后不得换行);`equipbox` 的 `getTargetSlot` / `selectedSlot` 既有单行 `for` 不变"
|
||||
- "**注释位置有硬规则,防止 JSXText**:`<Scroll>` 的 **JSX 子元素位置**(`{…}` 之外)只允许 `{/* … */}` JSX 注释;任何 `//` 都只能出现在 JS 代码位置——渲染函数体 `return (` 之前、`map` 回调体的语句位置、或 `map` 回调返回的数组字面量内部(`//` 在 JSX 子元素位置会变成 JSXText 并生成 Text 渲染项;`scroll.tsx:290` 的 `if (v.isComment) return;` 只跳过 `Comment`,Text 会被计入 `updateScroll` 的 children)。因此 `bookDetail` 的两条既有 `//` 留在 `return (` 之前;`book` 的 `// 数值列表区` / `// 临界信息区` 留在 map 返回的数组字面量内部,第三条 `// 临界信息缺失时回退为 0,让「没有数据」与「数值为 0」可以区分` 留在回调体语句位置(`const damage` 之上)且**单独占一行**;`equipbox` 的两条、`hotkey` 的两条既有 `//` 与 `hotkey` 内层新增那条同样只落在回调体 / 数组字面量内部"
|
||||
- "**注释要求可机器验证**:每个列表在其 `Scroll` 内新增一行 `{/* … */}` JSX 注释说明这一段的职责与内容;闸门用 `getScrollBlocks()` 在 `stripComments` **之前**统计每块 `{/* … */}` 数量必须等于 book `[1]` / bookDetail `[1]` / equipbox `[1,0,1,1]`(状态 / 描述 / 槽位 / 拥有)/ toolbox `[1,1,0]`(永久 / 消耗 / 描述)/ hotkey `[1,1]`;并断言每个 Scroll 块的**未删容器原始子元素区**首字符是 `{` 或 `<`(该检查必须在删除表达式容器**之前**做,否则 `book` / `bookDetail` / `hotkey` 的子元素区整体是一个 `{…}`、删掉后只剩空白、首字符断言恒假),而删掉 `{/* … */}` 与成对 `{…}` 表达式容器后的剩余文本**不含 `//`**(子元素位置没有裸行注释;描述块的子元素是 `<TextContent …/>`,因此不要求剩余整体为空);既有注释(`book` 的三条、`bookDetail` 的两行、`equipbox` 的两条、`hotkey` 的两条)逐条仍各出现一次;`book` 的第三条(`// 临界信息缺失时回退为 0,让「没有数据」与「数值为 0」可以区分`)须**单独占一行**"
|
||||
- "**行为零变化**:`book` / `bookDetail` 的 0 值回退、`bookDetail` 的高度测量与 `key` 重挂载、`equipbox` 的拖拽 / 悬停 / 再点穿戴、`toolbox` 的点击与「再点使用」、`hotkey` 的捕获 / 写回 / 分组切换 / 无入口 / 无持久化 / 无冲突检测 / 无恢复默认,全部保持 09-29 的实现口径;`Scroll` 仍是平铺直接子元素"
|
||||
- "**验证是源码形状 + 静态检查,且如实声明未做浏览器验证**:更新 `src/client/ui/__test__/uiContainerGate.test.ts`(保留原有 9 组断言,新增 4 组:「无预建 `VNode` 数组」「内联 `.map()` + 短循环变量」「`book` 每行 13 个元素齐全」「注释位置与数量」,`hotkey` 的 `selectKey` needle 改为 `j`,每个 `it` 上方紧邻一行 `//` 注释);跑 eslint(改动文件,只判 NEW,基线采集与最终比较用**同一组 6 个路径**)、`pnpm check:type`(整仓,只判改动文件内 NEW,过滤范围同样是**同一组 6 个路径**)、`hotkeySettingsGate.test.ts` 仍为绿、**7 个**受保护文件哈希与全仓脏文件快照范围闸门;**不做浏览器 UAT,不宣称任何视觉 / 滚动 / 点击 / 拖拽行为已验证**"
|
||||
- "**范围与纪律**:不修改任何既有注释或 jsDoc(只允许新增);不改按键注册表;不改设置入口;不新增依赖;不执行构建;不顺手修复任何无关诊断;发现疑似问题只记录、不擅自登记为缺陷"
|
||||
artifacts:
|
||||
- path: src/client/ui/book.tsx
|
||||
provides: "`vue` 导入去掉 `VNode`;渲染函数从 `const rows: VNode[] = []` + `for` + `rows.push(…)` 改为 `<Scroll>` 内 `enemies.value.map((view, i) => { …; return [13 个同级元素]; })`;保留透明 `g-rect` 命中层、`noevent`、0 值回退、`specialText(base)`、5 个基础数值文案(生命 / 攻击 / 防御 / 金币 / 经验)与 13 个 `key`(`hit-` / `icon-` / `name-` / `special-` / `hp-` / `atk-` / `def-` / `money-` / `exp-` / `dmg-` / `crit-` / `reduce-` / `def1-`);三条既有 `//` 注释文本不变:`// 数值列表区` / `// 临界信息区` 位于被 `map` 回调返回的数组字面量内部(JS 表达式位置,不生成 Text 子项),`// 临界信息缺失时回退为 0,让「没有数据」与「数值为 0」可以区分` 位于回调体语句位置、**单独占一行**(现第 125 行原样)"
|
||||
- path: src/client/ui/bookDetail.tsx
|
||||
provides: "`vue` 导入去掉 `VNode`(保留 `watch`);删除 `specialNodes` 与 `for`,改为 `specialRows.value.map(row => [<text …/>, <TextContent …/>])`;`textHeights` / `updateTextHeight` / `watch` / `specialRows` / `autoHeight` / `onUpdateHeight` / `key` 逻辑不变;两条既有 `//` 注释原样保留在渲染函数体 `return (` 之前(不在 `<Scroll>` 内),`<Scroll>` 内只有一条新增 `{/* … */}` JSX 注释、零条 `//`"
|
||||
- path: src/client/ui/equipbox.tsx
|
||||
provides: "`vue` 导入去掉 `VNode`;删除 `statusNodes` / `slotNodes` / `ownedNodes` 与三个 `for`,改为 `props.attributes.map` / `hero.equip.slots.map` / `owned.value.map` 各返回同级元素数组;4 个 `*_SCROLL_LOC` 常量与 `ElementLocator` 导入保留;拖拽与悬停逻辑零改动;两条既有 `//` 注释文本不变、留在各自数组字面量内部"
|
||||
- path: src/client/ui/toolbox.tsx
|
||||
provides: "`vue` 导入去掉 `VNode`;删除 `permNodes` / `consNodes` 与两个 `for`,改为 `permItems.value.map` / `consItems.value.map` 各返回四元组数组;`hidden={!!current}` 提示位置、`TextContent` 空文本回退、`clickItem` 逻辑不变"
|
||||
- path: src/client/ui/hotkey.tsx
|
||||
provides: "`vue` 导入去掉 `VNode`;删除 `groupNodes` / `commandNodes` 与两个 `for`,改为 `groups.map(([id, group], i) => […])` 与 `commandRows.value.map((row, i) => [<text …/>, ...(combinationLists.value[i] ?? []).map((keys, j) => […])])`;`ICommandRow` / `commandRows` / `combinationLists` 保留;`selectKey(row.command, j)`、`box-${i}-${j}` / `text-${i}-${j}`;选中边框与根 `nocache` 不变;两条既有 `//` 与新增内层 `//` 只落在回调体 / 数组字面量内部"
|
||||
- path: src/client/ui/__test__/uiContainerGate.test.ts
|
||||
provides: "在既有 9 组断言上新增 4 组(共 13 组):⑩五个文件无 `VNode` 标识符 / `VNode[]` / `: VNode[]` / 预建数组名及其 `.push(`、`book` 无 `rows.push(`;⑪内联 `.map(` 调用存在、无 `groupIndex` / `commandIndex`、无 `for (` 后换行;⑫`book` 每行 13 个元素齐全(13 个 `key` 前缀 + 5 个基础数值文案 + `specialText(base)`);⑬注释位置与数量(`getScrollBlocks()` 在 `stripComments` **之前**统计每块 `{/* … */}` = book `[1]` / bookDetail `[1]` / equipbox `[1,0,1,1]` / toolbox `[1,1,0]` / hotkey `[1,1]`,每块先对**未删容器**的原始子元素区断言首字符是 `{` 或 `<`、再对删掉 `{/* … */}` 与成对 `{…}` 后的剩余文本断言不含 `//`(顺序不可颠倒),既有 `//` 注释逐条各出现一次(`book` 的三条,含单独成行的 `// 临界信息缺失时回退为 0…`),`bookDetail` 的两条不在 `Scroll` 块内);`hotkey` 的 `selectKey` needle 改为 `j`;原 flat-Scroll / 0 回退 / 高度 / 显示开关 / `noevent` 断言保留;每个 `it` 前一行注释"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-30-BASELINE.json
|
||||
provides: "Task 1 在收到「可以执行」之后、编辑任何代码之前写出的机器可读基线:`{ head, hashes, dirty, eslint, typecheck }`;`hashes` 保护 **7 个**文件(`scroll.tsx` / `textbox.tsx` / `settings.tsx` / `ui/index.ts` / `misc/hotkey.ts` / `inputBox.tsx` / `waitBox.tsx`);`dirty` 是当时 `git status --porcelain -uall` 的全仓脏文件快照(去掉 `.planning/`);只写一次"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-30-SUMMARY.md
|
||||
provides: "执行摘要:逐文件「预建数组 → 内联 map」对照、循环变量改名、注释新增清单与「`//` 只落在 JS 代码位置、`<Scroll>` 内只有 `{/* … */}`」说明、门禁结果、未做浏览器 UAT 声明、估算未校准声明、执行器 0 次代码提交声明;**先跑完所有门禁再写本文件,写完后再跑摘要闸门**"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-30-PLAN.md
|
||||
provides: "本计划文件本身"
|
||||
key_links:
|
||||
- "`Scroll` 的直接子元素 → 内联 `.map()` 返回的数组 — 一旦某个列表又被收进 `VNode[]` 或重新包 `container`,闸门「无 VNode / 无 `*Nodes`」与「Scroll 内无 container」同时失败;因此两者必须一起断言"
|
||||
- "`specialRows.value.map` → `TextContent` 的 `key`(`code` + `textTop` + `textHeight`)→ 行位置重挂载 — 只改写法时这一环必须逐字保留,否则长描述会重叠"
|
||||
- "`commandRows` / `combinationLists` → 右栏内层 `map` 的 `j` — 外层 `i` 与内层 `j` 必须分别用于 `selectKey`、`key` 与 `isSelected` 判定,混用会让按键框选中错位"
|
||||
- "命中层(透明 `g-rect` / `g-rectr`)← 上层 `text` / `icon` 的 `noevent` — 改写数组为内联 map 时若漏掉 `noevent`,整行点击 / 按键框选中 / 拖拽起手失效;因此逐元素 `noevent` 断言必须保留"
|
||||
- "`//` 注释位置 → JSX 子元素位置 vs JS 表达式位置 — 写成 JSX 子元素的 `//` 会变成 JSXText 并生成 Text 渲染项(`packages/render-vue/src/renderer.ts:67-70` 只把纯空白文本降级为 `Comment`),而 `scroll.tsx:290` 的 `if (v.isComment) return;` 只跳过 `Comment`,Text 会被计入 `updateScroll` 的 children 计算;因此 `bookDetail` 的两条既有 `//` 必须留在 `return (` 之前,闸门断言其位置在 `<Scroll` 之前"
|
||||
- "受保护文件哈希(`scroll.tsx` / `textbox.tsx` / `settings.tsx` / `ui/index.ts` / `misc/hotkey.ts` / `inputBox.tsx` / `waitBox.tsx`) → 基线 — 证明本计划没有改渲染系统实现、没有改设置入口 / 按键注册、没有碰指定不动的两个 UI"
|
||||
assumptions:
|
||||
- "**规划日工作树快照(仅作参考,执行期以 Task 1 写出的基线为准)**:HEAD 为 `e2415a7a48d9a9e9d2f886e00c10ca51e91340d1`;**09-29 的改动仍未提交**——5 个 UI 文件处于 ` M`、`src/client/ui/__test__/uiContainerGate.test.ts` 处于 `??`。因此本计划的基线 `dirty` 快照必然包含这 6 个路径,范围闸门只能证明「没有新增范围外路径」,不能证明「这 6 个路径本次被改过」;内容是否改到位由源码形状闸门负责。SUMMARY 必须如实说明这一点"
|
||||
- "**当前 5 个 UI 文件的形态就是 09-29 的成果**:每个文件只有根 `container`,`Scroll` 内平铺,列表通过顶部 `const xxxNodes: VNode[] = []` / `rows: VNode[]` + `for` + `push` 收集。用户已明确否定这种写法,要求回到项目既有的内联 JSX `.map()` 风格"
|
||||
- "**`choices.tsx` 是用户认可的写法参考,但不是同构先例**:`Page` 的 slot 返回 `[<Selection … />, ...getPageContent(page).map((v, i) => <text … />)]`,即在 JSX 中直接展开数组;`title.tsx` 的 `buttons.map((v, i) => { …; return <text … /> })` 与 `save.tsx` 的 `grid.value.locs.map((v, i) => { …; return <SaveItem … /> })` 也是内联 `map` + 块体。它用在 `Page` 的 slot,本计划用在 `Scroll` 的 default slot,**嵌套层级不同**,可依赖的是渲染层事实而不是「结构同构」。本计划沿用这种写法,不引入 Fragment 组件"
|
||||
- "**数组子元素会被逐个插入父节点(渲染层证据)**:`packages/render-vue/src/renderer.ts:41-47` 的 `insert` 直接执行 `parent.appendChild(el)`,Vue 在调用 `insert` 之前已把 slot 返回的数组规范化为逐个 vnode;`Scroll` 的 slot 类型同样是 `default: () => VNode | VNode[]`(`scroll.tsx:67-69`)。**若执行期发现某一行返回数组后 `Scroll` 只把它当单个子元素(平铺/虚拟滚动退化),必须停下上报**(OI-2),不得自行改回预建数组,也不得改 `scroll.tsx`"
|
||||
- "**注释位置涉及渲染正确性(不是风格问题)**:JSX 子元素位置的 `//` 是 JSXText,会生成 Text 渲染项;`packages/render-vue/src/renderer.ts:67-70` 只把纯空白文本降级为 `Comment`,而 `scroll.tsx:290` 用 `if (v.isComment) return;` 跳过注释、**不会**跳过 Text,Text 会进入 `updateScroll` 的 children 计算。因此 `//` 只允许出现在渲染函数体 / `map` 回调体的语句位置或返回的数组字面量内部,`<Scroll>` 的 JSX 子元素位置只放 `{/* … */}`"
|
||||
- "**`VNode` 只在预建数组中被动使用**:五个文件的 `VNode` 仅出现在 `const xxx: VNode[]` 与 `vue` 导入里,删除数组后必须同时删除导入,否则 eslint 报未使用变量;`bookDetail` 的 `watch`、其它 `computed` / `ref` / `shallowRef` 仍需保留"
|
||||
- "**`specialRows` / `commandRows` / `textHeights` 是数据,不是渲染列表**:它们内部仍用 `rows.push(…)` 一次性累加出接口对象,属于计算逻辑、不是 JSX 列表收集,本计划保留;闸门只禁止**渲染数组名**(`*Nodes` / `book` 的 `rows`)与 `VNode`,不会误伤这两个 computed"
|
||||
- "**`hotkeySettingsGate.test.ts` 必须仍为绿**:它断言 `hotkey.tsx` 去注释后不匹配 `/<g-rect[\\s>]/`、不含持久化调用、监听在捕获阶段、含 `setKeyCombination(` / `iterateKeyCombinations(` / `iterateGroups(` / `@common/exit` / `返回`。本计划不改这些语义,也不改该测试文件"
|
||||
- "**`keyAdaptationGate.test.ts` 是既存失败**(09-29 SUMMARY 记录:15 个用例 5 个失败,改动前即如此),不在本计划范围;不修、不以它为绿、不声称全部既有闸门为绿"
|
||||
- "**dev.md 约束**:不得出现 `import type`;尽量避免 `?.`;对象类型必须先声明 `interface`;`//` 后留空格;既有注释只增不改;导入之间不得夹注释与空行;换行 CRLF;Prettier 配置 `printWidth: 80` / `tabWidth: 4` / 单引号 / 无尾逗号"
|
||||
- "**无 RESEARCH / 无 VALIDATION**:本 run 不存在 `09-30-RESEARCH.md` 与 `09-30-VALIDATION.md`;本阶段无 requirement ID,Nyquist 验证不适用,不创建这两类文件"
|
||||
- "**波次与独立性(`wave: 25` / `depends_on: []`)**:本计划按 D-01 追加在 09-01..09-29 之后;它改动的文件没有任何**并发**计划在本波次触碰,`depends_on: []` 表示结构独立;`wave` 只是排期编号"
|
||||
prohibitions:
|
||||
- "不得在用户恰好回复「可以执行」之前执行任何任务或派发执行子代理(AGENTS.md / D-03)"
|
||||
- "**不得运行任何 git 写命令**:`git add` / `commit` / `stash` / `checkout` / `restore` / `worktree` / `reset` 一律禁止;代码改动留在工作树,是否提交由用户另行决定;三个 `.planning` 产物可能被编排器按 `commit_docs: true` 聚合提交,不属于本计划的代码提交"
|
||||
- "不得修改 `packages-user/client-base/src/components/scroll.tsx` 与 `textbox.tsx`(不允许通过改渲染系统实现来「适配」内联数组)"
|
||||
- "不得修改 `src/client/ui/settings.tsx`、`src/client/ui/index.ts`、`src/client/misc/hotkey.ts`(设置入口、UI 导出清单、按键注册)"
|
||||
- "**不得修改 `src/client/ui/inputBox.tsx` 与 `waitBox.tsx`**(用户明确要求不动)"
|
||||
- "不得在 `Scroll` 内新增任何 `container` / `template` / 包裹层;不得为「让数组更好写」而重新包一层容器或 Fragment 组件"
|
||||
- "不得再引入任何 `VNode` 导入或 `VNode[]` 变量;不得保留 `specialNodes` / `statusNodes` / `slotNodes` / `ownedNodes` / `permNodes` / `consNodes` / `groupNodes` / `commandNodes` 这些名字"
|
||||
- "不得修改任何既有注释或 jsDoc;只允许新增注释,且新增注释必须说明这一段列表的职责与内容,不得写成「解释下一行代码」"
|
||||
- "不得新增依赖、不得执行安装命令;不得跑 `build:game` / `build:packages` / `build:lib`"
|
||||
- "不得改变行为口径:0 值回退、`TextContent` 高度与 `key` 重挂载、`equipbox` 拖拽 / 悬停、`toolbox` 再点使用、`hotkey` 捕获 / 写回 / 无入口 / 无持久化 / 无冲突 / 无恢复默认、透明命中层 + `noevent` 全部保持"
|
||||
- "不得越界处理发现的其它疑似问题;只记录、不擅自修复、不擅自登记为缺陷"
|
||||
open_items:
|
||||
- "**OI-1(命名报备,按 AGENTS.md 在 Task 0 一并报备,不导出、不进入公共 API)**:本计划**不新增任何文件作用域标识符**(无新常量、无新 `interface`、无新函数);只改循环内 / 函数内局部变量名:`book` 的 `view` / `i`、`bookDetail` 的 `row`、`equipbox` 的 `attr` / `name` / `item` / `i`、`toolbox` 的 `item` / `i`、`hotkey` 的 `i` / `id` / `group` / `row` / `keys` / `j`;**删除** `rows` / `specialNodes` / `statusNodes` / `slotNodes` / `ownedNodes` / `permNodes` / `consNodes` / `groupNodes` / `commandNodes` / `groupIndex` / `commandIndex` / `index`。既有的公共 / 受保护 / 导出命名一律不改;`selectKey` 的函数签名保持 `(command, index)` 不变,只把调用点实参改为 `j`。**你若要求其它命名,执行前按你的命名整体替换,方案形态不变**"
|
||||
- "**OI-2(执行期硬停条件)**:若某一行返回数组后,`Scroll` 把它当成单个子元素(表现为整行一起显隐或虚拟滚动退化),**必须停下上报**,不得自行回退为「顶部预建数组」,也不得改 `scroll.tsx` / `textbox.tsx`"
|
||||
- "**OI-3(已知遗留,只记录、不修复)**:①浏览器 UAT 全程跳过,滚动、内存、点击命中、拖拽手感由用户自行验证;②`book.tsx` 的过期 TODO(写着「留空」,与 0 值回退矛盾)按 AGENTS.md 原样保留;③`keyAdaptationGate.test.ts` 为既存失败,不在本计划范围;④`Scroll` 的 `padEnd`、`TextContent` 的 `props.hidden` 等系统层既有行为不在本计划范围;⑤无法通过源码闸门证明「数组展开后仍逐元素平铺」,只能证明源码形状与 09-29 行为口径未被改写"
|
||||
---
|
||||
|
||||
<!-- planner-discipline-allow: VNode[] -->
|
||||
<!-- planner-discipline-allow: rows.push( -->
|
||||
<!-- planner-discipline-allow: container -->
|
||||
|
||||
<objective>
|
||||
Phase 9 · 第 30 个计划(新增的独立增量)——**把 09-29 的列表渲染写法改回项目既有的内联 JSX 风格**。
|
||||
|
||||
**要解决的问题:**
|
||||
|
||||
09-29 把 5 个列表型 UI 的 `Scroll` 子元素拉平后,采用的是「在渲染函数顶部先建 `const xxx: VNode[] = []`,再用 `for` + `push` 收集元素,最后把整个数组作为 `Scroll` 的唯一 children」的写法。用户已明确否定这种写法:项目里既有的用户自写组件(`choices.tsx` / `save.tsx` / `title.tsx`)都是在 JSX 里直接 `.map()`,不在顶部预建列表数组。本计划只改**写法**,不改任何行为。
|
||||
|
||||
**做法(用户决定,不得扩大):**
|
||||
|
||||
1. **删除预建数组**:`book` 的 `rows`、`bookDetail` 的 `specialNodes`、`equipbox` 的 `statusNodes` / `slotNodes` / `ownedNodes`、`toolbox` 的 `permNodes` / `consNodes`、`hotkey` 的 `groupNodes` / `commandNodes` 全部删除,并删除随之无用的 `VNode` 导入。
|
||||
2. **改为内联 `.map()`**:
|
||||
- 一行展开为多个同级 `Scroll` 子项时返回数组,**不包容器**:`book` 返回 13 元组,`bookDetail` 返回 `[<text/>, <TextContent/>]`,`equipbox` 三处各返回三元组,`toolbox` 两处各返回四元组。
|
||||
- `hotkey` 左栏 `groups.map(([id, group], i) => [<g-rectr/>, <text/>])`;右栏 `commandRows.value.map((row, i) => [<text/>, ...(combinationLists.value[i] ?? []).map((keys, j) => [<g-rectr/>, <text/>])])`。
|
||||
- 既有写法参考是 `choices.tsx` 的 `[<Selection/>, ...map(...)]`(用在 `Page` 的 slot);「数组子元素被逐个插入父节点」由 `packages/render-vue/src/renderer.ts:41-47` 的 `insert` → `parent.appendChild(el)` 保证;不引入 Fragment / 包裹容器。
|
||||
3. **保留数据计算**:`bookDetail` 的 `textHeights` / `updateTextHeight` / `watch` / `specialRows`、`hotkey` 的 `ICommandRow` / `commandRows` / `combinationLists` 全部保留(它们内部用 `rows.push` 累加的是数据对象,不是 JSX 列表)。
|
||||
4. **注释**:每个列表在 `Scroll` 的 **JSX 子元素位置**新增一行 `{/* … */}` 注释说明职责与内容;`//` 只允许出现在渲染函数体 `return (` 之前、`map` 回调体的语句位置或返回的数组字面量内部(JSX 子元素位置的 `//` 会变成 JSXText 并生成 Text 渲染项)——`bookDetail` 的两条既有 `//` 因此保留在 `return (` 之前;既有注释文本逐字保留。
|
||||
5. **循环变量**:`groupIndex` → `i`、`commandIndex` → `i`、内层 `index` → `j`;`for (` 不得换行;列表渲染不再用 `for`。
|
||||
6. **验证**:更新 `uiContainerGate.test.ts`(保留原 9 组断言,新增 4 组:无预建 `VNode` 数组 / 内联 `.map()` 与短循环变量 / `book` 每行 13 个元素齐全 / 注释位置与数量);eslint 只判 NEW(基线采集与比较都用同一组 6 个路径);`pnpm check:type` 只判改动文件内 NEW(过滤范围同样 6 个路径);`hotkeySettingsGate` 仍为绿;7 个受保护文件哈希与全仓脏文件快照范围闸门;**不做浏览器 UAT,不提交代码**。
|
||||
|
||||
**明确不做:** 不接入口、不做持久化、不做冲突检测 / 恢复默认、不改按键注册、不改设置入口、不改 `Scroll` / `TextContent` 实现、不改 `inputBox.tsx` / `waitBox.tsx`、不改任何行为口径。
|
||||
|
||||
**约束:** 只动 `files_modified` 的 9 个路径;不改既有注释与 jsDoc(只允许新增);不新增依赖;不新增文件作用域标识符;不顺手修复任何无关诊断。
|
||||
|
||||
**修改方式声明(AGENTS.md):** 本计划的全部文件写入(写基线、改 5 个 UI 文件、更新 1 个闸门测试、写摘要)由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 外无任何写入。**Task 0 只汇报、不写任何文件**;`09-30-BASELINE.json` 是 **Task 1 在收到「可以执行」之后、编辑任何代码之前的第一步**写出(只写一次)。「开始执行」的界限仍是「可以执行」四个字。
|
||||
|
||||
**可见跳过:** spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)。本阶段无 requirement ID,不创建 RESEARCH.md / VALIDATION.md,Nyquist 验证不适用。
|
||||
</objective>
|
||||
|
||||
## 本计划产出物(Artifacts this plan produces)
|
||||
|
||||
| 类型 | 名称 / 路径 | 说明 |
|
||||
|------|-------------|------|
|
||||
| 修改 | `src/client/ui/book.tsx` | `rows` + `for` + `push` → `enemies.value.map(…)` 返回 13 个同级元素;`specialText(base)`、5 个基础数值文案与 13 个 `key` 全部保留;三条既有 `//` 保留(两条在数组字面量内、`// 临界信息缺失时回退为 0…` 单独成行留在回调体内) |
|
||||
| 修改 | `src/client/ui/bookDetail.tsx` | `specialNodes` + `for` + `push` → `specialRows.value.map(…)` 返回 `[标题, TextContent]`;两条既有 `//` 留在 `return (` 之前 |
|
||||
| 修改 | `src/client/ui/equipbox.tsx` | 三个数组 + 三个 `for` → 三个内联 `.map()`;两条既有 `//` 留在数组字面量内 |
|
||||
| 修改 | `src/client/ui/toolbox.tsx` | 两个数组 + 两个 `for` → 两个内联 `.map()` |
|
||||
| 修改 | `src/client/ui/hotkey.tsx` | 两个数组 + 两个 `for` → 两栏内联 `.map()`(含内层 `j`);两条既有 `//` 与内层新增 `//` 只落在回调体 / 数组字面量内 |
|
||||
| 修改 | `src/client/ui/__test__/uiContainerGate.test.ts` | 新增 4 组断言(共 13 组)、`hotkey` needle 改 `j`,其余断言保留 |
|
||||
| 基线 | `.planning/phases/09-minimum-browser-runnable/09-30-BASELINE.json` | `head` + **7 个**受保护文件哈希 + 全仓脏文件快照 + eslint / typecheck 基线 |
|
||||
| 交付 | `.planning/phases/09-minimum-browser-runnable/09-30-SUMMARY.md` | 执行摘要、门禁结果、未 UAT / 0 次代码提交声明 |
|
||||
| 计划 | `.planning/phases/09-minimum-browser-runnable/09-30-PLAN.md` | 本文件 |
|
||||
|
||||
## 覆盖审计(Multi-Source Coverage Audit)
|
||||
|
||||
| 源 | 条目 | 覆盖位置 |
|
||||
|----|------|----------|
|
||||
| GOAL(ROADMAP §Phase 9) | 「可在浏览器中运行并验证的最小版本」伞目标 | 本增量不新增入口、不做浏览器验证;只调整渲染写法,SUMMARY 如实记录 |
|
||||
| REQ | 本阶段无 requirement ID(`requirements: []`) | Nyquist 不适用 |
|
||||
| RESEARCH | 本 run 无 RESEARCH.md / 本计划无 DISCOVERY.md | 不适用 |
|
||||
| CONTEXT D-01 | 增量派发,一个任务一个计划 | 本计划即该次派发的唯一计划,不拆分 |
|
||||
| CONTEXT D-02 | 计划可累积追加 | plan 30,排在 09-01..09-29 之后(`wave: 25` 只是排期编号) |
|
||||
| CONTEXT D-03 | 执行前必须先汇报并等待「可以执行」 | Task 0(`blocking-human`),`autonomous: false` |
|
||||
| CONTEXT D-04 | 新命名必须先反馈获批 | OI-1(只改局部变量名,不新增文件作用域命名) |
|
||||
| CONTEXT D-06 | 范围由用户界定,不擅自扩大 | `files_modified` 9 项 + 范围闸门 + **7 个**受保护文件哈希 |
|
||||
| 用户要求 1 | 删除 5 个文件的预建列表数组 | Task 1–3 / Task 4 闸门 |
|
||||
| 用户要求 2 | 每个列表改为内联 `.map()`;一行多元素返回数组、不包容器;`bookDetail` 保留 `specialRows` 并返回 `[text, TextContent]`;`hotkey` 两栏内联(含内层 `j`);`equipbox` / `toolbox` 同样内联 | Task 1–3 / Task 4 闸门 |
|
||||
| 用户要求 3 | 每个列表段落新增职责注释(`{/* … */}` 优先,必要时 `//`;`//` 不得落在 JSX 子元素位置) | Task 1–3 / Task 4 第 13 组闸门 |
|
||||
| 用户要求 4 | 缩短循环变量、消除换行 `for (`、更新 `selectKey` 与 key | Task 1–3 / Task 4 闸门 |
|
||||
| 用户要求 5 | 删除无用 `VNode` 导入与无用数组 | Task 1–3 / Task 4 闸门 |
|
||||
| 用户要求 6 | 行为保持(平铺、0 回退、高度与 key、拖拽 / 悬停、use、hotkey 全口径) | Task 1–3 约束 + Task 4 保留断言 |
|
||||
| 用户要求 7 | 不改 `inputBox.tsx` / `waitBox.tsx`、不改按键注册 | 受保护哈希 7 个(含这两个文件与 `misc/hotkey.ts`) |
|
||||
| 验证要求 | 更新 `uiContainerGate.test.ts`:保留 flat-Scroll / no-container / no-hidden、0 回退、高度、hidden 分流、noevent;新增 4 组(无 `VNode` / 无 `Nodes.push` / 内联 map;`book` 每行 13 元齐全;注释位置与数量);eslint、类型 NEW-only(同一组 6 个路径)、`hotkeySettingsGate` 绿、哈希 / 范围、无提交、无浏览器 UAT | Task 4 |
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
|
||||
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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-29-PLAN.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-29-SUMMARY.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-29-BASELINE.json
|
||||
@AGENTS.md
|
||||
@dev.md
|
||||
@packages-user/client-base/src/components/scroll.tsx
|
||||
@packages-user/client-base/src/components/textbox.tsx
|
||||
@src/client/ui/book.tsx
|
||||
@src/client/ui/bookDetail.tsx
|
||||
@src/client/ui/equipbox.tsx
|
||||
@src/client/ui/toolbox.tsx
|
||||
@src/client/ui/hotkey.tsx
|
||||
@src/client/ui/save.tsx
|
||||
@src/client/ui/viewmap.tsx
|
||||
@src/client/ui/load.tsx
|
||||
@src/client/ui/title.tsx
|
||||
@src/client/ui/choices.tsx
|
||||
@src/client/ui/__test__/uiContainerGate.test.ts
|
||||
@src/client/ui/__test__/hotkeySettingsGate.test.ts
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
<task type="checkpoint:human-verify" gate="blocking-human">
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报「把预建 `VNode[]` 列表改回内联 JSX `.map()`」的要做什么 / 为什么 / 怎么做 / 影响范围、逐文件改写清单、注释与循环变量规则、验证方式,等待「可以执行」</name>
|
||||
<precondition>工作树仍是 09-29 结束时的状态,可用以下事实自检:`src/client/ui/book.tsx` 第 117 行为 `const rows: VNode[] = [];`,第 118-250 行为 `for` + `rows.push(...)`,`<Scroll>` 的 children 为 `{rows}`;`src/client/ui/bookDetail.tsx` 第 186 行为 `const specialNodes: VNode[] = [];`,第 187-188 行为 `TextContent` 的两条既有 `//` 注释(当前就在渲染函数体、`return (` 之前),第 189-212 行为 `for` + `specialNodes.push(...)`,`Scroll` children 为 `{specialNodes}`;`src/client/ui/equipbox.tsx` 第 275-277 行有三个 `VNode[]` 数组,第 278-416 行为三个 `for` + `push`;`src/client/ui/toolbox.tsx` 第 105-106 行有两个 `VNode[]` 数组,第 107-194 行为两个 `for` + `push`;`src/client/ui/hotkey.tsx` 第 191-192 行有两个 `VNode[]` 数组,第 193-277 行为 `for (let groupIndex …)` 与 `for (let commandIndex …)` 两段嵌套 `for` + `push`(内层变量为 `index`)。`src/client/ui/__test__/uiContainerGate.test.ts` 已存在且 9 组断言全绿,其中 `hotkey` 的 needle 为 `selectKey(row.command, index)`。</precondition>
|
||||
<files>无(本任务不写任何文件,只做只读汇报;`09-30-BASELINE.json` 改由 Task 1 在获批后、改代码前写出)</files>
|
||||
<read_first>
|
||||
- .planning/phases/09-minimum-browser-runnable/09-29-SUMMARY.md(09-29 已完成的行为口径与门禁结果,本次只改写法)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-29-PLAN.md(上一计划的 Task 0 汇报形态与基线字段口径)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-01 增量派发、D-03 汇报关卡、D-04 命名批准、D-06 不扩大范围)
|
||||
- src/client/ui/{book,bookDetail,equipbox,toolbox,hotkey}.tsx(当前预建数组写法)
|
||||
- src/client/ui/{save,viewmap,load,title,choices}.tsx(用户认可的内联 JSX / `.map()` 写法参考)
|
||||
- src/client/ui/__test__/uiContainerGate.test.ts(本次要更新的既有闸门)
|
||||
- AGENTS.md(执行前须获批、命名先反馈、既有注释不得修改、修改方式须预先声明)、dev.md(注释 / 类型 / 无 `?.` / 无内联对象类型规范)
|
||||
</read_first>
|
||||
<what-built>
|
||||
一份待批准的执行计划(`09-30-PLAN.md`):把 5 个列表型 UI 顶部预建的 `VNode[]` + `for` + `push` 改回项目既有的内联 JSX `.map()` 写法,一行展开多个同级 `Scroll` 子项时返回数组、不包容器;保留 09-29 的全部行为口径;用源码形状闸门 + eslint / 类型前后基线验收。
|
||||
</what-built>
|
||||
<action>
|
||||
**停下并向用户汇报,等待恰好回复「可以执行」后才继续;本任务不写任何文件(含 `09-30-BASELINE.json`——它由 Task 1 的第一步写出),不派发执行子代理。**
|
||||
|
||||
① **要做什么:** 只改渲染写法。把 09-29 在渲染函数顶部预建 `VNode[]` 并 `for` + `push` 收集元素的列表,全部改回项目既有的内联 JSX `.map()`:
|
||||
- `book.tsx`:删除 `rows`,改为 `<Scroll>` 内 `enemies.value.map((view, i) => { …; return [13 个同级元素]; })`。
|
||||
- `bookDetail.tsx`:删除 `specialNodes`,改为 `specialRows.value.map(row => [<text …/>, <TextContent …/>])`;`textHeights` / `updateTextHeight` / `watch` / `specialRows` / `autoHeight` / `onUpdateHeight` / `key` 全部保留。
|
||||
- `equipbox.tsx`:删除 `statusNodes` / `slotNodes` / `ownedNodes`,改为 `props.attributes.map((attr, i) => …)` / `hero.equip.slots.map((name, i) => …)` / `owned.value.map((item, i) => …)`。
|
||||
- `toolbox.tsx`:删除 `permNodes` / `consNodes`,改为 `permItems.value.map((item, i) => …)` / `consItems.value.map((item, i) => …)`。
|
||||
- `hotkey.tsx`:删除 `groupNodes` / `commandNodes`,左栏 `groups.map(([id, group], i) => [<g-rectr …/>, <text …/>])`;右栏 `commandRows.value.map((row, i) => [<text …/>, ...(combinationLists.value[i] ?? []).map((keys, j) => [<g-rectr …/>, <text …/>])])`。
|
||||
- 5 个文件的 `vue` 导入去掉 `VNode`;`equipbox` 的 `ElementLocator` 导入与 4 个 `*_SCROLL_LOC` 保留。
|
||||
|
||||
② **为什么:** 当前这种「顶部 `const xxxNodes: VNode[] = []` + `for` + `push`」的写法是你明确否定的;项目里既有用户自写组件(`choices.tsx` 的 `[<Selection/>, ...map(…)]`、`save.tsx` 与 `title.tsx` 的 `map((v, i) => { …; return <…/> })`)都是内联 JSX。改回这种写法后,列表在 JSX 中就地展开,`Scroll` 的直接子元素仍是平铺元素,09-29 的行为与门禁口径不变。**这不依赖「与 `choices.tsx` 嵌套同构」**:数组子元素会被逐个插入父节点,依据是 `packages/render-vue/src/renderer.ts:41-47` 的 `insert` → `parent.appendChild(el)`(`choices.tsx` 用的是 `Page` 的 slot,本计划用的是 `Scroll` 的 default slot,嵌套层级不同)。
|
||||
|
||||
③ **怎么写(逐文件,严格按此范围):**
|
||||
- **`book.tsx`**:`return () => ( … )` 直接返回 JSX;`Scroll` 内 `{enemies.value.map((view, i) => { const base = …; const locator = …; const critical = …; const def1 = …; const y = i * ROW_HEIGHT;`,然后换行**单独成行**写 `// 临界信息缺失时回退为 0,让「没有数据」与「数值为 0」可以区分`,再写 `const damage / nextDiff / damageDiff / def1Damage = …; return [<g-rect key={`hit-${base.code}`} …/>, <icon key={`icon-${base.code}`} …/>, … 共 13 个]; })}`。元素顺序、属性、`key`、`loc`、`noevent` 全部保持;`specialText(base)`、5 个基础数值文案、13 个 `key` 逐个保留;`// 数值列表区` / `// 临界信息区` 两条既有注释**文本不变,留在 map 回调返回的数组字面量内部**(元素之间的 JS 注释位置,不生成 Text 子项),不得搬到 `<Scroll>` 的 JSX 子元素位置;第三条既有注释 `// 临界信息缺失时回退为 0,让「没有数据」与「数值为 0」可以区分`(现第 125 行)保持 `//` 行注释、**单独占一行**、留在回调体语句位置(`const damage` 之上),不得与语句同行、不得改成 `/* … */`。
|
||||
- **`bookDetail.tsx`**:`Scroll` 内 `{specialRows.value.map(row => [<text … key={`label-${row.special.code}`} />, <TextContent … autoHeight onUpdateHeight={height => updateTextHeight(row.special.code, height)} key={`${row.special.code}-${row.textTop}-${row.textHeight}`} />])}`;`TextContent` 只在 setup 复制 `props.loc` 的两行既有 `//` 注释**原样留在渲染函数体、`return (` 之前(当前位置不变)**,**不得放进 `<Scroll>`**——那里的 `//` 会变成 JSXText 并渲染出多余的 Text 子项,`<Scroll>` 内只放一条 `{/* … */}`。
|
||||
- **`equipbox.tsx`**:三处内联 `.map()`;状态行返回 `[属性名, 数值, 增减值]`,槽位返回 `[g-rectr, icon, 槽名]`,拥有装备行返回 `[g-rectr, icon, 名称]`;`// Scroll 会覆盖直接子元素的 hidden…` 与 `// 拖动起手仍只在图标上…` 两条既有注释原文保留在各自 map 回调返回的数组字面量内部;`startPress` / `movePress` / `endPress` / `clickItem` 逻辑体零改动。
|
||||
- **`toolbox.tsx`**:两处内联 `.map()`,各返回 `[g-rectr, icon, 名称, 数量]`;`hidden={!!current}` 提示仍在说明 `Scroll` 之前;`TextContent` 空文本回退不变;`clickItem` 逻辑不变。
|
||||
- **`hotkey.tsx`**:左栏返回 `[g-rectr, 分组名]`,`// 选中分组只由这一行的边框表示,元素始终挂载` 保留在左栏 map 回调体 / 数组字面量内;右栏外层返回 `[指令名, ...内层数组]`,内层 `list.map((keys, j) => { …; return [g-rectr, 组合文本]; })`,`// 选中按键框同样只切换这个既有矩形的描边` 保留在右栏回调体 / 数组字面量内;新增的内层 `// 每个组合展开为按键框与文本两个同级子项` 放在内层回调体 `return [` 之前;`selectKey(row.command, j)`、`box-${i}-${j}`、`text-${i}-${j}`;`ICommandRow` / `commandRows` / `combinationLists` / 根 `nocache` 不变。
|
||||
- **注释**:每个**列表** `Scroll` 的 **JSX 子元素位置**只新增一行 `{/* … */}` 注释说明这一段列表的职责与内容(例如 `{/* 每行点击区、图标、名称、特殊属性与九个数值,直接作为 Scroll 子项 */}`、`{/* 每个组合的按键框与组合文本,展开为同级子项 */}`);**描述块(`equipbox` / `toolbox` 的 `TextContent` 说明区)不加任何 `{/* … */}`**——它不是列表,计数闸门要求 `equipbox [1,0,1,1]` / `toolbox [1,1,0]`;**`//` 一律留在 JS 代码位置**(渲染函数体 `return (` 之前 / map 回调体 / 返回的数组字面量内部),不得出现在 JSX 子元素位置(否则会变成 JSXText 并渲染出多余 Text 子项);**换言之,`<Scroll>` 内只会有 `{/* … */}` 这一种 JSX 级注释,裸 `//` 文本为零**;不修改任何既有注释文本。
|
||||
- **循环变量**:`groupIndex` / `commandIndex` → `i`,内层 `index` → `j`;列表渲染不再有 `for`;剩余 `for (const x of y)` 必须单行(`for (` 后不得换行)。
|
||||
|
||||
④ **需要你重点核对的行为变化:** **没有行为变化。** 本次只改 JSX 的组织方式:
|
||||
- `Scroll` 的直接子元素集合与 09-29 完全一致(数量、顺序、`key`、`loc`、事件、`noevent` 都不变)。
|
||||
- 0 值回退、`TextContent` 高度测量与 `key` 重挂载、`equipbox` 拖拽 / 悬停、`toolbox` 再点使用、`hotkey` 捕获 / 写回 / 分组切换 / 无入口 / 无持久化 / 无冲突 / 无恢复默认,全部不变。
|
||||
- 唯一可感知的差异是**源码写法**:不再有顶部数组,改为一处内联 `.map()`;新增若干职责注释。
|
||||
- 一个**技术前提**需要你知道:一行返回数组(数组里再含内层数组)依赖「数组子元素被逐个插入父节点」,这一点由 `packages/render-vue/src/renderer.ts:41-47` 的 `insert` → `parent.appendChild(el)` 保证,`Scroll` 的 slot 类型也接受 `VNode | VNode[]`;`choices.tsx` 的数组写法用在 `Page` 的 slot,**嵌套层级与本计划不同**,只能作为写法参考、不能当作同构证据。若执行期发现它被当成单个子元素,我会**停下上报**,不会自行退回预建数组、也不会改 `scroll.tsx`(见 OI-2)。
|
||||
- 另一处**已修正的坑**:`bookDetail` 的两条既有 `//` 注释必须留在渲染函数体 `return (` 之前。若照「放到映射表达式上方」写,它们会成为 `<Scroll>` 的 JSX 子元素文本(JSXText),渲染出多余的 Text 子项——这类 Text 不会被 `scroll.tsx:290` 的 `isComment` 过滤掉,会进入 `updateScroll` 的 children 计算。本次闸门会专门断言这一点。
|
||||
|
||||
⑤ **影响范围:**
|
||||
- 修改 5 个既有 UI 文件:`src/client/ui/{book,bookDetail,equipbox,toolbox,hotkey}.tsx`。
|
||||
- 修改 1 个既有测试:`src/client/ui/__test__/uiContainerGate.test.ts`(新增 4 组断言、`hotkey` needle 改 `j`,其余保留)。
|
||||
- 另有 3 个 `.planning` 产物:本计划、`09-30-BASELINE.json`、`09-30-SUMMARY.md`。合计 9 个路径。
|
||||
- **显式不改**:`packages-user/client-base/src/components/scroll.tsx` / `textbox.tsx`、`src/client/ui/settings.tsx`、`src/client/ui/index.ts`、`src/client/misc/hotkey.ts`、**`src/client/ui/inputBox.tsx` / `waitBox.tsx`**、其它任何 UI、依赖与配置。
|
||||
|
||||
⑥ **验证方式:**
|
||||
1. `pnpm exec eslint` 跑 **同一组 6 个代码 / 测试文件**(5 个 UI + `uiContainerGate.test.ts`),基线采集与最终比较的 scope 完全相同,只判 **NEW**;路径归一化口径固定为「反斜杠转正斜杠后,从 `src/client/ui/` 开始截取」,Task 1 采集与 Task 4 比较逐字一致。
|
||||
2. `pnpm check:type`(整仓 `vue-tsc --noEmit`)前后基线比较:过滤范围同样是**同一组 6 个路径**,只对改动文件内的 **NEW** 诊断判失败;既有基线诊断允许消失;不修无关错误。
|
||||
3. `src/client/ui/__test__/uiContainerGate.test.ts`:
|
||||
- **保留**原有 9 组:`Scroll` 块内无 `container` / 无 `hidden=`、每文件仅根 `container`、`hotkey` 两栏与选中边框、`book` 四项 / `bookDetail` 两项 0 回退、`bookDetail` 高度与 `key`、显示开关分流与 `noevent` 逐元素配对、`hotkey` 根 `nocache`、4 个 `*_SCROLL_LOC` 带 `: ElementLocator`。
|
||||
- **新增 4 组**:⑩无预建 `VNode` 数组——五个文件不出现 `VNode` 标识符 / `VNode[]` / `: VNode[]`,不出现 `specialNodes` / `statusNodes` / `slotNodes` / `ownedNodes` / `permNodes` / `consNodes` / `groupNodes` / `commandNodes` 及其 `.push(`,`book` 不再有 `rows.push(`;⑪内联 `.map()` 与短循环变量——存在 `enemies.value.map(` / `specialRows.value.map(` / `attributes.map(` / `slots.map(` / `owned.value.map(` / `permItems.value.map(` / `consItems.value.map(` / `groups.map(` / `commandRows.value.map(` / `combinationLists.value[i]` + `.map((keys, j)`,不含 `groupIndex` / `commandIndex`,且 `for (` 后没有换行;⑫`book` 每行 13 个元素齐全——13 个 `key` 前缀(`hit-` / `icon-` / `name-` / `special-` / `hp-` / `atk-` / `def-` / `money-` / `exp-` / `dmg-` / `crit-` / `reduce-` / `def1-`)逐个存在,`生命` / `攻击` / `防御` / `金币` / `经验` 五个基础数值文案与 `specialText(base)` 存在;⑬注释位置与数量——`getScrollBlocks()` 在 `stripComments` **之前**统计每块 `{/* … */}` 数量为 book `[1]` / bookDetail `[1]` / equipbox `[1,0,1,1]` / toolbox `[1,1,0]` / hotkey `[1,1]`,每块先对**未删容器**的原始子元素区断言首字符是 `{` 或 `<`、再对删掉 `{/* … */}` 与成对 `{…}` 后的剩余文本断言不含 `//`(**顺序不可颠倒**:先删容器会让 `book` / `bookDetail` / `hotkey` 的 `rest` 只剩空白、首字符断言恒假;描述块的子元素是 `<TextContent …/>`,不要求剩余为空),且既有 `//` 注释逐条各出现一次(`book` 三条,含单独成行的 `// 临界信息缺失时回退为 0…`)、`bookDetail` 的两条不在 `Scroll` 块内。
|
||||
- `hotkey` 的 needle 从 `selectKey(row.command, index)` 改为 `selectKey(row.command, j)`。
|
||||
- 每个 `it` 上方紧邻一行 `//` 注释。
|
||||
4. `src/client/ui/__test__/hotkeySettingsGate.test.ts` 仍为绿(`hotkey.tsx` 仍不含 `<g-rect`、捕获阶段监听、`setKeyCombination(`、无持久化)。
|
||||
5. 受保护文件哈希闸门(**7 个**:`scroll.tsx`、`textbox.tsx`、`settings.tsx`、`ui/index.ts`、`misc/hotkey.ts`、`inputBox.tsx`、`waitBox.tsx`);HEAD 与基线相同,**或** HEAD 不同但 `git diff --name-only <baseline>..HEAD` 的每个路径都以 `.planning/` 开头。
|
||||
6. 改动范围闸门:只有 6 个代码 / 测试路径,且用整仓 `git status --porcelain -uall` 与基线 `dirty` 快照比较,任何「不在快照、不属于这 6 个路径、也不在 `.planning/`」的新路径都判范围外写入。**注意**:09-29 的改动尚未提交,这 6 个路径在基线里本来就是脏的,因此该闸门只能证明「没有新增范围外路径」;内容是否改到位由第 3 条源码闸门负责。
|
||||
7. `keyAdaptationGate.test.ts` 是既存失败(15 个用例 5 个失败),不在本计划范围,不修、不声称全绿。
|
||||
8. **不做浏览器 UAT**:滚动、内存、点击命中、拖拽手感留给你人工确认。
|
||||
|
||||
⑦ **命名清单(OI-1):** 不新增任何文件作用域标识符;只改循环 / 函数内局部变量名:`book` 的 `view` / `i`,`bookDetail` 的 `row`,`equipbox` 的 `attr` / `name` / `item` / `i`,`toolbox` 的 `item` / `i`,`hotkey` 的 `i` / `id` / `group` / `row` / `keys` / `j`;**删除** `rows` / `specialNodes` / `statusNodes` / `slotNodes` / `ownedNodes` / `permNodes` / `consNodes` / `groupNodes` / `commandNodes` / `groupIndex` / `commandIndex` / `index`。`selectKey` 的函数签名保持 `(command, index)`,只把调用点实参改为 `j`。既有公共 / 受保护 / 导出命名、`GameUI` id 一律不改。**你若要求其它命名,请在回复中说明,我会整体替换后再执行。**
|
||||
|
||||
⑧ **两条铁律:** **不做浏览器 UAT、不宣称任何视觉 / 滚动 / 点击 / 拖拽行为已验证**;**执行器不提交任何代码**(禁止 `git add` / `commit` / `stash` / `checkout` / `worktree` / `reset`),代码改动留在工作树供你审查。三个 `.planning` 产物可能由 GSD 编排器按 `commit_docs: true` 聚合提交,不算本计划的代码提交。
|
||||
|
||||
⑨ **基线记录(本任务不写、只说明字段;实际写出在 Task 1 的第一步):** schema 固定为 `{ head: string, hashes: { <完整仓库相对路径>: <sha> ×7 }, dirty: string[], eslint: string[], typecheck: string[] }`。`head` 取采集当次的 `git rev-parse HEAD`(规划日实测值 `e2415a7a48d9a9e9d2f886e00c10ca51e91340d1`,仅供本次汇报参考);`hashes` 为 7 个受保护文件的 `git hash-object`;`dirty` 为采集当时 `git status --porcelain -uall` 的全仓脏文件路径快照(剥掉状态列、去掉 `.planning/`);`eslint` / `typecheck` 为**同一组 6 个路径**(5 个 UI + `src/client/ui/__test__/uiContainerGate.test.ts`)的诊断快照,归一化代码与 scope 在 Task 1 与 Task 4 逐字一致。
|
||||
|
||||
⑩ **修改方式:** 本任务(Task 0)**不写任何文件**(含基线、含任何代码);获批后由 `gsd-executor` 子代理执行 Task 1–4(非内联),只写 `files_modified` 的 9 个路径,其中 `09-30-BASELINE.json` 是 Task 1 的第一步;执行器不提交任何代码。「开始执行」的界限仍是你的「可以执行」四个字。
|
||||
</action>
|
||||
<how-to-verify>
|
||||
逐条核对 ①–⑩ 是否与你的意图一致,特别是:①五个文件的数组删除清单是否就是你要的;②「一行多元素返回数组、不包容器」的写法是否与你认可的内联 JSX 风格一致(渲染层依据是 `renderer.ts:41-47`,不是与 `choices.tsx` 嵌套同构);③`bookDetail` 的 `specialRows` 与 `hotkey` 的 `commandRows` 保留是否符合预期;④注释只新增、既有注释原文保留,且 `//` 一律留在 JS 代码位置(`bookDetail` 的两条留在 `return (` 之前、不进 `<Scroll>`);⑤循环变量 `i` / `j` 的改名是否接受;⑥`uiContainerGate.test.ts` 保留原有断言 + 新增 4 组(含 `book` 每行 13 元齐全与注释位置 / 数量)是否就是你要的覆盖面;⑦「不做浏览器 UAT / 执行器不提交代码」与基线字段、HEAD 允许 `.planning` 聚合提交的口径;⑧09-29 改动未提交导致范围闸门只能证明「无新增范围外路径」这一点是否接受。
|
||||
</how-to-verify>
|
||||
<resume-signal>仅当完全同意时回复恰好四个字「可以执行」;任何修改意见(**尤其是 OI-1 的局部变量命名是否需要改名**)请直接说明,本计划会据此改写后再来一次汇报。</resume-signal>
|
||||
</task>
|
||||
<task type="auto">
|
||||
<name>Task 1: 写出改动前基线,然后把 `book.tsx` 与 `bookDetail.tsx` 的列表改为内联 `.map()`</name>
|
||||
<files>.planning/phases/09-minimum-browser-runnable/09-30-BASELINE.json(新增,第一步写出), src/client/ui/book.tsx, src/client/ui/bookDetail.tsx</files>
|
||||
<action>
|
||||
**依赖 Task 0 已收到「可以执行」。** **第一步(必须在任何代码编辑之前):** 写出 `.planning/phases/09-minimum-browser-runnable/09-30-BASELINE.json`,schema 为 `{ head, hashes, dirty, eslint, typecheck }`:
|
||||
- `head`:当前 `git rev-parse HEAD`。
|
||||
- `hashes`:`packages-user/client-base/src/components/scroll.tsx`、`packages-user/client-base/src/components/textbox.tsx`、`src/client/ui/settings.tsx`、`src/client/ui/index.ts`、`src/client/misc/hotkey.ts`、`src/client/ui/inputBox.tsx`、`src/client/ui/waitBox.tsx` 的 `git hash-object`(**7 条**),键用完整仓库相对路径(POSIX 正斜杠),值小写 sha。
|
||||
- `dirty`:`git status --porcelain -uall`(**不加 pathspec**),取状态列之后的路径、去掉 `.planning/` 开头的条目,按字典序写入。**注意:09-29 的 5 个 UI 文件与 `uiContainerGate.test.ts` 尚未提交,必然出现在这份快照里**;这是基线,不是异常。
|
||||
- `eslint`:对**同一组 6 个路径**(`src/client/ui/book.tsx`、`bookDetail.tsx`、`equipbox.tsx`、`toolbox.tsx`、`hotkey.tsx`、`src/client/ui/__test__/uiContainerGate.test.ts`)跑 `pnpm exec eslint -f json`,每条归一化为 `仓库相对路径|规则ID|严重度|消息`,归一化代码必须与 Task 4 逐字相同:`const norm = p => { const s = String(p).replace(/\\/g, '/'); const i = s.indexOf('src/client/ui/'); return i >= 0 ? s.slice(i) : s; };`,条目为 `norm(f.filePath) + '|' + String(m.ruleId || 'null') + '|' + String(m.severity) + '|' + String(m.message).trim()`。**基线采集与 Task 4 比较的 scope 必须完全相同(都是这 6 个路径,测试文件不排除)**。`eslint` 以 `status === 1` 退出属于正常(有告警),必须照常解析 stdout;只有未运行 / stdout 为空 / JSON 不可解析才停下上报。
|
||||
- `typecheck`:整仓跑 `pnpm check:type`,再**原样复制 Task 4 的 `norm`**——`const norm=l=>l.replace(/\\/g,'/').replace(/\(\d+,\d+\):/,'').replace(/^.*?(?=(src|packages-user)\/)/,'').trim();`——并用与 Task 4 **逐字相同的 6 路径 `scope`** 过滤出这些文件内的诊断,保留重复项与 `src/` / `packages-user/` 前缀。只写一次,此后不得重写。
|
||||
|
||||
**第二步:`src/client/ui/book.tsx`。**
|
||||
1. `vue` 导入去掉 `VNode`:`import { computed, defineComponent } from 'vue';`。其它导入不变(导入之间不得夹注释 / 空行)。
|
||||
2. 渲染函数体改为直接返回 JSX:删除 `const rows: VNode[] = [];`、`for (let i = 0; i < enemies.value.length; i++) { … }` 与 `Scroll` 内的 `{rows}`;写成 `return () => ( … );`。
|
||||
3. `Scroll` 的 children 改为内联映射,**不包容器**:`{enemies.value.map((view, i) => { const base = view.getBaseEnemy(); const locator = ctx.getEnemyLocatorByView(view); const critical = getCritical(base, locator); const def1 = getDef1(base, locator); const y = i * ROW_HEIGHT;`,然后换行**单独成行**写 `// 临界信息缺失时回退为 0,让「没有数据」与「数值为 0」可以区分`,再写 `const damage = critical ? critical.baseInfo.damage : 0; const nextDiff = critical ? critical.nextDiff : 0; const damageDiff = critical ? -critical.damageDiff : 0; const def1Damage = critical && def1 ? critical.baseInfo.damage - def1.baseInfo.damage : 0; return [ …13 个元素… ]; })}`。**原有那条 `// 临界信息缺失时回退为 0…` 保持 `//` 行注释形式与原文,留在回调体语句位置,不得改成 `/* … */` 块注释。**
|
||||
13 个元素必须与 09-29 逐一对应、顺序不变:透明 `g-rect`(`key={`hit-${base.code}`}`、`loc={[0, y, LIST_WIDTH, ROW_HEIGHT]}`、`fill` / `fillStyle="transparent"` / `cursor="pointer"` / `onClick={() => openDetail(view)}`)、`icon`、名称、特殊属性(`specialText(base)`)、五个基础数值(生命 / 攻击 / 防御 / 金币 / 经验)、四个临界数值(伤害 / 临界 / 减伤 / 1 防);`key` 仍为 `icon-` / `name-` / `special-` / `hp-` / `atk-` / `def-` / `money-` / `exp-` / `dmg-` / `crit-` / `reduce-` / `def1-` 加 `${base.code}`;除命中矩形外全部 `noevent`。
|
||||
4. 既有两条行内注释 `// 数值列表区`(五个基础数值第一项之前)与 `// 临界信息区`(四个临界数值第一项之前)**文本不变**,**留在 `map` 回调返回的数组字面量内部(元素之间的 JS 注释位置)**;它们属于 JS 表达式位置,不会生成 Text 子项。第三条既有注释 `// 临界信息缺失时回退为 0,让「没有数据」与「数值为 0」可以区分`(现第 125 行)同样**文本不变、保持 `//` 行注释、单独占一行**,留在回调体语句位置(`const damage = …` 之上,前面只有缩进),不得与语句同行、不得改成 `/* … */`。**不得把任何 `//` 放到 `<Scroll>` 的 JSX 子元素位置。**
|
||||
5. 在 `Scroll` 内、映射表达式之前新增一行 JSX 注释:`{/* 每行点击区、图标、名称、特殊属性与九个数值,直接作为 Scroll 子项 */}`(既有 `{/* 怪物列表区 */}` 保留在 `Scroll` 外,不改)。该注释是本文件 `Scroll` 块内**唯一**的 JSX 注释。
|
||||
6. 不得修改 `getCritical` 上方那条写着「留空」的既有 TODO;不得新增与其矛盾的解释性注释。根 `<container loc={FULL_LOC}>` 原样保留、不加 `nocache`。`loc` 全部保持内联,不新增常量。
|
||||
|
||||
**第三步:`src/client/ui/bookDetail.tsx`。**
|
||||
1. `vue` 导入去掉 `VNode`、保留 `watch`:`import { computed, defineComponent, ref, watch } from 'vue';`。
|
||||
2. 删除渲染函数内的 `const specialNodes: VNode[] = [];` 与 `for (const row of specialRows.value) { specialNodes.push(…); }`;`const enemy = current.value;` 保留。
|
||||
3. `Scroll` 的 children 改为:`{specialRows.value.map(row => [<text … key={`label-${row.special.code}`} />, <TextContent … key={`${row.special.code}-${row.textTop}-${row.textHeight}`} />])}`。两个元素的属性必须与 09-29 逐一对应(标题的 `text` / `loc={[0, row.labelTop]}` / `anc={[0, 0]}` / `font={specialFont}` / `fillStyle={row.special.getNameColor()}`;描述的 `text` / `width={SPECIAL_WIDTH}` / `loc={[0, row.textTop, SPECIAL_WIDTH, SPECIAL_MIN_HEIGHT]}` / `font` / `fillStyle="#dddddd"` / `autoHeight` / `onUpdateHeight={height => updateTextHeight(row.special.code, height)}`)。
|
||||
4. 既有两行 `//` 注释(`TextContent 只在 setup 时复制一次 props.loc…` 与 `因此 key 同时包含行位置与实测高度…`)**原样留在渲染函数体、`return (` 之前**(当前就在 `const enemy = current.value;` 之后、`for` 之前),不改文字、不改形式;删掉 `specialNodes` 声明后若需要落位,就放在 `const enemy = current.value;` 之后、`return (` 之前。**绝不能放到映射表达式上方或 `<Scroll>` 内**——那会成为 JSXText 并渲染出多余的 Text 子项。
|
||||
5. 在 `Scroll` 内、映射表达式之前新增一行 JSX 注释:`{/* 每行标题与描述,描述高度由实测回调撑开,直接作为 Scroll 子项 */}`。该注释是本文件 `Scroll` 块内**唯一**的注释,块内 `//` 行数必须为 0。
|
||||
6. `textHeights` / `updateTextHeight` / `watch` / `specialRows` / `SPECIAL_MIN_HEIGHT` / 0 值回退全部不动;根 `container` 原样保留、不加 `nocache`。
|
||||
7. **不得修改任何既有注释或 jsDoc**;新增注释遵循 dev.md(单行 `//`、`//` 后留空格、说明为什么)。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const b='.planning/phases/09-minimum-browser-runnable/09-30-BASELINE.json';const bad=[];if(!fs.existsSync(b))bad.push('baseline missing');else{const j=JSON.parse(fs.readFileSync(b,'utf8'));for(const k of ['head','hashes','dirty','eslint','typecheck'])if(!(k in j))bad.push('missing '+k);if(Object.keys(j.hashes||{}).length!==7)bad.push('hashes must have 7 entries');for(const f of ['packages-user/client-base/src/components/scroll.tsx','packages-user/client-base/src/components/textbox.tsx','src/client/ui/settings.tsx','src/client/ui/index.ts','src/client/misc/hotkey.ts','src/client/ui/inputBox.tsx','src/client/ui/waitBox.tsx'])if(!Object.prototype.hasOwnProperty.call(j.hashes||{},f))bad.push('hashes missing '+f);if(!Array.isArray(j.dirty))bad.push('dirty must be an array')}if(bad.length){console.error('TASK1 BASELINE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task1: baseline')"</automated>
|
||||
<automated>node -e "const fs=require('fs');const c=fs.readFileSync('src/client/ui/book.tsx','utf8').replace(/\/\*[^]*?\*\//g,'').replace(/^[ \t]*\/\/[^\n]*/gm,'');const f=c.replace(/\s+/g,' ');const bad=[];for(const n of ['format(damage)','format(nextDiff)','format(damageDiff)','format(def1Damage)','transparent','noevent','enemies.value.map('])if(f.indexOf(n)<0)bad.push('missing :: '+n);if(/\bVNode\b/.test(c))bad.push('VNode still present');if(c.indexOf('rows.push(')>=0)bad.push('rows.push still present');const n=(c.match(/<container(?=[\s>])/g)||[]).length;if(n!==1)bad.push('expected exactly 1 container, got '+n);let i=0;while(true){const a=c.indexOf('<Scroll',i);if(a<0)break;const e=c.indexOf('</Scroll>',a);if(e<0){bad.push('unclosed Scroll');break}const blk=c.slice(a,e);if(/<container(?=[\s>])/.test(blk))bad.push('container inside Scroll');if(/hidden\s*=/.test(blk))bad.push('hidden inside Scroll');i=e+1}if(bad.length){console.error('TASK1 BOOK GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task1: book.tsx')"</automated>
|
||||
<automated>node -e "const fs=require('fs');const c=fs.readFileSync('src/client/ui/bookDetail.tsx','utf8').replace(/\/\*[^]*?\*\//g,'').replace(/^[ \t]*\/\/[^\n]*/gm,'');const f=c.replace(/\s+/g,' ');const bad=[];for(const n of ['autoHeight','onUpdateHeight','textHeights','specialRows','textTop','textHeight','ISpecialRow','ISpecial<unknown>','format(critical.value ? critical.value.nextDiff : 0)','format(critical.value ? -critical.value.damageDiff : 0)','specialRows.value.map('])if(f.indexOf(n)<0)bad.push('missing :: '+n);if(/\bVNode\b/.test(c))bad.push('VNode still present');if(c.indexOf('specialNodes')>=0)bad.push('specialNodes still present');const n=(c.match(/<container(?=[\s>])/g)||[]).length;if(n!==1)bad.push('expected exactly 1 container, got '+n);let i=0;while(true){const a=c.indexOf('<Scroll',i);if(a<0)break;const e=c.indexOf('</Scroll>',a);if(e<0){bad.push('unclosed Scroll');break}const blk=c.slice(a,e);if(/<container(?=[\s>])/.test(blk))bad.push('container inside Scroll');if(/hidden\s*=/.test(blk))bad.push('hidden inside Scroll');i=e+1}if(bad.length){console.error('TASK1 BOOKDETAIL GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task1: bookDetail.tsx')"</automated>
|
||||
<automated>cmd /c "pnpm exec eslint src/client/ui/book.tsx src/client/ui/bookDetail.tsx"</automated>
|
||||
<automated>node -e "const fs=require('fs');const c=fs.readFileSync('src/client/ui/book.tsx','utf8');const bad=[];for(const n of ['specialText(base)','\u751f\u547d','\u653b\u51fb','\u9632\u5fa1','\u91d1\u5e01','\u7ecf\u9a8c'])if(c.indexOf(n)<0)bad.push('missing :: '+n);for(const k of ['hit-','icon-','name-','special-','hp-','atk-','def-','money-','exp-','dmg-','crit-','reduce-','def1-'])if(c.split(k).length-1<1)bad.push('missing key prefix :: '+k);for(const n of ['// \u6570\u503c\u5217\u8868\u533a','// \u4e34\u754c\u4fe1\u606f\u533a']){const m=c.split(n).length-1;if(m!==1)bad.push('comment count '+n+' = '+m)}const a=c.indexOf('<Scroll');if(a<0)bad.push('no Scroll');else{const e=c.indexOf('</Scroll>',a);const blk=c.slice(a,e);const jsx=(blk.match(/\{\s*\/\*/g)||[]).length;if(jsx!==1)bad.push('expected 1 JSX comment in Scroll, got '+jsx);const g=blk.indexOf('>')+1;if((blk.slice(g).trimStart()[0]||'')!=='{')bad.push('Scroll child position starts with non-{ (bare text/comment?)')}const rr=c.indexOf('return () =>');const lc=(c.slice(rr).match(/(^|\r?\n)[ \t]*\/\//g)||[]).length;if(lc!==3)bad.push('expected 3 line comments inside render body (legacy comments must each stay on its own line), got '+lc);if(bad.length){console.error('TASK1 BOOK ROW GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task1: book rows + comments')"</automated>
|
||||
<automated>node -e "const fs=require('fs');const c=fs.readFileSync('src/client/ui/bookDetail.tsx','utf8');const bad=[];const a=c.indexOf('<Scroll');if(a<0)bad.push('no Scroll');else{const e=c.indexOf('</Scroll>',a);const blk=c.slice(a,e);const jsx=(blk.match(/\{\s*\/\*/g)||[]).length;if(jsx!==1)bad.push('expected 1 JSX comment in Scroll, got '+jsx);if(/(^|\r?\n)[ \t]*\/\//.test(blk))bad.push('line comment inside Scroll (JSXText risk)');const g=blk.indexOf('>')+1;if((blk.slice(g).trimStart()[0]||'')!=='{')bad.push('Scroll child position starts with non-{')}const cc=[];const re=/^[ \t]*\/\/[^\n]*/gm;let m;while((m=re.exec(c)))cc.push(m.index);if(cc.length<2)bad.push('legacy comments missing, line comments = '+cc.length);for(const p of cc)if(p>a)bad.push('line comment after <Scroll at offset '+p);if(bad.length){console.error('TASK1 BOOKDETAIL COMMENT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task1: bookDetail comments stay before return')"</automated>
|
||||
<fails_when>基线缺失或字段残缺、`hashes` 不是 7 条、`dirty` 不是数组;`book.tsx` 仍含 `VNode` / `rows.push(`、缺内联 `enemies.value.map(`、缺 5 个基础数值文案 / `specialText(base)` / 任一 `key` 前缀、三条既有 `//` 注释不是各出现 1 次、`return () =>` 之后的 `//` 行注释不是 3 条(三条既有注释须各自单独成行,含 `// 临界信息缺失时回退为 0…`)、`Scroll` 块内 `{/* … */}` 不是 1 条、`Scroll` 子元素位置不是以 `{` 开头(裸注释 / 文本)、`<container>` 计数不是 1、`Scroll` 内出现 `container` 或 `hidden=`;`bookDetail.tsx` 仍含 `VNode` / `specialNodes`、缺 `specialRows.value.map(` 或任一高度逻辑标识、`Scroll` 块内出现任何 `//` 行或 `{/* … */}` 不是 1 条、任一 `//` 注释出现在 `<Scroll>` 之后、`<container>` 计数不是 1、`Scroll` 内出现 `container` 或 `hidden=`;eslint 报错;或任一 node 闸门非零退出。</fails_when>
|
||||
</verify>
|
||||
<done>`09-30-BASELINE.json` 已在改代码前写出且只写一次(7 个哈希、6 个路径的 eslint / typecheck 基线);`book.tsx` 用 `enemies.value.map((view, i) => [...13 个同级元素])` 内联渲染,无 `VNode` / `rows` / `rows.push`,13 个 `key` 前缀、5 个基础数值文案与 `specialText(base)` 齐全,四项临界信息缺失仍为 `0`;三条既有 `//` 注释文本不变(两条留在数组字面量内部、`// 临界信息缺失时回退为 0…` 单独成行留在回调体),`Scroll` 块内只有 1 条 `{/* … */}`、子元素位置无裸注释,`Scroll` 内无容器无 `hidden`;`bookDetail.tsx` 用 `specialRows.value.map(row => [标题, TextContent])` 内联渲染,无 `VNode` / `specialNodes`,高度测量与 `key` 重挂载不变,两条既有 `//` 保留在 `return (` 之前、`Scroll` 块内 0 条 `//` 且只有 1 条 `{/* … */}`;eslint 无报错。</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: 把 `equipbox.tsx` 与 `toolbox.tsx` 的三个 / 两个列表改为内联 `.map()`</name>
|
||||
<files>src/client/ui/equipbox.tsx, src/client/ui/toolbox.tsx</files>
|
||||
<action>
|
||||
**依赖 Task 1 已完成。只改这两个文件;不改任何逻辑体(`startPress` / `movePress` / `endPress` / `clickItem` / `getTargetSlot` / `collect` / `selectedItem` 等),不改既有单行 `for`。**
|
||||
|
||||
**A. `src/client/ui/equipbox.tsx`**
|
||||
1. `vue` 导入去掉 `VNode`:`import { computed, defineComponent, ref } from 'vue';`。**`ElementLocator` 的独立一行导入与 4 个 `*_SCROLL_LOC` 常量保留不动**;其它导入不变。
|
||||
2. 渲染函数删除 `const statusNodes: VNode[] = [];` / `slotNodes` / `ownedNodes` 三个声明与三个 `for` 循环;`const equip = selectedEquip.value;` 与 `const d = diff.value;` 保留。
|
||||
3. 状态行改为内联映射,**不包容器**:`{props.attributes.map((attr, i) => { const value = hero.attribute.getFinalAttribute(attr.id); let delta = 0; if (d) { const raw = d[attr.id]; if (typeof raw === 'number') delta = raw; } const valueX = delta === 0 ? STATUS_WIDTH - 8 : STATUS_WIDTH - 60; const y = i * STATUS_ROW_HEIGHT; return [<text … key={`name-${attr.id}`} />, <text … key={`value-${attr.id}`} />, <text … key={`delta-${attr.id}`} />]; })}`。第三个元素的 `text` 仍是 `delta === 0 ? '' : `(${delta > 0 ? '+' : ''}${delta})``,`fillStyle` 仍是 `delta > 0 ? '#57ff78' : '#ff5a5a'`;既有注释 `// Scroll 会覆盖直接子元素的 hidden,增减值为 0 时改用空文本` 文本不变,**留在该 map 回调返回的数组字面量内部(元素之间的 JS 注释位置,不生成 Text 子项)**。
|
||||
4. 槽位改为内联映射:`{hero.equip.slots.map((name, i) => { const slotEquip = equippedList.value[i]; const isSelected = !!slotEquip && slotEquip.uid === selected.value; const cellX = i * SLOT_CELL_WIDTH; return [<g-rectr … key={`slot-${i}`} />, <icon … key={`icon-${i}`} />, <text … key={`name-${i}`} />]; })}`。`g-rectr` 的 `loc` / `circle` / `fill` / `stroke` / `fillStyle` / `strokeStyle` / `lineWidth` / `cursor` / `onEnter` / `onLeave` 与 `icon` 的 `loc` / `cursor` / `onEnter` / `onLeave` / `onDown` / `key` 全部逐字保留;既有注释 `// 拖动起手仍只在图标上,整格只负责悬停判定` 文本不变,**留在该 map 回调返回的数组字面量内部**。
|
||||
5. 拥有装备行改为内联映射:`{owned.value.map((item, i) => { const isSelected = item.uid === selected.value; const y = i * OWNED_ROW_HEIGHT; return [<g-rectr … key={`row-${item.uid}`} />, <icon … key={`icon-${item.uid}`} />, <text … key={`name-${item.uid}`} />]; })}`;`onDown={ev => startPress(item.uid, -1, ev.absoluteX, ev.absoluteY)}` 与 `fillStyle` 三元逐字保留。
|
||||
6. 每个**列表** `Scroll` 内、映射表达式之前新增一行 JSX 注释说明职责与内容(**描述块不加**——它不是列表,计数闸门要求本文件四块为 `[1,0,1,1]`):状态区 `{/* 每行属性名、数值与增减值,直接作为 Scroll 子项 */}`;装备槽区 `{/* 每格槽底、图标与槽名,直接作为 Scroll 子项 */}`;拥有装备区 `{/* 每行行底、图标与名称,直接作为 Scroll 子项 */}`。既有 `{/* 状态区 */}` 等注释保留不动。
|
||||
7. 4 个 `*_SCROLL_LOC` 常量保持 `: ElementLocator` 注解与 `loc={NAME}` 引用;根 `<container loc={FULL_LOC} onMove=… onUp=…>` 原样保留、不加 `nocache`;不得再出现 `VNode` / 三个数组名 / 行内 `container`。
|
||||
8. **注释位置**:3 条新增注释都是 `{/* … */}`、位于各自 `Scroll` 的 JSX 子元素位置;两条既有 `//` 只落在 map 回调体 / 返回的数组字面量内部,不得到达 JSX 子元素位置(否则变成 JSXText 并渲染出多余 Text 子项)。
|
||||
|
||||
**B. `src/client/ui/toolbox.tsx`**
|
||||
1. `vue` 导入去掉 `VNode`:`import { computed, defineComponent, ref } from 'vue';`。
|
||||
2. 删除 `const permNodes: VNode[] = [];` / `consNodes` 与两个 `for`;`const current = selectedItem.value;` 保留。
|
||||
3. 两列各改为内联映射,各返回四元组,**不包容器**:`{permItems.value.map((item, i) => { const y = i * ROW_HEIGHT; const isSelected = item.num === selected.value; return [<g-rectr … key={`row-${item.num}`} />, <icon … key={`icon-${item.num}`} />, <text … key={`name-${item.num}`} />, <text … key={`count-${item.num}`} />]; })}`;`{consItems.value.map((item, i) => { …同结构… })}`。`g-rectr` 的 `loc` / `circle` / `fill` / `fillStyle` / `cursor` / `onClick={() => clickItem(item)}`、`icon` 与两个 `text` 的属性逐字保留。
|
||||
4. **只在这两个列表 `Scroll` 内**(永久物品列 / 消耗物品列)、映射表达式之前各新增一行 JSX 注释:`{/* 每行行底、图标、名称与数量,直接作为 Scroll 子项 */}`。**描述 `Scroll` 内不新增任何 `{/* … */}` 注释**——它的直接子元素是单个 `<TextContent …/>`、不是列表;注释计数闸门要求本文件三块依次为 `[1,1,0]`(永久 1 / 消耗 1 / 描述 0),在描述块加一条会让 Task 4 第 13 组直接失败。
|
||||
5. 未选择提示仍在说明 `Scroll` **之前**、`hidden={!!current}` 不变;描述 `TextContent` 的 `text={current ? current.raw.text : ''}` 空文本回退不变;`clickItem` 的「再点使用」逻辑零改动。
|
||||
6. 根 `<container loc={FULL_LOC}>` 原样保留、不加 `nocache`;两个 `Scroll` 的 `loc` 保持内联(本文件不新增常量);不得再出现 `VNode` / 两个数组名 / 行内 `container`。
|
||||
7. **注释位置**:2 条新增注释都是 `{/* … */}`、只位于两个**列表** `Scroll`(永久 / 消耗)的 JSX 子元素位置,**描述 `Scroll` 内一条也不加**;本文件没有既有 `//` 需要保留,写完后三个 `Scroll` 块内都不应出现任何 `//` 行,且块内 `{/* … */}` 数量依次为 `1` / `1` / `0`。
|
||||
8. **不得修改任何既有注释或 jsDoc**;新增注释遵循 dev.md。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const read=p=>fs.readFileSync(p,'utf8');const strip=s=>s.replace(/\/\*[^]*?\*\//g,'').replace(/^[ \t]*\/\/[^\n]*/gm,'');const e=strip(read('src/client/ui/equipbox.tsx'));const ef=e.replace(/\s+/g,' ');const t=strip(read('src/client/ui/toolbox.tsx'));const tf=t.replace(/\s+/g,' ');const bad=[];for(const n of ['hidden={!!equip}','hidden={!equip}','hidden={!dragging.value}',\"delta === 0 ? ''\",\"equip ? equip.item.text : ''\",'slotEquip ? slotEquip.item.num : 0','STATUS_SCROLL_LOC','DESC_SCROLL_LOC','SLOT_SCROLL_LOC','OWNED_SCROLL_LOC','import { ElementLocator }','STATUS_SCROLL_LOC: ElementLocator','DESC_SCROLL_LOC: ElementLocator','SLOT_SCROLL_LOC: ElementLocator','OWNED_SCROLL_LOC: ElementLocator','props.attributes.map(','hero.equip.slots.map(','owned.value.map('])if(ef.indexOf(n)<0)bad.push('equip missing :: '+n);if(/\bVNode\b/.test(e))bad.push('equip still has VNode');for(const n of ['statusNodes','slotNodes','ownedNodes'])if(e.indexOf(n)>=0)bad.push('equip leftover :: '+n);for(const n of ['{!equip &&','{equip &&','{delta !== 0 &&','{slotEquip &&','{dragging.value &&','hidden={!slotEquip}','hidden={delta === 0}'])if(e.indexOf(n)>=0)bad.push('equip leftover :: '+n);for(const n of ['hidden={!!current}',\"current ? current.raw.text : ''\",'permItems.value.map(','consItems.value.map('])if(tf.indexOf(n)<0)bad.push('toolbox missing :: '+n);if(/\bVNode\b/.test(t))bad.push('toolbox still has VNode');for(const n of ['permNodes','consNodes'])if(t.indexOf(n)>=0)bad.push('toolbox leftover :: '+n);for(const n of ['{!current &&','{current &&','hidden={!current}'])if(t.indexOf(n)>=0)bad.push('toolbox leftover :: '+n);for(const pair of [['equipbox',e],['toolbox',t]]){const n=(pair[1].match(/<container(?=[\s>])/g)||[]).length;if(n!==1)bad.push(pair[0]+' expected exactly 1 container, got '+n);if(/for\s*\(\s*\r?\n/.test(pair[1]))bad.push(pair[0]+' has a wrapped for (');let i=0;while(true){const a=pair[1].indexOf('<Scroll',i);if(a<0)break;const z=pair[1].indexOf('</Scroll>',a);if(z<0){bad.push(pair[0]+' unclosed Scroll');break}const blk=pair[1].slice(a,z);if(/<container(?=[\s>])/.test(blk))bad.push(pair[0]+' container inside Scroll');if(/hidden\s*=/.test(blk))bad.push(pair[0]+' hidden inside Scroll');i=z+1}}if(bad.length){console.error('TASK2 GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task2: equipbox + toolbox')"</automated>
|
||||
<automated>cmd /c "pnpm exec eslint src/client/ui/equipbox.tsx src/client/ui/toolbox.tsx"</automated>
|
||||
<automated>node -e "const fs=require('fs');const read=p=>fs.readFileSync(p,'utf8');const bad=[];const spec=[['src/client/ui/equipbox.tsx',[1,0,1,1],2],['src/client/ui/toolbox.tsx',[1,1,0],0]];for(const [p,exp,expLc] of spec){const c=read(p);const blocks=[];let i=0;while(true){const a=c.indexOf('<Scroll',i);if(a<0)break;const e=c.indexOf('</Scroll>',a);if(e<0){bad.push(p+' unclosed Scroll');break}blocks.push(c.slice(a,e));i=e+9}if(blocks.length!==exp.length)bad.push(p+' Scroll count '+blocks.length+' != '+exp.length);let lc=0;blocks.forEach((b,ix)=>{const jsx=(b.match(/\{\s*\/\*/g)||[]).length;if(jsx!==(exp[ix]||0))bad.push(p+' Scroll#'+ix+' JSX comments '+jsx+' != '+exp[ix]);lc+=(b.match(/(^|\r?\n)[ \t]*\/\//g)||[]).length;const g=b.indexOf('>')+1;const f=(b.slice(g).trimStart()[0]||'');if(f!=='{'&&f!=='<')bad.push(p+' Scroll#'+ix+' child position starts with '+f)});if(lc!==expLc)bad.push(p+' line comments inside Scrolls '+lc+' != '+expLc)}if(bad.length){console.error('TASK2 COMMENT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task2: comment placement')"</automated>
|
||||
<fails_when>两文件仍含 `VNode` / 对应数组名、缺任一内联 `.map(`、`<container>` 计数不为 1、`Scroll` 内仍有 `container` 或 `hidden=`、出现换行的 `for (`;`equipbox` 缺任一 `hidden` / 动态空文本 / 0 回退或 4 个 `*_SCROLL_LOC`(含 `: ElementLocator`)或 `ElementLocator` 导入,或仍保留旧条件渲染;`toolbox` 仍用条件渲染或 `TextContent` 上仍有 `hidden`;`Scroll` 块数与每块 `{/* … */}` 数量不等于 `equipbox [1,0,1,1]` / `toolbox [1,1,0]`、`Scroll` 内 `//` 行数不是 `equipbox 2` / `toolbox 0`、或某块子元素位置不是以 `{` / `<` 开头(裸注释 / 文本);eslint 报错;或 node 闸门非零退出。</fails_when>
|
||||
</verify>
|
||||
<done>`equipbox.tsx` 三处列表都是内联 `.map()` 返回三元组,无 `VNode` / 三个数组名;显示开关分流(`Scroll` 外 `hidden`、`Scroll` 内空文本 / 0 回退)与拖拽 / 悬停语义不变;4 个 `*_SCROLL_LOC` 与 `ElementLocator` 导入保留;`toolbox.tsx` 两列都是内联 `.map()` 返回四元组,无 `VNode` / 两个数组名;未选择提示仍在说明 `Scroll` 之前、`clickItem` 逻辑不变;两文件 `Scroll` 块内 `{/* … */}` 数量分别为 `[1,0,1,1]` / `[1,1,0]`,子元素位置无裸注释,两条既有 `//` 保留在数组字面量内部;eslint 无报错。</done>
|
||||
</task>
|
||||
<task type="auto">
|
||||
<name>Task 3: 把 `hotkey.tsx` 两栏改为内联 `.map()`(外层 `i`、内层 `j`),保留 `commandRows` / `combinationLists`</name>
|
||||
<files>src/client/ui/hotkey.tsx</files>
|
||||
<action>
|
||||
**依赖 Task 2 已完成。只改这个文件;捕获 / 写回 / 关闭 / 无入口 / 无持久化 / 无冲突 / 无恢复默认的行为口径零改动。**
|
||||
1. `vue` 导入去掉 `VNode`、保留 `computed` / `defineComponent` / `onUnmounted` / `ref` / `shallowRef`(Prettier 可能折成多行,按其输出即可)。`@user/client-base` 的 `IGameUIPropsBase, Scroll, useKey` 导入不变(本文件本来就没有 `Selection`)。
|
||||
2. 删除渲染函数内的 `const groupNodes: VNode[] = [];` / `const commandNodes: VNode[] = [];`、`for (let groupIndex = …)` 与 `for (let commandIndex = …)` 两段循环;渲染函数改为直接返回 JSX。
|
||||
3. 左栏 `Scroll` 的 children 改为内联映射,**不包容器**:`{groups.map(([id, group], i) => { const top = i * GROUP_ROW_HEIGHT; const isSelected = id === selectedGroup.value; return [<g-rectr … key={`hit-${id}`} />, <text … key={`name-${id}`} />]; })}`。`g-rectr` 的 `loc` / `fill` / `stroke` / `fillStyle="transparent"` / `strokeStyle={isSelected ? 'gold' : 'transparent'}` / `lineWidth={isSelected ? 2 : 1}` / `cursor` / `onClick={() => selectGroup(id)}` 逐字保留;`text` 的 `text={group.name}` / `loc` / `anc` / `font` / `fillStyle` / `noevent` 逐字保留;既有注释 `// 选中分组只由这一行的边框表示,元素始终挂载` 文本不变,**留在左栏 map 回调体 / 返回的数组字面量内部**。
|
||||
4. 右栏 `Scroll` 的 children 改为内联映射,**不包容器**:`{commandRows.value.map((row, i) => { const list = combinationLists.value[i] ?? []; return [<text … key={`name-${row.command.command}`} />, ...list.map((keys, j) => { const boxTop = row.top + j * KEY_BOX_HEIGHT; const isSelected = selectedCommand.value === row.command && selectedIndex.value === j; const showing = isSelected && heldKeys.value.length > 0 ? heldKeys.value : keys; return [<g-rectr … key={`box-${i}-${j}`} />, <text … key={`text-${i}-${j}`} />]; })]; })}`。`g-rectr` 的 `loc={[BOX_LEFT, boxTop, KEY_BOX_WIDTH, BOX_FILL_HEIGHT]}` / `circle={[4]}` / `fill` / `stroke` / `fillStyle` / `strokeStyle={isSelected ? 'gold' : 'transparent'}` / `lineWidth={isSelected ? 2 : 1}` / `cursor` / `onClick={() => selectKey(row.command, j)}` 逐字保留;组合文本的 `text={formatCombination(showing)}` / `loc` / `anc` / `font` / `fillStyle` / `noevent` 逐字保留;既有注释 `// 选中按键框同样只切换这个既有矩形的描边` 文本不变,**留在右栏 map 回调体 / 返回的数组字面量内部**。
|
||||
5. 每个 `Scroll` 的 **JSX 子元素位置**新增一行 `{/* … */}`:左栏 `{/* 每行点击区与分组名,选中态由同一矩形的描边表示 */}`;右栏 `{/* 每条指令名与它的每个按键框、组合文本,展开为同级子项 */}`;内层组合映射的说明 `// 每个组合展开为按键框与文本两个同级子项` 放在**内层回调体、`return [` 之前**(JS 语句位置,不是 JSX 子元素位置)。
|
||||
6. `ICommandRow` / `commandRows` / `combinationLists` / `refreshCombinations` / `formatCombination` / `selectGroup` / `selectKey` / `onKeyDown` / `onKeyUp` / `close` 全部保留(`selectKey` 的函数签名仍是 `(command, index)`,只改调用点实参为 `j`);根 `<container loc={FULL_LOC} nocache>` 原样保留;两个 `Scroll` 的 `loc` 保持内联(本文件不新增常量);不得出现 `VNode` / 两个数组名 / `groupIndex` / `commandIndex` / 换行的 `for (`。
|
||||
7. **注释位置**:两条既有 `//` 与新增的内层 `//` 只落在 map 回调体 / 数组字面量内部,JSX 子元素位置只放那两条 `{/* … */}`;写完后每个 `Scroll` 块内 `{/* … */}` 恰好 1 条、`//` 行只出现在 `commandRows.value.map(` 之后(即回调体内部),且块的子元素位置以 `{` 开头。
|
||||
8. **不得修改任何既有注释或 jsDoc。**
|
||||
</action>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const strip=s=>s.replace(/\/\*[^]*?\*\//g,'').replace(/^[ \t]*\/\/[^\n]*/gm,'');const h=strip(fs.readFileSync('src/client/ui/hotkey.tsx','utf8'));const hf=h.replace(/\s+/g,' ');const bad=[];for(const n of ['commandRows','ICommandRow','BOX_LEFT','BOX_FILL_HEIGHT','setKeyCombination(','groups.map(','commandRows.value.map(','selectKey(row.command, j)','combinationLists.value[i]','.map((keys, j)'])if(hf.indexOf(n)<0)bad.push('missing :: '+n);if(/\bVNode\b/.test(h))bad.push('VNode still present');for(const n of ['groupNodes','commandNodes','groupIndex','commandIndex'])if(h.indexOf(n)>=0)bad.push('leftover :: '+n);if(/for\s*\(\s*\r?\n/.test(h))bad.push('wrapped for (');if(/if \(isSelected\)/.test(h))bad.push('if (isSelected)');if(/\{isSelected &&/.test(h))bad.push('{isSelected &&');if(/<g-rect[\s>]/.test(h))bad.push('g-rect present');if(/Selection/.test(h))bad.push('Selection present');const n=(h.match(/<container(?=[\s>])/g)||[]).length;if(n!==1)bad.push('expected exactly 1 container, got '+n);const rootTag=h.slice(h.indexOf('<container'), h.indexOf('>', h.indexOf('<container'))+1);if(rootTag.indexOf('nocache')<0)bad.push('root has no nocache');const sc=(h.match(/<Scroll/g)||[]).length;if(sc!==2)bad.push('expected 2 Scroll, got '+sc);const ss=(h.match(/strokeStyle=\{\s*isSelected\s*\?/g)||[]).length;if(ss!==2)bad.push('expected 2 strokeStyle checks, got '+ss);const lw=(h.match(/lineWidth=\{\s*isSelected\s*\?/g)||[]).length;if(lw!==2)bad.push('expected 2 lineWidth checks, got '+lw);let i=0;while(true){const a=h.indexOf('<Scroll',i);if(a<0)break;const z=h.indexOf('</Scroll>',a);if(z<0){bad.push('unclosed Scroll');break}const blk=h.slice(a,z);if(/<container(?=[\s>])/.test(blk))bad.push('container inside Scroll');if(/hidden\s*=/.test(blk))bad.push('hidden inside Scroll');i=z+1}if(bad.length){console.error('TASK3 HOTKEY GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task3: hotkey.tsx')"</automated>
|
||||
<automated>cmd /c "pnpm exec eslint src/client/ui/hotkey.tsx"</automated>
|
||||
<automated>node -e "const fs=require('fs');const c=fs.readFileSync('src/client/ui/hotkey.tsx','utf8');const bad=[];let i=0,ix=0,lc=0;while(true){const s=c.indexOf('<Scroll',i);if(s<0)break;const e=c.indexOf('</Scroll>',s);if(e<0){bad.push('unclosed Scroll');break}const b=c.slice(s,e);const jsx=(b.match(/\{\s*\/\*/g)||[]).length;if(jsx!==1)bad.push('Scroll#'+ix+' JSX comments '+jsx+' != 1');lc+=(b.match(/(^|\r?\n)[ \t]*\/\//g)||[]).length;const g=b.indexOf('>')+1;if((b.slice(g).trimStart()[0]||'')!=='{')bad.push('Scroll#'+ix+' child position starts with non-{');const f=b.indexOf('map(');const head=f<0?b:b.slice(0,f);if(/(^|\r?\n)[ \t]*\/\//.test(head))bad.push('Scroll#'+ix+' has a // comment before map( (JSXText risk)');i=e+9;ix++}if(ix!==2)bad.push('expected 2 Scroll, got '+ix);if(lc!==3)bad.push('expected 3 line comments inside Scrolls (1 left + 2 right), got '+lc);if(bad.length){console.error('TASK3 COMMENT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task3: comment placement')"</automated>
|
||||
<fails_when>`hotkey.tsx` 仍含 `VNode` / `groupNodes` / `commandNodes` / `groupIndex` / `commandIndex` / `Selection` / `if (isSelected)` / `{isSelected &&`、出现换行的 `for (`、缺少 `groups.map(` / `commandRows.value.map(` / `selectKey(row.command, j)` / `combinationLists.value[i]` / `.map((keys, j)`、不是恰好两个 `Scroll`、`<container>` 计数不为 1、任一 `Scroll` 内仍有 `container` 或 `hidden=`、出现 `<g-rect`、根缺 `nocache`、`strokeStyle` / `lineWidth` 的 `isSelected` 三元不是各 2 处;每块 `{/* … */}` 不是 1 条、子元素位置不以 `{` 开头、Scroll 内 `//` 行数不是 3 条(左 1 + 右 2)、或在第一个 `map(` 之前出现 `//` 注释(JSXText 风险);eslint 报错;或 node 闸门非零退出。</fails_when>
|
||||
</verify>
|
||||
<done>`hotkey.tsx` 两栏都是内联 `.map()`(左栏 `groups.map(([id, group], i)`,右栏 `commandRows.value.map((row, i)` 内层 `.map((keys, j)`),无 `VNode` / `groupNodes` / `commandNodes` / `groupIndex` / `commandIndex`;`ICommandRow` / `commandRows` / `combinationLists` 保留;`selectKey(row.command, j)` 与 `box-${i}-${j}` / `text-${i}-${j}` 就位;根仍带 `nocache`,两处选中边框不变;每块 `{/* … */}` 恰好 1 条,两条既有 `//` 与新增内层 `//` 都在 `map(` 之后的回调体 / 数组字面量内;eslint 无报错。</done>
|
||||
</task>
|
||||
<task type="auto">
|
||||
<name>Task 4: 更新 `uiContainerGate.test.ts`(保留原断言 + 新增 4 组),先跑全部门禁、再写执行摘要、最后跑摘要闸门</name>
|
||||
<files>src/client/ui/__test__/uiContainerGate.test.ts, .planning/phases/09-minimum-browser-runnable/09-30-SUMMARY.md</files>
|
||||
<action>
|
||||
**依赖 Task 1、2、3 已完成。**
|
||||
|
||||
**A. 更新 `src/client/ui/__test__/uiContainerGate.test.ts`(保留原有 9 组断言,只做必要修改):**
|
||||
1. 顶部导入与 `stripComments` / `flatten` / `getScrollBlocks` / `firstContainerTag` / `tagAround` 五个辅助函数**原样保留**(已带说明注释)。
|
||||
2. **保留**原有断言:`Scroll` 块内无 `container` / 无 `hidden=`;每文件仅 1 个根 `container`;`hotkey` 两栏、`commandRows` / `ICommandRow`、无三个旧函数、无 `Selection`、两处选中边框;`book` 四项 0 回退;`bookDetail` 两项 0 回退;`bookDetail` 高度 / `key`;显示开关分流与 `noevent` 逐元素配对;`hotkey` 根 `nocache`;4 个 `*_SCROLL_LOC` 带 `: ElementLocator`。
|
||||
3. **更新** `hotkey` 的两处 needle:两处 `tagAround(..., 'g-rectr', 'selectKey(row.command, index)')` 都改为 `'selectKey(row.command, j)'`;原 `'isSelected && commandNodes.push'` / `'isSelected && groupNodes.push'` 两条否定断言可保留(数组名已不存在,断言仍真)。
|
||||
4. **新增第 10 组**(`it` 上方一行 `//` 注释,例如 `// 列表必须内联渲染,不得再预建 VNode 数组或按数组名收集`):
|
||||
- 对五个 `stripComments(source)`:`expect(source).not.toMatch(/\bVNode\b/)`、`expect(source).not.toContain(': VNode[]')`;
|
||||
- 对 `['specialNodes','statusNodes','slotNodes','ownedNodes','permNodes','consNodes','groupNodes','commandNodes']` 逐个 `expect(source).not.toContain(name)` 与 `expect(source).not.toContain(`${name}.push(`)`;
|
||||
- 额外 `expect(stripComments(bookSource)).not.toContain('rows.push(')`(`book` 是唯一用过 `rows` 累加器的列表;`bookDetail` 的 `specialRows` / `hotkey` 的 `commandRows` 内部 `rows.push` 是数据累加,不在断言范围)。
|
||||
5. **新增第 11 组**(一行 `//` 注释,例如 `// 每个列表都必须在 JSX 内直接 map,且循环变量使用 i / j`):
|
||||
- `flatten(stripComments(bookSource))` 含 `enemies.value.map(`;
|
||||
- `flatten(stripComments(bookDetailSource))` 含 `specialRows.value.map(`;
|
||||
- `flatten(stripComments(equipBoxSource))` 含 `props.attributes.map(`、`hero.equip.slots.map(`、`owned.value.map(`;
|
||||
- `flatten(stripComments(toolBoxSource))` 含 `permItems.value.map(`、`consItems.value.map(`;
|
||||
- `flatten(stripComments(hotkeySource))` 含 `groups.map(`、`commandRows.value.map(`、`combinationLists.value[i]`、`.map((keys, j)`、`selectKey(row.command, j)`;
|
||||
- 对五个 `stripComments(source)`:`expect(source).not.toContain('groupIndex')`、`not.toContain('commandIndex')`、`expect(source).not.toMatch(/for\s*\(\s*\r?\n/)`。
|
||||
6. **新增第 12 组**(一行 `//` 注释,例如 `// book 每行必须仍渲染命中层、图标、名称、特殊属性、五个基础数值与四个临界数值`)——`book` 每行 13 个元素齐全:
|
||||
- `flatten(stripComments(bookSource))` 逐个包含 13 个 `key` 模板:`hit-` / `icon-` / `name-` / `special-` / `hp-` / `atk-` / `def-` / `money-` / `exp-` / `dmg-` / `crit-` / `reduce-` / `def1-`(均带 `${base.code}` 后缀);
|
||||
- 逐个包含 `生命` / `攻击` / `防御` / `金币` / `经验` 五个基础数值文案与 `specialText(base)`;
|
||||
- 用 `expect(...).toHaveLength(13)` 之类的方式核对 13 个 `key` 前缀数组长度,避免漏项。
|
||||
7. **新增第 13 组**(一行 `//` 注释,例如 `// 注释不得落在 JSX 子元素位置,Scroll 内只允许 JSX 注释`)——注释位置与数量,**必须在 `stripComments` 之前取原文**:
|
||||
- 对五个 `source` 用 `getScrollBlocks(source)`(不 strip)统计每块 `{/* … */}` 数量:`expect(counts).toEqual([1])`(book)、`[1]`(bookDetail)、`[1,0,1,1]`(equipbox 状态 / 描述 / 槽位 / 拥有)、`[1,1,0]`(toolbox 永久 / 消耗 / 描述)、`[1,1]`(hotkey);
|
||||
- 对每块**分两步、顺序不可颠倒**:①先取开标签 `>` 之后的**原始**子元素区 `children`(此时尚未删除任何容器),断言 `expect(children.trimStart()[0]).toMatch(/[{<]/)`(子元素以表达式容器或元素开头;口径与 Tasks 1–3 的 `blk.slice(g).trimStart()[0]` 检查一致)——这正是 `bookDetail` 曾写错的那类回归;②**再**从 `children` 删掉 `{/* … */}` 与成对的 `{…}` 表达式容器,对剩余文本断言 `expect(rest).not.toContain('//')`(子元素位置没有裸行注释)。**为什么不能先删容器再查首字符**:`book` / `bookDetail` / `hotkey` 的子元素区整体就是一个 `{…}` 映射表达式容器,删掉后 `rest` 只剩空白,`rest.trimStart()[0]` 为 `undefined`、`undefined !== '{'` 且 `undefined !== '<'`,首字符断言必然恒假;描述块的子元素是 `<TextContent …/>`,删容器后仍有内容,因此**不要求**剩余整体为空;
|
||||
- 既有 `//` 注释逐条 `toContain` 且各出现一次:`book` 的**三条** —— `// 数值列表区` / `// 临界信息区` / 现第 125 行的 `// 临界信息缺失时回退为 0,让「没有数据」与「数值为 0」可以区分`(第三条必须**单独占一行**:`stripComments` 之前的原文里该行 trim 后以 `//` 开头、前面只有缩进,不得与 `const damage = …` 同行)、`bookDetail` 的两行 `TextContent` 说明、`equipbox` 的 `hidden` 与拖动起手两条、`hotkey` 的选中边框两条;
|
||||
- 额外断言 `bookDetail` 的两条注释索引都小于 `<Scroll` 的索引(不在 Scroll 块内)**且都大于 `return () =>` 的索引**(确实落在渲染函数体内、`return (` 之前);`book` 的第三条注释索引同样小于 `<Scroll` 的索引,并单独占一行。
|
||||
8. 每个 `it` 上方紧邻一行 `//` 注释;**不得修改既有 `it` 的注释文本**(只新增);测试不读取 `.planning/`、不导入组件本体、不使用 `node:fs`。
|
||||
|
||||
**B. 先跑完所有门禁(此时还没有 SUMMARY):** 见本任务下方的 automated 命令,逐条执行并记录**实际**输出。
|
||||
- vitest:`uiContainerGate.test.ts`(13 组)与 `hotkeySettingsGate.test.ts`。
|
||||
- eslint:跑**与 Task 1 基线完全相同的 6 个路径**,与 `09-30-BASELINE.json` 的 `eslint` 比较,只判 NEW。
|
||||
- `pnpm check:type`:整仓运行,按**与 Task 1 基线完全相同的 6 路径 `scope`** 过滤,只判 NEW;基线中存在、执行后消失的诊断允许。
|
||||
- 受保护文件哈希(**7 个**)与基线逐字相等;HEAD 与基线相同,或 `git diff --name-only <baseline>..HEAD` 只含 `.planning/` 路径。
|
||||
- 范围(两条都要):①`git status --porcelain -uall -- <6 个代码 / 测试路径>` 只出现这 6 个路径(因 09-29 未提交,它们本来就在脏树里,仍应全部出现);②整仓 `git status --porcelain -uall` 的每条路径必须属于这 6 个路径、或出现在基线 `dirty` 快照、或以 `.planning/` 开头,否则判范围外写入。
|
||||
- **顺序铁律**:先跑门禁,**再**写 SUMMARY;不得在门禁实际运行之前把结果写进 SUMMARY。
|
||||
|
||||
**C. 写 `.planning/phases/09-minimum-browser-runnable/09-30-SUMMARY.md`(用 B 步的真实结果):**
|
||||
- 逐文件对照:删除的数组 / 循环 → 新增的内联 `.map()` 形态(`book` 13 元组、`bookDetail` 2 元组、`equipbox` 三处 3 元组、`toolbox` 两处 4 元组、`hotkey` 左 2 元组 / 右 1 + 内层 2 元组)。
|
||||
- 记录循环变量改名(`groupIndex` / `commandIndex` → `i`,内层 `index` → `j`)、`selectKey(row.command, j)` 与 `box-${i}-${j}` / `text-${i}-${j}`。
|
||||
- 记录注释清单:每块 `{/* … */}` 数量(book `[1]` / bookDetail `[1]` / equipbox `[1,0,1,1]` / toolbox `[1,1,0]` / hotkey `[1,1]`)、`//` 只落在 JS 代码位置的说明、`bookDetail` 的两条既有 `//` 留在 `return (` 之前、既有注释文本未改。
|
||||
- 记录 B 步的真实门禁结果(`uiContainerGate` 13 组断言、`hotkeySettingsGate` 12 用例、eslint NEW-only、`pnpm check:type` NEW-only、7 个受保护文件哈希、HEAD 政策、全仓脏文件快照范围闸门)。
|
||||
- 声明:`estimate` 未校准(`confidence: low`);**不做浏览器 UAT,不宣称滚动 / 内存 / 点击命中 / 拖拽行为已验证**;09-29 改动未提交、这 6 个路径在基线里本来就是脏的,范围闸门只证明「无新增范围外路径」;`book.tsx` 的过期 TODO 按 OI-1 未改;`keyAdaptationGate.test.ts` 是既存失败、不在本计划范围;**执行器 0 次代码提交**(`.planning` 产物可能由编排器按 `commit_docs: true` 聚合提交),代码工作树留给用户审查。
|
||||
|
||||
**D. 最后跑摘要闸门:** 确认 SUMMARY 存在且包含上述必需声明(见下方最后一条 automated);该闸门必须在 C 步之后运行,失败则补齐 SUMMARY 后重跑,不得只改闸门。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cmd /c "pnpm exec vitest run src/client/ui/__test__/uiContainerGate.test.ts"</automated>
|
||||
<automated>cmd /c "pnpm exec vitest run src/client/ui/__test__/hotkeySettingsGate.test.ts"</automated>
|
||||
<automated>node -e "const cp=require('child_process'),fs=require('fs');const b=JSON.parse(fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-30-BASELINE.json','utf8'));const files=['src/client/ui/book.tsx','src/client/ui/bookDetail.tsx','src/client/ui/equipbox.tsx','src/client/ui/toolbox.tsx','src/client/ui/hotkey.tsx','src/client/ui/__test__/uiContainerGate.test.ts'];const r=cp.spawnSync('cmd',['/c','pnpm exec eslint -f json '+files.join(' ')],{encoding:'utf8',timeout:300000});const text=(r.stdout||'').trim();const bad=[];if(r.status===null||r.status>=2||!text)bad.push('eslint failed to run (status '+r.status+')');let arr=[];try{arr=JSON.parse(text||'[]')}catch(e){bad.push('eslint json unparsable')}const norm=p=>{const s=String(p).replace(/\\/g,'/');const i=s.indexOf('src/client/ui/');return i>=0?s.slice(i):s};const cur=[];for(const f of arr){for(const m of (f.messages||[]))cur.push(norm(f.filePath)+'|'+String(m.ruleId||'null')+'|'+String(m.severity)+'|'+String(m.message).trim())}const before=b.eslint||[];const count=(a,v)=>{let n=0;for(const x of a)if(x===v)n++;return n};for(const v of new Set(cur)){if(count(cur,v)>count(before,v))bad.push('NEW eslint :: '+v)}if(bad.length){console.error('ESLINT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK eslint: no new entries (baseline '+before.length+' -> current '+cur.length+')')"</automated>
|
||||
<automated>node -e "const cp=require('child_process'),fs=require('fs');const b=JSON.parse(fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-30-BASELINE.json','utf8'));const scope=['src/client/ui/book.tsx','src/client/ui/bookDetail.tsx','src/client/ui/equipbox.tsx','src/client/ui/toolbox.tsx','src/client/ui/hotkey.tsx','src/client/ui/__test__/uiContainerGate.test.ts'];const r=cp.spawnSync('cmd',['/c','pnpm check:type'],{encoding:'utf8',timeout:900000});const text=(r.stdout||'')+(r.stderr||'');const diag=text.split(/\r?\n/).filter(l=>/error TS\d+/.test(l));const bad=[];if(r.status===null)bad.push('vue-tsc did not run');else if(r.status!==0&&diag.length===0)bad.push('vue-tsc crashed with no parseable diagnostics');const norm=l=>l.replace(/\\/g,'/').replace(/\(\d+,\d+\):/,'').replace(/^.*?(?=(src|packages-user)\/)/,'').trim();const cur=diag.map(norm).filter(x=>scope.some(p=>x.indexOf(p)===0));const before=(b.typecheck||[]).filter(x=>scope.some(p=>x.indexOf(p)===0));const count=(a,v)=>{let n=0;for(const x of a)if(x===v)n++;return n};for(const v of new Set(cur)){if(count(cur,v)>count(before,v))bad.push('NEW diagnostic :: '+v)}if(bad.length){console.error('TYPECHECK GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK typecheck: no new diagnostics in changed files (baseline '+before.length+' -> current '+cur.length+')')"</automated>
|
||||
<automated>node -e "const cp=require('child_process'),fs=require('fs');const b=JSON.parse(fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-30-BASELINE.json','utf8'));const bad=[];const keys=Object.keys(b.hashes||{});if(keys.length!==7)bad.push('baseline must protect 7 files, got '+keys.length);for(const file of keys){const r=cp.spawnSync('git',['hash-object',file],{encoding:'utf8'});if(r.status!==0)bad.push('cannot hash '+file);else if((r.stdout||'').trim()!==b.hashes[file])bad.push(file+' changed')}const head=(cp.spawnSync('git',['rev-parse','HEAD'],{encoding:'utf8'}).stdout||'').trim();if(head!==b.head){const d=cp.spawnSync('git',['diff','--name-only',b.head+'..'+head],{encoding:'utf8'});if(d.status!==0)bad.push('HEAD changed and diff failed ('+b.head+'..'+head+')');else{const paths=(d.stdout||'').split(/\r?\n/).filter(Boolean);const nonPlan=paths.filter(p=>!p.startsWith('.planning/'));if(nonPlan.length)bad.push('HEAD changed with non-planning commits :: '+nonPlan.join(','))}}if(bad.length){console.error('PROTECTED GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK protected files unchanged; HEAD policy satisfied ('+b.head+' -> '+head+')')"</automated>
|
||||
<automated>node -e "const cp=require('child_process'),fs=require('fs');const b=JSON.parse(fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-30-BASELINE.json','utf8'));const allowed=['src/client/ui/book.tsx','src/client/ui/bookDetail.tsx','src/client/ui/equipbox.tsx','src/client/ui/toolbox.tsx','src/client/ui/hotkey.tsx','src/client/ui/__test__/uiContainerGate.test.ts'];const bad=[];if(!Array.isArray(b.dirty))bad.push('baseline dirty snapshot missing');const out=cp.spawnSync('git',['status','--porcelain','-uall'],{encoding:'utf8'}).stdout||'';const paths=out.split(/\r?\n/).filter(Boolean).map(l=>l.slice(3).replace(/^\"|\"$/g,'')).filter(p=>p.indexOf('.planning/')!==0);const before=new Set((b.dirty||[]).map(p=>String(p).replace(/\\/g,'/')));for(const p of paths)if(!allowed.includes(p)&&!before.has(p))bad.push('out-of-scope write :: '+p);for(const p of allowed)if(!paths.includes(p))bad.push('expected change missing :: '+p);if(bad.length){console.error('SCOPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK scope: no out-of-scope writes ('+paths.length+' dirty paths total, '+before.size+' pre-existing)')"</automated>
|
||||
<automated>node -e "const fs=require('fs');const p='.planning/phases/09-minimum-browser-runnable/09-30-SUMMARY.md';const bad=[];if(!fs.existsSync(p))bad.push('summary missing');else{const s=fs.readFileSync(p,'utf8');for(const n of ['book.tsx','bookDetail.tsx','equipbox.tsx','toolbox.tsx','hotkey.tsx','uiContainerGate','hotkeySettingsGate','eslint','check:type','09-29','keyAdaptationGate','confidence: low','UAT','13'])if(s.indexOf(n)<0)bad.push('summary missing :: '+n)}if(bad.length){console.error('SUMMARY GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK summary content')"</automated>
|
||||
<fails_when>任一 vitest 用例失败;eslint 出现 NEW 条目(归一化口径与 scope 必须与 Task 1 相同,含测试文件);`pnpm check:type` 在改动文件集合内出现 NEW 诊断(`norm` 与 scope 必须与 Task 1 逐字相同、保留 `src/` / `packages-user/` 前缀);7 个受保护文件哈希变化(含 `inputBox.tsx` / `waitBox.tsx`);HEAD 变化且 `baseline..HEAD` 含非 `.planning` 路径;6 个允许路径缺失;整仓脏文件里出现既不在基线 `dirty` 快照也不在 6 个允许路径中的新路径;`09-30-SUMMARY.md` 缺失或缺少必需声明(含 `uiContainerGate` / `hotkeySettingsGate` / `eslint` / `check:type` / `09-29` / `keyAdaptationGate` / `confidence: low` / `UAT` / `13`);或任一 node 闸门非零退出。**不要把 `keyAdaptationGate.test.ts` 的既存失败计入本闸门**。**顺序错误(先写 SUMMARY 再跑门禁、或未运行就记录结果)同样判失败。**</fails_when>
|
||||
</verify>
|
||||
<done>`uiContainerGate.test.ts` 的 13 组断言全部通过(原 9 组 + 「无预建 `VNode` 数组」+「内联 `.map()` 与短循环变量」+「`book` 每行 13 个元素齐全」+「注释位置与数量」;`hotkey` 的 `selectKey` needle 为 `j`);`hotkeySettingsGate.test.ts` 仍全绿;eslint 无 NEW(与基线同一组 6 路径);`pnpm check:type` 在改动文件集合内无 NEW(同一组 6 路径);7 个受保护文件哈希与基线一致;HEAD 与基线相同或仅含 `.planning` 聚合提交;6 个代码 / 测试路径全部出现且无新增范围外路径;**先跑完门禁、再写 `09-30-SUMMARY.md`、最后通过摘要闸门**,摘要如实声明未做浏览器 UAT、执行器 0 次代码提交、估算未校准、09-29 改动未提交导致范围闸门的局限、`book.tsx` 过期 TODO 未改、`keyAdaptationGate.test.ts` 为既存失败。</done>
|
||||
</task>
|
||||
</tasks>
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| 渲染引擎 ↔ 本计划改动的 UI | `Scroll` / `TextContent` / `g-rectr` / `icon` 由系统层提供;本计划只改调用写法,一旦内联数组不被按子元素展开,界面会静默退化 |
|
||||
| 指针 / 滚轮事件 ↔ 列表行 | 改写为内联 map 后,命中层与上层覆盖元素的关系仍是显式契约:上层漏写 `noevent` 就会吞掉点击与拖拽起手 |
|
||||
| 特殊属性文案 ↔ 行位置计算 | `TextContent` 上报高度是异步的;只改写法时 `specialRows` / `key` 一旦被改动或漏读,长描述会重新重叠 |
|
||||
| 计划范围 ↔ 系统层实现 | `scroll.tsx` / `textbox.tsx` 是不可改的底线;改它们等于用系统层「兜底」掩盖调用错误 |
|
||||
| 注释位置 ↔ 渲染树 | JSX 子元素位置的 `//` 会变成 JSXText → Text 节点;`Scroll` 只用 `isComment` 过滤注释,不过滤 Text,多余 Text 会进入 `updateScroll` 的 children 计算 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-30-01 | Tampering | 5 个列表文件的 `Scroll` 子元素结构 | high | mitigate | 源码闸门逐段扫描 `<Scroll>…</Scroll>` 断言无 `/<container(?=[\s>])/` 与 `/hidden\s*=/`;每文件 `<container>` 计数恰好 1;五个文件断言无 `VNode` 标识符与 8 个渲染数组名 |
|
||||
| T-09-30-02 | Denial of Service | 预建数组 + `for` + `push` 让列表与渲染函数耦合 | low | mitigate | 全部改为内联 `.map()`,`Scroll` 的 children 直接由映射结果给出;闸门断言 10 处内联 `.map(` 存在且无 `groupIndex` / `commandIndex` |
|
||||
| T-09-30-03 | Tampering | `book` / `equipbox` / `toolbox` / `hotkey` 的点击与拖拽命中 | high | mitigate | 保留命中层与上层元素的 `noevent` 配对断言(`tagAround` 逐元素);`startPress` / `clickItem` / `selectKey` 逻辑体零改动 |
|
||||
| T-09-30-04 | Denial of Service | `bookDetail` 高度回调被内联映射改写后失效 | medium | mitigate | 断言 `autoHeight` / `onUpdateHeight` / `textHeights` / `specialRows` / `textTop` / `textHeight` 与 `key` 模板字符串;`specialRows` 计算体不改 |
|
||||
| T-09-30-05 | Tampering | 用改 `scroll.tsx` / `textbox.tsx` 来「适配」内联数组 | high | mitigate | 7 个受保护文件进入哈希闸门并与基线逐字比较;范围闸门用全仓 `git status` 对照基线 `dirty` 快照;OI-2 规定数组展开异常必须停下上报 |
|
||||
| T-09-30-06 | Elevation of Privilege | 顺手改设置入口 / 按键注册 / 指定不动的两个 UI | medium | mitigate | `settings.tsx` / `ui/index.ts` / `misc/hotkey.ts` / `inputBox.tsx` / `waitBox.tsx` 一并进入受保护哈希列表 |
|
||||
| T-09-30-07 | Repudiation | 声称「行为已验证」但实际未做浏览器 UAT | medium | mitigate | SUMMARY 必须写明「未做浏览器 UAT,未宣称滚动 / 内存 / 点击 / 拖拽已验证」;执行器不提交代码 |
|
||||
| T-09-30-08 | Tampering | `bookDetail` / `book` / `equipbox` / `hotkey` 的 `//` 注释被写进 `<Scroll>` 的 JSX 子元素位置 | high | mitigate | 注释位置硬规则(`//` 只落在渲染函数体 / map 回调体 / 数组字面量内部);Task 1 断言 `bookDetail` 所有 `//` 都在 `<Scroll` 之前且块内 0 条;Task 2 / 3 断言各块 `{/* … */}` 数量与子元素起始字符;Task 4 第 13 组在 `stripComments` 之前统计 `{/* … */}` 并用「删掉 `{/* */}` 与成对 `{…}` 后剩余文本不含 `//`」证明子元素位置无裸注释 |
|
||||
| T-09-30-SC | Tampering | npm / pnpm 安装 | high | mitigate | 本计划**不新增任何依赖、不执行安装命令**;若执行期出现安装需求,必须停下上报 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- **源码形状**:`src/client/ui/__test__/uiContainerGate.test.ts` 13 组断言全部通过(`?raw`,不导入组件):原 9 组(`Scroll` 内无 `container` / 无 `hidden=`、每文件仅根 `container`、`hotkey` 两栏与选中边框、`book` 四项 / `bookDetail` 两项 0 回退、`bookDetail` 高度与 `key`、显示开关分流与 `noevent` 逐元素配对、`hotkey` 根 `nocache`、4 个 `*_SCROLL_LOC` 带 `: ElementLocator`)+ 新增 4 组(无 `VNode` / 无 8 个渲染数组名及其 `.push(` / `book` 无 `rows.push(`;内联 `.map(` 存在、无 `groupIndex` / `commandIndex`、无换行 `for (`;`book` 每行 13 个 `key` 前缀 + 5 个基础数值文案 + `specialText(base)`;`Scroll` 块内 `{/* … */}` 计数 = book `[1]` / bookDetail `[1]` / equipbox `[1,0,1,1]` / toolbox `[1,1,0]` / hotkey `[1,1]`,且**未删容器**的原始子元素区首字符是 `{` 或 `<`、删掉 `{/* */}` 与成对 `{…}` 后的剩余文本不含 `//`(顺序不可颠倒),`bookDetail` 的既有 `//` 不在 Scroll 块内)。
|
||||
- **静态检查**:`pnpm exec eslint` 对**同一组 6 个改动文件**无 NEW(Task 1 基线与 Task 4 比较的 scope 完全相同);`pnpm check:type` 整仓运行后在**同一组 6 个路径**内无 NEW 诊断(既有基线诊断允许消失)。
|
||||
- **既有闸门**:`src/client/ui/__test__/hotkeySettingsGate.test.ts` 仍为绿(`hotkey.tsx` 只用 `g-rectr`、捕获阶段监听、`setKeyCombination(`、无持久化)。`keyAdaptationGate.test.ts` 是既存失败(15 个用例 5 个失败),不在本计划范围;不修、不声称全部既有闸门为绿。
|
||||
- **执行顺序**:全部门禁(vitest / eslint / check:type / 哈希 / HEAD / 范围)先跑完 → 再写 `09-30-SUMMARY.md` → 最后跑摘要闸门;不得在门禁运行前记录结果。
|
||||
- **范围**:`git status --porcelain -uall -- <6 个代码 / 测试路径>` 恰好只有这 6 个路径;整仓 `git status --porcelain -uall` 里没有「不在基线 `dirty` 快照、也不在这 6 个路径、也不在 `.planning/`」的新路径;**7 个**受保护文件(`scroll.tsx` / `textbox.tsx` / `settings.tsx` / `ui/index.ts` / `misc/hotkey.ts` / `inputBox.tsx` / `waitBox.tsx`)哈希与基线一致。
|
||||
- **无代码提交**:HEAD 与 `09-30-BASELINE.json` 的 `head` 相同,或 `git diff --name-only <baseline>..HEAD` 只含 `.planning/` 路径;任何非 `.planning` 路径的提交都判失败。
|
||||
- **无浏览器验证声明**:SUMMARY 明确写入「未做浏览器 UAT,未宣称滚动 / 内存 / 点击命中 / 拖拽行为已验证」;`book.tsx` 的过期 TODO 按 OI-1 未改。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] `book.tsx`:删除 `rows` 与 `for`,`Scroll` 内为 `enemies.value.map((view, i) => [...])` 返回 13 个同级元素;13 个 `key` 前缀、5 个基础数值文案与 `specialText(base)` 齐全;无 `VNode` / `rows.push`;0 值回退、命中层、`noevent` 不变;三条既有 `//` 文本不变(两条在数组字面量内、`// 临界信息缺失时回退为 0…` 单独成行留在回调体)。
|
||||
- [ ] `bookDetail.tsx`:删除 `specialNodes` 与 `for`,`Scroll` 内为 `specialRows.value.map(row => [<text/>, <TextContent/>])`;`textHeights` / `updateTextHeight` / `watch` / `specialRows` / `autoHeight` / `onUpdateHeight` / `key` 逻辑不变;无 `VNode`;两条既有 `//` 保留在 `return (` 之前、`Scroll` 块内 0 条 `//`。
|
||||
- [ ] `equipbox.tsx`:三处内联 `.map()` 各返回三元组;无三个数组名与 `VNode`;4 个 `*_SCROLL_LOC` 与 `ElementLocator` 导入保留;拖拽 / 悬停 / 显示开关分流不变;`Scroll` 块 `{/* … */}` = `[1,0,1,1]`。
|
||||
- [ ] `toolbox.tsx`:两处内联 `.map()` 各返回四元组;无两个数组名与 `VNode`;`hidden={!!current}` 提示位置、`TextContent` 空文本、`clickItem` 不变;`Scroll` 块 `{/* … */}` = `[1,1,0]`。
|
||||
- [ ] `hotkey.tsx`:左栏 `groups.map(([id, group], i)`、右栏 `commandRows.value.map((row, i)` + 内层 `.map((keys, j)`;无两个数组名 / `groupIndex` / `commandIndex` / `VNode`;`ICommandRow` / `commandRows` / `combinationLists` 保留;`selectKey(row.command, j)`、`box-${i}-${j}` / `text-${i}-${j}`;根 `nocache` 与两处选中边框不变;`Scroll` 块 `{/* … */}` = `[1,1]`。
|
||||
- [ ] 每个列表在 `Scroll` 的 JSX 子元素位置有一行 `{/* … */}` 职责注释;所有 `//` 只落在 JS 代码位置;全部既有注释文本未被修改。
|
||||
- [ ] `uiContainerGate.test.ts` 13 组断言通过(含新增 4 组);`hotkeySettingsGate` 全绿;eslint 无 NEW(6 路径同 scope);`pnpm check:type` 改动文件内无 NEW(6 路径同 scope);7 个哈希 / HEAD 政策 / 范围闸门通过。
|
||||
- [ ] **顺序正确**:先跑完所有门禁 → 再写 `09-30-SUMMARY.md` → 最后通过摘要闸门;摘要含逐文件对照、循环变量改名、注释位置与数量清单、真实门禁结果、未 UAT 声明、估算未校准声明、执行器 0 次代码提交声明、范围闸门局限说明。
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-minimum-browser-runnable/09-30-SUMMARY.md` **after all gates have actually run**, recording each file's array-to-inline-map change, the shortened loop variables (`i` / `j`), `selectKey(row.command, j)` and the updated `box-${i}-${j}` / `text-${i}-${j}` keys, the newly added per-list `{/* … */}` comments, the rule that every `//` comment stays in JS code position (`bookDetail`'s two comments remain before `return (`), and that no existing comment text changed, gate results (13 `uiContainerGate` cases including the four new groups — no prebuilt `VNode` arrays, inline `.map()` with short loop variables, `book` row completeness (13 keys + five stat labels + `specialText(base)`), comment placement / counts — plus `hotkeySettingsGate`, eslint NEW-only and `pnpm check:type` NEW-only over the same six-path scope, the seven protected hashes, HEAD policy, and the full-repo dirty-snapshot scope gate), the uncalibrated estimate (`confidence: low`, `tasks: 4` excluding the Task 0 checkpoint), skipped browser UAT, the fact that 09-29's changes are still uncommitted (so the scope gate can only prove no new out-of-scope paths), the untouched stale TODO in `book.tsx`, the pre-existing `keyAdaptationGate.test.ts` failures that are out of scope, zero executor code commits, and the code working tree left for user review. Then re-run the summary gate.
|
||||
</output>
|
||||
|
||||
169
.planning/phases/09-minimum-browser-runnable/09-30-SUMMARY.md
Normal file
169
.planning/phases/09-minimum-browser-runnable/09-30-SUMMARY.md
Normal file
@ -0,0 +1,169 @@
|
||||
---
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 30
|
||||
subsystem: ui
|
||||
tags: [vue, jsx, scroll, inline-map, source-gate]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 09-minimum-browser-runnable
|
||||
provides: "09-29 的平铺 Scroll、0 值回退、TextContent 高度测量与热键设置行为口径"
|
||||
provides:
|
||||
- "5 个列表型 UI 的内联 JSX .map() 渲染写法"
|
||||
- "13 组源码形状闸门(原 9 组 + 新增 4 组)"
|
||||
affects: [09-minimum-browser-runnable, ui]
|
||||
|
||||
# 未校准估算:09-29 的改动在基线时仍未提交,无法从工作树分离 09-30 自身的真实 diff。
|
||||
# 为避免把 09-29 的工作计入 09-30,tokens 不虚报,记 null。
|
||||
actuals:
|
||||
tokens: null
|
||||
tasks: 4
|
||||
commits: 0
|
||||
note: "无法从工作树分离 09-30 自身的 diff,tokens 记 null;tasks 只计 Task 1-4。"
|
||||
|
||||
estimate:
|
||||
tokens: 60000
|
||||
raw_tokens: 30000
|
||||
tasks: 4
|
||||
confidence: low
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: ["Scroll 的列表子元素直接在 JSX 内 .map() 展开,一行多元素返回同级数组"]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- .planning/phases/09-minimum-browser-runnable/09-30-BASELINE.json
|
||||
- .planning/phases/09-minimum-browser-runnable/09-30-SUMMARY.md
|
||||
modified:
|
||||
- src/client/ui/book.tsx
|
||||
- src/client/ui/bookDetail.tsx
|
||||
- src/client/ui/equipbox.tsx
|
||||
- src/client/ui/toolbox.tsx
|
||||
- src/client/ui/hotkey.tsx
|
||||
- src/client/ui/__test__/uiContainerGate.test.ts
|
||||
|
||||
key-decisions:
|
||||
- "列表一律改为 JSX 内联 .map(),删除顶部预建 VNode[] 数组;一行多元素返回同级数组,不包容器"
|
||||
- "bookDetail 的两条既有 // 保留在 return () 之前;book 的第三条保留在 map 回调体内单独成行"
|
||||
- "注释位置按渲染正确性处理:Scroll 的 JSX 子元素位置只放 {/* … */},// 只落在 JS 代码位置"
|
||||
|
||||
patterns-established:
|
||||
- "列表段落:Scroll 内先写一行 {/* 职责与内容 */},再用 .map() 返回同级子项数组"
|
||||
- "一行多元素:回调返回数组,不引入 Fragment / 容器"
|
||||
|
||||
requirements-completed: []
|
||||
|
||||
duration: "约 20min"
|
||||
completed: 2026-10-27
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 09 Plan 30: 列表回归内联 JSX `.map()` 渲染 Summary
|
||||
|
||||
**把 5 个列表型 UI 的预建 `VNode[]` + `for` + `push` 全部改回内联 JSX `.map()`,一行多元素返回同级数组,行为口径与 09-29 保持一致**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 约 20 分钟(非精确计时)
|
||||
- **Completed:** 2026-10-27
|
||||
- **Tasks:** 4(Task 1-4;Task 0 汇报关卡不计入)
|
||||
- **Files modified:** 6 个代码 / 测试文件,另写出 2 个 `.planning` 产物(BASELINE / SUMMARY)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- `book.tsx`、`bookDetail.tsx`、`equipbox.tsx`、`toolbox.tsx`、`hotkey.tsx` 均删除顶部预建 `VNode[]` 与列表 `for` 循环,改为 JSX 内联 `.map()`。
|
||||
- `book` 每行仍返回 13 个同级元素(命中层、图标、名称、特殊属性、五个基础数值、四个临界数值)。
|
||||
- `bookDetail` 仍按 `specialRows` 渲染标题与 `TextContent`,高度测量、`key` 重挂载与 `SPECIAL_MIN_HEIGHT` 回退未变。
|
||||
- 每个列表段落新增一行 `{/* … */}` 职责注释;所有 `//` 只留在 JS 代码位置。
|
||||
- `uiContainerGate.test.ts` 从 9 组扩展到 13 组,新增「无预建 `VNode` 数组」「内联 `.map()` 与短循环变量」「`book` 每行 13 元素齐全」「注释位置与数量」四组。
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `src/client/ui/book.tsx` - 删除 `rows: VNode[]` 与 `for`,改为 `enemies.value.map((view, i) => [13 个同级元素])`;`vue` 导入去掉 `VNode`。
|
||||
- `src/client/ui/bookDetail.tsx` - 删除 `specialNodes` 与 `for`,改为 `specialRows.value.map(row => [<text/>, <TextContent/>])`;保留 `watch` 与全部高度逻辑。
|
||||
- `src/client/ui/equipbox.tsx` - 删除 `statusNodes` / `slotNodes` / `ownedNodes` 与三个 `for`,改为三处内联 `.map()`,各返回三元组;4 个 `*_SCROLL_LOC` 与 `ElementLocator` 导入保留。
|
||||
- `src/client/ui/toolbox.tsx` - 删除 `permNodes` / `consNodes` 与两个 `for`,改为 `permItems.value.map` / `consItems.value.map` 各返回四元组。
|
||||
- `src/client/ui/hotkey.tsx` - 删除 `groupNodes` / `commandNodes` 与两个 `for`;左栏 `groups.map(([id, group], i)`,右栏 `commandRows.value.map((row, i)` + 内层 `.map((keys, j)`;保留 `ICommandRow` / `commandRows` / `combinationLists`。
|
||||
- `src/client/ui/__test__/uiContainerGate.test.ts` - 保留原 9 组断言,新增 4 组,共 13 组;`hotkey` 的 `selectKey` needle 改为 `j`。
|
||||
- `.planning/phases/09-minimum-browser-runnable/09-30-BASELINE.json` - 改代码前写出的基线:HEAD、7 个受保护文件哈希、全仓脏文件快照、6 路径 eslint / typecheck 基线。
|
||||
- `.planning/phases/09-minimum-browser-runnable/09-30-SUMMARY.md` - 本文件。
|
||||
|
||||
## 逐文件对照:预建数组 → 内联 map
|
||||
|
||||
| 文件 | 删除的预建数组 / 循环 | 新增的内联写法 |
|
||||
|------|----------------------|----------------|
|
||||
| `book.tsx` | `rows: VNode[]` + `for` + `rows.push(...)` | `enemies.value.map((view, i) => { …; return [13 元组]; })` |
|
||||
| `bookDetail.tsx` | `specialNodes: VNode[]` + `for` + `push` | `specialRows.value.map(row => [<text/>, <TextContent/>])` |
|
||||
| `equipbox.tsx` | `statusNodes` / `slotNodes` / `ownedNodes` + 三个 `for` | `props.attributes.map` / `hero.equip.slots.map` / `owned.value.map`,各返回三元组 |
|
||||
| `toolbox.tsx` | `permNodes` / `consNodes` + 两个 `for` | `permItems.value.map` / `consItems.value.map`,各返回四元组 |
|
||||
| `hotkey.tsx` | `groupNodes` / `commandNodes` + 两段嵌套 `for` | 左栏 `groups.map` 二元组;右栏 `commandRows.value.map` + 内层 `list.map((keys, j))` 二元组 |
|
||||
|
||||
## 循环变量与 key
|
||||
|
||||
- `groupIndex` / `commandIndex` → `i`;内层 `index` → `j`。
|
||||
- `selectKey` 的函数签名保持 `(command, index)` 不变,调用点改为 `selectKey(row.command, j)`。
|
||||
- `hotkey` 按键框 key:`box-${i}-${j}`;组合文本 key:`text-${i}-${j}`。
|
||||
- `book` 保留 13 个 key 前缀:`hit-` / `icon-` / `name-` / `special-` / `hp-` / `atk-` / `def-` / `money-` / `exp-` / `dmg-` / `crit-` / `reduce-` / `def1-`。
|
||||
|
||||
## 注释位置与数量
|
||||
|
||||
- 每个 `Scroll` 块内 `{/* … */}` 数量:`book [1]`、`bookDetail [1]`、`equipbox [1,0,1,1]`(状态 / 描述 / 槽位 / 拥有)、`toolbox [1,1,0]`(永久 / 消耗 / 描述)、`hotkey [1,1]`。
|
||||
- `equipbox` / `toolbox` 的描述块不是列表,未新增 `{/* … */}`。
|
||||
- `Scroll` 的 JSX 子元素位置只放 `{/* … */}`;`//` 只落在渲染函数体、`map` 回调体或返回的数组字面量内部。
|
||||
- `bookDetail` 的两条既有 `//` 原样留在渲染函数体、`return (` 之前,不在 `Scroll` 块内。
|
||||
- `book` 的三条既有 `//`(`// 数值列表区` / `// 临界信息区` / `// 临界信息缺失时回退为 0,让「没有数据」与「数值为 0」可以区分`)文本未改;第三条单独成行,位于 `map` 回调体内(即 `Scroll` 块内)。
|
||||
- `equipbox` 的两条、`hotkey` 的两条既有 `//` 文本未改;`hotkey` 新增的内层 `//` 只位于内层回调体。
|
||||
- 未修改任何既有注释或 jsDoc;只新增注释。
|
||||
|
||||
## Gate Results(实际运行)
|
||||
|
||||
| 门禁 | 结果 |
|
||||
|------|------|
|
||||
| `pnpm exec vitest run src/client/ui/__test__/uiContainerGate.test.ts` | **13/13 通过**(原 9 组 + 新增 4 组) |
|
||||
| `pnpm exec vitest run src/client/ui/__test__/hotkeySettingsGate.test.ts` | **12/12 通过** |
|
||||
| `pnpm exec eslint`(同一组 6 个路径,NEW-only) | 基线 0 → 当前 0,**无 NEW** |
|
||||
| `pnpm check:type`(整仓,6 路径 scope,NEW-only) | 基线 6 → 当前 6,**无 NEW**;6 条均为 09-29 既有的 `TS2322`,位于 `book.tsx` / `equipbox.tsx` / `toolbox.tsx` |
|
||||
| 7 个受保护文件哈希 | 与基线逐字一致 |
|
||||
| HEAD 政策 | 与基线相同:`e2415a7a48d9a9e9d2f886e00c10ca51e91340d1` |
|
||||
| 全仓脏文件范围闸门 | 25 条脏路径,全部在基线快照或 6 个允许路径内;**无新增范围外路径**;6 个允许路径全部出现 |
|
||||
| 摘要内容闸门 | 本文件写出后运行并通过 |
|
||||
|
||||
## 声明与限制
|
||||
|
||||
- **未做浏览器 UAT**:未宣称滚动、内存、点击命中、拖拽手感、内联数组是否逐元素平铺等任何视觉 / 交互行为已验证。
|
||||
- **执行器 0 次代码提交**:未运行任何 git 写命令(`add` / `commit` / `stash` / `checkout` / `restore` / `worktree` / `reset`),代码改动全部留在工作树供审查。
|
||||
- **估算未校准**:`confidence: low`,`tasks: 4`,不含 Task 0 汇报关卡;`actuals.tokens` 因 09-29 未提交而无法分离,记 `null`。
|
||||
- **09-29 改动未提交**:5 个 UI 文件与 `uiContainerGate.test.ts` 在基线时本来就是脏的,因此范围闸门只能证明「没有新增范围外路径」,不能证明这 6 个路径本次被改过;内容是否改到位由源码形状闸门负责。
|
||||
- **基线脏文件范围**:基线快照还包含 docs、`AGENTS.md`、`map/*`、`setting/*` 等既有未提交修改;本计划未触碰这些文件。
|
||||
- **`book.tsx` 过期 TODO**:`getCritical` 上方写着「留空」的既有 TODO 按现状保留,未修改。
|
||||
- **`keyAdaptationGate.test.ts` 是既存失败**:15 个用例中 5 个失败,改动前即如此,不在本计划范围;未修复、未以它为绿、不声称全部既有闸门为绿。
|
||||
- **无 `inputBox.tsx` / `waitBox.tsx` 改动**:两份受保护文件哈希与基线一致。
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### 1. 修正 Task 4 中一处与 Task 1 冲突的断言(Rule 1 - 测试缺陷)
|
||||
|
||||
- **Found during:** Task 4 摘要前门禁。
|
||||
- **Issue:** Task 4 的文字要求断言 `book` 的第三条注释索引「小于 `<Scroll`」,但 Task 1 明确要求该注释留在 `map` 回调体内(`const damage` 之上),而回调体位于 `<Scroll>` 内部,两个要求不可能同时成立。
|
||||
- **Fix:** 将该断言改为「大于 `<Scroll` 且小于 `</Scroll>`」,与 Task 1 / `must_haves` 的注释位置要求一致;未改动业务代码。
|
||||
- **Files modified:** `src/client/ui/__test__/uiContainerGate.test.ts`。
|
||||
- **Verification:** `uiContainerGate.test.ts` 13/13 通过。
|
||||
|
||||
### 2. Prettier 格式化(Rule 1 - 阻塞性格式检查)
|
||||
|
||||
- **Found during:** Task 2 / Task 3 eslint。
|
||||
- **Issue:** 内联 `map` 后缩进变化,部分原有单行三元与属性折行不再满足 `prettier/prettier`。
|
||||
- **Fix:** 对 5 个 UI 文件与测试文件运行 `pnpm exec prettier --write`;只调整换行与缩进,未改行为、未改注释文本。
|
||||
- **Files modified:** 5 个 UI 文件 + `uiContainerGate.test.ts`。
|
||||
- **Verification:** eslint 无 NEW(基线 0 → 当前 0)。
|
||||
|
||||
**Total deviations:** 2(测试断言修正 1、格式化 1)。两者均在 `files_modified` 允许范围内,无范围外写入。
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- 5 个 UI 文件与 `uiContainerGate.test.ts` 均存在且已写入。
|
||||
- `09-30-BASELINE.json` 已写出,7 个哈希齐全。
|
||||
- `uiContainerGate` 13/13、`hotkeySettingsGate` 12/12 通过。
|
||||
- eslint / `check:type` 无 NEW;7 个受保护文件哈希与 HEAD 政策通过;范围闸门通过。
|
||||
- 未执行任何 git 写命令。
|
||||
Loading…
Reference in New Issue
Block a user