From 3fb1f171b614e1300539d9618a0e964e698480ec Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Thu, 29 Oct 2026 20:27:23 +0800 Subject: [PATCH] docs(10-42): add VitePress render demo POC plan and summary --- .planning/ROADMAP.md | 3 + .planning/STATE.md | 13 +- .../phases/10-docs-restructure/10-42-PLAN.md | 378 ++++++++++++++++++ .../10-docs-restructure/10-42-POC-NOTES.md | 84 ++++ .../10-docs-restructure/10-42-SUMMARY.md | 61 +++ 5 files changed, 533 insertions(+), 6 deletions(-) create mode 100644 .planning/phases/10-docs-restructure/10-42-PLAN.md create mode 100644 .planning/phases/10-docs-restructure/10-42-POC-NOTES.md create mode 100644 .planning/phases/10-docs-restructure/10-42-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 7786435..551b9aa 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -589,6 +589,7 @@ Plans: - [x] 10-30-PLAN.md — 高级 I 导览 7 → 9 + 侧边栏注册 + 两篇互链收口 + 全量构建 - [x] 10-31-PLAN.md — 两篇补写内容的独立读者审阅(沿用高级 I 人格)→ 问题清单 → 迭代修正(≤3 轮)→ 复审清零 - [x] 10-32-PLAN.md — 两篇补写内容的用户主导修订循环 → 用户确认 → 最终格式 / 构建 / SUMMARY 收尾 +- [x] 10-42-PLAN.md — VitePress 内嵌自定义渲染器 demo 最小 POC(config alias / dedupe / isCustomElement / vue-jsx + 包装组件 + 渲染基础与 UISystem 两个 demo + 不进导航的 POC 页) **说明:** 每篇中级教程遵循 `10-CONTEXT.md` D-21..D-44 的逐篇要点与阅读顺序(4/8/9/3)。**旧内容删除已并入本批(D-04,Wave 0 / 10-01)**:删除 `docs/guide/*`、`docs/dev/*`、`docs/logger/*`、`docs/api/*` 与首页 `docs/index.md`,并清理 `docs/.vitepress/config.ts` 的旧 `nav`/`sidebar` 键。文档改动为 `docs/intermediate/*.md`(新增)、`docs/.vitepress/config.ts` 与 `docs/.vitepress/api.ts`(缺失目录守卫,保留按包生成机制)。构建命令 `cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"`;`init.ts` 在 `docs/api/` 缺失时安全跳过。本机 PowerShell 禁止 `pnpm.ps1`,命令经 `cmd /c "..."` 执行。 @@ -597,3 +598,5 @@ Plans: **高级 II 说明(已完成):** 第三批(10-21..10-27)按 D-67 / D-53 定位为「深入定制」**难度层**(难度标签,非领域分类),只写**数据端**内容;已建 `docs/advanced/ii/` **10 篇**(导览 + 8 个主题 + 点事件与图块事件)。写作按 D-59 库文档风格、D-60 简洁、D-61 主干与边界分离、D-62/D-70 代码块完整、D-63 模板顺序、D-64 只在需实现接口时给模板、D-65 注释充足、D-66 并列功能分开示例、D-69 内部实现章节后置;读者水平按 D-68(已理解绝大多数常用语法,认真阅读后能读明白一部分简单与中等复杂度系统,不预设资深开发者)。侧边栏分组名「高级 II · 深入定制」。渲染端 / 编辑器 / 状态栏 / 怪物图鉴按 D-47 非断链占位;高级 III 只用代码 span 或 `TODO(advanced-III)` 注释。10-25 负责导览与站点注册,10-26 执行独立读者审阅,10-27 执行用户主导修订循环,**用户已明确确认**(D-46 第 2 阶段)并于 2026-10-23 收尾。构建命令同上。 **高级 I 补写说明(第四批,待执行):** 按 D-75 / D-71,为高级 I 补两篇数据端文档——`docs/advanced/mover.md`(移动 Mover)与 `docs/advanced/face-handler.md`(FaceHandler 系列)。写作按高级 I 读者水平(D-57:了解基本语法、代码能力一般、不熟悉引擎内部架构)与 D-59..D-74 库文档风格;渲染端 / 编辑器按 D-47 非断链占位。10-28 / 10-29 并行写两篇(互引先用代码 span),10-30 负责导览 7 → 9、侧边栏注册与互链收口,10-31 执行独立读者审阅(沿用 10-19 已确认的高级 I 人格),10-32 执行用户主导修订循环,**用户明确确认后方可收尾**。文件名、页面标题、侧边栏显示名与示例标识符名以各计划 Task 0 的用户确认为准(D-13)。D-75 写的 `IMoveController` 与源码 `IMoverController` 不一致,以源码为准。 + +**10-42 增量说明:** `10-42-PLAN.md` 是 Phase 10 的独立增量(VitePress 内嵌自定义渲染器 demo 最小 POC),单独追加:Phase 10 的 `**Plans:**` 计数行与第 1..5 批描述不因本计划改动(计数收口仍归 `10-41`),`10-33`..`10-41` 的收口仍由 `10-41` 负责,`10-06` / `10-11` / `10-12` 仍未勾选。POC 页 `docs/poc/render-demo.md` 与现有命名暂时保留,后续正式渲染端文档批次再决定删除或迁移。 diff --git a/.planning/STATE.md b/.planning/STATE.md index 03a916d..d1c507c 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,9 +5,9 @@ current_phase: 10 current_phase_name: 文档重构 status: executing stopped_at: Phase 10 进行中:中级 24 篇 + 高级 I 9 篇 + 高级 II 10 篇已收尾;10-06/10-11/10-12 待补 -last_updated: "2026-10-24T00:00:00.000Z" -last_activity: 2026-10-24 -last_activity_desc: 高级 I 补写(Mover + FaceHandler)完成并经用户确认收尾 +last_updated: "2026-10-29T00:00:00.000Z" +last_activity: 2026-10-29 +last_activity_desc: 10-42 文档内嵌渲染 demo POC 完成并经用户人工验证 state_head: dbbcf533dce2f22078a82d26817f36b85865da62 progress: total_phases: 10 @@ -29,11 +29,11 @@ See: .planning/PROJECT.md (updated 2026-09-17) ## Current Position Phase: 10 (文档重构) — IN PROGRESS -Plan: 第一批(中级 24 篇)、第二批(高级 I 7 篇)、第三批(高级 II 10 篇)、第四批(高级 I 补写 Mover/FaceHandler 2 篇)已收尾;10-06 / 10-11 / 10-12 待补 +Plan: 第一批(中级 24 篇)、第二批(高级 I 7 篇)、第三批(高级 II 10 篇)、第四批(高级 I 补写 Mover/FaceHandler 2 篇)已收尾;10-42 文档内嵌渲染 demo POC 已完成并经用户验证;10-06 / 10-11 / 10-12 待补 Status: In Progress -Last activity: 2026-10-24 — 高级 I 补写完成并经用户确认收尾 +Last activity: 2026-10-29 — 10-42 文档内嵌渲染 demo POC 完成并经用户确认(人工验证通过) -Progress: Phase 10 · 29/32 plans(中级 + 高级 I 9 篇 + 高级 II 已收尾;总览 / 初级 / 高级 III / API / Agent 参考仍待推进) +Progress: Phase 10 · 29/32 plans(中级 + 高级 I 9 篇 + 高级 II 已收尾;总览 / 初级 / 高级 III / API / Agent 参考仍待推进)(另:10-42 文档内嵌渲染 demo POC 已完成并经用户验证,Phase 10 的计数收口仍归 10-41) ## Performance Metrics @@ -267,6 +267,7 @@ Recent decisions affecting current work: - Phase 9 added (2026-10-08): 最小浏览器可运行版本(在已完成且 Node 可运行/回放验证的数据端之上完成渲染端框架,并验证可在浏览器上运行);Depends on Phase 8 - Phase 10 added (2026-10-14): 文档重构(重构过老、结构不佳、新人难懂的项目文档,面向新人/不懂代码的作者/有基础作者等多背景读者提供分层指南,并专门为 AI Agent 编写参考文档);Depends on Phase 9 - Phase 10 第一批(中级教程 24 篇 + 旧内容清空 + 站点注册 + D-09 修订 + 读者/用户修订循环)已完成并收尾(2026-10-20);第二批(高级 I 7 篇 + 站点注册 + 独立读者审阅 + 用户 6 轮修订)已完成并经用户确认收尾(2026-10-20);第三批(高级 II 10 篇 + 站点注册 + 独立读者审阅 + 用户 3 轮修订)已完成并经用户确认收尾(2026-10-23);第四批(高级 I 补写 Mover + FaceHandler 2 篇 + 注册 + 读者/用户修订)已完成并经用户确认收尾(2026-10-24)。遗留 `10-06` / `10-11` / `10-12` 缺独立 SUMMARY,其范围已被后续修订覆盖 +- Phase 10 增量 10-42(2026-10-29):VitePress 内嵌自定义渲染器 demo 最小 POC(DOM 包装组件动态 import 引擎 + 渲染基础 / UISystem 两个 demo + 不进导航 POC 页 + docs 局部 tsconfig + ESLint docs override);用户人工验证通过(原话 `pass`),页面 `docs/poc/render-demo.md` 与命名暂时保留;未做 fence 插件 / 正式渲染端文档批次 / client-base 组件 / nav·sidebar 注册;无源码改动、无 commit ### Pending Todos diff --git a/.planning/phases/10-docs-restructure/10-42-PLAN.md b/.planning/phases/10-docs-restructure/10-42-PLAN.md new file mode 100644 index 0000000..6fad5dd --- /dev/null +++ b/.planning/phases/10-docs-restructure/10-42-PLAN.md @@ -0,0 +1,378 @@ +--- +phase: 10-docs-restructure +plan: 42 +type: execute +wave: 1 +depends_on: [] +files_modified: + - .planning/phases/10-docs-restructure/10-42-POC-NOTES.md + - .planning/phases/10-docs-restructure/10-42-SUMMARY.md + - docs/.vitepress/config.ts + - docs/tsconfig.json + - docs/.vitepress/components/renderDemo.vue + - docs/.vitepress/components/demos/basicRender.vue + - docs/.vitepress/components/demos/uiSystem.vue + - docs/poc/render-demo.md + - eslint.config.js + - .planning/ROADMAP.md + - .planning/STATE.md +autonomous: false +requirements: [] +user_setup: [] +estimate: + tokens: 34000 + raw_tokens: 23000 + tasks: 8 + confidence: low +must_haves: + truths: + - "`cmd /c \"pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build\"` 通过、输出含 `build complete`、dead link 为 0,并生成 `public/_docs/poc/render-demo.html`(D-09 构建通过)" + - "在浏览器打开 POC 页后,渲染基础 demo 真实绘制 container / g-rect / g-circle / text,点击后画面可见状态发生变化(计数或颜色)" + - "UISystem demo 能打开并关闭一个最小 UI,页面上的栈长度随打开 / 关闭变化、关闭后归零;`controller.active` / `controller.stack` 先经 `computed` 解包,再以字符串表达式绑定到 `:text`(如 `'栈长度:' + stackSize`、`active ? '已打开' : '未打开'`);所有 `:text` 的实参在运行期都是 string,裸 Ref / 裸 number / 裸 boolean 都会 assertType 失败并报错 21" + - "每个 demo 卸载时执行 `app.unmount()` 与 `renderer.destroy()`;离开页面再返回仍可正常使用,无 RAF 与 document 监听累积;动态 import 完成时若已卸载或 canvas 已消失则直接返回;卸载清理统一由 setup 阶段注册的 `onUnmounted` → `dispose?.()` → 置 null 完成,`dispose` 在 `app.mount()` 之前就已赋值、内部先判 `appRef`(有 app 则先 `app.unmount()`,随后 `renderer.destroy()`),清理路径不含任何异常捕获语法;canvas 守卫保证进入构造时 canvas 真实存在,因此不会出现未拿到 canvas 的实例被销毁" + - "包装组件不改变渲染器根锚点(保持构造器默认 0.5/0.5,与 `size()` 的根中心平移正好抵消为左上原点),因此绘制坐标与点击命中一致,`@click` 能触发" + - "包装组件与 POC 页面里 `@motajs/*` / `@user/*` 只出现在 `import(...)` 动态 import 中(任何 `from '@motajs…'` / `from '@user…'` 静态导入都判失败,含 prettier 折行的具名导入),因此 VitePress 预渲染不会触碰 `document`" + - "`docs/.vitepress/config.ts` 只新增 alias / dedupe / isCustomElement / vue-jsx,`MermaidPlugin`、`base`、`outDir`、`nav`、`sidebar` 与既有注释保持不变" + - "docs 组件在 `docs/tsconfig.json` 下不产生任何以 `docs/` 开头的类型错误:`cmd /c \"pnpm exec vue-tsc --noEmit -p docs/tsconfig.json\"` 输出中路径以 `docs/` 开头且含 `error TS` 的行数必须为 0;`packages/` / `packages-user/` / `src/` 的既有报错不算本轮失败,但必须把条数与摘要写进 POC-NOTES 的 `引擎既有类型错误:` 行" + - "docs demo 通过 ESLint 门禁:`cmd /c \"pnpm exec eslint docs/.vitepress/components docs/.vitepress/config.ts\"` 为 0 error / 0 warning,输出中不再出现 `vue/attribute-hyphenation` 与 `vue/one-component-per-file`;渲染属性全程保持 camelCase(`fillStyle` / `strokeStyle` / `lineWidth`),不得改成连字符形式(改名会让 `RenderItem` 的属性匹配落空、demo 不渲染)" + - "`docs/tsconfig.json` 与 `eslint.config.js` 的新增内容只服务 docs:`docs/tsconfig.json` 不被加入 `pnpm check:type` 的既有范围,根 `tsconfig.json` / `tsconfig.node.json` 的既有 `include` 未被修改,`eslint.config.js` 新增的 override 只匹配 `docs/.vitepress/components/**/*.vue`,不影响 `packages/` / `packages-user/` / `src/` 的既有 lint 结果" + - "未修改 `packages/`、`packages-user/`、`src/`、既有 docs 页面与任何既有 JSDoc/注释(引擎既有类型错误只记录、不修复);所有新命名经 Task 0 用户确认(本次修订新增的 `AppHandle` 采用用户指定名;`docs/tsconfig.json` / `eslint.config.js` 是配置文件与规则覆盖,不引入新的对外命名);本计划不产生任何 git commit;Task 1 记录的 ROADMAP / STATE 基线原文未被本次修改改动" + artifacts: + - ".planning/phases/10-docs-restructure/10-42-POC-NOTES.md(命名确认 + 基线 HEAD / ROADMAP Plans 行 / STATE 计数 + 门禁结论 + 人工验证结论 + Task 7 追加的当前 HEAD 与「本计划文件未提交」记录)" + - ".planning/phases/10-docs-restructure/10-42-SUMMARY.md" + - "docs/.vitepress/config.ts(新增 alias / dedupe / isCustomElement / vue-jsx)" + - "docs/tsconfig.json(docs 局部 TS 项目:extends 根配置、baseUrl + 显式 paths、include `.vitepress/**/*.ts|*.vue` 与 `../src/types/vite-env.d.ts`、`ignoreDeprecations: \"6.0\"`)" + - "docs/.vitepress/components/renderDemo.vue(DOM 包装组件,动态 import 引擎与 demo)" + - "docs/.vitepress/components/demos/basicRender.vue(渲染基础 demo)" + - "docs/.vitepress/components/demos/uiSystem.vue(UISystem demo)" + - "docs/poc/render-demo.md(ClientOnly 包裹的 POC 页面,不进 nav / sidebar)" + - "eslint.config.js(新增只覆盖 `docs/.vitepress/components/**/*.vue` 的 override:`vue/attribute-hyphenation: ['warn', 'never']`、`vue/one-component-per-file: 'off'`)" + key_links: + - "包装组件 onMounted 动态 import → 规避 `packages/render/src/core/utils.ts:4-12`(模块顶层 document.createElement)、`core/graphics.ts:29-30`(模块顶层 new MotaOffscreenCanvas2D)、`core/misc.ts:21`(静态 measureCanvas)、`render-vue/src/tag.ts:32`(构造期 document)在预渲染阶段执行" + - "`createRendererFor(renderer).createApp(Demo).mount(renderer)`(`render-vue/src/renderer.ts:26-113`)是本 POC 唯一真实绘制链路;`MotaRenderer` 构造于 `render/src/core/render.ts:123-146`" + - "`controller.render()` 返回 VNode(`system/src/ui/controller.ts:101-103`),必须挂到同一个自定义渲染器上;若交给 DOM 渲染器,未注册的自定义标签会走 `resolveComponent()` 失败" + - "卸载顺序必须先 `app.unmount()` 再 `renderer.destroy()`:前者拆组件树(触发 `remove` → `el.destroy()`),后者停 RAF 并 abort document 监听(`render.ts:852-856`)" + - "根锚点必须保持构造器默认值:`size()` 把根 transform 平移到 (宽/2, 高/2)(`render.ts:281-285`),元素渲染再平移 -anchor×尺寸(`item.ts:201-202,221`),默认 0.5/0.5 正好抵消为左上原点;命中测试用 `event.offsetX/scale`(`render.ts:538-539`)与元素自身 transform(`item.ts:860-868`),**不含根 transform**,改锚点会让画面与点击错位" + - "渲染器构造必须拿到真实 canvas:无 canvas 时构造器 `logger.error(19)` 后直接 return(`render.ts:126-130`),`excitation` 未赋值,随后的 `destroy()` 会 TypeError(`render.ts:852-856`),所以 await 之后要先判 `disposed` 与 canvas 是否还在" + - "UISystem demo 必须先 `controller.setPropsBaseCreator(...)`,否则 `UIContainer` 渲染每个实例时会走 `logger.error(74)` 分支(`controller.ts:63-71`)" + - "属性名必须保持 camelCase(`fillStyle` / `strokeStyle` / `lineWidth`):docs 的 `g-*` 标签是 `isCustomElement` 的元素 vnode,`@vue/compiler-dom` 的 `v-bind` 不会对元素做 camelize,把 `fillStyle` 改写成连字符属性名会被原样编译成 `fill-style` 键;而属性匹配是 `Graphics.handleProps`(`packages/render/src/core/graphics.ts:255-263`)与 `Text.handleProps`(`packages/render/src/core/misc.ts:127-137`)里的 `case 'fillStyle' / 'strokeStyle' / 'lineWidth'` 字面量,改名即失配、demo 不渲染。因此连字符告警只能靠 `eslint.config.js` 里对 `docs/.vitepress/components/**/*.vue` 的局部 override 解决,不得改属性名,也不得为凑 lint 结果去改引擎源码" +--- + + +本计划 8 个任务、11 个 `files_modified`,超出「≤ 8 个文件」的默认上限,仍保持单计划,理由如下: + +- 8 个任务已是「0..7」上限:Task 0 是用户汇报关卡,Task 6 是人工浏览器验证关卡,两者都不能合并;其余任务已把「命名与基线记录 + 配置改动 + 首次构建」合并为一个任务,再合并会把「页面创建 + 六类门禁」或「人工验证 + 收尾」压成不可独立验证的巨块,反而降低可执行性。 +- 11 个文件里只有 7 个是实现文件(config.ts / `docs/tsconfig.json` / `eslint.config.js` / 包装组件 / 两个 demo / POC 页),另 2 个是本计划自己的记录(POC-NOTES / SUMMARY),2 个是 GSD 要求的账本追加(ROADMAP / STATE)。本次修订新增的两个文件都是**配置类**(docs 局部 TS 项目、docs demo 的 ESLint 局部 override),随已批准 Task 1 / Task 5 一起落盘,不新增任务、不新增对外命名、不改任何源码;拆成两个计划只会让记录文件与账本追加跨计划重复,并额外引入一次人工关卡。 +- 六类门禁(prettier / ESLint / docs 类型检查 / 构建 / 静态断言 / 人工浏览器验证)共享同一次构建产物与同一份 POC-NOTES,切片后无法独立验证。 +- 成本约占单计划预算 34%(见 `estimate`),无跨计划依赖(`depends_on: []`、`wave: 1`)。 + +不为凑数删除门禁:Task 0 关卡、SSR 动态 import、UISystem 生命周期、prettier / ESLint / docs 类型检查 / 构建 / 静态断言、人工浏览器验证全部保留。 +**修订说明(组件类型与 ESLint 告警修复)**:为修掉 docs 组件的隐式 any / 缺类型 / ESLint 属性连字符告警,本计划并入 `docs/tsconfig.json`(docs 局部 TS 项目)与 `eslint.config.js` 的 docs demo override;两者都在既有 Task 1 / Task 5 范围内完成,任务数仍为 8,命名方案(方案 A)与 Task 0 的批准不变。 + + + +在 VitePress 文档站里做一个**最小 POC**:把 `@motajs/render` + `@motajs/render-vue` 的自定义渲染器嵌进文档页面,用两个 demo 证明「渲染基础」与「UISystem」都能在文档站里真实运行、可交互、可安全卸载。 + +Purpose: 这是「文档内嵌可运行示例」的前置技术验证。用户已定:**不做 markdown fence 插件**,先做独立 demo 文件 + 包装组件;fence 插件留到 POC 通过后再评估。POC 必须在 **SSR 预渲染**(`vitepress build docs`)下不炸,因此包装组件对引擎包只能动态 import(对应 D-09「构建通过」的硬门禁)。 +Output: `docs/.vitepress/config.ts` 的最小配置扩展、一个 DOM 包装组件、两个 demo、一个不进导航的 POC 页面,以及可复跑的门禁与记录;本修订并入两个只服务 docs 的配置落点(`docs/tsconfig.json` 的局部 TS 项目、`eslint.config.js` 的 docs demo override),用于消掉组件的隐式 any / 缺类型与 ESLint 属性连字符告警。**不写正式文档、不改源码、不提交 git。** + + + +@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/phases/10-docs-restructure/10-CONTEXT.md +@.planning/phases/10-docs-restructure/10-VALIDATION.md +@.planning/phases/10-docs-restructure/10-41-PLAN.md +@docs/.vitepress/config.ts +@vite.config.ts +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/config.json + +**流程约定(D-10 / D-11 / D-12 / D-13 / D-14):** +- 本计划是 Phase 10 的一次增量派发,**只新增 10-42**,不重规划、不修改 `10-01`..`10-41`,不补 `10-06` / `10-11` / `10-12`。 +- **Task 0 是硬关卡**:先向用户汇报计划内容与命名清单,等用户明确回复「可以执行」;未收到前不得修改任何文件、不得派发修改子代理。 +- 所有新组件名、props 名、内部类型名、demo 目录 / 文件名、页面路径、config 新增键都属 D-13,**必须先经用户同意**;本计划中的名称一律标注为「推荐值(方案 A),待 Task 0 确认」。 +- 越界内容(源码修改、正式文档正文、nav / sidebar、fence 插件、`client-base` 真实组件)只记录、不实施(D-14)。 +- 每次需要改文件前,执行者必须先声明本次修改是「内联执行」还是「派发子代理执行」并说明范围;计划文件、代码、demo、页面一律由子代理执行(AGENTS.md)。 +- **不得调用 `opencode` 命令**(含 `opencode run` 及任何子命令),不得启动额外 opencode 会话。 +- **不得修改任何既有 JSDoc / 注释**;新增注释可以,但必须符合 `dev.md`。 +- 本计划**不执行任何 `git commit` / `git push`**(D-90 精神:由用户在 POC 通过后自行决定)。Task 7 用「`git log <基线 HEAD>..HEAD -- <本计划文件>` 为空」+「本计划文件仍在 `git status --porcelain` 输出里」来证明这一点;**不要求 HEAD 与基线相等**——用户可能并发提交把 HEAD 前移,只要那些提交没有带入本计划文件就不算违规。 +- 命令一律经 `cmd /c "..."` 执行(本机 PowerShell 5.1 不能直接跑 `pnpm.ps1`)。 +- **本计划自带门禁,不依赖 `10-VALIDATION.md` 的更新**(阶段级 Nyquist 漂移是既有状态,本计划不改它)。 + +**已核实的源码事实(执行时以这些为准,不必重新调查):** +- 渲染器创建:`createRendererFor()` 封装在 `packages/render-vue/src/renderer.ts:26-113`;返回 `{ tagManager, createApp, render }`。 +- 渲染器实例:`new MotaRenderer({ canvas, width, height })`(`packages/render/src/core/render.ts:123-146`;配置接口 `IMotaRendererConfig` 见 `core/types.ts:431-442`)。构造器内部 `this.setAnchor(0.5, 0.5)`(`render.ts:140`)、`this.setScale(1)`(`render.ts:142`);`canvas` 既可是元素也可是选择器(`getMountCanvas`,`render.ts:168-176`)。**真实实例化从不调用锚点设置接口**(`packages-user/client-modules/src/render/renderer.ts:35-41`)。 +- 坐标与根锚点(关键):`size(width, height)`(`render.ts:281-285`)把根 transform 平移到 `(width / 2, height / 2)`;元素绘制时 `renderContent` 再 `translate(-anchorX × width, -anchorY × height)`(`item.ts:201-202`、`221`),构造器默认锚点 0.5/0.5 **正好抵消**,绘制原点就是左上角。元素自身 `anchorX` 默认 0(`item.ts:62`),所以 `x` / `y` 即左上角坐标。命中测试用 `event.offsetX / this.scale`(`render.ts:538-539`)再套**元素自身** transform(`item.ts:860-868`),**不含根 transform**。因此**必须保持构造器默认锚点**;若改成 (0,0),画面整体偏移 `(w/2, h/2)` 而点击仍按原坐标判定,导致 `@click` 不触发、点击与画面错位。 +- 尺寸与缩放:`onResize(scale)`(`render.ts:272-279`)= `target.setScale(scale)` + 把 `canvas.style.width/height` 写成「逻辑尺寸 × scale」;画布属性尺寸 = 逻辑尺寸 × `devicePixelRatio` × scale。**默认 `scale = 1` 时 CSS 尺寸就等于逻辑尺寸**。包装组件不要给 canvas 写内联宽高去和引擎对抗,尺寸完全交给 `size()` 与 `onResize()`。 +- 构造失败的半成品:拿不到 canvas 时构造器 `logger.error(19)` 后直接 `return`(`render.ts:126-130`),`excitation` 未赋值;而 `destroy()`(`render.ts:852-856`)无条件 `this.excitation.destroy()` → TypeError。所以**不得对未完整构造的实例调用 `destroy()`**,且 await 之后必须先判组件是否已卸载、canvas 是否还在。 +- 卸载:`MotaRenderer.destroy()`(`render.ts:852-856`)→ `excitation.destroy()`(停 RAF)+ `abort?.abort()`(撤销全部 document 监听)。 +- SSR 硬约束:`core/utils.ts:4-12` 模块顶层 `document.createElement`;`core/graphics.ts:29-30` 模块顶层 `new MotaOffscreenCanvas2D(false)` + `size(1, 1)`;`core/misc.ts:21` 静态 `measureCanvas`;`render-vue/src/tag.ts:32-35` 构造期 `document.createElement`。因此**任何在模块顶层 import `@motajs/render` 或 `@motajs/render-vue` 的模块都会在预渲染阶段炸**。 +- `@motajs/system` 的 `ui/types.ts:1` 从 `@motajs/render` 引入 `Props`(运行期 import,非 `import type`),所以 **`@motajs/system` 也按「引擎包」对待**:包装组件与页面顶层不得静态 import 它(demo 文件本身由包装组件动态 import,故 demo 内可以静态 import)。 +- `packages/render/src/assets/composer.ts:14-15` 使用 `?raw` 引入 `.vert` / `.frag`:Vite 的 raw loader 对其生效,docs 侧无需额外配置;若构建在此处失败,属需上报的问题,不得私自加配置绕过。 +- 点击事件:`RenderItem.patchProp` 把 `onXxx` 解析为事件名(`core/item.ts:934-942`);未知 key 走 `parseEvent` 后静默忽略(`item.ts:1111-1118`),故 SFC 里的 `@click` 会编译成 `onClick` 并被注册为 `click` 监听。 +- 属性名:位置用 `loc` / `x` / `y` / `width` / `height` / `anchorX` / `anchorY`(`render-vue/src/props.ts:24-81`);图形用 `fillStyle` / `strokeStyle` / `lineWidth` / `fill`(`props.ts:125-142`,默认 `fill = '#ddd'` 见 `core/graphics.ts:37`);圆需要 `radius`(`props.ts:144-154`);文字用 `text` / `fillStyle` / `font`(`props.ts:103-114`,默认 `fillStyle = '#fff'` 见 `core/misc.ts:13`)。 +- 文字属性的类型校验:`text` 由 `handleProps` 处理并 `assertType(nextValue, 'string', key)`(`core/misc.ts:122-131`),失败即 `logger.error(21)`;插值文本走自定义渲染器的 `createText`,会 `logger.warn(38)`(`render-vue/src/renderer.ts:67-74`)。**因此 `:text` 只能绑定字符串,不能绑定 Ref 对象**。 +- `controller.active` 是**裸 Ref**(`system/src/ui/controller.ts:33`:`active: Ref = ref(false)`),`UIController` 本身不是 reactive;模板里写 `controller.active` 得到的是 RefImpl,直接交给 `:text` 会 assertType 失败。`controller.stack` 是 `shallowReactive([])`(`controller.ts:29`),长度可在模板直接读,但本计划统一用 `computed` 解包,避免歧义。 +- UI:`UIController.render()` 返回 `h(UIContainer, { controller: this })`(`system/src/ui/controller.ts:101-103`);`open` / `close` / `closeAll` / `keep` 见同文件 121-154;`GameUI` 形如 `new GameUI(name, component)`(`system/src/ui/ui.ts:3-12`);`UIInstance` 生命周期见 `system/src/ui/instance.ts:13-40`;`UIContainer` 是 `.tsx`(`system/src/ui/container.tsx:14`),**需要 `@vitejs/plugin-vue-jsx`**。 +- `setPropsBaseCreator` 真实用法见 `packages-user/client-modules/src/client.ts:101-105` 与 `src/ui/func.ts:14-27`(返回 `{ instance, controller, ... }`);不设置会走 `logger.error(74)`(`controller.ts:63-71`)。 +- VitePress 1.6.4 的 `vue` 配置项会转发给 `@vitejs/plugin-vue`(`node_modules/vitepress/dist/node/index.d.ts` 的 `vue?: Options`),`vite` 配置项会合并进 Vite 配置;当前 `docs/.vitepress/config.ts:301-310` 只有 `MermaidPlugin()` + `optimizeDeps` + `ssr.noExternal`。 +- `docs/.vitepress/config.ts:302` 的 `// @ts-expect-error 类型错误` 是既有注释,**不得修改**;若新增插件后该注释变成「未使用」,按 GSD 流程停下报告,不得擅自删改注释。 +- `vite.config.ts:8-11` 的 custom 标签数组与 `g-` 前缀是主站的既有做法,docs 侧应与之保持一致。 +- `dev.md` 的硬约束:禁止 `as`;禁止 `import type`(全部用普通 import,`dev.md:54`);函数类型用 `type` 别名(`dev.md:97`)、对象类型用 `interface`(`dev.md:98`),不得内联对象类型;禁止模块顶层副作用;新增文件必须 CRLF、prettier 可格式化。 +- **docs 组件当前不在任何 TS 项目内**:根 `tsconfig.json` 没有 `baseUrl`,`include` 只覆盖 `src` / `packages` / `packages-user`;`tsconfig.node.json` 覆盖 `docs/**/*.ts` 与 `docs/.vitepress/{api,apiSidebar,config,init}.ts`,但**不含 `docs/.vitepress/components/*.vue`**。所以 IDE 与 `vue-tsc` 都解析不到 `@motajs/*` / `@user/*` 路径与 `.vue` 声明,需要新增 `docs/tsconfig.json`(只服务 docs 与 `vue-tsc -p docs/tsconfig.json`,**不并入** `pnpm check:type`,也不改根 `tsconfig.json` / `tsconfig.node.json` 的既有 `include`)。本机 TypeScript 为 6.0.3,`baseUrl` 已弃用,需 `ignoreDeprecations: "6.0"` 才能压掉告警。 +- **ESLint 规则来源**:`eslint.config.js` 是 flat config,`eslintPluginVue.configs['flat/recommended']` 继承 `vue3-strongly-recommended`,其中有 `vue/attribute-hyphenation: 'warn'` 与 `vue/one-component-per-file: 'warn'`(`eslint-plugin-vue@9.33.0` 的 `lib/configs/vue3-strongly-recommended.js`);`docs/.vitepress/components` 不在任何 ignore 里。 +- **属性名必须 camelCase 的证据**:属性匹配是 `Graphics.handleProps` 的 `case 'fillStyle' / 'strokeStyle' / 'lineWidth'`(`packages/render/src/core/graphics.ts:255-263`)与 `Text.handleProps` 的 `case 'fillStyle' / 'strokeStyle'`(`packages/render/src/core/misc.ts:127-137`)里的字面量;docs 的 `g-*` / `container` / `text` 是 `isCustomElement` 的元素 vnode,`@vue/compiler-dom` 的 `v-bind` 不对元素做 camelize,把 `fillStyle` 改写成连字符属性名会被原样编译成 `fill-style` 键,落不到上面的 case,于是被 `patchProp` 当未知属性忽略、demo 不渲染。`@vue/compiler-core` 只对组件名 / 组件 props 做 camelize,对本 POC 不适用。 +- **引擎既有类型错误(非本轮引入、不得修)**:`packages/render/src/core/types.ts` 缺 `CanvasStyle` 导入、`packages/common/src/logger.ts` 缺 `main` 全局。`docs/tsconfig.json` 会把 `@motajs/*` 的类型图带进 `vue-tsc`,因此这些报错会出现在输出里;本计划只要求 `docs/` 路径 0 报错,并把引擎报错条数与摘要写进 POC-NOTES。 +- 当前已落盘的三个 docs 组件(`renderDemo.vue`、`demos/basicRender.vue`、`demos/uiSystem.vue`)里待修的点:`renderDemo.vue` 的 app 引用是无类型注解的 null 初始化(隐式 any,TS7034/TS7005);`basicRender.vue` 的 `count` / `color` ref 与 `onClick` 缺显式类型;`uiSystem.vue` 的 `active` / `stackSize` computed 与 `openUi` / `closeUi` 缺显式类型(`instance` 已是 `IUIInstance | null`,保持不变);三个文件的所有图形属性都是 camelCase。 + +**命名与落点候选(Task 0 必须整份交给用户确认,未确认前不得落盘):** + +| 项 | 方案 A(推荐) | 方案 B(备选) | +|----|---------------|---------------| +| 包装组件文件 | `docs/.vitepress/components/renderDemo.vue` | `docs/.vitepress/components/RenderCanvas.vue` | +| 组件名 | `RenderDemo` | `RenderCanvas` | +| 包装组件 props | `loader` / `width` / `height` / `scale` / `background` | `mount` / `logicalWidth` / `logicalHeight` / `pixelScale` / `backgroundColor` | +| 包装组件内部类型 | `RenderDemoProps` / `DemoLoader` / `DemoModule` | `RenderCanvasProps` / `CanvasLoader` / `CanvasModule` | +| demo 目录 | `docs/.vitepress/components/demos/` | `docs/.vitepress/components/demo/` | +| 渲染基础 demo | `basicRender.vue`(组件名 `BasicRenderDemo`) | `basic.vue`(`BasicDemo`) | +| UISystem demo | `uiSystem.vue`(组件名 `UISystemDemo`) | `ui.vue`(`UIDemo`) | +| POC 页面 | `docs/poc/render-demo.md` | `docs/poc/canvas.md` | +| 命名确认记录 | `.planning/phases/10-docs-restructure/10-42-POC-NOTES.md`(命名清单与确认标记直接写在这一份 POC 记录里) | 同左 | +| config 新增键 | `vite.resolve.alias` / `vite.resolve.dedupe` / `vue.template.compilerOptions.isCustomElement` / `vite.plugins`(追加 `vuejsx`) | 同左 | + +**推荐方案 A 的理由:** 组件名直接说明用途(渲染 demo 容器,而不是「canvas 封装」);`width` / `height` 与 `IMotaRendererConfig` 同名,减少「逻辑尺寸 / 像素尺寸」的心智映射;两个 demo 文件名分别对应覆盖范围(渲染基础 / UISystem);页面放 `docs/poc/` 明确临时性,不并入 D-02 的六大分区,也不进 nav / sidebar;命名确认并入 POC-NOTES,减少一个只记录一次的文件。 +**若用户改选方案 B 或自定义命名:** 执行时按确认结果整体替换本计划中出现的对应名称,自动化断言中的路径同步替换,其余结构不变。 + +**用户已确认的 POC 设计前提(不得更改):** +1. 承载方式 = 独立 demo 文件 + 包装组件;**fence 插件本次不做**(POC 通过后再评估)。 +2. 覆盖范围 = 渲染基础(container / g-* / text / 一个点击事件)+ UISystem(UIController + GameUI(或等效最小 UI)+ UIInstance 生命周期)。 +3. 命名与落点由本计划先给候选,执行时 **Task 0 汇报并等待「可以执行」**;计划里可以推荐,但关卡必须拦住。 +4. 运行期在客户端,但发布仍走 `vitepress build docs`(构建期预渲染),**SSR guard 不可省略**。 + +**ROADMAP / STATE 处理(最小方案,追加式 + 基线比对):** +- `ROADMAP.md`:只在 Phase 10 的 `Plans:` 列表末尾追加一条 `10-42-PLAN.md` 条目,并在列表后追加一句「增量说明」;**不改 Phase 10 的 `**Plans:**` 计数行(10-41 可能已把它改成 38/41)、不改 Goal、不改第 1..5 批描述、不碰 `10-33`..`10-41`**,保留 `10-06` / `10-11` / `10-12` 的未勾选状态。 +- `STATE.md`:只更新 frontmatter 的 `last_updated` / `last_activity` / `last_activity_desc`、`Current Position` 的 `Plan` / `Last activity`、`Progress:` 行(追加 10-42 一句)与 `### Roadmap Evolution`(追加一条);**不改 frontmatter 的 `progress.total_plans` / `progress.completed_plans`**(项目级累计值),不动既有 Decisions / Deferred Items / Blockers。 +- 两个文件都只用 `Edit` 做定点追加,**禁止整文件 `Write`**。 +- **不写死任何计数字面量**:Task 1 把「执行开始时的 ROADMAP `**Plans:**` 行原文」「STATE 的 `total_plans` / `completed_plans` 原值」记进 POC-NOTES 作为基线;Task 7 只断言这些基线**未被本次修改改动**,不断言等于某个固定数字。 + + + + + Task 0: 汇报 POC 计划与命名清单,等待「可以执行」 + .planning/phases/10-docs-restructure/10-CONTEXT.md(D-09、D-10、D-12、D-13、D-14)、.planning/phases/10-docs-restructure/10-41-PLAN.md(Task 0 汇报与关卡体例)、docs/.vitepress/config.ts、packages/render/src/core/utils.ts、packages/render/src/core/graphics.ts、packages/render/src/core/misc.ts、packages/render-vue/src/tag.ts、packages/render-vue/src/renderer.ts、packages/render/src/core/render.ts(构造 / size / setScale / onResize / destroy)、packages/system/src/ui/controller.ts + 向用户做四段汇报并停下等待(D-12):**要做什么**——在文档站里加一个最小、可回退的 POC,把自定义渲染器嵌进 VitePress 页面,用「渲染基础」与「UISystem」两个 demo 证明可运行、可交互、可卸载;不写正式文档、不做 fence 插件。**为什么**——先验证「文档内嵌可运行示例」在 SSR 预渲染站点里是否成立(`vitepress build docs` 会预渲染,而 `@motajs/render` 的模块顶层会碰 `document`),验证通过后再决定是否投入 fence 插件与正式渲染端文档批次。**大致怎么做**——Task 1 记录命名确认与基线(HEAD / ROADMAP 的 Phase 10 `**Plans:**` 行原文 / STATE 的 `total_plans` 与 `completed_plans` 原值),给 `docs/.vitepress/config.ts` 加 alias / dedupe / isCustomElement / vue-jsx,然后跑本计划第一次全量构建;Task 2 写 DOM 包装组件(`onMounted` 动态 import 引擎与 demo,`onUnmounted` 调 `app.unmount()` + `renderer.destroy()`,保持构造器默认根锚点,处理「导入未完成就卸载」的竞态);Task 3 / Task 4 写两个 demo;Task 5 写 `<ClientOnly>` 包裹的 POC 页面并跑 prettier + 全量构建 + Node 静态断言,把门禁结论记进 POC-NOTES;Task 6 由用户在浏览器人工验证;Task 7 在用户确认后写 SUMMARY 并做 ROADMAP / STATE 的追加式记录。**影响范围**——只改 `docs/.vitepress/config.ts`、新增 `docs/.vitepress/components/**`、新增 `docs/poc/render-demo.md`,以及 `.planning` 下两个记录文件(POC-NOTES / SUMMARY)与 ROADMAP / STATE 的追加;不动 `packages/`、`packages-user/`、`src/`、既有文档、nav / sidebar、发布流程。汇报时必须**完整给出**上文「命名与落点候选」表(方案 A 与方案 B 两套:包装组件文件名与组件名、props 名、内部类型名、demo 目录、两个 demo 文件名与组件名、页面路径、config 新增键),并给出**推荐方案 A 的理由**、以及「命名清单与确认标记直接写入 POC-NOTES、不另建独立命名确认文件」的说明;同时明确:**未确认前不得修改任何文件**;命名与落点逐项确认后方可落盘;本次修改方式为**派发子代理执行**;执行期间**不得调用 `opencode` 命令**;**不得修改任何既有 JSDoc / 注释**;本计划**不做 git commit**。然后停下等待用户明确回复「可以执行」四个字;其它回复视为未批准。 + 尚无产物;本任务只汇报计划内容与命名清单,供用户裁决。 + 确认:①是否同意做该 POC 及范围(渲染基础 + UISystem 两个 demo,不做 fence 插件);②选择方案 A / 方案 B / 自定义命名(逐项:包装组件名、props 名、内部类型名、demo 目录、两个 demo 文件名、页面路径、config 新增键);③是否接受「不 commit、不改源码、不进 nav / sidebar」。 + 回复「可以执行」以继续(可同时给出命名选择);其它回复视为未批准,本计划停在此处。 + 用户已回复「可以执行」;命名与落点已逐项确定(方案 A / 方案 B / 自定义),后续任务按确认结果执行。用户未回复「可以执行」前,不得创建或修改任何文件。汇报中已完整给出方案 A / 方案 B 与推荐理由。 + + 用户已回复「可以执行」,且命名清单已逐项确认(含最终采用的组件名、props 名、内部类型名、demo 目录与文件名、页面路径)。 + + 用户回复「可以执行」且命名清单已确定。 + + + + Task 1: 记录命名与基线 + docs 配置加法(config.ts / tsconfig.json)+ 首次构建与 docs 类型检查 + .planning/phases/10-docs-restructure/10-42-POC-NOTES.md, docs/.vitepress/config.ts, docs/tsconfig.json + 用户已在 Task 0 明确回复「可以执行」,且命名清单已逐项确认;docs 命令在仓库根目录执行(`pnpm docs:build` 由根 `package.json` 脚本驱动)。 + .planning/phases/10-docs-restructure/10-CONTEXT.md(D-12、D-13)、docs/.vitepress/config.ts(全文,注意 1-16 的 markdown / outDir、17-267 的 nav / sidebar、301-310 的 vite 段)、vite.config.ts:8-49(主站的 custom 数组、alias 生成方式)、.planning/ROADMAP.md(Phase 10 段落,抄写 `**Plans:**` 行原文)、.planning/STATE.md:1-40(frontmatter 的 `progress`)、.planning/config.json(security_enforcement / ASVS 级别) + 本任务做三件事,顺序不可颠倒。 + +**(a)新建 `.planning/phases/10-docs-restructure/10-42-POC-NOTES.md`**(CRLF、UTF-8;后续 Task 5 / Task 6 / Task 7 会向它追加内容,不得改名)。内容必须包含: +1. 记录日期。 +2. **完整命名清单**:逐项写最终采用值并注明「用户已确认」,覆盖包装组件文件名、组件名、五个 props 的名称 / 类型 / 默认值、内部类型名(`RenderDemoProps` / `DemoLoader` / `DemoModule` 或用户改名后的对应名)、demo 目录、两个 demo 文件名与组件名、POC 页面路径、config 新增键。 +3. 独占一行的 `用户确认:可以执行`,**紧接下一行**以 `用户原文:` 开头的用户原话(不要把原文接在标记同一行)。 +4. `基线 HEAD:` + `git rev-parse HEAD` 的输出(完整 sha)。另预留 `当前 HEAD:` 字段留空(由 Task 7 写入写 SUMMARY 时的 `git rev-parse HEAD`);两值**允许不同**——用户可能在 POC 期间并发提交,判定口径见 Task 7(只看本计划文件是否被 commit 带入,不看 HEAD 是否相等)。 +5. `ROADMAP 基线 Plans 行:` + **执行开始时** `.planning/ROADMAP.md` 里 Phase 10 的 `**Plans:** ...` 整行原文(照抄,不改写、不截断)。 +6. `STATE 基线 total_plans:` + `.planning/STATE.md` frontmatter `progress.total_plans` 的原值(纯数字)。 +7. `STATE 基线 completed_plans:` + `progress.completed_plans` 的原值(纯数字)。 +8. 一句说明:以上基线项(ROADMAP 计数行原文 + STATE 两个计数原值 + HEAD)是「本次修改不得改动」的比对基准,**不写死任何期望数字**(10-41 可能已把 Phase 10 的计数改成 38/41,本计划只保证自己不改它)。 + +**(b)在 `docs/.vitepress/config.ts` 内做加法**(不得删除、不得重写既有配置):①文件顶部新增一个 `custom` 常量数组与 `isCustomElement(tag)` 判断,数组内容与 `vite.config.ts:8-11` 逐项一致(`container` / `custom` / `text` / `image` / `shader` / `comment` / `custom-container` / `map-render` / `animate` / `damage` / `graphics` / `icon` / `winskin`),判断再加 `tag.startsWith('g-')`;②在 `defineConfig` 里新增顶层 `vue: { template: { compilerOptions: { isCustomElement } } }`(VitePress 会把它转给 `@vitejs/plugin-vue`);③在既有 `vite` 段内新增 `resolve: { alias: [...], dedupe: ['vue'] }`:alias 覆盖 `@motajs/<包名>` → `<repoRoot>/packages/<包名>/src` 与 `@user/<包名>` → `<repoRoot>/packages-user/<包名>/src`,用两条带捕获组的正则(`/^@motajs\/(.+)$/`、`/^@user\/(.+)$/`)+ 以正斜杠拼出的替换串(`<repoRoot>/packages/$1/src`),`repoRoot` 取 `process.cwd()`(与既有 `outDir` 的写法一致);④在 `vite.plugins` 里**追加** `vuejsx({ isCustomElement })`,保留 `MermaidPlugin()`;⑤保留 `base` / `outDir` / `markdown` / `themeConfig` / `locales` / `optimizeDeps` / `ssr` 原样。**不要**引入新依赖(`@vitejs/plugin-vue-jsx` 已在 devDependencies)、**不要**改 `// @ts-expect-error 类型错误` 等既有注释、**不要**把 `/poc` 加进 nav 或 sidebar。若新增插件后该 `@ts-expect-error` 变成「未使用」而导致类型报错,停下报告,不得删改注释。改完不做格式化(格式化统一在 Task 5)。 + +**(c)新建 `docs/tsconfig.json`**(CRLF、UTF-8;只服务 IDE 与 `vue-tsc -p docs/tsconfig.json`,**不并入** `pnpm check:type`,**不改** 根 `tsconfig.json` / `tsconfig.node.json` 的既有 `include`):`extends` 指向 `"../tsconfig.json"`;`compilerOptions` 里加 `baseUrl: ".."`、显式 `paths`(`"@motajs/*": ["./packages/*/src"]`、`"@user/*": ["./packages-user/*/src"]`)与 `ignoreDeprecations: "6.0"`(本机 TypeScript 6.0.3,`baseUrl` 已弃用,不加会有一串弃用告警);`include` 为 `["./.vitepress/**/*.ts", "./.vitepress/**/*.vue", "../src/types/vite-env.d.ts"]`。随后跑 `cmd /c "pnpm exec vue-tsc --noEmit -p docs/tsconfig.json"`:**输出里路径以 `docs/` 开头且含 `error TS` 的行必须为 0**;`packages/` / `packages-user/` / `src/` 的既有报错允许存在,但必须计入 POC-NOTES 的 `引擎既有类型错误:` 行(条数 + 一句摘要 + 注明「既有、非本轮引入、不得修」),并另记一行 `docs 类型检查:` 写本次结论。若出现 `docs/` 路径报错,先判断是否由本任务新增的 tsconfig 引起(典型是 config.ts 的 `// @ts-expect-error` 变成未使用):属既有 docs 问题时**停下报告**,不得改源码或删注释绕门禁。 + +**(d)本计划第一次全量构建**:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"`,把结论写进 POC-NOTES:`首次构建:green` 并在其下附命令与关键输出(`build complete` / dead link 行数)。**注意:本任务不再单独跑「改动前基线构建」**,第一次构建就发生在配置改动之后。若构建 red:先**只回退本任务 (b) 新增的行**再跑一次,以判断失败是否由本次改动引起;回退后仍 red 则**停止后续所有 Task**,把完整报错贴回对话并等用户指令;不得自行修复既有问题、不得把失败记成计划缺陷(AGENTS.md)。 + + $ErrorActionPreference='Stop'; $f='.planning/phases/10-docs-restructure/10-42-POC-NOTES.md'; if(-not(Test-Path -LiteralPath $f)){ Write-Error "MISSING $f" }; $p=[IO.File]::ReadAllText((Resolve-Path -LiteralPath $f)); if($p -notmatch "(?m)^用户确认:可以执行\s*$"){ Write-Error 'CONFIRM-MARKER' }; if($p -notmatch '(?m)^用户原文:'){ Write-Error 'CONFIRM-QUOTE' }; if($p -notmatch '命名清单'){ Write-Error 'NAMING-LIST' }; if($p -notmatch '基线 HEAD:[0-9a-f]{7,40}'){ Write-Error 'BASE-HEAD-MISSING' }; if($p -notmatch '(?m)^ROADMAP 基线 Plans 行:\S'){ Write-Error 'NO-BASE-PLANS' }; if($p -notmatch '(?m)^STATE 基线 total_plans:\s*\d+'){ Write-Error 'NO-BASE-TOTAL' }; if($p -notmatch '(?m)^STATE 基线 completed_plans:\s*\d+'){ Write-Error 'NO-BASE-COMPLETED' }; if($p -notmatch '首次构建:green'){ Write-Error 'FIRST-BUILD-NOT-GREEN' }; if($p -match "(?=0){console.error("CONFIG-POC-IN-NAV");process.exit(1)}if(!/@ts-expect-error/.test(s)){console.error("CONFIG-LOST-EXISTING-COMMENT");process.exit(1)}const t="docs/tsconfig.json";if(!fs.existsSync(t)){console.error("TSCONFIG-MISSING");process.exit(1)}const tj=fs.readFileSync(t,"utf8");const tm=["extends","baseUrl","paths","@motajs/*","@user/*","vite-env.d.ts",".vitepress/**/*.vue",".vitepress/**/*.ts","ignoreDeprecations"].filter(function(k){return tj.indexOf(k)<0});if(tm.length){console.error("TSCONFIG-MISSING-KEY:"+tm.join("|"));process.exit(1)}console.log("CONFIG-OK")'; $out1=node -e $js 2>&1; if($LASTEXITCODE -ne 0){ $out1 | Write-Output; Write-Error 'CONFIG-FAIL' }; $ErrorActionPreference='Continue'; $tc = cmd /c "pnpm exec vue-tsc --noEmit -p docs/tsconfig.json" 2>&1; $ErrorActionPreference='Stop'; $tcTxt=($tc | ForEach-Object { $_.ToString() }) -join "`n"; if($tcTxt -match 'TS5023|TS6046|TS5101|TS5102|TS18003|Cannot read file'){ $tc | Write-Output; Write-Error 'TSCONFIG-INVALID' }; $docsErr=@($tc | Where-Object { $x=$_.ToString(); ($x -match 'error TS') -and ($x -match '(^|\s)docs/') }); if($docsErr.Count -gt 0){ $tc | Write-Output; Write-Error 'DOCS-TYPE-ERROR' }; $engErr=@($tc | Where-Object { $x=$_.ToString(); ($x -match 'error TS') -and ($x -notmatch '(^|\s)docs/') }); Write-Output ("ENGINE-TYPE-ERRORS=" + $engErr.Count); $p4=[IO.File]::ReadAllText((Resolve-Path -LiteralPath $f)); if($p4 -notmatch '(?m)^docs 类型检查:\S'){ Write-Error 'NOTES-NO-DOCS-TYPECHECK' }; if($p4 -notmatch '(?m)^引擎既有类型错误:\S'){ Write-Error 'NOTES-NO-ENGINE-TYPE-ERRORS' }; $ErrorActionPreference='Continue'; $out = cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build" 2>&1; $ec=$LASTEXITCODE; $ErrorActionPreference='Stop'; if($ec -ne 0){ $out | Write-Output; Write-Error 'BUILD-FAIL' }; $txt=($out | ForEach-Object { $_.ToString() }) -join "`n"; if($txt -notmatch 'build complete'){ Write-Error 'NO-BUILD-COMPLETE' }; if($txt -match 'Dead link|dead link'){ Write-Error 'DEAD-LINK' }; if((($out1 | ForEach-Object { $_.ToString() }) -join "`n") -notmatch 'CONFIG-OK'){ Write-Error 'NO-CONFIG-OK' }; Write-Output 'T1-OK' + non-zero exit, or output contains "MISSING", "CONFIRM-MARKER", "CONFIRM-QUOTE", "NAMING-LIST", "BASE-HEAD-MISSING", "NO-BASE-PLANS", "NO-BASE-TOTAL", "NO-BASE-COMPLETED", "FIRST-BUILD-NOT-GREEN", "NOTES-LF-ONLY", "TSCONFIG-MISSING", "TSCONFIG-MISSING-KEY", "TSCONFIG-INVALID", "DOCS-TYPE-ERROR", "NOTES-NO-DOCS-TYPECHECK", "NOTES-NO-ENGINE-TYPE-ERRORS", "CONFIG-MISSING", "CONFIG-BASE", "CONFIG-OUTDIR", "CONFIG-NAV-SIDEBAR", "CONFIG-ISCUSTOM-LOW", "CONFIG-NO-VUEJSX-IMPORT", "CONFIG-DEDUPE-VUE", "CONFIG-NO-ALIAS", "CONFIG-NO-REPLACEMENT", "CONFIG-POC-IN-NAV", "CONFIG-LOST-EXISTING-COMMENT", "CONFIG-FAIL", "BUILD-FAIL", "NO-BUILD-COMPLETE", or "DEAD-LINK"; or output lacks "T1-OK" + + POC-NOTES 存在,含独占一行的 `用户确认:可以执行`、下一行的 `用户原文:`、完整命名清单、基线 HEAD、`ROADMAP 基线 Plans 行` 原文、`STATE 基线 total_plans` / `completed_plans` 原值,以及 `首次构建:green`;config.ts 含 `@motajs/` 与 `@user/` 别名(指向 `packages/*/src`、`packages-user/*/src`)、`dedupe: ['vue']`、`isCustomElement`(custom 数组 + `g-`,SFC 与 vue-jsx 两处使用)、`@vitejs/plugin-vue-jsx` 导入与 `vuejsx` 插件;自动断言机械校验 `base: '/_docs/'`、`outDir` 仍含 `public` 与 `_docs`、`nav:` / `sidebar:` 键未缺失;`MermaidPlugin` / `base` / `outDir` / `optimizeDeps` / nav / sidebar / 既有注释未被改动;`/poc` 未进导航;构建 green、无 dead link;未安装任何新依赖。若首次构建为 red,执行已按规则回退定位并向用户报告。`docs/tsconfig.json` 存在且含 `extends`、`baseUrl`、显式 `paths`(`@motajs/*` 与 `@user/*`)、`ignoreDeprecations: "6.0"`、`include` 含 `.vitepress/**/*.ts` / `.vitepress/**/*.vue` / `../src/types/vite-env.d.ts`;`vue-tsc --noEmit -p docs/tsconfig.json` 输出中路径以 `docs/` 开头且含 `error TS` 的行为 0,`packages/` 等既有报错条数与摘要已记入 POC-NOTES 的 `引擎既有类型错误:` 行,另有 `docs 类型检查:` 结论行;根 `tsconfig.json` / `tsconfig.node.json` 的既有 `include` 未被改动,`pnpm check:type` 的范围未变。 + 命名与基线记录落盘,config.ts 完成加法式扩展,`docs/tsconfig.json` 建立且 docs 路径类型检查为 0 报错、引擎既有报错已记账,首次全量构建 green 且已记录;未修改任何源码或既有文档。 + + + + Task 2: DOM 包装组件(动态 import + 竞态保护 + 卸载清理 + 尺寸交给引擎) + docs/.vitepress/components/renderDemo.vue + 10-42-POC-NOTES.md 含独占一行的「用户确认:可以执行」(每个 auto 任务的自动门禁都会复检该标记),且包装组件文件名 / 组件名 / props 名已确认;Task 1 的首次全量构建为 green。 + packages/render-vue/src/renderer.ts:26-113(createRendererFor 与返回的 createApp)、packages/render/src/core/render.ts:123-146、264-285、538-539、852-856、packages/render/src/core/canvas2d.ts:32-74、packages/render/src/core/types.ts:431-442、packages/render/src/core/item.ts:196-227、860-868、packages-user/client-modules/src/render/renderer.ts:35-41(真实实例化方式,注意它不调整根锚点)、docs/tsconfig.json(docs 局部 TS 项目,`AppHandle` 与显式类型注解的类型上下文) + 按 Task 0 确认的名称新建包装组件(推荐 `docs/.vitepress/components/renderDemo.vue`,组件名 `RenderDemo`,CRLF)。职责仅限「DOM 壳 + 生命周期」。 + +**类型(遵守 `dev.md:54`、`dev.md:97-98`)**:用普通 import 写 `import { Component } from 'vue'`(**禁止** `import type`);定义 `interface DemoModule { default: Component }`、`type DemoLoader = () => Promise<DemoModule>`、`interface RenderDemoProps { loader: DemoLoader; width?: number; height?: number; scale?: number; background?: string }`;再定义本地 `interface AppHandle { unmount(): void }`(用户指定的名字;只声明清理所需的 `unmount`,避免为类型引入 `App` 值导入、也不用 `import type`;`mount` 由下面的局部 `app` 调用,`AppHandle` 不需要它)。props 用 `withDefaults(defineProps<RenderDemoProps>(), { width: 480, height: 320, scale: 1, background: '#101418' })` 取值;**不得**使用内联对象类型、不得使用 `as`、不得使用 `import type`。 + +**模板**:只渲染一个包裹 `div`(用 `background` prop 上色,加 `overflow: hidden`)与其中的 `<canvas ref="canvasRef">`。**不要**给 canvas 写 `style.width/height`,也不要写 `width` / `height` 属性——尺寸由 `MotaRenderer.size()` 与 `onResize()` 负责(`render.ts:272-285`;默认 `scale = 1` 时引擎把 CSS 尺寸写成逻辑尺寸)。**不得**在模板里使用任何自定义渲染标签。 + +**脚本骨架**:`const canvasRef = ref<HTMLCanvasElement | null>(null)`;在 `<script setup>` 作用域内声明两个局部变量 `let disposed = false` 与 `let dispose: (() => void) | null = null`(每个组件实例各一份,**不要**提到模块顶层共享)。**整个文件禁止** `from '@motajs…'` / `from '@user…'` 静态 import(含 prettier 折行的具名导入);`@motajs/render` / `@motajs/render-vue` / `@motajs/system` / `@user/*` 只允许用 `import(...)` 动态导入(由 verify 强制)。 + +`onMounted`(async)里依次 `await import('@motajs/render')`(取 `MotaRenderer`)、`await import('@motajs/render-vue')`(取 `createRendererFor`)、`await props.loader()`(取 `default` 组件)。**三个 await 之后的第一件事**必须是守卫:`if (disposed || !canvasRef.value) return;` —— 组件卸载后 `canvasRef.value` 为 null,此时若继续 `new MotaRenderer({ canvas: null })` 会走 `logger.error(19)` 的半成品分支(`render.ts:126-130`:`excitation` 未赋值),随后任何 `destroy()` 都会 TypeError(`render.ts:852-856`)。守卫通过后再取局部 `const canvas = canvasRef.value`,执行 `new MotaRenderer({ canvas, width, height })`;构造器在拿不到 canvas 时只是 `return` 并返回**已创建的实例**(`excitation` 未赋值),不存在「有 / 无返回值」的差异,因此上面的 canvas 守卫才是唯一防线——进入构造即保证 canvas 真实存在,无需任何「构造成功」标记;随后仅当 `scale !== 1` 时调用 `renderer.setScale(scale)`。清理依据只有 `dispose` 的赋值时机,见下段。 + +**挂载与清理路径(不使用异常捕获语法)**:`renderer`、`app` 与 `appRef` 都是 `onMounted` 内的局部引用(每个实例各一份)。可清理引用必须写成 `let appRef: AppHandle | null = null` —— **必须带显式类型注解**,无注解的 null 初始化会被 TypeScript 推断成 any(赋值前后各一次,触发 TS7034 / TS7005),**禁用** `as` 与 `import type`。顺序固定且不可调换:① 用局部 `canvas` 构造 `renderer`;② **在创建 app 之前**赋值 `dispose = () => { if (appRef) { appRef.unmount(); } renderer.destroy(); }`,使渲染器一存在就有清理入口;③ `const app = createRendererFor(renderer).createApp(demoComponent)`(局部常量,类型由返回值推导,`mount` 由它调用);④ `appRef = app`(`App` 结构上满足 `AppHandle`);⑤ `app.mount(renderer)`。任何一步抛错都**原样向上冒泡、不得吞错**,清理由 setup 阶段就已注册的 `onUnmounted` → `dispose` 统一完成(`appRef` 仍为 null 时只执行 `renderer.destroy()`,停 RAF、abort document 监听),因此 mount 失败也不会泄漏 RAF 与 document 监听。三个确认要点:`dispose` 必须在 `app.mount(renderer)` **之前**赋值;`onUnmounted` 必须在 setup 阶段注册,与 mount 是否成功无关;包装组件里不得出现任何异常捕获语句。 + +**根锚点**:不要调用任何设置根锚点的接口。构造器已设好默认 0.5/0.5(`render.ts:140`),配合 `size()` 的根中心平移(`render.ts:281-285`)正好是左上原点;改动它会让画面整体偏移而命中测试仍按原坐标判定(`render.ts:538-539`、`item.ts:860-868`),导致 `@click` 不触发。 + +`onUnmounted`:先 `disposed = true`,再 `dispose?.()`,最后 `dispose = null`;顺序必须是 `app.unmount()` → `renderer.destroy()`(前者拆组件树触发 `remove` → `el.destroy()`,后者停 RAF 并 abort document 监听)。`onUnmounted` 在 setup 阶段注册,不依赖挂载成功:若 `createApp` / `appRef.mount(renderer)` 抛错,`dispose` 已在渲染器创建后赋值,卸载时仍会执行 `renderer.destroy()`(`appRef` 为空则跳过 unmount)。清理后 `dispose` 置 null,**不要**在 `onUnmounted` 里再补一次 `destroy()`(避免重复销毁)。若 `app.unmount()` 在 mount 未完成时抛错,同一次 `dispose` 中的 `renderer.destroy()` 不会执行;本计划接受这一残留风险,不引入异常捕获语法兜底。 + +不实现 `loader` 变更后的重挂载(一个包装实例只服务一个 demo,页面按 demo 各用一个实例),并在文件内新增一行注释说明该限制。`renderer` 与 `app` 是 `onMounted` 内的局部常量,可清理引用是 `let appRef: AppHandle | null = null`(显式注解,见上);`renderer` 的类型由 `new MotaRenderer(...)` 推导、`app` 的类型由 `createRendererFor(...).createApp(...)` 推导,不写顶层引擎类型 import;若确实需要类型别名,只能用类型级 `typeof import('@motajs/render')` 写法,**不要**用 `import type` 或 `as`。新增注释遵循 `dev.md`,不得修改任何既有注释(本文件为新增文件)。 + + $ErrorActionPreference='Stop'; $n='.planning/phases/10-docs-restructure/10-42-POC-NOTES.md'; if(-not(Test-Path -LiteralPath $n)){ Write-Error "MISSING $n" }; $np=[IO.File]::ReadAllText((Resolve-Path -LiteralPath $n)); if($np -notmatch '(?m)^用户确认:可以执行\s*$'){ Write-Error 'NO-CONFIRM-MARKER' }; $js='const fs=require("fs");const f="docs/.vitepress/components/renderDemo.vue";if(!fs.existsSync(f)){console.error("WRAPPER-MISSING");process.exit(1)}const s=fs.readFileSync(f,"utf8");if(/from\s+[\x22\x27]@(?:motajs|user)\//.test(s.replace(/import\s*\([^)]*\)/g,""))){console.error("WRAPPER-STATIC-ENGINE-IMPORT");process.exit(1)}const need=["import(","onMounted","onUnmounted","unmount(","destroy(","\x3ccanvas","canvasRef","width","height","loader","@motajs/render","@motajs/render-vue","interface DemoModule","type DemoLoader","interface RenderDemoProps","disposed","appRef"];const miss=need.filter(function(k){return s.indexOf(k)<0});if(miss.length){console.error("WRAPPER-MISSING-KEY:"+miss.join("|"));process.exit(1)}if((s.match(/import\(/g)||[]).length<2){console.error("WRAPPER-DYNAMIC-LOW");process.exit(1)}if(/\bsetAnchor\s*\(/.test(s)){console.error("WRAPPER-ANCHOR-OVERRIDE");process.exit(1)}if(!/import[^;\n]*from\s+[\x22\x27]vue[\x22\x27]/.test(s)){console.error("WRAPPER-NO-VUE-IMPORT");process.exit(1)}if(/\bimport\s+type\b/.test(s)){console.error("WRAPPER-IMPORT-TYPE");process.exit(1)}if(!/disposed/.test(s)||!(/canvasRef\.value/).test(s)){console.error("WRAPPER-NO-RACE-GUARD");process.exit(1)}if(!/\bdispose\s*=/.test(s)){console.error("WRAPPER-NO-DISPOSE-PATH");process.exit(1)}if(!/\r\n/.test(s)||/[^\r]\n/.test(s)){console.error("WRAPPER-CRLF");process.exit(1)}if(!/interface\s+AppHandle\s*\{[^}]*unmount\s*\(\s*\)\s*:\s*void/.test(s)){console.error("WRAPPER-NO-APPHANDLE");process.exit(1)}if(!/let\s+appRef\s*:\s*AppHandle\s*\|\s*null/.test(s)){console.error("WRAPPER-APPREF-UNTYPED");process.exit(1)}if(/let\s+appRef\s*=\s*null/.test(s)){console.error("WRAPPER-APPREF-BARE");process.exit(1)}console.log("WRAPPER-OK")'; $out1=node -e $js 2>&1; if($LASTEXITCODE -ne 0){ $out1 | Write-Output; Write-Error 'WRAPPER-FAIL' }; $ErrorActionPreference='Continue'; $out = cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build" 2>&1; $ec=$LASTEXITCODE; $ErrorActionPreference='Stop'; if($ec -ne 0){ $out | Write-Output; Write-Error 'BUILD-FAIL' }; $txt=($out | ForEach-Object { $_.ToString() }) -join "`n"; if($txt -notmatch 'build complete'){ Write-Error 'NO-BUILD-COMPLETE' }; if($txt -match 'Dead link|dead link'){ Write-Error 'DEAD-LINK' }; if((($out1 | ForEach-Object { $_.ToString() }) -join "`n") -notmatch 'WRAPPER-OK'){ Write-Error 'NO-WRAPPER-OK' }; Write-Output 'T2-OK' + non-zero exit, or output contains "MISSING", "NO-CONFIRM-MARKER", "WRAPPER-MISSING", "WRAPPER-STATIC-ENGINE-IMPORT", "WRAPPER-MISSING-KEY", "WRAPPER-DYNAMIC-LOW", "WRAPPER-ANCHOR-OVERRIDE", "WRAPPER-NO-VUE-IMPORT", "WRAPPER-IMPORT-TYPE", "WRAPPER-NO-RACE-GUARD", "WRAPPER-NO-DISPOSE-PATH", "WRAPPER-CRLF", "WRAPPER-NO-APPHANDLE", "WRAPPER-APPREF-UNTYPED", "WRAPPER-APPREF-BARE", "WRAPPER-FAIL", "BUILD-FAIL", "NO-BUILD-COMPLETE", "DEAD-LINK", or "NO-WRAPPER-OK"; or output lacks "T2-OK" + + 包装组件只含 DOM 壳;`@motajs/` / `@user/` 只出现在 `import(...)` 动态 import 中(任何 `from '@motajs…'` / `from '@user…'` 都判失败,含 prettier 折行的具名导入),引擎与 demo 均在 `onMounted` 内动态 import;`await` 之后先判 `disposed` 与 `canvasRef.value` 再取 canvas 构造渲染器;渲染器与 app 均为局部引用;`dispose` 在渲染器创建后、`app.mount()` 之前赋值,内部先判 `appRef` 再 `app.unmount()`,随后 `renderer.destroy()`;卸载统一由 setup 阶段注册的 `onUnmounted` 完成,mount 失败也走同一路径;清理路径不含任何异常捕获语法,canvas 守卫保证不会对未拿到 canvas 的实例做销毁;卸载按 `app.unmount()` → `renderer.destroy()`;**未调用任何根锚点设置接口**(保持构造器默认 0.5/0.5 即左上原点);canvas 不写内联宽高(尺寸交给引擎);props 用 `DemoModule` / `DemoLoader` / `RenderDemoProps` 具名类型且无内联对象类型;另定义本地 `interface AppHandle { unmount(): void }`,可清理引用带显式注解 `AppHandle | null`(自动断言同时校验注解形态与「不存在无注解的 null 初始化」),`mount` 由局部 `app` 调用;无 `import type`(自动断言)、无非必要 `as`(人工复核);CRLF;构建 green。 + 包装组件可创建并挂载渲染器、可安全卸载(含竞态保护),且不破坏 SSR 构建。 + + + + Task 3: 渲染基础 demo(container / g-* / text / 点击事件) + docs/.vitepress/components/demos/basicRender.vue + 10-42-POC-NOTES.md 含独占一行的「用户确认:可以执行」(每个 auto 任务的自动门禁都会复检该标记);demo 目录与文件名已确认;Task 2 的包装组件已存在。 + packages/render-vue/src/props.ts:24-160(位置 / 图形 / 文字属性)、packages/render/src/core/graphics.ts:37-49(默认填充)、packages/render/src/core/misc.ts:10-32(文字默认填充色)、packages/render-vue/src/tag.ts:44-88(已注册标签)、packages/render/src/core/item.ts:62(元素默认 anchor 0)、196-227(绘制原点)、934-942(onXxx → 事件名) + 按确认名称新建渲染基础 demo(推荐 `docs/.vitepress/components/demos/basicRender.vue`,组件名 `BasicRenderDemo`,CRLF)。要求:①根节点是 `<container>` 并给定逻辑宽高(与包装组件传入的一致,推荐 480×320);②至少一个 `<g-rect>` 与一个 `<g-circle>`(圆需 `radius`;图形用 `fillStyle` / 可选 `strokeStyle`,默认填充为浅灰,需显式给色);③至少一个 `<text>`(`text` 属性 + `fillStyle`,默认白色,配深色背景);④一个**点击事件**:在某个图形上写 `@click`,点击后改变一个 `ref` 计数与图形颜色,并用一个**字符串表达式**把计数显示到 `<text>`(证明事件与响应式都通):写成 `:text="'点击次数:' + count"` 这种「字符串字面量 + 拼接」形式;**不得**把裸标识符直接交给 `:text`(例如只写一个变量名),因为 `text` 会经 `assertType(nextValue, 'string', key)` 校验(`core/misc.ts:122-131`),非字符串即 `logger.error(21)` 并丢弃写入,与 Task 6 ⑤「控制台无 21」冲突;⑤坐标以左上角为原点——包装组件保持构造器默认根锚点,`size()` 的根中心平移与元素自身 anchor 0 正好抵消,所以 `x` / `y` 就是左上角坐标;全部使用 `x` / `y` / `width` / `height` 等渲染属性,**不得**使用 `style` / `class` / `v-show` / `v-html` / `Teleport` / `Transition` / DOM 查询(`patchProp` 会忽略 `style` / `class`);⑥不使用 `key`(顺序固定、无状态列表);⑦`<script setup>` 内只做状态与事件;**必须**从 vue 显式导入本 demo 实际用到的符号——即 `import { ref } from 'vue'`(本 demo 只用 `ref`,不要多导未使用符号),`ref` 用于计数与颜色两个状态;两个状态都必须带显式泛型参数——计数写成 `const count = ref(0)`、颜色写成 `const color = ref('#2f6fed')`;点击处理函数必须带显式返回类型——`function onClick(): void`;这些显式类型不改变运行期行为,只为让 docs 组件在 `docs/tsconfig.json` 下不再依赖隐式推断;**不得**为消 lint 告警把属性名改成连字符形式(`fillStyle` / `strokeStyle` / `lineWidth` 一律保持 camelCase,连字符告警由 Task 5 的 ESLint docs override 处理);此处「不 import 引擎包」仅指**不静态 import** `@motajs/*` / `@user/*`(该文件由包装组件动态 import),Vue 的 `ref` 仍必须 import,否则运行期会 `ReferenceError`;文件保持最小化更便于后续复用;⑧文件 CRLF,注释符合 `dev.md`。不得在 demo 内创建 renderer 或 canvas。 + + $ErrorActionPreference='Stop'; $n='.planning/phases/10-docs-restructure/10-42-POC-NOTES.md'; if(-not(Test-Path -LiteralPath $n)){ Write-Error "MISSING $n" }; $np=[IO.File]::ReadAllText((Resolve-Path -LiteralPath $n)); if($np -notmatch '(?m)^用户确认:可以执行\s*$'){ Write-Error 'NO-CONFIRM-MARKER' }; $js='const fs=require("fs");const f="docs/.vitepress/components/demos/basicRender.vue";if(!fs.existsSync(f)){console.error("BASIC-MISSING");process.exit(1)}const s=fs.readFileSync(f,"utf8");const need=["\x3ccontainer","\x3cg-rect","\x3cg-circle","\x3ctext",":text","\x3cscript","@click","ref"];const miss=need.filter(function(k){return s.indexOf(k)<0});if(miss.length){console.error("BASIC-MISSING-KEY:"+miss.join("|"));process.exit(1)}if(!/import\s*\{[^}]*\bref\b[^}]*\}\s*from\s*[\x22\x27]vue[\x22\x27]/.test(s)){console.error("BASIC-NO-VUE-IMPORT");process.exit(1)}if(!/fillStyle/.test(s)){console.error("BASIC-NO-FILL");process.exit(1)}if(/:text\s*=\s*"\s*(?:[A-Za-z_$][\w$]*|\d+(?:\.\d+)?)\s*"/.test(s)){console.error("BASIC-TEXT-NON-STRING");process.exit(1)}if(/style=|class=|v-show|v-html|Teleport|Transition/.test(s)){console.error("BASIC-DOM-FEATURE");process.exit(1)}if(!/\r\n/.test(s)||/[^\r]\n/.test(s)){console.error("BASIC-CRLF");process.exit(1)}if(!/ref\s*<\s*number\s*>\(\s*0\s*\)/.test(s)){console.error("BASIC-COUNT-UNTYPED");process.exit(1)}if(!/ref\s*<\s*string\s*>\(\s*[\x27]#[0-9a-fA-F]{6}[\x27]\s*\)/.test(s)){console.error("BASIC-COLOR-UNTYPED");process.exit(1)}if(!/function\s+onClick\s*\(\s*\)\s*:\s*void/.test(s)){console.error("BASIC-ONCLICK-NO-RETURN-TYPE");process.exit(1)}console.log("BASIC-OK")'; $out1=node -e $js 2>&1; if($LASTEXITCODE -ne 0){ $out1 | Write-Output; Write-Error 'BASIC-FAIL' }; $ErrorActionPreference='Continue'; $out = cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build" 2>&1; $ec=$LASTEXITCODE; $ErrorActionPreference='Stop'; if($ec -ne 0){ $out | Write-Output; Write-Error 'BUILD-FAIL' }; $txt=($out | ForEach-Object { $_.ToString() }) -join "`n"; if($txt -notmatch 'build complete'){ Write-Error 'NO-BUILD-COMPLETE' }; if($txt -match 'Dead link|dead link'){ Write-Error 'DEAD-LINK' }; if((($out1 | ForEach-Object { $_.ToString() }) -join "`n") -notmatch 'BASIC-OK'){ Write-Error 'NO-BASIC-OK' }; Write-Output 'T3-OK' + non-zero exit, or output contains "MISSING", "NO-CONFIRM-MARKER", "BASIC-MISSING", "BASIC-MISSING-KEY", "BASIC-NO-FILL", "BASIC-NO-VUE-IMPORT", "BASIC-TEXT-NON-STRING", "BASIC-DOM-FEATURE", "BASIC-CRLF", "BASIC-COUNT-UNTYPED", "BASIC-COLOR-UNTYPED", "BASIC-ONCLICK-NO-RETURN-TYPE", "BASIC-FAIL", "BUILD-FAIL", "NO-BUILD-COMPLETE", "DEAD-LINK", or "NO-BASIC-OK"; or output lacks "T3-OK" + + demo 从 vue 显式导入所用符号(`import { ref } from 'vue'`,自动断言校验),且不含任何 `@motajs/*` / `@user/*` 静态 import;两个状态带显式泛型(`ref(0)` / `ref('#2f6fed')`)且点击函数为 `function onClick(): void`(自动断言校验),属性名保持 camelCase;demo 含 container + g-rect + g-circle + text + 一个 `@click`;点击后计数与颜色变化并在 text 中可见,计数通过 `'点击次数:' + count` 这类字符串表达式显示,所有 `:text` 绑定在运行期均为 string(不触发 `logger.error(21)`);只用渲染属性、无 DOM 类功能;CRLF;构建 green 且标签不会因 `resolveComponent` 失败。 + 渲染基础 demo 完成,能覆盖容器、图形、文字与点击事件。 + + + + Task 4: UISystem demo(UIController + GameUI + UIInstance 生命周期) + docs/.vitepress/components/demos/uiSystem.vue + 10-42-POC-NOTES.md 含独占一行的「用户确认:可以执行」(每个 auto 任务的自动门禁都会复检该标记);Task 2 的包装组件已存在。 + packages/system/src/ui/controller.ts:26-71、101-103、121-154、packages/system/src/ui/ui.ts:3-12、packages/system/src/ui/instance.ts:13-40、packages/system/src/ui/container.tsx:14-47、packages/system/src/ui/types.ts:27-40、118-121、packages-user/client-modules/src/ui/func.ts:10-27(真实 setPropsBaseCreator 形态)、packages-user/client-modules/src/client.ts:101-105、packages/render/src/core/misc.ts:122-131(text 类型校验) + 按确认名称新建 UISystem demo(推荐 `docs/.vitepress/components/demos/uiSystem.vue`,组件名 `UISystemDemo`,CRLF)。要求:**导入必须显式且与所用符号一一对应**——`import { computed, defineComponent, h, onUnmounted } from 'vue'` 与 `import { UIController, GameUI, IUIDefaultPropsBase, IUIInstance } from '@motajs/system'`(`IUIDefaultPropsBase` / `IUIInstance` 只作类型,按 `dev.md:54` 仍用普通 import,禁止 `import type`;本 demo 由包装组件动态 import,因此可以静态 import 引擎包)。漏写任何一个实际用到的符号都会在运行期 `ReferenceError`,构建不会替你发现,所以导入清单必须与下文出现的 `computed` / `defineComponent` / `h` / `onUnmounted` / `UIController` / `GameUI` 一一对应。①用 `new GameUI(name, component)` 定义一个**最小 UI 组件**:因为 `files_modified` 里没有第二个 SFC,且 docs 侧未启用运行时模板编译器,该组件**必须用渲染函数构造**——`defineComponent` + 返回 `h('container', ...)`,内部用 `h('g-rect', ...)` / `h('text', ...)` 摆放标题文字与一个关闭按钮,**不得**使用运行时 `template:` 字符串(构建产物里会失败);本 demo 自身的 `<template>` 仍用 `container` / `g-rect` / `text` 摆放「打开 UI」「关闭 UI」两个按钮与两行状态文字;②在 setup 里 `new UIController<IUIDefaultPropsBase>()`(`IUIDefaultPropsBase` 按 `dev.md:54` 用普通 import,不用 `import type`),并**必须**调用 `setPropsBaseCreator((instance, controller) => ({ instance, controller }))`,否则渲染实例时会触发 `logger.error(74)`(见 controller.ts:63-71);③把 `controller.render()` 的返回值挂进渲染树:用一个本地定义的 setup 组件(返回 `controller.render()`)在模板里以大写组件名使用,**不要**直接在模板里调用 `controller.render()`;④提供「打开 UI」与「关闭 UI」两个可点击图形:在 setup 作用域声明 `let instance: IUIInstance | null = null`(`IUIInstance` 从 `@motajs/system` 普通 import,泛型有默认值,见 `types.ts:238-241`;禁止 `as` / `import type`),打开时写 `instance = controller.open(ui, {})`(`open` 返回 `IUIInstance`,见 `controller.ts:121-132`),关闭时写 `if (instance) { controller.close(instance); instance = null; }`(`close` 接收实例对象,见 `controller.ts:134-144`);两端必须复用**同一个** `instance` 变量,**不得**让 `close` 的实参悬空、另造新实例,也不得在点击回调内用 `const` 就地声明后再去关闭;`instance` 必须保持显式注解 `let instance: IUIInstance | null = null`,两个处理函数必须带显式返回类型——`function openUi(): void` 与 `function closeUi(): void`;所有图形属性保持 camelCase(`fillStyle` / `strokeStyle` / `lineWidth`),**不得**为消 lint 告警改成连字符形式(连字符告警由 Task 5 的 ESLint docs override 处理);⑤在页面上用两个 `<text>` 分别显示栈长度与打开状态:**先用 `computed` 解包 Ref**——`const active = computed(() => controller.active.value)`、`const stackSize = computed(() => controller.stack.length)`(两个 `computed` 必须带显式泛型参数,避免 docs 组件依赖隐式推断;解包语义与不加泛型完全一致),再把这两个 computed **以字符串表达式**绑定到 `:text`:栈长度写成 `:text="'栈长度:' + stackSize"`,打开状态写成 `:text="active ? '已打开' : '未打开'"`。**不得**把 computed 裸值直接交给 `:text`(`stackSize` 是 number、`active` 是 boolean)——`assertType(nextValue, 'string', key)` 只接受 string(`misc.ts:122-131`),非字符串会 `logger.error(21)` 并丢弃写入,与 Task 6 ⑤「控制台无 21」冲突。补充事实:`controller.active` 是裸 `Ref<boolean>`(controller.ts:33)、`controller.stack` 是 `shallowReactive([])`(controller.ts:29),而 `UIController` 本身不是 reactive——所以必须先 `computed` 解包;直接写 `controller.active` 得到的是 RefImpl。⑥`onUnmounted` 调 `controller.closeAll()`,保证离开页面时实例全部出栈;⑦最小 UI 组件可以声明 `controller` / `instance` 两个 props,也可以不声明——未声明时它们会作为 attrs 落到根元素,被 `patchProp` 对未知 key 静默忽略(`core/item.ts:1111-1118`),**不得**依赖这两个值做逻辑;⑧不使用 DOM 类功能(`style` / `class` / `Teleport` / `Transition` / `v-html` / `v-show` / DOM 查询);⑨CRLF;注释符合 `dev.md`;不 import `@user/client-base`、不使用 `client-base` 的素材 / 组件(它们依赖 `core.*` 全局,见 `client-base/src/elements/cache.ts:10`)。该文件由包装组件动态 import,可以静态 import `@motajs/system`。 + + $ErrorActionPreference='Stop'; $n='.planning/phases/10-docs-restructure/10-42-POC-NOTES.md'; if(-not(Test-Path -LiteralPath $n)){ Write-Error "MISSING $n" }; $np=[IO.File]::ReadAllText((Resolve-Path -LiteralPath $n)); if($np -notmatch '(?m)^用户确认:可以执行\s*$'){ Write-Error 'NO-CONFIRM-MARKER' }; $js='const fs=require("fs");const f="docs/.vitepress/components/demos/uiSystem.vue";if(!fs.existsSync(f)){console.error("UI-MISSING");process.exit(1)}const s=fs.readFileSync(f,"utf8");const need=["@motajs/system","UIController","GameUI","setPropsBaseCreator",".open(",".close(","closeAll","render(","onUnmounted","defineComponent","h(","\x3ccontainer","\x3ctext",":text","computed<","controller.active.value","controller.stack.length"];const miss=need.filter(function(k){return s.indexOf(k)<0});if(miss.length){console.error("UI-MISSING-KEY:"+miss.join("|"));process.exit(1)}const vm=(s.match(/import\s*\{[^}]*\}\s*from\s*[\x22\x27]vue[\x22\x27]/)||[])[0]||"";if(!vm){console.error("UI-NO-VUE-IMPORT");process.exit(1)}if(!/\bcomputed\b/.test(vm)||!/\bdefineComponent\b/.test(vm)||!/\bonUnmounted\b/.test(vm)||!/[{\s,]h[,\s}]/.test(vm)){console.error("UI-VUE-IMPORT-SYMBOLS");process.exit(1)}if(!/import\s*\{[^}]*\bUIController\b[^}]*\}\s*from\s*[\x22\x27]@motajs\/system[\x22\x27]/.test(s)||!/import\s*\{[^}]*\bGameUI\b[^}]*\}\s*from\s*[\x22\x27]@motajs\/system[\x22\x27]/.test(s)){console.error("UI-NO-SYSTEM-IMPORT");process.exit(1)}if(/:text\s*=\s*"controller\.active"/.test(s)){console.error("UI-ACTIVE-REF-BOUND");process.exit(1)}if(/(^|\n)\s*:text\s*=\s*"[^"]*controller\.active\s*"/.test(s)){console.error("UI-ACTIVE-REF-BOUND");process.exit(1)}if(/template\s*:/.test(s)){console.error("UI-RUNTIME-TEMPLATE");process.exit(1)}if(/:text\s*=\s*"\s*(?:[A-Za-z_$][\w$]*|\d+(?:\.\d+)?)\s*"/.test(s)){console.error("UI-TEXT-NON-STRING");process.exit(1)}if(/style=|class=|v-show|v-html|Teleport|Transition/.test(s)){console.error("UI-DOM-FEATURE");process.exit(1)}if(/@user\/client-base/.test(s)){console.error("UI-CLIENT-BASE-USED");process.exit(1)}if(!/\r\n/.test(s)||/[^\r]\n/.test(s)){console.error("UI-CRLF");process.exit(1)}if(!/computed\s*<\s*boolean\s*>\(\s*\(\s*\)\s*=>\s*controller\.active\.value\s*\)/.test(s)){console.error("UI-ACTIVE-UNTYPED");process.exit(1)}if(!/computed\s*<\s*number\s*>\(\s*\(\s*\)\s*=>\s*controller\.stack\.length\s*\)/.test(s)){console.error("UI-STACKSIZE-UNTYPED");process.exit(1)}if(!/function\s+openUi\s*\(\s*\)\s*:\s*void/.test(s)||!/function\s+closeUi\s*\(\s*\)\s*:\s*void/.test(s)){console.error("UI-CLICK-NO-RETURN-TYPE");process.exit(1)}if(!/let\s+instance\s*:\s*IUIInstance\s*\|\s*null/.test(s)){console.error("UI-INSTANCE-UNTYPED");process.exit(1)}console.log("UI-OK")'; $out1=node -e $js 2>&1; if($LASTEXITCODE -ne 0){ $out1 | Write-Output; Write-Error 'UI-FAIL' }; $ErrorActionPreference='Continue'; $out = cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build" 2>&1; $ec=$LASTEXITCODE; $ErrorActionPreference='Stop'; if($ec -ne 0){ $out | Write-Output; Write-Error 'BUILD-FAIL' }; $txt=($out | ForEach-Object { $_.ToString() }) -join "`n"; if($txt -notmatch 'build complete'){ Write-Error 'NO-BUILD-COMPLETE' }; if($txt -match 'Dead link|dead link'){ Write-Error 'DEAD-LINK' }; if((($out1 | ForEach-Object { $_.ToString() }) -join "`n") -notmatch 'UI-OK'){ Write-Error 'NO-UI-OK' }; Write-Output 'T4-OK' + non-zero exit, or output contains "MISSING", "NO-CONFIRM-MARKER", "UI-MISSING", "UI-MISSING-KEY", "UI-NO-VUE-IMPORT", "UI-VUE-IMPORT-SYMBOLS", "UI-NO-SYSTEM-IMPORT", "UI-ACTIVE-REF-BOUND", "UI-RUNTIME-TEMPLATE", "UI-TEXT-NON-STRING", "UI-DOM-FEATURE", "UI-CLIENT-BASE-USED", "UI-CRLF", "UI-ACTIVE-UNTYPED", "UI-STACKSIZE-UNTYPED", "UI-CLICK-NO-RETURN-TYPE", "UI-INSTANCE-UNTYPED", "UI-FAIL", "BUILD-FAIL", "NO-BUILD-COMPLETE", "DEAD-LINK", or "NO-UI-OK"; or output lacks "T4-OK" + + demo 显式 `import { computed, defineComponent, h, onUnmounted } from 'vue'` 与 `import { UIController, GameUI, IUIDefaultPropsBase, IUIInstance } from '@motajs/system'`,导入符号与实际使用一一对应(自动断言校验两条 import 与 vue 侧四个符号);`controller.open(...)` 的返回值存入同一个 `instance` 变量(`let instance: IUIInstance | null`),`controller.close(instance)` 复用它;两个 `computed` 带显式泛型(`computed(...)` / `computed(...)`),`openUi` / `closeUi` 均为 `function ...(): void`(以上四项由自动断言校验);使用 `UIController` + `GameUI` 定义最小 UI,设置了 `setPropsBaseCreator`;最小 UI 用渲染函数(`defineComponent` + `h('container', ...)`)构造,未使用运行时 `template:` 字符串;`controller.render()` 被挂进同一棵渲染树;打开 / 关闭可点击,栈长度与打开状态经 `computed` 解包后以字符串表达式(`'栈长度:' + stackSize`、`active ? '已打开' : '未打开'`)绑定到 `:text`,所有 `:text` 在运行期均为 string(不触发 `logger.error(21)`),关闭后归零;`onUnmounted` 执行 `closeAll()`;未把裸 Ref 裸值直接绑定到 `:text`;未使用 DOM 类功能与 `client-base`;CRLF;构建 green。 + UISystem demo 完成,UIController / GameUI / UIInstance 生命周期可交互验证。 + + + + Task 5: POC 页面 + prettier / ESLint / docs 类型 / 构建 / 静态断言门禁与记录 + docs/poc/render-demo.md, .planning/phases/10-docs-restructure/10-42-POC-NOTES.md, eslint.config.js + 10-42-POC-NOTES.md 含独占一行的「用户确认:可以执行」(本任务的自动门禁会复检该标记);Task 2..4 的组件与 demo 已存在且命名与确认清单一致。 + docs/.vitepress/config.ts(确认 `/poc` 不在 nav / sidebar)、docs/advanced/index.md:1-12(md 单 H1 与 `<script setup>` 的写法参考)、docs/.vitepress/components/renderDemo.vue、docs/.vitepress/components/demos/basicRender.vue、docs/.vitepress/components/demos/uiSystem.vue、.prettierrc(endOfLine: crlf)、eslint.config.js(flat config 结构,override 的追加位置与既有风格)、docs/tsconfig.json(docs 局部 TS 项目) + **(1)新建 POC 页面**(推荐 `docs/poc/render-demo.md`,CRLF):单 `#` 一级标题;正文用中文简短说明这是**临时技术验证页**(不可进导航、POC 通过后再决定去留),并说明两个 demo 各自证明什么;主体用 `<ClientOnly>` 包裹两个包装组件实例,各传一个 `loader`(`() => import('...')` 动态导入对应的 demo `.vue`)与逻辑尺寸(推荐 480×320);给 `<ClientOnly>` 一个 `#fallback` 提示「渲染示例需要浏览器环境」;`<script setup>` 里静态 import 包装组件(相对路径 `../.vitepress/components/renderDemo.vue`;包装组件本身不含引擎静态 import,SSR 安全),两个 loader 用动态 import 写,**不得**静态 import demo 文件;不添加任何指向站内其他页面的链接(避免 dead link);不修改 nav / sidebar。 + +**(2)先格式化再检查**:`cmd /c "pnpm exec prettier --write docs/.vitepress/config.ts docs/.vitepress/components docs/poc/render-demo.md"`,随后 `cmd /c "pnpm exec prettier --check docs/.vitepress/config.ts docs/.vitepress/components docs/poc/render-demo.md"`(D-45)。**不要**对 `.prettierignore` 内文件或既有页面做格式化。顺序必须是 `--write` → `--check` → ESLint 门禁 → docs 类型检查 → 静态断言 → 全量构建:prettier 会统一换行符,若先跑 CRLF 断言,尚未格式化的 LF 文件会先失败,掩盖真正的问题;ESLint 与类型检查插在 prettier 之后、构建之前,保证格式化不会带出新的告警或类型错误,同时让构建仍是最后一道门。ESLint 目标不含 `eslint.config.js`(不在 prettier 目标里,也不应被格式化重排)。本步与第(3)步的文件改动全部落盘后,才按上述顺序跑门禁(自动门禁在所有改动完成后一次性执行)。 + +**(3)新增 docs demo 的 ESLint 局部 override**:用 `Edit` 在根 `eslint.config.js` 的 `tseslint.config(...)` 参数列表**末尾追加一个对象**(不得改动任何既有对象、规则、`ignores` 或注释):`files` 为 `['docs/.vitepress/components/**/*.vue']`,`rules` 为 `{ 'vue/attribute-hyphenation': ['warn', 'never'], 'vue/one-component-per-file': 'off' }`;新增行按文件既有风格书写(4 空格缩进、单引号、对象尾逗号)。理由(随后必须写进 POC-NOTES):docs demo 是自定义渲染器元素,`g-rect` / `text` 的属性名必须保持 camelCase(`fillStyle` / `strokeStyle` / `lineWidth`)——改写成连字符属性名会被 `@vue/compiler-dom` 原样编译成 `fill-style` 键(元素 vnode 不做 camelize),而 `Graphics.handleProps`(`packages/render/src/core/graphics.ts:255-263`)与 `Text.handleProps`(`packages/render/src/core/misc.ts:127-137`)只匹配 camelCase 字面量,改名即失配、demo 不渲染,所以只能放宽 lint 规则;`vue/one-component-per-file` 的告警来自 `uiSystem.vue` 内的 `DemoPanel` / `UIRenderer` 两个 POC 局部组件,按「不拆文件」的既定设计关闭该规则。override 的 `files` **只**匹配 `docs/.vitepress/components/**/*.vue`,不得放宽到全仓,也不得给 `packages/` / `packages-user/` / `src/` 增删规则。 + +**(4)静态断言组合**(同一条 Node 脚本,在 prettier 与 ESLint / 类型检查之后跑):config 关键项(含 `@motajs` / `@user` 前缀 alias——源码为正则转义形态 `/^@motajs\/(.+)$/`,故断言用无斜杠形式、`dedupe`、两处 `isCustomElement`、`plugin-vue-jsx`、`MermaidPlugin`、`base: '/_docs/'`、`outDir` 含 `public` 与 `_docs`、`nav:` / `sidebar:` 键仍在)、包装组件(`@motajs/` / `@user/` 只允许出现在 `import(...)` 动态 import 行,任何 `from '@motajs…'` / `from '@user…'` 一律失败;清理逻辑(`dispose` 在 mount 前赋值、内部判 `appRef`)/ 无根锚点设置(只匹配 `setAnchor(` 调用形态)/ 具名类型)、两个 demo 的标签与 API(含所有 `:text` 绑定的值必须是字符串表达式,不得是裸标识符或数字字面量)、页面 `<ClientOnly>` 与两个 `() => import('...')` loader 且页面同样不得静态 `from '@motajs…'` / `from '@user…'`、四个新增文件均为 CRLF。 + +**(5)全量构建**:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"`,确认 green、`build complete`、无 dead link。 + +**(6)向 `10-42-POC-NOTES.md` 追加门禁结论**:六个门禁命令与结论(prettier `--write` / `--check`、ESLint、docs 类型检查、静态断言、全量构建、产物路径)、`public/_docs/poc/render-demo.html` 已生成、**待人工验证项清单**(见 Task 6 的六条)、以及「POC 结束后是否保留该页 / 命名 / 落点」的待议项(不在本任务中做决定)。另在 POC-NOTES 追加一节「组件类型与 ESLint」,至少四行(每行以下列标签开头,供自动断言校验):`属性名保持 camelCase:`(写清为什么不能改成连字符——`@vue/compiler-dom` 对元素 vnode 不做 camelize,连字符属性名会原样成为 `fill-style` 键,而 `graphics.ts:255-263` / `misc.ts:127-137` 的 `case` 只认 camelCase)、`eslint 覆盖范围:`(override 只匹配 `docs/.vitepress/components/**/*.vue`,逐条列出被放宽的 `vue/attribute-hyphenation` 与关闭的 `vue/one-component-per-file`)、`docs 类型检查:`(`vue-tsc --noEmit -p docs/tsconfig.json` 的结论,`docs/` 路径 0 报错)、`引擎既有类型错误:`(条数 + 摘要 + 「既有、非本轮引入、不得修」)。本任务不改任何 docs 内容语义,只做格式化、规则覆盖与记录;**不得**顺手修改 `packages/` / `packages-user/` / `src/` 里的既有报错。 + + $ErrorActionPreference='Stop'; $n='.planning/phases/10-docs-restructure/10-42-POC-NOTES.md'; if(-not(Test-Path -LiteralPath $n)){ Write-Error "MISSING $n" }; $np=[IO.File]::ReadAllText((Resolve-Path -LiteralPath $n)); if($np -notmatch '(?m)^用户确认:可以执行\s*$'){ Write-Error 'NO-CONFIRM-MARKER' }; $js='const fs=require("fs");const bad=[];function rd(f){return fs.readFileSync(f,"utf8")}const cfg=rd("docs/.vitepress/config.ts");["@motajs","@user","dedupe","isCustomElement","plugin-vue-jsx","MermaidPlugin","base:","outDir:","nav:","sidebar:"].forEach(function(k){if(cfg.indexOf(k)<0)bad.push("CFG:"+k)});if(!/base\s*:\s*[\x22\x27]\/_docs\/[\x22\x27]/.test(cfg))bad.push("CFG:BASE-VALUE");if(cfg.indexOf("public")<0||cfg.indexOf("_docs")<0)bad.push("CFG:OUTDIR-CONTENT");if(cfg.indexOf("/poc")>=0)bad.push("CFG:POC-IN-NAV");const w="docs/.vitepress/components/renderDemo.vue";if(!fs.existsSync(w))bad.push("NO-WRAPPER");else{const s=rd(w);if(/from\s+[\x22\x27]@(?:motajs|user)\//.test(s.replace(/import\s*\([^)]*\)/g,"")))bad.push("WRAPPER-STATIC-IMPORT");["onMounted","onUnmounted","unmount(","destroy(","import(","\x3ccanvas","interface DemoModule","type DemoLoader","interface RenderDemoProps","disposed","appRef"].forEach(function(k){if(s.indexOf(k)<0)bad.push("WRAPPER:"+k)});if(/\bsetAnchor\s*\(/.test(s))bad.push("WRAPPER-ANCHOR-OVERRIDE");if(!/\bdispose\s*=/.test(s))bad.push("WRAPPER-NO-DISPOSE-PATH")}const b="docs/.vitepress/components/demos/basicRender.vue";if(!fs.existsSync(b))bad.push("NO-BASIC");else{const s=rd(b);["\x3ccontainer","\x3cg-rect","\x3cg-circle","\x3ctext",":text","@click"].forEach(function(k){if(s.indexOf(k)<0)bad.push("BASIC:"+k)});if(/:text\s*=\s*"\s*(?:[A-Za-z_$][\w$]*|\d+(?:\.\d+)?)\s*"/.test(s))bad.push("BASIC-TEXT-NON-STRING")}const u="docs/.vitepress/components/demos/uiSystem.vue";if(!fs.existsSync(u))bad.push("NO-UI");else{const s=rd(u);["UIController","GameUI","setPropsBaseCreator","closeAll",".open(",".close(","computed<","controller.active.value","defineComponent","h(",":text"].forEach(function(k){if(s.indexOf(k)<0)bad.push("UI:"+k)});if(/:text\s*=\s*"controller\.active"/.test(s))bad.push("UI-ACTIVE-REF-BOUND");if(/template\s*:/.test(s))bad.push("UI-RUNTIME-TEMPLATE");if(/:text\s*=\s*"\s*(?:[A-Za-z_$][\w$]*|\d+(?:\.\d+)?)\s*"/.test(s))bad.push("UI-TEXT-NON-STRING")}const p="docs/poc/render-demo.md";if(!fs.existsSync(p))bad.push("NO-PAGE");else{const s=rd(p);if(s.indexOf("\x3cClientOnly")<0)bad.push("PAGE-NO-CLIENTONLY");if((s.match(/import\(/g)||[]).length<2)bad.push("PAGE-LOADERS");if((s.match(/\(\)\s*=>\s*import\s*\(/g)||[]).length<2)bad.push("PAGE-LOADER-NOT-DYNAMIC");if(/from\s+[\x22\x27]@(?:motajs|user)\//.test(s.replace(/import\s*\([^)]*\)/g,"")))bad.push("PAGE-STATIC-IMPORT")}["docs/.vitepress/components/renderDemo.vue",b,u,p].forEach(function(f){const s=rd(f);if(!/\r\n/.test(s)||/[^\r]\n/.test(s))bad.push("CRLF:"+f)});const e="eslint.config.js";if(!fs.existsSync(e))bad.push("NO-ESLINT-CONFIG");else{const s=rd(e);if(s.indexOf("docs/.vitepress/components/**/*.vue")<0)bad.push("ESLINT-NO-DOCS-OVERRIDE");if(!/attribute-hyphenation/.test(s))bad.push("ESLINT-NO-HYPHENATION");if(!/one-component-per-file/.test(s))bad.push("ESLINT-NO-ONECOMP");if(!/[\x27\x22]never[\x27\x22]/.test(s))bad.push("ESLINT-NO-NEVER");if((s.match(/ignores\s*:/g)||[]).length!==1)bad.push("ESLINT-IGNORES-CHANGED")}if(bad.length){console.error("GATE:"+bad.join("|"));process.exit(1)}console.log("GATE-OK")'; $ErrorActionPreference='Continue'; $pw = cmd /c "pnpm exec prettier --write docs/.vitepress/config.ts docs/.vitepress/components docs/poc/render-demo.md" 2>&1; $ew=$LASTEXITCODE; $pc = cmd /c "pnpm exec prettier --check docs/.vitepress/config.ts docs/.vitepress/components docs/poc/render-demo.md" 2>&1; $ec2=$LASTEXITCODE; if($ew -ne 0){ $pw | Write-Output; Write-Error 'PRETTIER-WRITE-FAIL' }; if($ec2 -ne 0){ $pc | Write-Output; Write-Error 'PRETTIER-FAIL' }; $ErrorActionPreference='Continue'; $el = cmd /c "pnpm exec eslint docs/.vitepress/components docs/.vitepress/config.ts" 2>&1; $eel=$LASTEXITCODE; $tc2 = cmd /c "pnpm exec vue-tsc --noEmit -p docs/tsconfig.json" 2>&1; $ErrorActionPreference='Stop'; $elTxt=($el | ForEach-Object { $_.ToString() }) -join "`n"; if($eel -ne 0){ $el | Write-Output; Write-Error 'ESLINT-FAIL' }; if($elTxt -match 'vue/attribute-hyphenation|vue/one-component-per-file'){ $el | Write-Output; Write-Error 'ESLINT-DEMO-RULE-HIT' }; if($elTxt -match '(\d+)\s+warning'){ $el | Write-Output; Write-Error 'ESLINT-WARNING' }; if($elTxt -match '(\d+)\s+error'){ $el | Write-Output; Write-Error 'ESLINT-ERROR' }; $docsErr2=@($tc2 | Where-Object { $x=$_.ToString(); ($x -match 'error TS') -and ($x -match '(^|\s)docs/') }); if($docsErr2.Count -gt 0){ $tc2 | Write-Output; Write-Error 'DOCS-TYPE-ERROR' }; $out1=node -e $js 2>&1; if($LASTEXITCODE -ne 0){ $out1 | Write-Output; Write-Error 'GATE-FAIL' }; $ErrorActionPreference='Continue'; $out = cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build" 2>&1; $ec=$LASTEXITCODE; $ErrorActionPreference='Stop'; if($ec -ne 0){ $out | Write-Output; Write-Error 'BUILD-FAIL' }; $txt=($out | ForEach-Object { $_.ToString() }) -join "`n"; if($txt -notmatch 'build complete'){ Write-Error 'NO-BUILD-COMPLETE' }; if($txt -match 'Dead link|dead link'){ Write-Error 'DEAD-LINK' }; if(-not(Test-Path -LiteralPath 'public/_docs/poc/render-demo.html')){ Write-Error 'PAGE-NOT-BUILT' }; $np2=[IO.File]::ReadAllText((Resolve-Path -LiteralPath $n)); foreach($k in @('门禁','prettier','构建','静态断言','待人工验证','产物')){ if($np2 -notmatch [regex]::Escape($k)){ Write-Error "NOTES-MISSING $k" } }; foreach($k in @('属性名保持 camelCase:','eslint 覆盖范围:','docs 类型检查:','引擎既有类型错误:')){ if($np2 -notmatch ('(?m)^'+[regex]::Escape($k))){ Write-Error "NOTES-MISSING $k" } }; if($np2 -match "(? + non-zero exit, or output contains "MISSING", "NO-CONFIRM-MARKER", "GATE:", "GATE-FAIL", "CFG:", "NO-WRAPPER", "WRAPPER-STATIC-IMPORT", "WRAPPER-ANCHOR-OVERRIDE", "NO-BASIC", "BASIC-TEXT-NON-STRING", "NO-UI", "UI-ACTIVE-REF-BOUND", "UI-RUNTIME-TEMPLATE", "UI-TEXT-NON-STRING", "NO-PAGE", "PAGE-NO-CLIENTONLY", "PAGE-LOADERS", "PAGE-LOADER-NOT-DYNAMIC", "PAGE-STATIC-IMPORT", "PRETTIER-WRITE-FAIL", "PRETTIER-FAIL", "ESLINT-FAIL", "ESLINT-DEMO-RULE-HIT", "ESLINT-WARNING", "ESLINT-ERROR", "DOCS-TYPE-ERROR", "NO-ESLINT-CONFIG", "ESLINT-NO-DOCS-OVERRIDE", "ESLINT-NO-HYPHENATION", "ESLINT-NO-ONECOMP", "ESLINT-NO-NEVER", "ESLINT-IGNORES-CHANGED", "BUILD-FAIL", "NO-BUILD-COMPLETE", "DEAD-LINK", "PAGE-NOT-BUILT", "NOTES-MISSING", "NOTES-LF-ONLY", or "NO-GATE-OK"; or output lacks "T5-OK" + + POC 页面存在、单 H1、`<ClientOnly>` 包裹两个包装组件、两个 loader 均为 `() => import('...')` 动态导入且指向正确文件、页面自身无 `from '@motajs…'` / `from '@user…'` 静态导入;页面不在 nav / sidebar;门禁顺序为 prettier `--write` → `--check` → ESLint → docs 类型检查 → 静态断言 → 全量构建,`--write` 与 `--check` 均通过;`cmd /c "pnpm exec eslint docs/.vitepress/components docs/.vitepress/config.ts"` 为 0 error / 0 warning,输出不含 `vue/attribute-hyphenation` / `vue/one-component-per-file`;`cmd /c "pnpm exec vue-tsc --noEmit -p docs/tsconfig.json"` 输出中路径以 `docs/` 开头且含 `error TS` 的行为 0;`eslint.config.js` 只新增一个 `files: ['docs/.vitepress/components/**/*.vue']` 的 override(`vue/attribute-hyphenation: ['warn','never']`、`vue/one-component-per-file: 'off'`),既有 `ignores` 与规则一条未改;构建 green、`build complete`、无 dead link;静态断言组合全绿(config 关键项含 alias / dedupe / 两处 `isCustomElement` / vue-jsx / `MermaidPlugin` / `base: '/_docs/'` / `outDir` 含 `public` 与 `_docs` / `nav:` 与 `sidebar:` 仍在;包装组件里 `@motajs` / `@user` 只出现在 `import(...)` 行、清理逻辑含 `dispose` / `appRef`(`dispose` 在 mount 前赋值、内部判空)、具名类型、无根锚点设置(断言只匹配 `setAnchor(` 调用形态);两个 demo 的标签与 API 齐全且所有 `:text` 绑定均为字符串表达式;页面 ClientOnly 与两个动态 loader;四个文件 CRLF);`public/_docs/poc/render-demo.html` 已生成;POC-NOTES 记录了门禁结论与待人工验证项,并含 `属性名保持 camelCase:` / `eslint 覆盖范围:` / `docs 类型检查:` / `引擎既有类型错误:` 四行。 + POC 页面可通过 URL 访问,自动化门禁全绿并有记录,产物页已生成,且不污染站点导航结构。 + + + + Task 6: 用户在浏览器人工验证两个 demo 与卸载清理 + .planning/phases/10-docs-restructure/10-42-POC-NOTES.md + Task 5 已通过(构建 green、产物页存在、POC-NOTES 已记录门禁结论与待验证项)。 + .planning/phases/10-docs-restructure/10-42-POC-NOTES.md(待人工验证项)、docs/poc/render-demo.md、docs/.vitepress/components/renderDemo.vue、docs/.vitepress/components/demos/uiSystem.vue + 请用户用 `cmd /c "pnpm docs:dev"`(或对构建产物用 `cmd /c "pnpm docs:preview"`)打开 POC 页面:dev 下访问终端打印的本地地址并追加 `/poc/render-demo`,preview 下访问 `/_docs/poc/render-demo.html`。请用户逐条确认:①渲染基础 demo 出现 container 背景、矩形、圆与文字;②点击矩形 / 圆的交互使计数与颜色可见变化(若点击无反应,先查是否有人改动过根锚点或给 canvas 写了内联宽高);③UISystem demo 的「打开 UI」能看到最小 UI,「关闭 UI」后栈长度归零、打开状态变 false;④在站点内来回切换路由或反复进出该页后,两个 demo 仍能正常显示与交互,画面不叠加、控制台无 RAF / 事件相关报错(说明 `app.unmount()` + `renderer.destroy()` 生效);⑤控制台无 `logger` 错误(特别是 74 / 21 / 19);⑥明确答复「POC 结束后该页 / 命名 / 落点是否保留」。用户确认通过时,在 `10-42-POC-NOTES.md` 追加**独占一行**的 `用户人工验证:通过`,紧接着**下一行**以 `用户原文:` 开头的用户原话,并记录保留 / 移除的决定;若发现问题,逐条记到 `10-42-POC-NOTES.md` 并标 `状态:待处理`,回到 Task 2..5 修复后重跑门禁、再请用户复验(循环不限轮次)。**只有用户明确通过才可进入 Task 7**;用户沉默或只回复「可以」不算通过。 + 可访问的 POC 页面与两个 demo,构建产物在 `public/_docs/poc/render-demo.html`。 + 按上面①–⑥逐条在浏览器验证;重点看点击交互、UI 打开 / 关闭后的栈长度归零,以及反复进出页面后是否仍正常。 + 若全部通过,回复「通过」;若有问题,请逐条描述现象(可附控制台报错);另外请说明该页与命名是否保留。 + 用户明确回复两个 demo 可交互、卸载后无残留,并已把结论(含保留 / 移除决定)记录进 `10-42-POC-NOTES.md`;或用户提出了具体问题(则回到修复循环)。 + + 用户已明确确认两个 demo 可交互、路由切换后无残留,并给出该页 / 命名 / 落点的保留意见;`10-42-POC-NOTES.md` 中已有对应记录。 + + 用户人工验证结论已记录(通过,或问题清单已进入修复循环)。 + + + + Task 7: 收尾:SUMMARY + ROADMAP / STATE 追加式记录 + 基线未变校验 + .planning/phases/10-docs-restructure/10-42-SUMMARY.md, .planning/ROADMAP.md, .planning/STATE.md + 10-42-POC-NOTES.md 含独占一行的「用户人工验证:通过」与 `用户确认:可以执行`,且无 `状态:待处理` 条目。 + .planning/ROADMAP.md(Phase 10 段落:Goal、`**Plans:**` 行、Plans 列表、说明)、.planning/STATE.md:1-40 与 Roadmap Evolution 段、.planning/phases/10-docs-restructure/10-41-PLAN.md(收尾体例)、.planning/phases/10-docs-restructure/10-42-POC-NOTES.md(命名确认、基线、门禁与人工验证结论) + **(1)写 `.planning/phases/10-docs-restructure/10-42-SUMMARY.md`**(CRLF):说明本计划解决的问题(文档站内嵌自定义渲染器的可行性)、最终落点与**最终命名**(与 POC-NOTES 的命名清单一致)、四项门禁结论(prettier / 构建 / 静态断言 / 产物路径)、ESLint 与 docs 类型检查结论(含引擎既有报错的记录口径)、人工验证结论与用户原话、**未做事项**(fence 插件、正式渲染端文档批次、`client-base` 组件、nav / sidebar 注册、任何源码改动)、以及「POC 页 / 命名 / 落点是否保留」的用户决定与后续建议。 + +**(2)用 `Edit` 在 `.planning/ROADMAP.md` 的 Phase 10 `Plans:` 列表末尾追加一行** `- [x] 10-42-PLAN.md — VitePress 内嵌自定义渲染器 demo 最小 POC(config alias / dedupe / isCustomElement / vue-jsx + 包装组件 + 渲染基础与 UISystem 两个 demo + 不进导航的 POC 页)`,并在该列表之后**追加一句**增量说明:本计划单独追加,Phase 10 的 `**Plans:**` 计数行与第 1..5 批描述不因本计划改动(计数收口仍归 `10-41`),`10-33`..`10-41` 的收口仍由 `10-41` 负责,`10-06` / `10-11` / `10-12` 仍未勾选。**不得**改写 Goal、**不得**改 Phase 10 的 `**Plans:**` 行(它可能已被 10-41 改成 38/41,本计划只保证不改动它)、不得改其他 Phase、不得给 `10-33`..`10-41` 增删任何条目、不得整文件重写。 + +**(3)用 `Edit` 更新 `.planning/STATE.md`**:①frontmatter 只改 `last_updated` / `last_activity` / `last_activity_desc`;**`progress.total_plans` 与 `progress.completed_plans` 保持 Task 1 记录的基线原值**(不写新的数字);②`Current Position` 的 `Plan:` 与 `Last activity:` 各追加一句 10-42 POC 已验证;③`Progress:` 行追加「(另:10-42 文档内嵌渲染 demo POC 已完成并经用户验证,Phase 10 的计数收口仍归 10-41)」;④`### Roadmap Evolution` 末尾追加一条 10-42 记录(范围、两个 demo、验证结论、未做事项)。**不得**改动既有 Decisions / Deferred Items / Blockers / Quick Tasks。 + +**(4)不做任何 `git commit` / `git push`**;在 SUMMARY 中单独列一节「HEAD 与提交状态」,写三行 ASCII 标记(各自独占一行、供自动断言校验):`BASE-HEAD: `、`CURRENT-HEAD: <写 SUMMARY 时 git rev-parse HEAD 的完整 sha>`、`PLAN-FILES-COMMITTED: none`;随后用中文说明判定口径:**允许 HEAD 与基线不同**(用户可能在 POC 期间并发提交,例如 `refactor: 删除 legacy-ui 包`、`feat: 设置系统类型声明`),只要 ① `git log --oneline <基线 HEAD>..HEAD -- <本计划文件>` 输出为空(本计划文件没有被任何新 commit 带入),且 ② `git status --porcelain -- <路径>` 对本计划六个关键路径(`docs/poc/render-demo.md`、`docs/tsconfig.json`、`docs/.vitepress/components`、`docs/.vitepress/config.ts`、`eslint.config.js`、`.planning/phases/10-docs-restructure/10-42-POC-NOTES.md`)仍有未提交输出。同样的三项(基线 HEAD / 当前 HEAD / 本计划文件未提交)也追加到 `.planning/phases/10-docs-restructure/10-42-POC-NOTES.md`(该文件已在本计划 `files_modified` 内;只追加,不改 Task 1 写入的 `基线 HEAD:` 行)。**不得**为了凑「HEAD 未变」而要求用户回退或 reset 其并发提交。 + + $ErrorActionPreference='Stop'; $sum='.planning/phases/10-docs-restructure/10-42-SUMMARY.md'; $notes='.planning/phases/10-docs-restructure/10-42-POC-NOTES.md'; if(-not(Test-Path -LiteralPath $sum)){ Write-Error "MISSING $sum" }; if(-not(Test-Path -LiteralPath $notes)){ Write-Error "MISSING $notes" }; $n=[IO.File]::ReadAllText((Resolve-Path -LiteralPath $notes)); if($n -notmatch "(?m)^用户人工验证:通过\s*$"){ Write-Error 'NO-HUMAN-CONFIRM' }; if($n -notmatch '(?m)^用户原文:'){ Write-Error 'NO-HUMAN-QUOTE' }; if($n -notmatch "(?m)^用户确认:可以执行\s*$"){ Write-Error 'NO-CONFIRM-MARKER' }; if($n -match "(?m)^\s*(?:[-*]\s*)?状态:待处理\s*$"){ Write-Error 'PENDING-ITEMS' }; $s=[IO.File]::ReadAllText((Resolve-Path -LiteralPath $sum)); foreach($k in @('POC','构建','prettier','ESLint','类型检查','命名','人工验证','未做','commit')){ if($s -notmatch [regex]::Escape($k)){ Write-Error "SUMMARY-MISSING $k" } }; if($s -match "(? + non-zero exit, or output contains "MISSING", "NO-HUMAN-CONFIRM", "NO-HUMAN-QUOTE", "NO-CONFIRM-MARKER", "PENDING-ITEMS", "SUMMARY-MISSING", "SUMMARY-LF-ONLY", "ROADMAP-MISSING-10-42", "ROADMAP-LOST-10-06", "ROADMAP-LOST-10-11", "ROADMAP-LOST-10-12", "ROADMAP-NO-NOTE", "NO-BASE-PLANS", "ROADMAP-PLANS-CHANGED", "STATE-MISSING-10-42", "STATE-LOST-10-06", "NO-BASE-TOTAL", "STATE-TOTAL-CHANGED", "NO-BASE-COMPLETED", "STATE-COMPLETED-CHANGED", "NO-BASE-HEAD", "SUMMARY-NO-BASE-HEAD", "SUMMARY-NO-CURRENT-HEAD", "SUMMARY-NO-COMMIT-NOTE", "PLAN-FILES-COMMITTED", "PLAN-FILE-CLEAN", or "PAGE-NOT-BUILT"; or output lacks "CLOSEOUT-OK" + + SUMMARY 完整(落点、命名、prettier / ESLint / docs 类型 / 构建 / 静态断言 / 产物六类门禁、人工验证、未做事项、保留决定、未提交说明);ROADMAP 仅追加 10-42 条目与增量说明,Phase 10 的 `**Plans:**` 计数行原文(Task 1 记录的基线)逐字未变,`10-06` / `10-11` / `10-12` 与 `10-33`..`10-41` 状态未被改动;STATE 仅做增量更新,全局 `total_plans` / `completed_plans` 与基线一致;人工验证标记与用户原文在案、无待处理项;**本计划未产生 commit**——`git log <基线 HEAD>..HEAD -- <本计划六个关键路径>` 必须为空,且 `git status --porcelain` 对这些路径仍有未提交输出(HEAD 与基线**不要求相等**;若 HEAD 前移,必须是用户并发提交且未包含本计划文件),SUMMARY 已按 `BASE-HEAD` / `CURRENT-HEAD` / `PLAN-FILES-COMMITTED: none` 三行记录;产物页仍存在。 + 收尾记录与 SUMMARY 完成,ROADMAP / STATE 追加式更新且基线未被篡改;本计划未产生 commit(HEAD 可能因用户并发提交前移),本计划文件未被任何 commit 带入且仍处于未提交状态。 + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| 浏览器 → demo 组件 | demo 只读本地静态资源与页面静态传入的 loader;无外部输入、无网络请求、无持久化 | +| VitePress 构建期(SSR 预渲染)→ 渲染引擎源码 | 预渲染在 Node 中执行,而引擎模块顶层会访问 `document`;这条边界一旦被顶层 import 穿透,构建即失败 | +| docs 配置 → 仓库源码 | `resolve.alias` 把包名映射到 `packages/*/src`、`packages-user/*/src`,是只读映射,不引入远程或任意路径 | +| 站点导航 → POC 页 | POC 页故意不进 nav / sidebar,避免未验收内容被当成正式文档入口 | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-10-124 | Tampering | `docs/.vitepress/config.ts` 的 `resolve.alias` | medium | mitigate | alias 只允许 `packages/*/src` 与 `packages-user/*/src`;静态断言检查替换串形态与包前缀,禁止指向远程 / 任意路径 | +| T-10-125 | Denial of Service(构建不可用) | 预渲染阶段的模块求值 | high | mitigate | 包装组件与页面顶层禁止引擎静态 import,只在 `onMounted` 动态 import;页面用 `<ClientOnly>`;每条自动构建门禁都必须 `build complete` | +| T-10-126 | Tampering | 两个 demo 的 props / loader | low | accept | loader 由页面静态传入(`() => import(...)`),不接受用户输入、不解析字符串、不执行外部代码 | +| T-10-127 | Information Disclosure | POC 页被当成正式文档 | low | mitigate | 页面不进 nav / sidebar,标题与正文明确标注「临时技术验证」;静态断言禁止 `/poc` 出现在 config 的 nav / sidebar | +| T-10-128 | Repudiation | 命名与验证结论的记账 | medium | mitigate | 命名确认与人工验证都以「独占一行的标记 + 下一行用户原文」记录,且每个 auto 任务的自动断言都会复检确认标记;未确认不得落盘 | +| T-10-SC | Tampering | 包管理器安装 | high | mitigate | 本计划不安装任何新包(`@vitejs/plugin-vue-jsx` 已在 devDependencies);若执行时发现依赖缺失,停止并报告,不得临时安装 | + + + +- 确认门禁:`10-42-POC-NOTES.md` 含独占一行的 `用户确认:可以执行` + 下一行 `用户原文:` + 完整命名清单 + 基线 HEAD + `ROADMAP 基线 Plans 行` 原文 + `STATE 基线 total_plans` / `completed_plans` 原值;`10-42-POC-NOTES.md` 另含独占一行的 `用户人工验证:通过` + 下一行 `用户原文:`,且无 `状态:待处理`。**每个 auto 任务的自动门禁都复检 `用户确认:可以执行` 标记。** +- 构建门禁(每次):`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` green、`build complete`、dead link = 0。 +- 产物门禁:`public/_docs/poc/render-demo.html` 存在。 +- SSR 门禁:包装组件与页面里 `@motajs/*` / `@user/*` 只出现在 `import(...)` 动态 import 行;任何 `from '@motajs…'` / `from '@user…'`(含折行的具名导入)一律失败;页面两个 loader 必须是 `() => import('...')`。 +- 坐标门禁:包装组件不调用任何根锚点设置接口(静态断言为 `/\bsetAnchor\s*\(/`,只匹配调用形态,不误伤解释性注释),保持构造器默认 0.5/0.5;canvas 不写内联宽高(尺寸由 `size()` / `onResize()` 负责)。 +- 生命周期门禁:`await` 之后先判 `disposed` 与 `canvasRef.value`;`dispose` 在渲染器创建后、`app.mount()` 之前赋值,内部先判 `appRef` 再 `app.unmount()`,随后 `renderer.destroy()`;卸载统一由 setup 阶段注册的 `onUnmounted` → `dispose?.()` → 置 null 完成,mount 未完成也走同一路径;包装组件内不含任何异常捕获语法。 +- 配置门禁:config 含 `@motajs` / `@user` 前缀 alias(源码为正则转义形态 `/^@motajs\/(.+)$/` 与 `/^@user\/(.+)$/`,断言按无斜杠形式匹配)、`dedupe: ['vue']`、两处 `isCustomElement`、`@vitejs/plugin-vue-jsx`;机械校验 `base: '/_docs/'`、`outDir` 含 `public` 与 `_docs`、`nav:` 与 `sidebar:` 键仍在;`MermaidPlugin` / 既有注释未变。 +- 覆盖门禁:渲染基础 demo 有 container / g-rect / g-circle / text / 点击;UISystem demo 有 `UIController` / `GameUI` / `setPropsBaseCreator` / 打开关闭 / `closeAll` / `render()` 挂载,最小 UI 用渲染函数(`h(...)`)而非运行时 `template:` 字符串,且 `controller.active` / `controller.stack` 经 `computed` 解包后再以字符串表达式绑定到 `:text`(所有 `:text` 实参运行期为 string)。 +- 格式门禁:prettier `--check` 通过;四个新增 docs 文件均为 CRLF。 +- docs 类型门禁:`cmd /c "pnpm exec vue-tsc --noEmit -p docs/tsconfig.json"` 输出中路径以 `docs/` 开头且含 `error TS` 的行为 0;`packages/` / `packages-user/` / `src/` 的既有报错条数与摘要写入 POC-NOTES 的 `引擎既有类型错误:` 行(标注「既有、非本轮引入、不得修」);`docs/tsconfig.json` 不被加入 `pnpm check:type`,根 `tsconfig.json` / `tsconfig.node.json` 的既有 `include` 未改。 +- ESLint 门禁:`cmd /c "pnpm exec eslint docs/.vitepress/components docs/.vitepress/config.ts"` 为 0 error / 0 warning,输出不出现 `vue/attribute-hyphenation` / `vue/one-component-per-file`;`eslint.config.js` 只新增 `docs/.vitepress/components/**/*.vue` 的 override(`vue/attribute-hyphenation: ['warn','never']`、`vue/one-component-per-file: 'off'`),既有 `ignores` 与规则未改;渲染属性保持 camelCase(`fillStyle` / `strokeStyle` / `lineWidth`),未改成连字符形式。 +- 引擎源码门禁:`git status --porcelain packages/ packages-user/ src/` 输出为空;引擎既有类型错误只记录、不修复,也不因为 lint 或类型门禁去改任何引擎源码。 +- 越界门禁:未修改 `packages/`、`packages-user/`、`src/`、既有 docs 页面、nav / sidebar、任何既有 JSDoc/注释;未安装新依赖;未 `git commit`(本计划文件未被任何 commit 带入,且 `git status --porcelain` 仍显示其未提交;**HEAD 允许因用户并发提交前移,不要求等于基线**);ROADMAP 的 `**Plans:**` 行与 STATE 的 `total_plans` / `completed_plans` 与 Task 1 记录的基线逐字一致。 + + + +- 文档站在**不放弃 SSR 预渲染**的前提下,能内嵌自定义渲染器 demo,并且发布构建仍 green、无 dead link。 +- docs 组件在 IDE 与 `vue-tsc --noEmit -p docs/tsconfig.json` 下不再有 `docs/` 路径的类型错误,docs demo 的 ESLint 输出为 0 error / 0 warning;达成方式是补显式类型 + docs 局部 TS 项目 + ESLint docs override,没有改属性名、没有改引擎源码。 +- 两个 demo 在浏览器中真实绘制、可交互;点击命中与绘制坐标一致;UI 的打开 / 关闭与栈长度归零可观察;卸载后无 RAF / 监听累积,且不会对半成品渲染器调用销毁。 +- 命名、落点、验证结论均有可复查的记录,且命名与页面去留由用户决定,AI 未自行收尾。 +- 只做了本次派发范围内的加法修改;没有源码改动、没有新依赖、没有 commit;ROADMAP / STATE 只做追加,基线未被篡改。 + + + +Create `.planning/phases/10-docs-restructure/10-42-SUMMARY.md` when done + + diff --git a/.planning/phases/10-docs-restructure/10-42-POC-NOTES.md b/.planning/phases/10-docs-restructure/10-42-POC-NOTES.md new file mode 100644 index 0000000..c8e8eb9 --- /dev/null +++ b/.planning/phases/10-docs-restructure/10-42-POC-NOTES.md @@ -0,0 +1,84 @@ +# 10-42 POC 记录:VitePress 内嵌自定义渲染器 + +记录日期:2026-10-29 + +## 命名清单(用户已确认) + +方案 A,逐项最终值如下(包装组件文件名、组件名、props、内部类型、demo 目录、两个 demo 文件名与组件名、POC 页面路径、config 新增键均已确认): + +| 项 | 最终值 | 类型 / 默认值 | +| --- | --- | --- | +| 包装组件文件 | `docs/.vitepress/components/renderDemo.vue` | — | +| 包装组件名 | `RenderDemo` | — | +| 包装组件 props | `loader` / `width` / `height` / `scale` / `background` | `DemoLoader`(必填)/ `number`(480)/ `number`(320)/ `number`(1)/ `string`(`'#101418'`) | +| 包装组件内部类型 | `RenderDemoProps` / `DemoLoader` / `DemoModule` | `interface` / `type` / `interface` | +| demo 目录 | `docs/.vitepress/components/demos/` | — | +| 渲染基础 demo | `basicRender.vue` | 组件名 `BasicRenderDemo` | +| UISystem demo | `uiSystem.vue` | 组件名 `UISystemDemo` | +| POC 页面 | `docs/poc/render-demo.md` | 不进 nav / sidebar | +| 命名确认记录 | `.planning/phases/10-docs-restructure/10-42-POC-NOTES.md` | 本文件 | +| config 新增键 | `vite.resolve.alias` / `vite.resolve.dedupe` / `vue.template.compilerOptions.isCustomElement` / `vite.plugins`(追加 `vuejsx`) | — | + +用户确认:可以执行 +用户原文:可以执行 + +## 基线 + +基线 HEAD:db726ff58d2fe4311704d375cd34a4b7e755eba3 +ROADMAP 基线 Plans 行:**Plans:** 29/32 completed(第一批:中级教程 24 篇 + 站点注册 + D-09 复审修订;第二批:高级 I 7 篇 + 注册 + 读者审阅 + 用户修订循环;第三批:高级 II 10 篇 + 注册 + 读者审阅 + 用户修订循环;第四批:高级 I 补写 Mover + FaceHandler 2 篇 + 注册 + 读者/用户修订,已完成)。**未完成 3 个**(第一批遗留):`10-06`(中级全批验收)、`10-11`(中级读者审阅)、`10-12`(中级最终验收)——其范围已被后续 D-09 修订与高级 I/II 期间的最终状态覆盖,缺独立 SUMMARY,待补跑或并入后续验收。 +STATE 基线 total_plans: 134 +STATE 基线 completed_plans: 126 + +以上基线项(ROADMAP 计数行原文 + STATE 两个计数原值 + HEAD)是「本次修改不得改动」的比对基准,不写死任何期望数字(10-41 可能已把 Phase 10 的计数改成 38/41,本计划只保证自己不改它)。 + +## 门禁 + +首次构建:green + +- 命令:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` +- 关键输出:`build complete in 19.38s.`;无 dead link 报告;仅有既有的 chunk 体积警告 + +### Task 5 门禁结论 + +| 门禁 | 命令 | 结论 | +| --- | --- | --- | +| prettier `--write` | `cmd /c "pnpm exec prettier --write docs/.vitepress/config.ts docs/.vitepress/components docs/poc/render-demo.md"` | 通过(exit 0) | +| prettier `--check` | `cmd /c "pnpm exec prettier --check docs/.vitepress/config.ts docs/.vitepress/components docs/poc/render-demo.md"` | 通过(`All matched files use Prettier code style!`) | +| ESLint | `cmd /c "pnpm exec eslint docs/.vitepress/components docs/.vitepress/config.ts"` | 0 error / 0 warning(exit 0,无规则命中) | +| docs 类型检查 | `cmd /c "pnpm exec vue-tsc --noEmit -p docs/tsconfig.json"` | `docs/` 路径 0 报错;仅 `packages/` 既有报错 | +| 静态断言 | 计划 Task 5 的 Node 断言组合(config / 包装组件 / 两个 demo / 页面 / `eslint.config.js` / 四个文件 CRLF) | `GATE-OK` | +| 全量构建 | `cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` | green:`build complete`,无 dead link | +| 产物 | `public/_docs/poc/render-demo.html` | 已生成(`Test-Path` = True) | + +注:首次 Task 5 构建曾因既有 `packages/system/src/hotkey/index.ts` 的失效 `export * from './hotkey'` 而 red;该文件已由用户自行修复,本次重跑构建 green。本计划未修改该文件。 + +## 组件类型与 ESLint + +属性名保持 camelCase:docs demo 的 `g-*` / `container` / `text` 是 `isCustomElement` 的元素 vnode,`@vue/compiler-dom` 的 `v-bind` 不会对元素做 camelize——把 `fillStyle` / `strokeStyle` / `lineWidth` 改写成连字符会被原样编译成 `fill-style` 键;而 `packages/render/src/core/graphics.ts:255-263` 与 `packages/render/src/core/misc.ts:127-137` 的 `handleProps` 只匹配 camelCase 字面量,改名即失配、demo 不渲染,因此属性名一律保持 camelCase,连字符告警只能靠 ESLint 局部 override 放宽。 +eslint 覆盖范围:`eslint.config.js` 仅新增一个 `files: ['docs/.vitepress/components/**/*.vue']` 的 override(`vue/attribute-hyphenation: ['warn', 'never']`、`vue/one-component-per-file: 'off'`);既有 `ignores` 与其它规则、`packages/` / `packages-user/` / `src/` 的 lint 范围均未改动。 +docs 类型检查:`cmd /c "pnpm exec vue-tsc --noEmit -p docs/tsconfig.json"` 通过,`docs/` 路径 0 报错;`docs/tsconfig.json` 只服务 docs,不并入 `pnpm check:type`,根 `tsconfig.json` / `tsconfig.node.json` 未改。 +引擎既有类型错误:11 条(`packages/common/src/logger.ts` 缺 `main` 全局 3 条;`packages/render/src/core/types.ts` 缺 `CanvasStyle` 导入 8 条);既有、非本轮引入、不得修,只记录不修复。 + +## 待人工验证项 + +1. 渲染基础 demo 出现 container 背景、矩形、圆与文字。 +2. 点击矩形 / 圆后计数与颜色可见变化(若点击无反应,先查根锚点是否被改动、canvas 是否被写内联宽高)。 +3. UISystem demo「打开 UI」能看到最小 UI,「关闭 UI」后栈长度归零、打开状态变为未打开。 +4. 站点内来回切换路由或反复进出该页后,两个 demo 仍能正常显示与交互,画面不叠加、控制台无 RAF / 事件相关报错。 +5. 控制台无 `logger` 错误(特别是 74 / 21 / 19)。 +6. 明确答复「POC 结束后该页 / 命名 / 落点是否保留」。 + +## 待议项 + +POC 结束后该页 / 命名 / 落点是否保留,由用户在 Task 6 决定;本记录不做决定。 + +## 人工验证与提交状态 + +用户人工验证:通过 +用户原文:pass + +页面 / 命名 / 落点决定:**暂时保留** `docs/poc/render-demo.md` 与现有命名、落点;后续正式渲染端文档批次再决定删除或迁移。 + +当前 HEAD:e05cc55b957104e730421e3996adb5912143a253 + +提交状态:本计划文件未被任何 commit 带入,且仍未提交——`git log <基线 HEAD>..HEAD -- <本计划文件>` 输出为空,`git status --porcelain` 对本计划六个关键路径仍有未提交输出。Task 1 的 `基线 HEAD:` 行保持 `db726ff58d2fe4311704d375cd34a4b7e755eba3` 不变;HEAD 前移来自用户并发提交。 diff --git a/.planning/phases/10-docs-restructure/10-42-SUMMARY.md b/.planning/phases/10-docs-restructure/10-42-SUMMARY.md new file mode 100644 index 0000000..33147c6 --- /dev/null +++ b/.planning/phases/10-docs-restructure/10-42-SUMMARY.md @@ -0,0 +1,61 @@ +# Phase 10 Plan 42: VitePress 内嵌自定义渲染器 demo 最小 POC Summary + +## 一句话 + +在保留 VitePress SSR 预渲染的前提下,用 DOM 包装组件 + `onMounted` 动态 import,把 `@motajs/render` / `@motajs/render-vue` 的自定义渲染器嵌进文档站;「渲染基础」与「UISystem」两个 demo 真实绘制、可交互、可安全卸载。 + +## 解决的问题 + +验证「文档内嵌可运行示例」在 `vitepress build docs` 的预渲染阶段是否可行:引擎模块顶层会碰 `document`(`core/utils.ts`、`core/graphics.ts`、`core/misc.ts`、`render-vue/src/tag.ts`),因此包装组件与页面顶层只能对引擎包动态 import,页面用 `` 跳过预渲染。POC 通过后,才值得评估 fence 插件与正式渲染端文档批次。 + +## 最终落点与命名 + +- 包装组件:`docs/.vitepress/components/renderDemo.vue`(组件名 `RenderDemo`;props `loader` / `width` / `height` / `scale` / `background`;内部类型 `RenderDemoProps` / `DemoLoader` / `DemoModule` / `AppHandle`) +- 渲染基础 demo:`docs/.vitepress/components/demos/basicRender.vue`(`BasicRenderDemo`) +- UISystem demo:`docs/.vitepress/components/demos/uiSystem.vue`(`UISystemDemo`) +- POC 页面:`docs/poc/render-demo.md`(不进 nav / sidebar) +- 配置:`docs/.vitepress/config.ts` 的 `vite.resolve.alias` / `dedupe` / `isCustomElement` / `vuejsx` +- docs 局部 TS 项目:`docs/tsconfig.json`(不并入 `pnpm check:type`,根 `tsconfig.json` / `tsconfig.node.json` 未改) +- ESLint docs override:`eslint.config.js`(只匹配 `docs/.vitepress/components/**/*.vue`) + +## 六类门禁 + +- prettier:`--write` → `--check` 均通过(`All matched files use Prettier code style!`)。 +- ESLint:`cmd /c "pnpm exec eslint docs/.vitepress/components docs/.vitepress/config.ts"` 为 0 error / 0 warning,无 `vue/attribute-hyphenation` / `vue/one-component-per-file`;渲染属性保持 camelCase(`fillStyle` / `strokeStyle` / `lineWidth`),未为了 lint 改属性名。 +- docs 类型检查:`cmd /c "pnpm exec vue-tsc --noEmit -p docs/tsconfig.json"` 中 `docs/` 路径 0 报错;仅 11 条 `packages/` 既有报错(`packages/common/src/logger.ts` 缺 `main` 3 条、`packages/render/src/core/types.ts` 缺 `CanvasStyle` 8 条),既有、非本轮引入、不得修,只记录。 +- 构建:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` green,输出含 `build complete`,无 dead link。 +- 静态断言:计划 Task 5 的 Node 组合(config / 包装组件 SSR 边界 / 两个 demo / 页面 ClientOnly 与两个动态 loader / `eslint.config.js` override / 四文件 CRLF)为 `GATE-OK`。 +- 产物:`public/_docs/poc/render-demo.html` 已生成,且 POC 页不在 nav / sidebar。 + +## 人工验证 + +用户人工验证:通过 +用户原文:pass + +浏览器中确认:两个 demo 真实绘制、点击交互生效、UI 打开 / 关闭后栈长度归零、反复进出页面无残留、控制台无 `logger` 74 / 21 / 19。 + +## 页面保留决定 + +**暂时保留** `docs/poc/render-demo.md` 与现有命名、落点;后续正式渲染端文档批次再决定删除或迁移。 + +## 未做事项 + +- 不做 markdown fence 插件(POC 通过后再评估)。 +- 不写正式渲染端文档批次。 +- 不使用 `client-base` 的真实素材 / 组件。 +- 不把 POC 页加入 nav / sidebar。 +- 不改任何引擎源码(`packages/` / `packages-user/` / `src/` 均未改);引擎既有类型错误只记录。 +- 本计划未产生任何 git commit / push。 + +## HEAD 与提交状态 + +BASE-HEAD: db726ff58d2fe4311704d375cd34a4b7e755eba3 +CURRENT-HEAD: e05cc55b957104e730421e3996adb5912143a253 +PLAN-FILES-COMMITTED: none + +判定口径:允许 HEAD 与基线不同——POC 期间用户有并发提交(例如 `refactor: 删除 legacy-ui 包`、`feat: 设置系统类型声明`)。本计划未产生 commit 的证明是:① `git log --oneline <基线 HEAD>..HEAD -- <本计划六个关键路径>` 输出为空(本计划文件没有被任何新 commit 带入);② `git status --porcelain -- <同一组路径>` 仍有未提交输出(`docs/poc/render-demo.md`、`docs/tsconfig.json`、`docs/.vitepress/components`、`docs/.vitepress/config.ts`、`eslint.config.js`、`.planning/phases/10-docs-restructure/10-42-POC-NOTES.md`)。 + +## Self-Check: PASSED + +- 产物与记录文件均存在:`docs/.vitepress/config.ts`、`docs/tsconfig.json`、`eslint.config.js`、三个组件、`docs/poc/render-demo.md`、本 SUMMARY、POC-NOTES、`ROADMAP.md`、`STATE.md`、`public/_docs/poc/render-demo.html`。 +- 本计划未产生 commit:`git log db726ff58d2fe4311704d375cd34a4b7e755eba3..HEAD -- <本计划六个关键路径>` 输出为空,`git status --porcelain` 对这些路径仍有输出。