docs(09-21): 重写为 revision 4(数据端硬门禁 + 顺序 + baseline)

This commit is contained in:
unanmed 2026-10-20 14:37:34 +08:00
parent 30b7d867c6
commit a14718865c

View File

@ -3,7 +3,7 @@ phase: 09-minimum-browser-runnable
plan: 21
type: execute
wave: 18
depends_on: []
depends_on: [09-19, 09-20]
files_modified:
- script/vite-plugin-data-isolation.ts
- script/vite-plugin-data-isolation.test.ts
@ -14,37 +14,38 @@ files_modified:
- script/build-lib.ts
- package.json
- index.html
- dev.md
- .planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md
autonomous: false
requirements: []
estimate:
tokens: 60000
raw_tokens: 30000
tokens: 36000
raw_tokens: 36000
tasks: 3
confidence: low
must_haves:
truths:
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03 / D-93)**:Task 0 必须先汇报「任务 11 · 构建重构(适配新引擎)」的**要做什么 / 为什么 / 大致怎么做 / 影响范围**,必须列明:D-93 脚本重构与删除清单、D-94 隔离插件(specifier + 解析路径双路扫描 + `this.error` 终止,取代 `checkClientData`)、D-95 数据端库模式 ESM 与 `dist/data/data.js`、D-96 content 压缩(保留 `// --- SYSTEM PREFIX END --- //`)/ fontmin / 不分块 / 不 zip、D-97 dist 产物排除清单与 `LICENSE` 保留、`index.html` 去旧样式表 link、D-98 `package.json` script 调整、D-99 保留 `vue-tsc --noEmit`、D-100 移除 babel 二次压缩并微调 `manualChunks`、**OI-11 的实测跨端引用链原文**、**数据端构建按设计终止属预期(OI-11)、门禁按渲染端 + content + LICENSE 分层**、全部拟新增命名(OI-1 / OI-2)、写操作边界(**不改 `src/**`**)、验证门禁方式与 `open_items`(OI-1..OI-11),然后必须**停下等待**;**回复「可以执行」即视为通过(本计划不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行)**;用户若对任一 `open_items` 给出指定,按指定执行。"
- "**D-93 / D-98 脚本重构与删除:** 重构 `script/build-game.ts` 与 `script/build-resource.ts`;**删除** `script/build-packages.ts` 与 `script/build-lib.ts`;从 `package.json` 的 `scripts` 中删除 `build:packages` / `build:lib`;`script/dev.ts` **不改**;`build:game` 保留(按 OI-6 调整其前置链,仍保留 `vue-tsc --noEmit`)。"
- "**D-94 数据端独立包不得引用渲染端(取代 `checkClientData`):** 新增 Vite 插件在**数据端构建**时扫描模块图;命中渲染端模块(`@user/client-*`、`@user/entry-client`、`@motajs/render*`、`@motajs/client*`、`@motajs/system`、`@motajs/audio`、`@motajs/legacy-*`、`vue`、`vue/*`、`@vue/*`、`ant-design-vue`、`@ant-design/icons-vue`,以及对应的 `packages-user/client-*` / `packages/render*` / `packages/system` / `packages/audio` / `packages/legacy-*` / `src/client/**` / `node_modules/vue|@vue|ant-design-vue|@ant-design` 解析路径)即 `this.error(...)` **报错并终止**构建;数据端自身依赖(`@user/data-*`、`@motajs/common`、`@motajs/loader`、`@motajs/types`、`@motajs/animate`、`anon-tokyo` 等)**不得**误判。**`checkClientData`、`getFileName`、`getFileSize`、`ClientDataLevel` 一并删除**。"
- "**D-95 数据端产物:** 数据端构建改为 **Vite 库模式 + ESM**(`formats: ['es']`,不使用 iife),入口 `src/data.ts`,输出 `dist/data/data.js`;不生成 `data.iife.js` / `data.process.js`。**受 OI-11 约束:** 在用户修复 `src` 端跨端引用前,插件会按设计终止数据端构建,此产物**允许缺失**。"
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03 / D-93)**:Task 0 必须先汇报「任务 11 · 构建重构(适配新引擎)」的**要做什么 / 为什么 / 大致怎么做 / 影响范围**,必须列明:D-93 脚本重构与删除清单、D-94 隔离插件(specifier + 解析路径双路扫描 + `this.error` 终止 + 错误含命中 token,取代 `checkClientData`)、D-95 数据端库模式 ESM 与 `dist/data/data.js`、D-96 content 压缩(保留 `// --- SYSTEM PREFIX END --- //`)/ fontmin / 不分块 / 不 zip、D-97 dist 产物排除清单与 `LICENSE` 保留、`index.html` 去旧样式表 link、D-98 `package.json` script 调整 + `dev.md` 构建表同步、D-99 保留 `vue-tsc --noEmit`、D-100 移除 babel 二次压缩并微调 `manualChunks`、**新的构建顺序(数据端先于渲染端,二者互不阻塞)**、**数据端 ESM 为硬门禁(不放宽、不软通过)**、**当前数据端引用链(data-only,已清理)原文**、**渲染端两处 stale import 原文(`src/client/initialize.ts:14` / `src/client/plugin/shop/shop.tsx:9`,属用户 `src` WIP,用户将自行修复)**、**门禁分层(渲染端条件分支 + 数据端 ESM 必过)**、**baseline 快照与范围门禁口径**、全部拟新增命名(OI-1 / OI-2)、写操作边界(**不改 `src/**`**)、验证门禁方式与 `open_items`(OI-1..OI-12),然后必须**停下等待**;**回复「可以执行」即视为通过(本计划不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行)**;用户若对任一 `open_items` 给出指定,按指定执行。"
- "**D-93 / D-98 脚本重构与删除:** 重构 `script/build-game.ts` 与 `script/build-resource.ts`;**删除** `script/build-packages.ts` 与 `script/build-lib.ts`;从 `package.json` 的 `scripts` 中删除 `build:packages` / `build:lib`;`script/dev.ts` **不改**;`build:game` 保留(按 OI-6 调整其前置链,仍保留 `vue-tsc --noEmit`);**同步更新 `dev.md` 的构建命令表**,使其不再列出已删除的两个 script(W5)。"
- "**D-94 数据端独立包不得引用渲染端(取代 `checkClientData`):** 新增 Vite 插件在**数据端构建**时扫描模块图;命中渲染端模块(`@user/client-*`、`@user/entry-client`、`@motajs/render*`、`@motajs/client*`、`@motajs/system`、`@motajs/audio`、`@motajs/legacy-*`、`vue`、`vue/*`、`@vue/*`、`ant-design-vue`、`@ant-design/icons-vue`,以及对应的 `packages-user/client-*` / `packages/render*` / `packages/client*` / `packages/system` / `packages/audio` / `packages/legacy-*` / `src/client/**` / `node_modules/vue|@vue|ant-design-vue|@ant-design` 解析路径)即 `this.error(...)` **报错并终止**构建;**`moduleParsed` 的错误消息必须包含命中的禁用 token**(`matched: <token>`,如 `@motajs/render` / `packages/render` / `packages/render-vue` / `packages/client` / `packages/system` / `packages/audio` / `packages/legacy-` / `packages-user/client-` / `src/client`),以便 `.catch` 分类器识别;数据端自身依赖(`@user/data-*`、`@motajs/common`、`@motajs/loader`、`@motajs/anon-tokyo`、`@motajs/types`、`@motajs/animate` 等)**不得**误判。**`checkClientData`、`getFileName`、`getFileSize`、`ClientDataLevel` 一并删除**。"
- "**D-95 数据端产物(与渲染端解耦):** 数据端构建改为 **Vite 库模式 + ESM**(`formats: ['es']`,不使用 iife),入口 `src/data.ts`,输出 `dist/data/data.js`;不生成 `data.iife.js` / `data.process.js`。**构建顺序改为:准备步骤清空 `dist` 一次 → 先构建数据端 → 再构建渲染端**;`buildClient` 与 `buildData` 均设 `emptyOutDir: false`(清空只在准备步骤做一次),因此**渲染端失败不会阻断数据端产物**。**当前实测数据端闭包无任何渲染端导入**(`src/data.ts` → `src/data/entry.ts` → `./constant` / `./initialize` / `./plugin` / `./event`,仅落到 `@user/data-*`、`@motajs/common`、`@motajs/loader`、`@motajs/anon-tokyo`),故 `dist/data/data.js` **必须产出且为 ESM**——这是**硬门禁**;`data.js` 缺失或非 ESM 一律判负(`bad.push` + 非零),**不存在 expected-pending 软通过、不存在 sentinel 豁免**。"
- "**D-96 content 处理:** 构建后 `src/content` → `dist/content`(路径去掉 `src` 前缀);`*.jsonc` 以 `JSON.parse + JSON.stringify` 压缩(去注释、**后缀仍为 `.jsonc`**);**压缩后必须保留且仅保留一行 `// --- SYSTEM PREFIX END --- //` 标记**,其后为压缩后的 JSON(理由:`PrefixedJSONCProcessor` 以该标记 `split` 且要求恰好 2 段,见 `packages-user/data-state/src/loader/jsoncProcessor.ts:14-18`;丢失标记会在运行期触发 error 66 并解析为 `{}`);字体用 fontmin(glyph 子集)压缩;**不分块**(不生成 `resource.<hash>.h5data`)、**不 zip**。`build-resource` 改为从 `src/content` 读取素材。"
- "**D-97 旧模板内容与 dist 产物:** 不复制 `public/**`(`copyPublicDir: false`,`public` 整体删除由用户执行,本计划不删);不产出 `editor.html`、`_server`、`_docs`、`libs`、`logo.png`、`styles.css`、`main.js`、`script/template/启动服务.exe`;不打包 `source-code.zip`;不生成 `loadList.json`;不生成 `dist.zip`;**保留 `LICENSE`(LGPL v3)并复制到 `dist/LICENSE`**;`index.html` 去掉旧 `<link id=\"mota-css\" ... href=\"/styles.css\">`。样式改由入口经 JS 打包引入属**用户侧 `src/**` 接线**(OI-5),本计划不改 `src/**`。"
- "**D-97 旧模板内容与 dist 产物:** 不复制 `public/**`(`copyPublicDir: false`,`public` 整体删除由用户执行,本计划不删);不产出 `editor.html`、`_server`、`_docs`、`libs`、`logo.png`、`styles.css`、`main.js`、`script/template/启动服务.exe`;不打包 `source-code.zip`;不生成 `loadList.json`;不生成 `dist.zip`;**保留 `LICENSE`(LGPL v3)并复制到 `dist/LICENSE`**;`index.html` 去掉旧样式表 link。样式改由入口经 JS 打包引入属**用户侧 `src/**` 接线**(OI-5),本计划不改 `src/**`。"
- "**D-99 / D-100:** 构建仍前置 `vue-tsc --noEmit`(保留于 `build:game`);移除 `main.js` 二次压缩(babel)步骤与相关依赖引用;`manualChunks` 按实际依赖微调——`audio` chunk(`codec-parser` / `opus-decoder` / `ogg-opus-decoder` / `@wasm-audio-decoders/ogg-vorbis`)保留,`antdv` chunk 移除(全仓源码无 `ant-design-vue` / `@ant-design/icons-vue` 导入);`@vitejs/plugin-legacy` 维持现状(目标浏览器列表不变)。"
- "**写入边界(D-56 / AGENTS.md / 用户裁定「你改 src,我做脚本」):** 只允许写 `files_modified` 所列路径;**完全不修改 `src/**`**(含 `src/bootstrap.ts`、`src/constant.ts`、`src/data/**`、`src/client/**`);不回退用户既有改动(`index.html`、`packages-user/client-modules/src/**`、`src/**` 等 WIP);不改数据端/渲染端源码、`public/**`、`vite.config.ts`、lockfile、`.planning/ROADMAP.md` / `STATE.md` / `09-CONTEXT.md` / 既有 PLAN 与 SUMMARY。"
- "**命名须先获批(AGENTS.md / D-04):** 新增文件 `script/vite-plugin-data-isolation.ts`、导出 `dataIsolationPlugin`、插件 `name: 'mota-data-isolation'`、测试文件 `script/vite-plugin-data-isolation.test.ts`(OI-1);`script/build-resource.ts` 新导出 `buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats` 与测试文件 `script/build-resource.test.ts`(OI-2)。未获批前不得落码(测试文件按 AGENTS.md 免审批,但仍列入报备)。"
- "**验证可运行性(Nyquist)与预期阻断分层:** Task 1 以**真实 Vite `build()`** 驱动插件单测(渲染端 specifier / 渲染端解析路径 / `src/client` 渲染目录分别被拒;纯数据端通过并输出 ESM);Task 2 以 `minifyJSONC` / `buildContent` 单测 + 构建脚本结构断言 + **渲染端 + content + LICENSE 产物门禁(今天即可通过)** + `git status` 范围门禁兜底;`pnpm exec tsx script/build-game.ts` **必须实跑**:数据端因 OI-11 已知跨端引用被插件按设计终止时,构建脚本记录为**预期 pending** 并继续产出渲染端 + content + LICENSE,**不得**据此判定本计划失败;`dist/data/data.js` 作为**条件门禁**(存在则断言 ESM,缺失则记录 OI-11 pending)。**不得**为通过门禁而修改 `src/**` 或包源码。"
- "**写入边界(D-56 / AGENTS.md / 用户裁定「你改 src,我做脚本」):** 只允许写 `files_modified` 所列路径;**完全不修改 `src/**`**(含 `src/bootstrap.ts`、`src/data/**`、`src/client/**`,以及用户待修复的两处 stale import);不回退用户既有改动(`index.html`、`packages-user/client-modules/src/**`、`src/**` 等 WIP);不改数据端/渲染端源码、`public/**`、`vite.config.ts`、lockfile、`.planning/ROADMAP.md` / `STATE.md` / `09-CONTEXT.md` / 既有 PLAN 与 SUMMARY。`dev.md` 仅按 W5 更新构建命令表中的 `build:packages` / `build:lib` / `build:game` 三行。**范围门禁以 Task 0 的 baseline 快照为基线:任何相对 baseline 新增或被二次修改的 `src/**` / `09-CONTEXT.md` 路径一律判负,不得整体白名单放行 `src/`。**"
- "**命名须先获批(AGENTS.md / D-04):** 新增文件 `script/vite-plugin-data-isolation.ts`、导出 `dataIsolationPlugin`、插件 `name: 'mota-data-isolation'`、测试文件 `script/vite-plugin-data-isolation.test.ts`(OI-1);`script/build-resource.ts` 新导出 `buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats` 与测试文件 `script/build-resource.test.ts`(OI-2)。**类型一致性(W2):`IContentBuildOptions.chars` 为 `string`,`getAllChars` 相应返回 `Promise<string>`(`[...set].join('')`),`Fontmin.glyph({ text })` 收到的是字符串。** 未获批前不得落码(测试文件按 AGENTS.md 免审批,但仍列入报备)。"
- "**验证可运行性(Nyquist)与门禁分层:** Task 1 以**真实 Vite `build()`** 驱动插件单测(渲染端 specifier(**未设 alias 的 `vue`**)被拒 / 渲染端解析路径(`packages-user/client-*`、`packages/render*`、`packages/client*`、`packages/system`、`packages/audio`、`packages/legacy-*`、`src/client`)被拒**且错误含命中 token** / 纯数据端通过并输出 ESM / 数据端路径不误判);Task 2 以 `minifyJSONC` / `buildContent` 单测 + 构建脚本结构断言(含**数据先于渲染的调用顺序**、**`dist` 只清空一次**、**两处 `emptyOutDir: false`**)+ 产物门禁 + **baseline 范围门禁**兜底;`pnpm exec tsx script/build-game.ts` **必须实跑**。**数据端 ESM 是独立硬门禁**:数据端先构建,`dist/data/data.js` 必须存在且含 `export`,否则 `bad.push` + 非零;**不因渲染端失败而跳过**。**渲染端产物门禁是条件分支**:`dist/index.html` 存在 → 断言不含 `styles.css` link;不存在 → 打印 BLOCKED 说明(渲染端因用户 `src/**` stale import 失败),而 `dist/data/data.js`、`dist/content/**`、`dist/LICENSE` 仍照常断言。**不存在任何无条件通过承诺。**"
artifacts:
- path: script/vite-plugin-data-isolation.ts
provides: "新增 Vite 插件 `dataIsolationPlugin`:`enforce: 'pre'` + `resolveId` 拦渲染端 specifier,`moduleParsed` 按解析后绝对路径拦渲染端包(含传递依赖与 `src/client/**`),命中即 `this.error(...)` 终止数据端构建(命名待批准,OI-1)"
provides: "新增 Vite 插件 `dataIsolationPlugin`:`enforce: 'pre'` + `resolveId` 拦渲染端 specifier,`moduleParsed` 按解析后绝对路径拦渲染端包(含传递依赖与 `src/client/**`),命中即 `this.error(...)` 终止数据端构建;错误消息含 `[mota-data-isolation]` + 命中模块 + **命中 token(`matched: <token>`)** + importers(命名待批准,OI-1)"
- path: script/vite-plugin-data-isolation.test.ts
provides: "运行期单测:真实 `vite.build()`(`configFile:false`、临时目录 fixture、`write:false`)验证「渲染端 specifier 被拒」「渲染端解析路径被拒」「`src/client` 渲染目录被拒」「纯数据端通过且产物为 ESM」「数据端路径不误判」;每个 `it` 前一行注释(AGENTS.md)"
provides: "运行期单测:真实 `vite.build()`(`configFile:false`、临时目录 fixture、`write:false`)验证「渲染端 specifier 被拒(未设 alias 的 `vue`)」「`packages-user/client-*` 解析路径被拒且错误含 token」「`src/client` 渲染目录被拒」「`packages/render` / `packages/render-vue` / `packages/system` / `packages/audio` / `packages/legacy-ui` 解析路径被拒且错误含各自 token」「纯数据端通过且产物为 ESM」「数据端路径不误判」;每个 `it` 前一行注释(AGENTS.md)"
- path: script/build-resource.ts
provides: "改为 content 处理模块:`minifyJSONC`(保留 `// --- SYSTEM PREFIX END --- //` 标记 + `JSON.parse/stringify` 去注释)、`buildContent`(递归 `src/content` → `dist/content`:jsonc 压缩、fontmin glyph 子集压缩字体、其它素材原样拷贝;无字体二进制时跳过并告警)与结果/选项接口(命名待批准,OI-2)"
provides: "改为 content 处理模块:`minifyJSONC`(保留 `// --- SYSTEM PREFIX END --- //` 标记 + `JSON.parse/stringify` 去注释)、`buildContent`(递归 `src/content` → `dist/content`:jsonc 压缩、fontmin glyph 子集压缩字体、其它素材原样拷贝;无字体二进制时跳过并告警)与结果/选项接口(`IContentBuildOptions.chars: string`;命名待批准,OI-2)"
- path: script/build-resource.test.ts
provides: "运行期单测:`minifyJSONC` 保留标记且压缩、去注释、非法 JSON 抛错、无标记纯 JSON 处理;`buildContent` 在临时目录完成 jsonc 压缩 + PNG 原样拷贝 + 无字体时跳过;每个 `it` 前一行注释"
provides: "运行期单测:`minifyJSONC` 保留标记且压缩、去注释、非法 JSON 抛错、无标记纯 JSON 处理;`buildContent` 在临时目录完成 jsonc 压缩 + PNG 原样拷贝 + 无字体时跳过(`chars` 以 `string` 传入);每个 `it` 前一行注释"
- path: script/build-game.ts
provides: "重写编排:渲染端构建到 `dist`(`copyPublicDir:false`、`manualChunks` 微调、移除 babel/zip/资源地图/资源分块/旧模板拷贝)→ 收集字体字符 → 数据端 ESM 库构建到 `dist/data`(注入 `dataIsolationPlugin`;OI-11 预期终止被分类记录后继续)→ `buildContent` 处理 `dist/content` → 复制 `LICENSE` → 输出体积与数据端状态;删除 `checkClientData` 等旧逻辑"
provides: "重写编排:`emptyDir(dist)` 一次 → **先**数据端 ESM 库构建到 `dist/data`(注入 `dataIsolationPlugin`;严格签名分类;失败不阻断后续诊断)→ **后**渲染端构建到 `dist`(`copyPublicDir:false`、`emptyOutDir:false`、`manualChunks` 微调、移除 babel/zip/资源地图/资源分块/旧模板拷贝;失败打印 BLOCKED 但不覆盖数据端)→ `getAllChars`(返回 `Promise<string>`)→ `buildContent` 处理 `dist/content` → 复制 `LICENSE` → 输出体积与数据端状态;Windows 路径用单反斜杠正则归一化;删除 `checkClientData` 等旧逻辑"
- path: script/build-packages.ts
provides: "**删除**(D-93 / D-98)"
- path: script/build-lib.ts
@ -53,82 +54,98 @@ must_haves:
provides: "删除 `build:packages` / `build:lib`;`build:game` 保留 `vue-tsc --noEmit`(按 OI-6 去掉 `declare.ts` 前置)"
- path: index.html
provides: "移除旧样式表 link(保留其余 head/body 内容与 `src/bootstrap.ts` module script)"
- path: dev.md
provides: "构建命令表同步:删除 `build:packages` / `build:lib` 两行,更新 `build:game` 说明(类型检查 + 渲染端游戏 + 数据端 ESM + content);不改其它章节"
- path: .planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md
provides: "本计划执行摘要:范围、插件扫描口径与错误行为、content 压缩口径(含标记保留这一兼容性约束)、dist 产物清单与排除项、删除清单、`open_items` 最终处置、门禁结果、OI-11 预期阻断的完整错误与归因、渲染端 + content + LICENSE 产物结果"
provides: "本计划执行摘要:范围、插件扫描口径与错误行为(含命中 token)、content 压缩口径(含标记保留这一兼容性约束)、dist 产物清单与排除项、删除清单、`dev.md` 同步、`open_items` 最终处置、门禁结果(渲染端 BLOCKED 分支或成功分支)、数据端 ESM 硬门禁结果、渲染端两处 stale import 的当前状态、baseline 范围门禁结果"
key_links:
- "`dataIsolationPlugin`(`script/vite-plugin-data-isolation.ts`)↔ `buildData` 的 `plugins` 数组(`script/build-game.ts`)—— D-94 的唯一执行点;取代 `checkClientData` 体积启发式"
- "插件 `this.error` 错误文本(含 `mota-data-isolation` 与命中标识)↔ `buildGame` 的数据端 `.catch` 分类 —— 只有匹配 OI-11 已知链(`@motajs/render` 或 `src/client`)才记为预期 pending;其余数据端错误一律 `process.exit(1)`"
- "`buildData` 的 `build.lib.fileName: 'data'` + `formats: ['es']` ↔ `dist/data/data.js` —— D-95 产物契约(受 OI-11 约束,允许暂缺)"
- "插件 `this.error` 错误文本(含 `mota-data-isolation`、命中模块与**命中 token**)↔ `buildGame` 的数据端 `.catch` 分类 —— 归一化后(`String(reason)` 上把 Windows 反斜杠替换为 `/`,**源码单反斜杠正则**)同时匹配插件标识与已知渲染端 token(`@motajs/render` / `packages/render` / `packages/render-vue` / `packages/client` / `packages/system` / `packages/audio` / `packages/legacy-` / `packages-user/client-` / `src/client`)才标记为 **OI-11 回归诊断**;其它数据端错误标记为普通数据端失败;两条路径都使最终退出码非零(数据端 ESM 是硬门禁)"
- "`buildGame` 的调用顺序 `await buildData(` 先于 `await buildClient(` ↔ 数据端产物与渲染端解耦(D-95 / B1)—— 渲染端失败不会抹掉 `dist/data/data.js`;清空 `dist` 只在准备步骤执行一次,两次构建均 `emptyOutDir: false`"
- "`buildData` 的 `build.lib.fileName: 'data'` + `formats: ['es']` ↔ `dist/data/data.js` —— D-95 产物契约(硬门禁)"
- "`minifyJSONC` 的标记保留 ↔ `PrefixedJSONCProcessor.process`(`packages-user/data-state/src/loader/jsoncProcessor.ts:14-18`,`split(prefixSplit)` 要求恰好 2 段)—— 丢失标记会导致运行期 error 66 / `{}`"
- "`dist/content/**` ↔ `MotaDataLoader.resolveURL`(`packages-user/data-state/src/loader/loader.ts:74-82`,生产态为 `${BASE_URL}${url}`)与 `src/constant.ts` 的 `CORE_URL='content/core.jsonc'` / `CLIENT_CORE_URL='content/client.jsonc'` —— 目录落点契约"
- "`getAllChars` 返回 `Promise<string>` ↔ `IContentBuildOptions.chars: string` ↔ `Fontmin.glyph({ text })` —— W2 类型一致性;不得让 `Set<string>` 流入 `chars`"
- "`dist/content/**` ↔ `MotaDataLoader.resolveURL`(`packages-user/data-state/src/loader/loader.ts:74-82`,生产态为 `${BASE_URL}${url}`)与 `src/data/constant.ts` 的 `CORE_URL='content/core.jsonc'` / `src/client/constant.ts` 的 `CLIENT_CORE_URL='content/client.jsonc'` —— 目录落点契约"
- "`buildClient` 的 `copyPublicDir: false` ↔ D-97 —— 阻止旧 `public/**` 泄入 `dist`"
- "`package.json` 的 `build:game` ↔ `script/build-game.ts` / `vue-tsc --noEmit` —— D-98 / D-99"
- "`dev.md` 构建命令表 ↔ `package.json` scripts —— 删除 `build:packages` / `build:lib` 后文档不得再宣传这两个命令(W5)"
- "`index.html` 去 link ↔ 用户在 `src/**` 侧新增样式 import(OI-5,用户任务)—— D-97 样式链路由用户接线,本计划只负责移除 link"
- "Task 0 的 baseline 快照(`os.tmpdir()/gsd-09-21-baseline.json`,含 `git status --porcelain -uall` 路径哈希 + `git diff --name-only -- src`)↔ Task 2 的 baseline 范围门禁 —— 证明相对 baseline **零新增 / 零二次修改** `src/**` 与 `09-CONTEXT.md`(W4)"
assumptions:
- "**content 全部为「前缀注释 + 严格 JSON」且共 23 个:** 实测 `src/content/**/*.jsonc` 共 **23** 个文件,每个都**恰好包含一次** `// --- SYSTEM PREFIX END --- //`(含 `core.jsonc` / `client.jsonc` / `enemy.jsonc` / `item.jsonc` / `tile.jsonc` / `defaults/*` / `maps/*.jsonc` / `animation/.list.jsonc` / `audio/.list.jsonc` / `font/.list.jsonc` / `texture/.list.jsonc`);`PrefixedJSONCProcessor` 要求标记恰好出现一次且 `split` 后恰 2 段,故压缩必须保留该行。"
- "**content 全部为「前缀注释 + 严格 JSON」且共 23 个:** 实测 `src/content/**/*.jsonc` 共 **23** 个文件(`animation/.list.jsonc` / `audio/.list.jsonc` / `client.jsonc` / `core.jsonc` / 7 个 `defaults/*` / `enemy.jsonc` / `font/.list.jsonc` / `item.jsonc` / `maps/.list.jsonc` / 5 个 `maps/*.jsonc` / `texture/.list.jsonc` / `tile.jsonc`),每个都**恰好包含一次** `// --- SYSTEM PREFIX END --- //`;`PrefixedJSONCProcessor` 要求标记恰好出现一次且 `split` 后恰 2 段,故压缩必须保留该行。"
- "**生产态 content URL 以 `BASE_URL` 为根:** `MotaDataLoader.resolveURL` 在 `import.meta.env.DEV === false` 时返回 `${BASE_URL}${url}`;核心配置传 `content/core.jsonc` / `content/client.jsonc`,额外配置传 `content/${url}`,故 `dist/content/**` 是唯一正确落点。"
- "**字体目录当前无字体二进制:** `src/content/font/` 仅有 `.list.jsonc`(内容 `[\"testFont.ttf\"]`),`public/project/fonts/` 不存在;fontmin 无源可压,计划默认**跳过并告警**(OI-4),不得因缺字体阻断构建。"
- "**数据端入口存在跨端引用链(实测 2026-10-19,OI-11):** `src/data.ts` → `src/data/entry.ts:2`(`import { CORE_URL, HERO_FACE } from '../constant'`)→ `src/constant.ts:2`(`import { MAIN_HEIGHT, MAIN_WIDTH } from './client/ui/constant'`)→ `src/client/ui/constant.ts:1`(`import { ElementLocator, Font } from '@motajs/render'`,`Font` 以 `new Font('Verdana', 16)` 值使用)。因此 D-94 隔离插件会在数据端构建时按设计 `this.error` 终止,`dist/data/data.js` 在用户打断该链路前**不可产出**。链路修复(拆分 `src/constant.ts`、内联常量、把渲染常量移出数据端依赖等)是**用户任务**,在本计划 `files_modified` 之外。"
- "**`src/bootstrap.ts` 现指向工作树内的文件(上轮判断已过时):** 实测 `src/bootstrap.ts:2` 为 `import { createClientCore } from './client/entry'`、`:4` 为 `import { LoadSceneUI } from './client/ui/load'`,且 `src/client/ui/load.tsx` 存在。渲染端构建不再因「UI 文件被移动」而阻断。"
- "**数据端闭包已清理且无渲染端导入(实测 2026-10-19,revision 4):** `src/data.ts` → `src/data/entry.ts`(`@user/data-state`、`./constant`、`@motajs/loader`、`./initialize`、`./plugin`、`./event`);`src/data/initialize.ts` / `src/data/enemy/**` / `src/data/hook/**` / `src/data/plugin/**` 等落到 `@user/data-common` / `@user/data-base` / `@user/data-system` / `@user/data-state` / `@motajs/common` / `@motajs/loader` / `@motajs/anon-tokyo`;**对 `src/data/**` 的检索未发现 `@motajs/render` / `@user/client-*` / `vue` / `ant-design-vue` 导入**。历史上的跨端引用链(`src/data/entry.ts` → `../constant` → `src/constant.ts` → `./client/ui/constant` → `@motajs/render`)已由用户的 `src` 重构**解决**:`src/constant.ts` 已删除,拆分为数据侧 `src/data/constant.ts`(仅 `@user/data-common`)与渲染侧 `src/client/constant.ts`。"
- "**渲染端仍有两处 stale import(用户 WIP,用户将修复 `src/**`):** `src/client/initialize.ts:14` 为 `} from '../constant';`(已删除文件,应为 `'./constant'`);`src/client/plugin/shop/shop.tsx:9` 为 `import { RENDER_WIDTH } from '@/constant';`(alias 指向已删除的 `src/constant.ts`,应为 `'@/client/constant'`)。因此**渲染端构建当前可能失败**;本计划不修 `src/**`,产物门禁必须接受条件分支。"
- "**`src/bootstrap.ts` 现指向工作树内的文件:** 实测 `src/bootstrap.ts:2` 为 `import { createClientCore } from './client/entry'`、`:4` 为 `import { LoadSceneUI } from './client/ui/load'`,且 `src/client/ui/load.tsx` 存在。渲染端构建不因「UI 文件被移动」而阻断;当前唯一已知的 `src` 侧阻断是上述两处 stale import。"
- "**插件系统已由用户接线:** `src/data/plugin/index.ts` / `src/data/plugin/list.ts`(`dataPlugins`)、`src/data/initialize.ts`(`MainDataPlugin`)、`src/client/plugin/**` 均存在(09-20 引擎侧 + 用户入口接线)。本计划只消费其存在性,不改其源码。"
- "**`src/content/item.ts` 是源码而非运行时素材:** `core.jsonc` 的 `itemEffectDir` 指向 `item.js`,但 `src/content` 中只有 `item.ts` 且当前无消费者(用户 WIP);本计划按 D-96 原样复制 content 目录,不改该数据/不改加载器(OI-8)。"
- "**`ant-design-vue` 已不再被源码引用:** 对 `packages/`、`packages-user/`、`src/` 的源码检索(排除 `node_modules`)未发现 `ant-design-vue` / `@ant-design/icons-vue` 导入;`audio` 系依赖仍由 `@motajs/audio` 使用(`codec-parser` / `ogg-opus-decoder`)。"
- "**`vite.config.ts` 提供 `base: './'` 与 `@motajs/*` / `@user/*` / `@dp/*` / `@/` 别名:** 数据端构建复用 `loadConfigFromFile` + `mergeConfig`,因此插件可在 `enforce: 'pre'` 阶段先拦未解析 specifier,再由 `moduleParsed` 按解析后路径兜底(即使别名解析先于用户插件执行,路径检查仍能拦截)。"
- "**构建顺序安全:** 渲染端构建先执行并 `emptyOutDir` 清空 `dist`;数据端构建 `emptyOutDir:false` 写入 `dist/data`;content 最后写入 `dist/content`;`LICENSE` 最后复制。数据端按预期失败时,`dist` 中渲染端产物不受影响。"
- "**`ant-design-vue` 已不再被源码引用:** 对 `packages/`、`packages-user/`、`src/` 的源码检索(排除 `node_modules`)未发现 `ant-design-vue` / `@ant-design/icons-vue` 导入(仅旧 `script/build-game.ts` 的 `manualChunks` 命中);`audio` 系依赖仍由 `@motajs/audio` 使用(`codec-parser` / `ogg-opus-decoder`)。`package.json` 的依赖条目仍保留、不删除(OI-10)。"
- "**`vite.config.ts` 提供 `base: './'` 与 `@motajs/*` / `@user/*` / `@dp/*` / `@/` 别名:** 数据端构建复用 `loadConfigFromFile` + `mergeConfig`,因此插件可在 `enforce: 'pre'` 阶段先拦未解析 specifier,再由 `moduleParsed` 按解析后路径兜底。**单测中不得给 `vue` 设 `resolve.alias`**(alias 解析可能先于 `enforce:'pre'` 插件),否则 specifier 守卫未被真正执行(W2)。"
- "**构建顺序与清空策略(revision 4):** 准备步骤 `ensureDir(dist)` + `emptyDir(dist)` **只执行一次**;随后**先数据端**(`dist/data`)、**后渲染端**(`dist`),两者均 `emptyOutDir: false`,故渲染端失败不会删除数据端产物;content 与 `LICENSE` 在两次构建之后执行,且**无论渲染端成败都会尝试产出**。数据端失败不阻断 content / `LICENSE`(用于诊断与门禁),但最终退出码非零。"
- "**`dev.md` 构建表已过期(W5):** `dev.md:28-37` 仍列出 `build:packages` / `build:lib`;本计划删除这两个 script 后必须同步该表(`build:game` 行保留并更新说明),否则文档与 `package.json` 不一致。"
- "**行号可能漂移:** 本计划引用行号为规划日实测;门禁以「文件存在 + 结构/行为断言」为准,不硬编码行号。"
prohibitions:
- "不得在用户回复通过 Task 0 关卡之前执行本计划的任何任务、或派发任何执行子代理。"
- "**未经批准的命名一律不得使用:** `script/vite-plugin-data-isolation.ts` / `dataIsolationPlugin` / `mota-data-isolation` / `script/vite-plugin-data-isolation.test.ts`(OI-1)与 `buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats` / `script/build-resource.test.ts`(OI-2)必须先经 Task 0 报备并获批准;未获批前不得落码。"
- "**content 压缩必须保留标记行:** 不得只输出 `JSON.stringify` 结果而丢掉 `// --- SYSTEM PREFIX END --- //`;不得改后缀(仍为 `.jsonc`)。此为本计划对 D-96 的**兼容性补充**(OI-3),不是范围扩张。"
- "**构建顺序不得倒置:** 不得先构建渲染端再构建数据端;不得在 `buildClient` 或 `buildData` 内使用 `emptyOutDir: true`(会删除已产出的 `dist/data`);不得让渲染端失败以任何方式跳过后面的 content / `LICENSE` / 产物断言。"
- "**数据端门禁不得软化:** 不得为 `dist/data/data.js` 缺失写 expected-pending 软通过、不得写 sentinel 豁免、不得以「渲染端失败」为由跳过数据端 ESM 断言。严格签名只用于把数据端错误标记为 OI-11 回归诊断,最终退出码仍为非零。"
- "**不得删除 `public/**`:** D-97 的「整体删除」由用户执行;本计划只通过 `copyPublicDir: false` 阻止其进入 `dist`。"
- "**写入边界(D-56 + 用户裁定「你改 src,我做脚本」):** 只允许写 `files_modified`;**不得修改 `src/**` 的任何文件**(包括 `src/bootstrap.ts`、`src/constant.ts`、`src/data/**`、`src/client/**`);不得改 `vite.config.ts`、`packages/**`、`packages-user/**`、`public/**`、`script/dev.ts`、`script/declare.ts`、`script/types.ts`、`script/utils.ts`、lockfile;不得回退用户在工作树中的任何既有改动。"
- "**`.planning/**` 边界:** 不得改 `ROADMAP.md` / `STATE.md` / `09-CONTEXT.md` / 任何既有 PLAN 或 SUMMARY;只允许新增 `09-21-SUMMARY.md`(与 09-19 / 09-20 的既有做法一致)。"
- "**写入边界(D-56 + 用户裁定「你改 src,我做脚本」):** 只允许写 `files_modified`;**不得修改 `src/**` 的任何文件**(包括 `src/bootstrap.ts`、`src/data/**`、`src/client/**`、两处 stale import);不得改 `vite.config.ts`、`packages/**`、`packages-user/**`、`public/**`、`script/dev.ts`、`script/declare.ts`、`script/types.ts`、`script/utils.ts`、lockfile;不得回退用户在工作树中的任何既有改动。`dev.md` 只允许改构建命令表三行。"
- "**`.planning/**` 边界:** 不得改 `ROADMAP.md` / `STATE.md` / `09-CONTEXT.md` / 任何既有 PLAN 或 SUMMARY;只允许新增 `09-21-SUMMARY.md`(与 09-19 / 09-20 的既有做法一致)。范围门禁以 baseline 快照为准,**不得整体白名单放行 `src/` 或 `09-CONTEXT.md`**。"
- "**不新增依赖:** 不得改 `package.json` 的 `dependencies` / `devDependencies`、不得改 lockfile;`vite-plugin-dts` 虽因 `build-lib.ts` 删除而失去使用点,也**不删除**(OI-10)。"
- "**不得改既有注释 / jsDoc(AGENTS.md):** 重写 `build-game.ts` / `build-resource.ts` 时保留/更新其内部注释属重写范围,但不得改动其它文件的既有注释;新增注释须遵循 dev.md。"
- "**不得以全仓门禁代替:** 不设 `pnpm check:type` / `pnpm build` / 全量 `pnpm test:ci` 作为通过条件(用户 WIP 存在历史类型错误)。"
- "**不得为通过门禁而修改范围外文件:** 数据端若因 OI-11 按预期终止,只能记录归因;渲染端若因用户其它 WIP 失败,也只能记录归因;不得修 `src/**` 或包源码。"
- "**不得把预期阻断当作通过一切数据端错误的借口:** `.catch` 分类必须同时匹配插件错误标识与 OI-11 已知链(`@motajs/render` 或 `src/client`);任何不匹配的数据端错误必须 `process.exit(1)`,不得吞掉。"
- "**不得为通过门禁而修改范围外文件:** 数据端若出现严格签名,只能记录 OI-11 回归诊断;渲染端若因用户两处 stale import 或其它 WIP 失败,也只能记录完整错误与归因;不得修 `src/**` 或包源码。"
- "**不得伪造条件门禁:** 渲染端失败时不得手写 `dist/index.html` 以骗过产物门禁;不得把 BLOCKED 分支描述为「通过」;不得给出任何无条件通过承诺。注意:content 与 `LICENSE` 是构建脚本**正常产出**的,不是手写补的。"
- "**不得使用双反斜杠正则做路径归一化:** 源码必须是单反斜杠正则(`String(reason)` 上把 Windows 反斜杠替换为 `/`),不得写成四反斜杠(在 Windows 上会把两个反斜杠误当一次匹配,导致分类失效)。"
- "**不得把被删除的旧 pipeline 标识符写进注释或字符串:** Task 2 的结构门禁对 `transformAsync` / `archiver` / `compressing` / `checkClientData` / `ClientDataLevel` / `getFileName` / `getFileSize` / `generateResourceJSON` / `splitResource` / `source-code.zip` / `loadList.json` / `dist.zip` / `data.iife.js` / `_temp` 做负向检查,残留会被判负。"
open_items:
- "**OI-1(插件文件与导出命名):** 提议新增 `script/vite-plugin-data-isolation.ts`,导出 `dataIsolationPlugin(): Plugin`,插件 `name: 'mota-data-isolation'`;测试文件 `script/vite-plugin-data-isolation.test.ts`。备选命名:`script/build-plugin-data-isolation.ts` / `forbidRenderDependencies` / `renderIsolationPlugin`。**待用户报备批准**(D-04;测试文件按 AGENTS.md 免审批,但仍一并报备)。"
- "**OI-2(content 模块命名):** 提议 `script/build-resource.ts` 导出 `buildContent(options: IContentBuildOptions): Promise<IContentBuildStats>`、`minifyJSONC(text: string): string`,接口 `IContentBuildOptions`(`sourceDir` / `outputDir` / `chars`)与 `IContentBuildStats`(`files` / `jsonc` / `fonts`);测试文件 `script/build-resource.test.ts`。备选:`processContent` / `compressJSONC`。**待批准**(dev.md 要求对象入参声明 interface)。"
- "**OI-2(content 模块命名与类型):** 提议 `script/build-resource.ts` 导出 `buildContent(options: IContentBuildOptions): Promise<IContentBuildStats>`、`minifyJSONC(text: string): string`,接口 `IContentBuildOptions`(`sourceDir` / `outputDir` / **`chars: string`**)与 `IContentBuildStats`(`files` / `jsonc` / `fonts`);测试文件 `script/build-resource.test.ts`。**W2 类型一致性:`getAllChars` 返回 `Promise<string>`,`chars` 不做 `Set`。** 备选:`processContent` / `compressJSONC`。**待批准**(dev.md 要求对象入参声明 interface)。"
- "**OI-3(jsonc 标记保留与 `Type:` 注释丢失):** 提议压缩输出 = `// --- SYSTEM PREFIX END --- //` 一行 + 压缩 JSON;`// Type: ...` 等标记前注释**全部丢失**(含 `texture/.list.jsonc` 的类型指针)。D-96 说「去注释」,但加载器要求保留标记,故标记必须保留;`Type:` 指针丢失是否可接受**请用户确认**。"
- "**OI-4(字体来源):** `src/content/font/` 仅 `.list.jsonc`(引用 `testFont.ttf`),无字体二进制;提议「无字体文件时跳过 fontmin 并告警」。若用户要求压缩特定字体,请指定字体文件落点。"
- "**OI-5(样式引入落点 = 用户侧):** D-97 要求样式走 JS 打包,但 `src/styles.less` 当前无导入点;按用户裁定「你改 src,我做脚本」,**本计划不改 `src/**`**,样式 import 由用户后续在 `src/**` 侧添加。本计划只移除 `index.html` 的旧样式表 link;在用户接线前,`#game-draw` 居中样式可能缺失(记入 SUMMARY)。"
- "**OI-6(`build:game` 前置链):** 现 `build:game` = `tsx script/declare.ts && vue-tsc --noEmit && tsx script/build-game.ts`,而 `script/declare.ts` 读取 `public/project/*`(D-97 将删除 `public/**`)。提议改为 `vue-tsc --noEmit && tsx script/build-game.ts`(保留 D-99 的类型检查),**不删除** `script/declare.ts` 文件。**待确认**。"
- "**OI-7(`manualChunks` / legacy 插件):** 提议移除 `antdv` chunk、保留 `audio` chunk、`@vitejs/plugin-legacy` 维持现状(含目标浏览器列表与 `renderModernChunks:false`)。若用户希望移除 legacy 或调整目标,请在 Task 0 指明。"
- "**OI-8(`itemEffectDir` 缺文件):** `core.jsonc` 引用 `item.js`,`src/content` 仅有 `item.ts` 且无消费者。本计划原样复制 content,不生成 `item.js`、不改数据;**请确认该缺口由用户后续处理**。"
- "**OI-9(全量构建的已知阻断 = 指向 OI-11):** 唯一已测阻断是 OI-11 的数据端入口跨端引用(数据端构建按设计终止);渲染端依赖链已就位(`src/bootstrap.ts` → `./client/entry` / `./client/ui/load`,`src/client/ui/load.tsx` 存在),预期可构建。门禁分层:**渲染端 + `dist/content` + `LICENSE` 为必过**;`dist/data/data.js` 为条件门禁(存在则断言 ESM,缺失则记为 OI-11 pending)。若渲染端仍因用户其它 WIP 失败,记录完整错误与归因,不改范围外文件。**待确认**。"
- "**OI-9(全量构建的可验证性 = 数据端硬门禁 + 渲染端条件分层):** 当前实测:**数据端闭包无渲染端导入**,`dist/data/data.js` **必须产出并断言 ESM(硬失败,无 pending 豁免)**;**渲染端仍有两处 stale import**(`src/client/initialize.ts:14` / `src/client/plugin/shop/shop.tsx:9`,用户 WIP),渲染端构建可能失败。门禁分层:数据端先构建且独立断言;渲染端成功 → 追加渲染产物断言;渲染端失败 → 打印 BLOCKED(不出 `ARTIFACT GATE FAILED`,数据端断言不受影响)。**待确认**。"
- "**OI-10(遗留依赖与文件):** `script/types.ts` 的 `RequiredData` / `RequiredIconsData` 及 `vite-plugin-dts`、`@babel/*`、`archiver`、`compressing` 等依赖在重构后可能失去使用点,但**不删除**(避免 lockfile/依赖面变更)。**请确认**保留。"
- "**OI-11(数据端入口跨端引用 · 待用户修复):** 实测链路 `src/data.ts` → `src/data/entry.ts:2`(`import { CORE_URL, HERO_FACE } from '../constant'`)→ `src/constant.ts:2`(`import { MAIN_HEIGHT, MAIN_WIDTH } from './client/ui/constant'`)→ `src/client/ui/constant.ts:1`(`import { ElementLocator, Font } from '@motajs/render'`,`Font` 以 `new Font('Verdana', 16)` 值使用)。D-94 隔离插件会按设计终止数据端构建,`dist/data/data.js` 在用户打断该链路前**不可产出**。链路修复(拆分 `src/constant.ts` / 内联常量 / 移出渲染常量等)是**用户任务**,在本计划 `files_modified` 之外;本计划不改 `src/**`。Task 2 把「插件按预期终止」记为 expected pending blocker(**不是本计划失败**),并要求渲染端 + `dist/content` + `LICENSE` 仍产出并通过门禁。"
- "**OI-11(数据端跨端引用 · 历史问题已解决):** 历史上的链路 `src/data/entry.ts` → `../constant` → `src/constant.ts` → `./client/ui/constant` → `@motajs/render` **已由用户的 `src` 重构解决**:`src/constant.ts` 已删除,数据侧改为 `src/data/constant.ts`(仅 `@user/data-common`),渲染侧为 `src/client/constant.ts`。本计划保留严格签名分类作为回归诊断:若触发,说明 `src` 又被改回跨端引用,记为 **OI-11 回归**并**按数据端失败非零退出**(不再有 pending 放行)。**待用户确认链路稳定**。"
- "**OI-12(渲染端两处 stale import · 用户待修):** `src/client/initialize.ts:14`(`'../constant'` → 应为 `'./constant'`)与 `src/client/plugin/shop/shop.tsx:9`(`'@/constant'` → 应为 `'@/client/constant'`)指向已删除的 `src/constant.ts`。按用户裁定「你改 src,我做脚本」,**本计划不改 `src/**`**;请用户确认修复时点。修复前渲染端构建失败属预期,产物门禁走 BLOCKED 分支(数据端 ESM 仍硬断言)。"
---
<objective>
Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎)**。
**要解决的问题:** 现有 `script/build-game.ts` / `script/build-resource.ts` 仍面向**旧引擎 + 旧模板**:构建产物依赖 `public/project`(旧 `data.js` / `icons.js` / `floors/` / `materials/` 等)、数据端产出 iife、按体积启发式 `checkClientData` 猜测「数据端是否引用了渲染端」、`main.js` 二次 babel 压缩、资源按 2MB 分块并 zip 成 `resource.<hash>.h5data`、拷贝 `editor.html` / `_server` / `_docs` / `libs` / `logo.png` / `styles.css` / `main.js` / `启动服务.exe`、打包 `source-code.zip`、生成 `loadList.json`、再整体压成 `dist.zip`。新引擎的数据落在 `src/content`(jsonc + 贴图 + 字体目录),加载器按 `content/**` 相对路径读取,渲染端/数据端已分家。继续沿用旧脚本既无法跑通新数据,也无法保证「数据端独立包不得引用渲染端」。
**目标(D-93..D-100):**
1. **D-94** 用 Vite 插件扫描数据端模块图,命中渲染端模块即报错终止,**取代** `checkClientData`。
2. **D-95** 数据端改为库模式 ESM,入口 `src/data.ts`,产物 `dist/data/data.js`。
1. **D-94** 用 Vite 插件扫描数据端模块图,命中渲染端模块即报错终止(错误含命中 token),**取代** `checkClientData`。
2. **D-95** 数据端改为库模式 ESM,入口 `src/data.ts`,产物 `dist/data/data.js`;**数据端先于渲染端构建,二者互不阻塞**。
3. **D-96** `src/content` → `dist/content`:jsonc 压缩(**保留系统标记行**)、fontmin 压缩字体、**不分块 / 不 zip**;`build-resource` 改为从 `src/content` 读取。
4. **D-97** dist 不再携带旧模板内容;保留 `LICENSE`;`index.html` 去掉旧样式表 link,样式由用户在 `src/**` 侧经 JS 引入(OI-5)。
5. **D-93 / D-98** 删除 `build-packages.ts` / `build-lib.ts` 及对应 npm script;`build:game` 调整并保留 `vue-tsc --noEmit`(D-99)。
5. **D-93 / D-98** 删除 `build-packages.ts` / `build-lib.ts` 及对应 npm script;`build:game` 调整并保留 `vue-tsc --noEmit`(D-99);同步 `dev.md` 构建表(W5)。
6. **D-100** 移除 `main.js` 二次压缩;`manualChunks` 按实际依赖微调(`antdv` 已无源码引用 → 移除;`audio` 保留);legacy 插件维持现状。
**关键兼容性约束(本计划对 D-96 的实现补充):** 所有 `src/content/**/*.jsonc`(**实测 23 个,每个恰好一次标记**)都是「前缀注释 + 严格 JSON」,运行期由 `PrefixedJSONCProcessor` 以 `// --- SYSTEM PREFIX END --- //` **split** 并要求恰好 2 段(`packages-user/data-state/src/loader/jsoncProcessor.ts:14-18`)。因此「`JSON.parse + JSON.stringify` 去注释」**必须保留该标记行**;否则运行期触发 error 66、配置解析为 `{}`。`// Type: ...` 等标记前注释会按 D-96 丢失(OI-3 待确认)。
**已知预期阻断(OI-11):** 数据端入口存在跨端引用链 `src/data.ts` → `src/data/entry.ts:2` → `src/constant.ts:2` → `src/client/ui/constant.ts:1`(`@motajs/render` 值使用)。D-94 插件会在数据端构建时**按设计终止**;这是**预期行为**(等待用户修复 `src`),不是本计划失败。本计划据此把门禁分层:渲染端 + `dist/content` + `LICENSE` 为必过;`dist/data/data.js` 为条件门禁。
**当前实测状态(revision 4,2026-10-19):**
- **数据端已清理且无渲染端导入:** `src/data.ts` → `src/data/entry.ts` → `./constant` / `./initialize` / `./plugin` / `./event`,闭包落到 `@user/data-*`、`@motajs/common`、`@motajs/loader`、`@motajs/anon-tokyo`;`src/constant.ts` 已删除,对 `src/data/**` 的检索未发现 `@motajs/render` / `@user/client-*` / `vue`。因此 `dist/data/data.js` **必须产出且为 ESM**,这是**硬门禁**(缺失 / 非 ESM → `bad.push` + 非零,无软通过)。
- **渲染端仍有两处 stale import(用户 WIP,用户将修复 `src/**`):** `src/client/initialize.ts:14`(`'../constant'`)、`src/client/plugin/shop/shop.tsx:9`(`'@/constant'`)。渲染端构建**当前可能失败**,因此渲染端产物门禁是**条件分支**(存在则断言、缺失则打印 BLOCKED),但**不影响数据端 ESM 的硬断言**。
**大致解决方案(本计划 = 09-21):**
- **Task 0(`checkpoint:decision@blocking-human`)= 预执行汇报关卡**:汇报要做什么 / 为什么 / 大致怎么做 / 影响范围(含插件扫描口径、content 压缩口径与标记保留理由、dist 产物清单、删除清单、`manualChunks` 调整、命名报备、验证方式、OI-11 预期阻断与门禁分层),停下等待「可以执行」。
- **Task 1(`tracer`)= 分离插件端到端最小切片**:新增 Vite 插件并在**真实 `vite.build()`** 的临时 fixture 上验证——渲染端 specifier 被拒、渲染端解析路径(含传递依赖)/ `src/client` 渲染目录被拒、纯数据端通过且产物为 ESM、数据端路径不误判。这是本增量唯一的全新机制,先用运行期测试把「扫描 + 终止」跑通。
- **Task 2 = 构建编排重写与收口**:重写 `build-resource.ts`(content 压缩/拷贝/字体)与 `build-game.ts`(渲染端 → 数据端 ESM(注入插件,OI-11 预期失败分类)→ content → LICENSE);删除旧脚本;调整 `package.json` 与 `index.html`;补 `build-resource.test.ts`;实跑全量构建并按分层门禁收口;写 `09-21-SUMMARY.md`。
- **Task 0(`checkpoint:decision@blocking-human`)= 预执行汇报关卡**:汇报要做什么 / 为什么 / 大致怎么做 / 影响范围(含插件扫描口径与命中 token、content 压缩口径与标记保留理由、dist 产物清单、删除清单、`manualChunks` 调整、`dev.md` 同步、命名报备、验证方式、当前数据端链路与两处渲染端 stale import、**数据先行的构建顺序与硬门禁**、baseline 范围门禁),并**捕获 baseline 快照**(写入系统临时目录,不写仓库),停下等待「可以执行」。
- **Task 1(`tracer`)= 分离插件端到端最小切片**:新增 Vite 插件并在**真实 `vite.build()`** 的临时 fixture 上验证——未设 alias 的 `vue` 被拒、渲染端解析路径(含 `packages-user/client-*` / `packages/render*` / `packages/client*` / `packages/system` / `packages/audio` / `packages/legacy-*` / `src/client`)被拒**且错误含命中 token**、纯数据端通过且产物为 ESM、数据端路径不误判。这是本增量唯一的全新机制,先用运行期测试把「扫描 + 终止 + 可分类错误」跑通。
- **Task 2 = 构建编排重写与收口**:重写 `build-resource.ts`(content 压缩/拷贝/字体,`chars: string`)与 `build-game.ts`(**清空一次 → 数据端 ESM(注入插件,严格签名分类)→ 渲染端(条件 BLOCKED)→ content → LICENSE**,Windows 路径用单反斜杠正则归一化);删除旧脚本;调整 `package.json`、`index.html` 与 `dev.md` 构建表;补 `build-resource.test.ts`;实跑全量构建并按硬数据门禁 + 条件渲染门禁 + baseline 范围门禁收口;写 `09-21-SUMMARY.md`。
**为何 `depends_on: []`:** 本计划只改构建脚本与其直接产物契约,不依赖 09-19 / 09-20 的源码改动;它依赖的是**已存在**的数据端加载器、`src/content` 数据与 `src/data.ts` 入口(均已就位)。因此可与既有计划解耦,独占 wave 18。
**为何 `depends_on: [09-19, 09-20]`(W1):** 本计划消费 09-19 产出的 `src/content` 数据落点/素材与 09-20 的插件系统(`src/data/plugin/**` / `src/client/plugin/**` 由用户接线),二者的产物契约必须在场;本计划不修改其源码,只按契约构建与打包,因此 wave 取 `max(16, 17) + 1 = 18`。
**API coverage:** `detected:false`。**`No external API integration: 本计划为本地构建流程重构,不涉及任何外部 API。`**
**修改方式声明(AGENTS.md):** 本计划执行含**文件写入与删除**(新增插件与测试、重写两个构建脚本、删除两个旧脚本、改 `package.json` / `index.html`、写 SUMMARY),将由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 所列路径外无任何写入,**`src/**` 零改动**。
**修改方式声明(AGENTS.md):** 本计划执行含**文件写入与删除**(新增插件与测试、重写两个构建脚本、删除两个旧脚本、改 `package.json` / `index.html` / `dev.md` 构建表、写 SUMMARY),将由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 所列路径外无任何写入,**`src/**` 零改动**。
</objective>
<execution_context>
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
@ -150,8 +167,10 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
@vite.config.ts
@src/data.ts
@src/data/entry.ts
@src/constant.ts
@src/client/ui/constant.ts
@src/data/constant.ts
@src/client/constant.ts
@src/client/initialize.ts
@src/client/plugin/shop/shop.tsx
@src/bootstrap.ts
@packages-user/data-state/src/loader/jsoncProcessor.ts
@packages-user/data-state/src/loader/loader.ts
@ -160,93 +179,100 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 11 · 构建重构(适配新引擎)」的要做什么 / 为什么 / 大致怎么做 / 影响范围(含 OI-11 预期阻断与门禁分层),等待用户回复「可以执行」</name>
<precondition>`script/build-game.ts`(703 行)与 `script/build-resource.ts`(225 行)仍为旧引擎实现;`script/build-packages.ts` 与 `script/build-lib.ts` 存在;`package.json` scripts 含 `build:packages` / `build:lib` / `build:game`(`build:game` 含 `tsx script/declare.ts` 与 `vue-tsc --noEmit`);`index.html` 含 `<link id="mota-css" ... href="/styles.css">`;`src/content/**` 共 **23** 个 `.jsonc`(每个恰好一个 `// --- SYSTEM PREFIX END --- //`)与 `texture/*.png`;`src/content/font/` 无字体二进制;`src/data.ts` 存在并导出 `createCoreState`;`src/bootstrap.ts` 现导入 `./client/entry` 与 `./client/ui/load`(`src/client/ui/load.tsx` 存在);**OI-11 跨端引用链存在**(`src/data.ts` → `src/data/entry.ts` → `src/constant.ts` → `src/client/ui/constant.ts` → `@motajs/render`);`script/vite-plugin-data-isolation.ts` / `script/vite-plugin-data-isolation.test.ts` / `script/build-resource.test.ts` **尚不存在**。</precondition>
<files>(只读汇报,不修改任何文件)</files>
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 11 · 构建重构(适配新引擎)」的要做什么 / 为什么 / 大致怎么做 / 影响范围(含数据先行顺序、数据端硬门禁、渲染端条件分支、baseline 范围门禁),等待用户回复「可以执行」</name>
<precondition>`script/build-game.ts`(703 行)与 `script/build-resource.ts`(225 行)仍为旧引擎实现;`script/build-packages.ts` 与 `script/build-lib.ts` 存在;`package.json` scripts 含 `build:packages` / `build:lib` / `build:game`(`build:game` 含 `tsx script/declare.ts` 与 `vue-tsc --noEmit`);`index.html` 含 `<link id="mota-css" ... href="/styles.css">`;`dev.md:28-37` 仍列出 `build:packages` / `build:lib`;`src/content/**` 共 **23** 个 `.jsonc`(每个恰好一个 `// --- SYSTEM PREFIX END --- //`)与 `texture/*.png`;`src/content/font/` 无字体二进制;`src/data.ts` 存在并导出 `createCoreState`;**数据端闭包已清理且无渲染端导入**:`src/constant.ts` 不存在,`src/data.ts` → `src/data/entry.ts`(`from './constant'` 等)→ `src/data/constant.ts`(仅 `@user/data-common`);**渲染端有两处 stale import**:`src/client/initialize.ts:14`(`'../constant'`)与 `src/client/plugin/shop/shop.tsx:9`(`'@/constant'`),属用户 `src` WIP;插件系统已接线(`src/data/plugin/list.ts` / `src/client/plugin/list.ts` / `src/data/initialize.ts`);`src/bootstrap.ts` 导入 `./client/entry` 与 `./client/ui/load`(后者存在);`script/vite-plugin-data-isolation.ts` / `script/vite-plugin-data-isolation.test.ts` / `script/build-resource.test.ts` **尚不存在**。</precondition>
<files>(只读汇报,不修改仓库文件;仅在系统临时目录写入 baseline 快照)</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(**D-93 / D-94 / D-95 / D-96 / D-97 / D-98 / D-99 / D-100**;D-03 / D-04 / D-56)
- script/build-game.ts(全文:`buildClient:40` / `buildData:91` / `checkClientData:212` / `getAllChars:254` / `generateResourceJSON:310` / `buildGame:329`,含 babel、archiver、compressing、资源分块、旧模板拷贝、`source-code.zip`、`loadList.json`、`启动服务.exe`、`dist.zip`)
- script/build-resource.ts(全文:`splitResource:123` 从 `public/project/{animates,fonts,images,sounds,tilesets,autotiles,materials}` 读取并按 2MB 分块 zip 成 `resource.<hash>.h5data`)
- script/build-packages.ts / script/build-lib.ts(待删除);script/types.ts / script/utils.ts(`formatSize`)
- package.json(只读:scripts、依赖面);index.html(只读:`<link href='/styles.css'>` 与 `/src/bootstrap.ts` module script)
- src/data.ts / src/data/entry.ts / src/client/entry.ts / src/constant.ts(`CORE_URL='content/core.jsonc'`、`CLIENT_CORE_URL='content/client.jsonc'`)/ src/client/ui/constant.ts(`@motajs/render` 值使用)
- package.json(只读:scripts、依赖面);index.html(只读:`<link href='/styles.css'>` 与 `/src/bootstrap.ts` module script);dev.md(只读:构建命令表 `dev.md:28-37`)
- src/data.ts / src/data/entry.ts / src/data/constant.ts / src/client/constant.ts(`CORE_URL='content/core.jsonc'`、`CLIENT_CORE_URL='content/client.jsonc'`、`RENDER_WIDTH`/`RENDER_HEIGHT`)
- src/client/initialize.ts(只读:`:14` stale import)与 src/client/plugin/shop/shop.tsx(只读:`:9` stale import)
- src/data/plugin/list.ts / src/client/plugin/list.ts / src/data/initialize.ts(插件系统接线现状,只读)
- packages-user/data-state/src/loader/loader.ts:70-142(`resolveURL` 生产态 = `${BASE_URL}${url}`)与 packages-user/data-state/src/loader/jsoncProcessor.ts:4-28(标记 split 必须恰好 2 段)
- src/content/**(**23 个 jsonc**:`client.jsonc` / `core.jsonc` / `enemy.jsonc` / `item.jsonc` / `tile.jsonc` / `defaults/*` / `maps/*` / `texture/.list.jsonc` / `font/.list.jsonc` / `animation/.list.jsonc` / `audio/.list.jsonc`)与 src/styles.less
- vite.config.ts(`base: './'`、`@motajs/*` / `@user/*` / `@dp/*` / `@/` 别名);`git status`(用户 WIP 面)
- vite.config.ts(`base: './'`、`@motajs/*` / `@user/*` / `@dp/*` / `@/` 别名);`git status --porcelain -uall`(用户 WIP 面)
- AGENTS.md(执行前须获批、命名须先反馈、修改方式须预先声明、禁改既有注释);dev.md(命名规则、对象类型须声明 interface、模块无副作用、非空判断约定、jsDoc 规范)
</read_first>
<action>
**停下并向用户汇报,等待用户回复「可以执行」后才继续;汇报本身只读。**
**停下并向用户汇报,等待用户回复「可以执行」后才继续;汇报本身只读(不修改仓库文件),但必须先捕获 baseline 快照。**
① **要解决的问题:** 旧构建脚本面向旧引擎与旧模板(依赖 `public/project`、数据端 iife、体积启发式、`main.js` 二次压缩、资源分块 zip、旧模板拷贝、源码 zip、`loadList.json`、`dist.zip`),无法适配新引擎的 `src/content` 数据与双端分离。
② **范围(D-93):** 重构 `script/build-game.ts` 与 `script/build-resource.ts`;删除 `script/build-packages.ts` 与 `script/build-lib.ts`;`script/dev.ts` 不改。
② **范围(D-93):** 重构 `script/build-game.ts` 与 `script/build-resource.ts`;删除 `script/build-packages.ts` 与 `script/build-lib.ts`;`script/dev.ts` 不改;同步 `dev.md` 构建命令表(W5)。
③ **D-94 分离插件:** 新增 Vite 插件,在数据端构建时扫描**解析后的模块图**:`enforce: 'pre'` 的 `resolveId` 先拦渲染端 specifier(`@user/client-*`、`@user/entry-client`、`@motajs/render*`、`@motajs/client*`、`@motajs/system`、`@motajs/audio`、`@motajs/legacy-*`、`vue`、`vue/*`、`@vue/*`、`ant-design-vue`、`@ant-design/icons-vue`),`moduleParsed` 再按绝对路径兜底(`packages-user/client-*`、`packages/render*`、`packages/system`、`packages/audio`、`packages/legacy-*`、**`src/client/**`**、`node_modules/{vue,@vue,ant-design-vue,@ant-design}`),命中即 `this.error(...)` **终止构建**;数据端自身依赖不误判。删除 `checkClientData` / `getFileName` / `getFileSize` / `ClientDataLevel`。
③ **D-94 分离插件:** 新增 Vite 插件,在数据端构建时扫描**解析后的模块图**:`enforce: 'pre'` 的 `resolveId` 先拦渲染端 specifier(`@user/client-*`、`@user/entry-client`、`@motajs/render*`、`@motajs/client*`、`@motajs/system`、`@motajs/audio`、`@motajs/legacy-*`、`vue`、`vue/*`、`@vue/*`、`ant-design-vue`、`@ant-design/icons-vue`),`moduleParsed` 再按绝对路径兜底(`packages-user/client-*`、`packages/render*`、`packages/client*`、`packages/system`、`packages/audio`、`packages/legacy-*`、**`src/client/**`**、`node_modules/{vue,@vue,ant-design-vue,@ant-design}`),命中即 `this.error(...)` **终止构建**;**错误消息必须带命中模块与命中 token**(`matched: <token>`,如 `packages/render` / `packages/render-vue` / `packages/client` / `packages/system` / `packages/audio` / `packages/legacy-` / `packages-user/client-` / `src/client` / `@motajs/render`),供数据端 `.catch` 分类;数据端自身依赖不误判。删除 `checkClientData` / `getFileName` / `getFileSize` / `ClientDataLevel`。
④ **D-95 数据端:** 库模式 + `formats: ['es']`,入口 `src/data.ts`,产物 `dist/data/data.js`;`build.lib.fileName: 'data'`(package.json `type: module` → es 扩展名 `.js`);`emptyOutDir:false`;`copyPublicDir:false`;注入分离插件。
④ **D-95 数据端(与渲染端解耦):** 库模式 + `formats: ['es']`,入口 `src/data.ts`,产物 `dist/data/data.js`;`build.lib.fileName: 'data'`(package.json `type: module` → es 扩展名 `.js`);`emptyOutDir: false`;`copyPublicDir: false`;注入分离插件。**构建顺序:准备步骤清空 `dist` 一次 → 先数据端 → 后渲染端**;`buildClient` 也设 `emptyOutDir: false`,因此**渲染端失败不会删除数据端产物**。
⑤ **D-96 content:** `src/content` → `dist/content`;`.jsonc` = **保留一行系统标记 + `JSON.stringify(JSON.parse(body))`**(去注释、后缀仍 `.jsonc`;标记必须保留,否则 `PrefixedJSONCProcessor` 运行期 error 66);字体用 fontmin glyph 子集压缩;**不分块、不 zip**。`build-resource` 改为从 `src/content` 读取素材。实测 `src/content/**` 共 **23** 个 jsonc,每个恰好一个标记。
⑥ **D-97 产物:** 不复制 `public/**`(`copyPublicDir:false`;整体删除由用户执行);dist 不携带旧模板内容(`editor.html` / `_server` / `_docs` / `libs` / `logo.png` / `styles.css` / `main.js` / `启动服务.exe`);不打包源码 zip;不生成资源地图 JSON;不生成 `dist.zip`;**复制 `LICENSE` 到 `dist/LICENSE`**;`index.html` 去掉旧样式表 link。样式 import 由用户后续在 `src/**` 侧添加(OI-5),本计划不改 `src/**`。
⑥ **D-97 产物:** 不复制 `public/**`(`copyPublicDir: false`;整体删除由用户执行);dist 不携带旧模板内容(`editor.html` / `_server` / `_docs` / `libs` / `logo.png` / `styles.css` / `main.js` / `启动服务.exe`);不打包源码 zip;不生成资源地图 JSON;不生成 `dist.zip`;**复制 `LICENSE` 到 `dist/LICENSE`**;`index.html` 去掉旧样式表 link。样式 import 由用户后续在 `src/**` 侧添加(OI-5),本计划不改 `src/**`。
⑦ **D-98 / D-99 / D-100:** 删除 `build:packages` / `build:lib`;`build:game` 调整(OI-6:移除读取已删 `public/project` 的 `declare.ts` 前置;**保留 `vue-tsc --noEmit`**);移除 babel 二次压缩;`manualChunks` 移除 `antdv`(源码已无引用)、保留 `audio`;legacy 插件维持现状。
⑦ **D-98 / D-99 / D-100:** 删除 `build:packages` / `build:lib`;`build:game` 调整(OI-6:移除读取已删 `public/project` 的 `declare.ts` 前置;**保留 `vue-tsc --noEmit`**);移除 babel 二次压缩;`manualChunks` 移除 `antdv`(源码已无引用)、保留 `audio`;legacy 插件维持现状;**`dev.md` 构建表删除 `build:packages` / `build:lib` 两行并更新 `build:game` 说明**。
⑧ **拟新增命名(须报备批准):** `script/vite-plugin-data-isolation.ts` + `dataIsolationPlugin` + 插件 `name: 'mota-data-isolation'` + 测试文件(OI-1);`buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats` + `script/build-resource.test.ts`(OI-2)。
⑧ **拟新增命名(须报备批准):** `script/vite-plugin-data-isolation.ts` + `dataIsolationPlugin` + 插件 `name: 'mota-data-isolation'` + 测试文件(OI-1);`buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats` + `script/build-resource.test.ts`(OI-2)。**类型一致性(W2):`IContentBuildOptions.chars: string`;`getAllChars` 返回 `Promise<string>`(`[...set].join('')`)。**
⑨ **待确认项:** 标记保留与 `Type:` 注释丢失(OI-3);无字体二进制的处置(OI-4);样式 import 由用户在 `src/**` 侧完成(OI-5);`build:game` 前置链(OI-6);`manualChunks` / legacy(OI-7);`itemEffectDir` 缺 `item.js`(OI-8);全量构建阻断归因(OI-9,指向 OI-11);遗留文件与依赖保留(OI-10);**数据端入口跨端引用待用户修复(OI-11)**。
⑨ **待确认项:** 标记保留与 `Type:` 注释丢失(OI-3);无字体二进制的处置(OI-4);样式 import 由用户在 `src/**` 侧完成(OI-5);`build:game` 前置链(OI-6);`manualChunks` / legacy(OI-7);`itemEffectDir` 缺 `item.js`(OI-8);全量构建的门禁分层(OI-9);遗留文件与依赖保留(OI-10);数据端历史跨端引用已解决(OI-11);渲染端两处 stale import 待用户修复(OI-12)。
⑩ **OI-11 实测跨端引用链(原样告知,必须完整复述):** `src/data.ts` → `src/data/entry.ts:2`(`import { CORE_URL, HERO_FACE } from '../constant'`)→ `src/constant.ts:2`(`import { MAIN_HEIGHT, MAIN_WIDTH } from './client/ui/constant'`)→ `src/client/ui/constant.ts:1`(`import { ElementLocator, Font } from '@motajs/render'`,`Font` 以 `new Font('Verdana', 16)` 值使用)。因此 D-94 隔离插件会在数据端构建时**按设计** `this.error` 终止;在用户打断该链路前 **`dist/data/data.js` 不可产出**。链路修复(拆分 `src/constant.ts` / 内联常量 / 把渲染常量移出数据端依赖等)是**用户任务**,在本计划 `files_modified` 之外;**本计划不改 `src/**`**。
⑩ **当前数据端引用链(data-only,原样告知):** `src/data.ts`(`export { createCoreState } from './data/entry';`)→ `src/data/entry.ts`(`import { CORE_URL, HERO_FACE } from './constant';`,并导入 `./initialize` / `./plugin` / `./event`)→ `src/data/constant.ts`(仅 `import { FaceGroup } from '@user/data-common';`;`CORE_URL='content/core.jsonc'`、`HERO_FACE=FaceGroup.Dir8`)。**对 `src/data/**` 的检索未发现 `@motajs/render` / `@user/client-*` / `vue` 导入,数据端构建应能通过。** 历史上的跨端引用(`../constant` → `src/constant.ts` → `./client/ui/constant` → `@motajs/render`)**已由用户的 `src` 重构解决**:`src/constant.ts` 已删除,拆分为数据侧 `src/data/constant.ts` 与渲染侧 `src/client/constant.ts`。
⑪ **门禁分层(必须原样告知):** 数据端构建因 OI-11 被插件按设计终止 = **预期 pending blocker,不是本计划失败**;Task 2 的构建脚本必须把「插件按预期终止」与「本计划脚本/插件错误」分开:只有错误文本同时匹配插件标识与 OI-11 已知链(`@motajs/render` 或 `src/client`)才记为预期并继续;其余数据端错误一律 `process.exit(1)`。**渲染端产物 + `dist/content` + `dist/LICENSE` 为必过门禁(今天即可通过)**;`dist/data/data.js` 为条件门禁(存在则断言 ESM,缺失则记录 OI-11 pending)。
⑪ **渲染端两处 stale import(原样告知,属用户 `src` WIP,用户将修复):** ① `src/client/initialize.ts:14` 为 `} from '../constant';`(目标文件已删除,应为 `'./constant'`);② `src/client/plugin/shop/shop.tsx:9` 为 `import { RENDER_WIDTH } from '@/constant';`(`@/` 别名指向已删除的 `src/constant.ts`,应为 `'@/client/constant'`)。因此**渲染端构建当前可能失败**;本计划**不改 `src/**`**,须在汇报中说明修复属用户任务。
⑫ **产出文件:** 新增 `script/vite-plugin-data-isolation.ts`、`script/vite-plugin-data-isolation.test.ts`、`script/build-resource.test.ts`;重写 `script/build-resource.ts` / `script/build-game.ts`;删除 `script/build-packages.ts` / `script/build-lib.ts`;改 `package.json` / `index.html`;新增 `09-21-SUMMARY.md`。**不修改 `src/**`。**
⑫ **门禁分层(必须原样告知):** **数据端先行且独立:`dist/data/data.js` 必须产出且为 ESM,这是硬门禁**(缺失 / 非 ESM → `bad.push` + 非零);严格签名只用于把数据端错误标记为 **OI-11 回归诊断**,**不再有 pending 软通过或 sentinel 豁免**。**渲染端是条件分支**:`dist/index.html` 存在 → 断言不含 `styles.css` link;不存在(因用户 stale import)→ 打印 **BLOCKED** 说明,且 `dist/data/data.js`、`dist/content/**`、`dist/LICENSE` 仍照常断言(content 与 LICENSE 在任何构建结果下都会尝试产出)。**不存在任何无条件通过承诺。**
⑬ **不在本增量:** 不删 `public/**`;**不改 `src/**`(零改动)**;不改数据端/渲染端源码、`vite.config.ts`、`script/dev.ts`、`script/declare.ts`、`script/types.ts`、lockfile;不新增依赖;不改 `ROADMAP.md` / `STATE.md` / `09-CONTEXT.md` / 既有 PLAN 与 SUMMARY。
⑬ **baseline 与范围门禁(W4):** Task 0 必须先执行 baseline 捕获(`git status --porcelain -uall` + `git diff --name-only -- src` + 每个路径的 SHA-256),快照写入**系统临时目录**(`gsd-09-21-baseline.json`,不写仓库)。Task 2 的范围门禁据此判定:**任何相对 baseline 新增、或被二次修改的路径**(除 `files_modified` 白名单外)一律判负;`src/**` 与 `09-CONTEXT.md` 不再整体白名单放行,仅 baseline 中已存在且哈希未变的用户 WIP 被豁免。
⑭ **已知阻断(须一并告知并记入 SUMMARY):** 唯一已测阻断 = OI-11 数据端跨端引用;渲染端依赖链已就位(`src/bootstrap.ts` → `./client/entry` / `./client/ui/load`,后者存在),此前「`bootstrap.ts` 仍指向 `packages-user/client-modules/src/ui/load`」的判断**已过时**。若渲染端仍因用户其它 WIP 失败,记录完整错误与归因,**不得**修改范围外文件。
⑭ **产出文件:** 新增 `script/vite-plugin-data-isolation.ts`、`script/vite-plugin-data-isolation.test.ts`、`script/build-resource.test.ts`;重写 `script/build-resource.ts` / `script/build-game.ts`;删除 `script/build-packages.ts` / `script/build-lib.ts`;改 `package.json` / `index.html` / `dev.md`(仅构建命令表三行);新增 `09-21-SUMMARY.md`。**不修改 `src/**`。**
⑮ **验证方式:** Task 1 = `pnpm exec vitest run script/vite-plugin-data-isolation.test.ts`(真实 `vite.build()`:拒渲染端 specifier / 拒渲染端解析路径(传递依赖)/ 拒 `src/client` 渲染目录 / 通过纯数据端并输出 ESM / 不误判数据端路径)+ 结构断言;Task 2 = `pnpm exec vitest run script/build-resource.test.ts` + 构建脚本结构断言 + **渲染端 + content + LICENSE 产物门禁** + `git status` 范围门禁 + `pnpm exec tsx script/build-game.ts` 实跑(数据端 OI-11 预期终止 → 记为 pending 并继续;条件门禁判定 `dist/data/data.js`);**不设全仓 `check:type` / `build` / 全量 `test:ci` 门禁**。
⑮ **不在本增量:** 不删 `public/**`;**不改 `src/**`(零改动,含两处 stale import)**;不改数据端/渲染端源码、`vite.config.ts`、`script/dev.ts`、`script/declare.ts`、`script/types.ts`、lockfile;不新增依赖;不改 `ROADMAP.md` / `STATE.md` / `09-CONTEXT.md` / 既有 PLAN 与 SUMMARY。
⑯ **修改方式声明(AGENTS.md):** 本计划执行含文件写入与删除,由**派发子代理执行**(`gsd-executor`),非内联执行。
⑯ **已知阻断(须一并告知并记入 SUMMARY):** ① 数据端**当前无已知阻断**(历史 OI-11 链路已解决);② 渲染端可能因两处 stale import 失败(用户 WIP,用户修复);③ 若渲染端仍因用户其它 WIP 失败,记录完整错误与归因,**不得**修改范围外文件。
⑰ **关卡性质:** **无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户回复;回复「可以执行」即视为通过。** 若用户就 OI-1..OI-11 给出指定,按指定执行。
⑰ **验证方式:** Task 1 = `pnpm exec vitest run script/vite-plugin-data-isolation.test.ts`(真实 `vite.build()`:未设 alias 的 `vue` 被拒 / 拒渲染端解析路径(含 token 断言:`packages-user/client-`、`packages/render`、`packages/render-vue`、`packages/system`、`packages/audio`、`packages/legacy-`、`src/client`)/ 通过纯数据端并输出 ESM / 不误判数据端路径)+ 结构断言;Task 2 = `pnpm exec vitest run script/build-resource.test.ts` + 构建脚本结构断言(含 **数据先于渲染的调用顺序**、**`dist` 只清空一次**、**两处 `emptyOutDir: false`**、`getAllChars` 返回 `Promise<string>`、`dev.md` 构建表)+ 产物门禁(**数据端 ESM 硬断言** / 渲染端条件分支)+ **baseline 范围门禁** + `pnpm exec tsx script/build-game.ts` 实跑;**不设全仓 `check:type` / `build` / 全量 `test:ci` 门禁**。
⑱ **修改方式声明(AGENTS.md):** 本计划执行含文件写入与删除,由**派发子代理执行**(`gsd-executor`),非内联执行。
⑲ **关卡性质:** **无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户回复;回复「可以执行」即视为通过。** 若用户就 OI-1..OI-12 给出指定,按指定执行。
</action>
<decision>是否按本计划(09-21)执行「任务 11 · 构建重构」——新增数据端/渲染端分离 Vite 插件并注入数据端库模式 ESM 构建(`dist/data/data.js`)、重写 `build-resource` 把 `src/content`(23 个 jsonc)压缩拷贝到 `dist/content`(jsonc 保留系统标记、fontmin、不分块不 zip)、重写 `build-game` 去掉旧模板/babel/资源分块/源码 zip/资源地图/dist.zip 并保留 `LICENSE`、删除 `build-packages.ts` / `build-lib.ts` 与对应 npm script、`index.html` 去旧样式表 link、保留 `vue-tsc --noEmit`、微调 `manualChunks`;**不删 `public/**`、不改 `src/**`(含跨端引用链,交由用户修复)、不动用户其它并发 WIP**;**接受 OI-11 数据端预期阻断并按渲染端 + content + LICENSE 分层门禁**。</decision>
<decision>是否按本计划(09-21)执行「任务 11 · 构建重构」——新增数据端/渲染端分离 Vite 插件(错误含命中 token)并注入数据端库模式 ESM 构建(**数据端先构建、`dist/data/data.js` 为硬门禁**)、重写 `build-resource` 把 `src/content`(23 个 jsonc)压缩拷贝到 `dist/content`(jsonc 保留系统标记、fontmin、不分块不 zip)、重写 `build-game`(清空一次 → 数据端 → 渲染端条件 BLOCKED → content → LICENSE;去掉旧模板/babel/资源分块/源码 zip/资源地图/dist.zip)、删除 `build-packages.ts` / `build-lib.ts` 与对应 npm script、同步 `dev.md` 构建表、`index.html` 去旧样式表 link、保留 `vue-tsc --noEmit`、微调 `manualChunks`;**不删 `public/**`、不改 `src/**`(含用户待修复的两处渲染端 stale import)、不动用户其它并发 WIP**;**数据端 ESM 硬失败、渲染端产物条件分支**。</decision>
<context>
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。执行期含写操作与**删除两个脚本**。`src/content` 共 **23** 个 jsonc,均为「前缀注释 + 严格 JSON」且每个恰好一个标记,加载器 `PrefixedJSONCProcessor` 依赖 `// --- SYSTEM PREFIX END --- //` 的 split(必须恰好 2 段),故压缩保留标记是运行期兼容硬约束(去的是标记**之外**的注释)。`src/content/font` 无字体二进制、`src/content/item.ts` 与 `core.jsonc` 的 `itemEffectDir` 不匹配——属既有缺口,本计划只记录不改。**OI-11 是本次最重要的已知事实:** 数据端入口跨端引用会让 D-94 插件按设计终止数据端构建;用户已明确「你改 src,我做脚本」,故该链路由用户修复,本计划不改 `src/**`,并以「渲染端 + content + LICENSE 必过、`data.js` 条件门禁」保证今天的可验证性。工作树含用户并发改动(`index.html`、`packages-user/client-modules/src/**`、`src/**` 等),提交时须逐条核对暂存面、只暂存本计划 `files_modified`。行号为规划日实测、可能漂移,门禁以结构/行为断言为准。
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。执行期含写操作与**删除两个脚本**。`src/content` 共 **23** 个 jsonc,均为「前缀注释 + 严格 JSON」且每个恰好一个标记,加载器 `PrefixedJSONCProcessor` 依赖 `// --- SYSTEM PREFIX END --- //` 的 split(必须恰好 2 段),故压缩保留标记是运行期兼容硬约束(去的是标记**之外**的注释)。`src/content/font` 无字体二进制、`src/content/item.ts` 与 `core.jsonc` 的 `itemEffectDir` 不匹配——属既有缺口,本计划只记录不改。**revision 4 的关键事实:** 用户的 `src` 重构已删除 `src/constant.ts`,数据端闭包(`src/data/**`)**不含任何渲染端导入**,因此 `dist/data/data.js` 必须产出并断言 ESM(硬门禁);渲染端仍有两处指向已删除文件的 stale import(`src/client/initialize.ts:14` / `src/client/plugin/shop/shop.tsx:9`),属用户 WIP,本计划**不改 `src/**`**,故渲染端产物门禁是条件分支,但**绝不能拖累数据端硬门禁**。工作树含用户并发改动(`index.html`、`packages-user/client-modules/src/**`、`09-CONTEXT.md`、`pnpm-lock.yaml` 等),提交时须逐条核对暂存面、只暂存本计划 `files_modified`。行号为规划日实测、可能漂移,门禁以结构/行为断言为准。
</context>
<options>
<option id="execute-default">
<name>可以执行(按本计划默认口径:插件命名按 OI-1、content 命名按 OI-2、jsonc 保留标记且 `Type:` 注释丢失按 OI-3、无字体时跳过压缩按 OI-4、样式 import 交由用户在 `src/**` 侧完成按 OI-5、`build:game` 去掉 `declare.ts` 前置但保留 `vue-tsc` 按 OI-6、`manualChunks` 去 `antdv` 保 `audio` 且 legacy 维持按 OI-7、`item.js` 缺口留给用户按 OI-8、全量构建按 OI-9 / OI-11 分层门禁、遗留文件/依赖保留按 OI-10、数据端跨端引用交由用户修复按 OI-11)</name>
<pros>D-93..D-100 逐条落地;分离检查从体积启发式升级为模块图硬门禁;数据端 ESM 与 `dist/content` 落点与加载器实际解析路径一致;jsonc 标记保留避免运行期配置解析失败;产物清单与 D-97 对齐;插件与 content 处理均有运行期单测;OI-11 预期阻断不阻塞渲染端 + content + LICENSE 验证</pros>
<cons>引入若干待批准命名;会删除两个旧构建脚本;`dist/data/data.js` 在用户修复 `src` 前不可产出(已记录为 OI-11 pending,而非本计划失败)</cons>
<name>可以执行(按本计划默认口径:插件命名按 OI-1、content 命名与 `chars: string` 按 OI-2、jsonc 保留标记且 `Type:` 注释丢失按 OI-3、无字体时跳过压缩按 OI-4、样式 import 交由用户在 `src/**` 侧完成按 OI-5、`build:game` 去掉 `declare.ts` 前置但保留 `vue-tsc` 按 OI-6、`manualChunks` 去 `antdv` 保 `audio` 且 legacy 维持按 OI-7、`item.js` 缺口留给用户按 OI-8、数据端 ESM 硬门禁 + 渲染端条件分层按 OI-9、遗留文件/依赖保留按 OI-10、数据端历史跨端引用已解决按 OI-11、渲染端两处 stale import 交由用户修复按 OI-12)</name>
<pros>D-93..D-100 逐条落地 + dev.md 文档同步;分离检查从体积启发式升级为模块图硬门禁且错误可分类(含命中 token);数据端先构建、渲染端失败也不再影响数据端验证;数据端 ESM 与 `dist/content` 落点与加载器实际解析路径一致;jsonc 标记保留避免运行期配置解析失败;产物清单与 D-97 对齐;插件与 content 处理均有运行期单测;范围门禁以 baseline 快照证明 `src/**` 零新增改动</pros>
<cons>引入若干待批准命名;会删除两个旧构建脚本;渲染端在用户修复两处 stale import 前可能无法产出(已记录为 BLOCKED 条件分支,而非本计划失败)</cons>
</option>
<option id="revise">
<name>修订 OI-1..OI-11 的任一取向后重新汇报</name>
<name>修订 OI-1..OI-12 的任一取向后重新汇报</name>
<pros>避免带着未确认的命名 / 样式落点 / 前置链 / 标注口径落码</pros>
<cons>须修订 PLAN.md 后重新汇报</cons>
</option>
</options>
<resume-signal>回复「可以执行」即可(默认采用 OI-1..OI-11 的提议口径)。如需调整任一 `open_items`,请在回复中一并指明。</resume-signal>
<resume-signal>回复「可以执行」即可(默认采用 OI-1..OI-12 的提议口径)。如需调整任一 `open_items`,请在回复中一并指明。</resume-signal>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];const must=['script/build-game.ts','script/build-resource.ts','script/build-packages.ts','script/build-lib.ts','package.json','index.html','src/data.ts','src/content/core.jsonc','src/content/client.jsonc','src/content/texture/.list.jsonc','src/content/font/.list.jsonc','packages-user/data-state/src/loader/jsoncProcessor.ts','packages-user/data-state/src/loader/loader.ts'];for(const p of must){if(!fs.existsSync(p))bad.push('missing ref: '+p)}const pkg=JSON.parse(fs.readFileSync('package.json','utf8'));for(const s of ['build:packages','build:lib','build:game']){if(!(s in pkg.scripts))bad.push('package.json missing script '+s)}const bg=fs.readFileSync('script/build-game.ts','utf8');for(const t of ['checkClientData','splitResource','source-code.zip','loadList.json']){if(bg.indexOf(t)<0)bad.push('build-game.ts missing legacy token: '+t)}const html=fs.readFileSync('index.html','utf8');if(html.indexOf('styles.css')<0)bad.push('index.html missing legacy stylesheet link');const jp=fs.readFileSync('packages-user/data-state/src/loader/jsoncProcessor.ts','utf8');if(jp.indexOf('SYSTEM PREFIX END')<0&&jp.indexOf('prefixSplit')<0)bad.push('jsoncProcessor.ts missing prefix split logic');for(const p of ['script/vite-plugin-data-isolation.ts','script/vite-plugin-data-isolation.test.ts','script/build-resource.test.ts']){if(fs.existsSync(p))bad.push('post-state already exists: '+p)}const walk=d=>fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>e.isDirectory()?walk(d+'/'+e.name):[d+'/'+e.name]);const j=walk('src/content').filter(p=>p.endsWith('.jsonc'));if(j.length!==23)bad.push('expected 23 jsonc under src/content, got '+j.length);for(const p of j){const t=fs.readFileSync(p,'utf8');const n=t.split('// --- SYSTEM PREFIX END --- //').length-1;if(n!==1)bad.push('marker count != 1: '+p)}if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}const chain=['src/data.ts','src/data/entry.ts','src/constant.ts','src/client/ui/constant.ts'].every(p=>fs.existsSync(p));console.log('OK pre-execution state: legacy build scripts + scripts entries + legacy index.html + 23 jsonc (marker x1) present; new plugin/tests absent; OI-11 chain present='+chain)"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(旧构建脚本 / `package.json` scripts / `index.html` 旧 link / 加载器标记逻辑任一缺失;jsonc 数量不是 23 或某文件标记次数不是 1;新插件与测试文件已存在)</fails_when>
<human-check>用户在对话中明确回复「可以执行」(若就 OI-1..OI-11 给出非默认指定,须在汇报与后续任务中据其指定执行)</human-check>
<automated>node -e "const fs=require('fs'),os=require('os'),path=require('path'),cp=require('child_process'),crypto=require('crypto');const bad=[];const must=['script/build-game.ts','script/build-resource.ts','script/build-packages.ts','script/build-lib.ts','package.json','index.html','dev.md','src/data.ts','src/data/entry.ts','src/data/constant.ts','src/client/constant.ts','src/client/initialize.ts','src/client/plugin/shop/shop.tsx','src/data/plugin/list.ts','src/client/plugin/list.ts','src/content/core.jsonc','src/content/client.jsonc','src/content/texture/.list.jsonc','src/content/font/.list.jsonc','packages-user/data-state/src/loader/jsoncProcessor.ts','packages-user/data-state/src/loader/loader.ts'];for(const p of must){if(!fs.existsSync(p))bad.push('missing ref: '+p)}if(fs.existsSync('src/constant.ts'))bad.push('src/constant.ts must not exist');const pkg=JSON.parse(fs.readFileSync('package.json','utf8'));for(const s of ['build:packages','build:lib','build:game']){if(!(s in pkg.scripts))bad.push('package.json missing script '+s)}const bg=fs.readFileSync('script/build-game.ts','utf8');for(const t of ['checkClientData','splitResource','source-code.zip','loadList.json']){if(bg.indexOf(t)<0)bad.push('build-game.ts missing legacy token: '+t)}const html=fs.readFileSync('index.html','utf8');if(html.indexOf('styles.css')<0)bad.push('index.html missing legacy stylesheet link');const dev=fs.readFileSync('dev.md','utf8');for(const s of ['build:packages','build:lib','build:game']){if(dev.indexOf(s)<0)bad.push('dev.md missing build table row: '+s)}const entry=fs.readFileSync('src/data/entry.ts','utf8');if(!/from\s+'\.\/constant'/.test(entry))bad.push('src/data/entry.ts must import ./constant');const dc=fs.readFileSync('src/data/constant.ts','utf8');if(dc.indexOf('@user/data-common')<0)bad.push('src/data/constant.ts must stay data-only (@user/data-common)');if(dc.indexOf('content/core.jsonc')<0)bad.push('src/data/constant.ts must keep CORE_URL=content/core.jsonc');const cc=fs.readFileSync('src/client/constant.ts','utf8');if(cc.indexOf('content/client.jsonc')<0)bad.push('src/client/constant.ts must keep CLIENT_CORE_URL=content/client.jsonc');const jp=fs.readFileSync('packages-user/data-state/src/loader/jsoncProcessor.ts','utf8');if(jp.indexOf('SYSTEM PREFIX END')<0&&jp.indexOf('prefixSplit')<0)bad.push('jsoncProcessor.ts missing prefix split logic');const walk=d=>fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>e.isDirectory()?walk(path.join(d,e.name)):[path.join(d,e.name)]);const jsonc=walk('src/content').filter(p=>p.endsWith('.jsonc'));if(jsonc.length!==23)bad.push('expected 23 jsonc files, got '+jsonc.length);for(const p of jsonc){const n=fs.readFileSync(p,'utf8').split('// --- SYSTEM PREFIX END --- //').length-1;if(n!==1)bad.push('marker count '+n+' in '+p)}for(const p of ['script/vite-plugin-data-isolation.ts','script/vite-plugin-data-isolation.test.ts','script/build-resource.test.ts']){if(fs.existsSync(p))bad.push('post-state already exists: '+p)}let statusRaw='',srcRaw='';try{statusRaw=cp.execSync('git status --porcelain -uall',{encoding:'utf8'});srcRaw=cp.execSync('git diff --name-only -- src',{encoding:'utf8'})}catch(e){bad.push('baseline git capture failed: '+e.message)}const norm=p=>{if(p.charCodeAt(0)===34)p=p.slice(1);if(p.charCodeAt(p.length-1)===34)p=p.slice(0,-1);return p};const parse=raw=>raw.split(/\r?\n/).filter(Boolean).map(l=>{const p=l.slice(3).trim();const i=p.indexOf(' -> ');return norm(i>=0?p.slice(i+4):p)});const paths={};for(const p of parse(statusRaw)){paths[p]=fs.existsSync(p)?crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex'):null}const baseline={capturedAt:new Date().toISOString(),paths,srcDiff:srcRaw.split(/\r?\n/).filter(Boolean)};const bp=path.join(os.tmpdir(),'gsd-09-21-baseline.json');try{fs.writeFileSync(bp,JSON.stringify(baseline,null,2))}catch(e){bad.push('baseline write failed: '+e.message)}if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK prestate :: baseline '+bp+' ('+Object.keys(paths).length+' pre-existing changes, srcDiff '+baseline.srcDiff.length+')')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED":必需引用文件缺失 / `src/constant.ts` 仍存在 / `package.json` 缺 `build:packages`、`build:lib`、`build:game` / 旧 `build-game.ts` 缺 legacy token / `index.html` 缺旧样式表 link / **`dev.md` 缺三行构建表任一行** / `src/data/entry.ts` 不再从 `./constant` 导入 / `src/data/constant.ts` 不再仅依赖 `@user/data-common` 或丢了 `content/core.jsonc` / `src/client/constant.ts` 丢了 `content/client.jsonc` / 加载器标记逻辑缺失 / **jsonc 数量不是 23 或某文件标记次数不是 1** / 新插件与测试文件已存在 / **baseline git 捕获或写入临时文件失败**</fails_when>
<human-check>用户在对话中明确回复「可以执行」(若就 OI-1..OI-12 给出非默认指定,须在汇报与后续任务中据其指定执行)</human-check>
</verify>
<acceptance_criteria>
- 用户回复中明确包含「可以执行」
- 汇报内容涵盖:要解决的问题、范围(两个脚本重构 + 两个脚本删除 + npm script 调整)、D-94 插件扫描口径(specifier + 解析路径 + `src/client` + 报错终止 + 不误判数据端)与取代 `checkClientData`、D-95 库模式 ESM 与 `dist/data/data.js`、D-96 content 压缩(**保留系统标记**、**23 个 jsonc**、fontmin、不分块不 zip)、D-97 产物清单与 `LICENSE` 保留、`index.html` 去 link、D-99 保留 `vue-tsc`、D-100 去 babel/微调 `manualChunks`、**全部拟新增命名(OI-1 / OI-2)**、写操作边界(含不删 `public/**`、**`src/**` 零改动**)、验证方式、OI-1..OI-11
- 汇报中已**原样完整复述 OI-11 的实测链**:`src/data.ts` → `src/data/entry.ts:2`(`import { CORE_URL, HERO_FACE } from '../constant'`)→ `src/constant.ts:2`(`import { MAIN_HEIGHT, MAIN_WIDTH } from './client/ui/constant'`)→ `src/client/ui/constant.ts:1`(`import { ElementLocator, Font } from '@motajs/render'`,`Font` 值使用),并说明 `dist/data/data.js` 在用户修复前不可产出
- 汇报中已原样告知**门禁分层**:数据端被插件按设计终止 = 预期 pending(不是本计划失败);渲染端 + `dist/content` + `LICENSE` 必过;`dist/data/data.js` 为条件门禁
- 汇报中已原样告知:**删除 `build-packages.ts` / `build-lib.ts` 与 `build:packages` / `build:lib` 属不可逆的仓库内删除(可经 git 恢复)**、**content 压缩保留标记是对 D-96 的兼容性补充**
- 汇报内容涵盖:要解决的问题、范围(两个脚本重构 + 两个脚本删除 + npm script 调整 + `dev.md` 构建表同步)、D-94 插件扫描口径(specifier + 解析路径 + 命中 token + `src/client` + 报错终止 + 不误判数据端)与取代 `checkClientData`、D-95 库模式 ESM 与 `dist/data/data.js`、**数据端先于渲染端的构建顺序与两处 `emptyOutDir: false`**、D-96 content 压缩(**保留系统标记**、**23 个 jsonc**、fontmin、不分块不 zip)、D-97 产物清单与 `LICENSE` 保留、`index.html` 去 link、D-99 保留 `vue-tsc`、D-100 去 babel/微调 `manualChunks`、**全部拟新增命名(OI-1 / OI-2)与 `chars: string` / `Promise<string>` 类型口径**、写操作边界(含不删 `public/**`、**`src/**` 零改动**)、验证方式、OI-1..OI-12
- 汇报中已**原样完整复述当前数据端链路**:`src/data.ts` → `src/data/entry.ts`(`from './constant'` + `./initialize` / `./plugin` / `./event`)→ `src/data/constant.ts`(仅 `@user/data-common`),并说明历史跨端引用(`src/constant.ts` → `./client/ui/constant` → `@motajs/render`)**已由用户的 `src` 重构解决**,且 `src/data/**` 无渲染端导入
- 汇报中已**原样复述两处渲染端 stale import**:`src/client/initialize.ts:14`(`'../constant'`)与 `src/client/plugin/shop/shop.tsx:9`(`'@/constant'`),并说明其属用户 `src` WIP、由用户修复,本计划不改
- 汇报中已原样告知**门禁分层**:**数据端 ESM 硬门禁(缺失 / 非 ESM 即非零,无 pending 软通过)**;渲染端成功 → 追加渲染产物断言;渲染端因用户 stale import 失败 → **BLOCKED**(打印说明,不出 `ARTIFACT GATE FAILED`,不影响数据端断言);**无任何无条件通过承诺**
- 汇报中已原样告知 **baseline 范围门禁口径**:Task 0 捕获 `git status --porcelain -uall` + `git diff --name-only -- src` + 路径 SHA-256 到系统临时目录;Task 2 对任何相对 baseline 新增或被二次修改的路径判负(白名单仅 `files_modified`),**`src/**` / `09-CONTEXT.md` 不再整体白名单放行**
- 汇报中已原样告知:**删除 `build-packages.ts` / `build-lib.ts` 与 `build:packages` / `build:lib` 属不可逆的仓库内删除(可经 git 恢复)**、**content 压缩保留标记是对 D-96 的兼容性补充**、**`dev.md` 构建表将同步删除这两个命令**
- 汇报中已原样告知**关卡性质**(必须停下等待;不因触发方式或 `yolo` / `auto_advance` 自动放行;回复「可以执行」即通过)
</acceptance_criteria>
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改或删除任何文件、未派发任何执行子代理</done>
<done>用户明确回复「可以执行」;baseline 快照已写入系统临时目录(含路径哈希与 `src` diff 清单);在此之前未执行任何任务、未修改或删除任何仓库文件、未派发任何执行子代理</done>
</task>
<task type="tracer">
<name>Task 1: 数据端/渲染端分离 Vite 插件端到端最小切片 —— `dataIsolationPlugin` + 真实 `vite.build()` 运行期单测</name>
<name>Task 1: 数据端/渲染端分离 Vite 插件端到端最小切片 —— `dataIsolationPlugin`(错误含命中 token)+ 真实 `vite.build()` 运行期单测</name>
<precondition>Task 0 已闭环(用户回复「可以执行」;OI-1 已获批或按用户指定命名)。`vite` 与 `vitest` 可用;`script/vite-plugin-data-isolation.ts` 与 `script/vite-plugin-data-isolation.test.ts` 尚不存在;`packages-user/*` 与 `packages/*` 的渲染端/数据端包名如前置读取所列;`src/client/**` 已存在(用户迁移后的渲染端 UI 目录)。</precondition>
<files>script/vite-plugin-data-isolation.ts, script/vite-plugin-data-isolation.test.ts</files>
<read_first>
@ -261,151 +287,151 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
<action>
(1) **新建 `script/vite-plugin-data-isolation.ts`(命名以 OI-1 获批结果为准):**
- 自 `vite` 导入**类型** `Plugin`(`import { Plugin } from 'vite'`,按 dev.md 不使用 `import type`)。
- 模块内声明两组模块级只读常量(不导出,避免额外命名面):**specifier 禁令**——`@user/client-` 前缀、`@user/entry-client`、`@motajs/render` / `@motajs/render-vue` / `@motajs/client` / `@motajs/client-base` / `@motajs/system` / `@motajs/audio` / `@motajs/legacy-*`、`vue` 与 `vue/*`、`@vue/*`、`ant-design-vue` 与子路径、`@ant-design/icons-vue` 与子路径;**解析路径禁令**——`[/\\]packages-user[\\/](client-modules|client-base|client-system|entry-client)[\\/]`、`[/\\]packages[\\/](render|render-vue|client|client-base|system|audio|legacy-[^\\/]+)[\\/]`、**`[/\\]src[\\/]client[\\/]`**、`[/\\]node_modules[\\/](vue|@vue|ant-design-vue|@ant-design)[\\/]`。
- 模块内声明两组模块级只读常量(不导出,避免额外命名面),每个条目都是 `{ test: RegExp, token: string }`:
- **specifier 禁令**——`@user/client-` 前缀(token `@user/client-*`)、`@user/entry-client`(token `@user/entry-client`)、`@motajs/render` / `@motajs/render-vue` 及其子路径(token `@motajs/render`)、`@motajs/client` / `@motajs/client-base` 及其子路径(token `@motajs/client`)、`@motajs/system`(token `@motajs/system`)、`@motajs/audio`(token `@motajs/audio`)、`@motajs/legacy-*`(token `@motajs/legacy-`)、`vue` 与 `vue/*`(token `vue`)、`@vue/*`(token `@vue/`)、`ant-design-vue` 与子路径(token `ant-design-vue`)、`@ant-design/icons-vue` 与子路径(token `@ant-design/icons-vue`)。
- **解析路径禁令**——`packages-user/(client-modules|client-base|client-system|entry-client)/`(token `packages-user/client-`)、`packages/render/`(token `packages/render`)、`packages/render-vue/`(token `packages/render-vue`)、`packages/client` / `packages/client-base`(token `packages/client`)、`packages/system/`(token `packages/system`)、`packages/audio/`(token `packages/audio`)、`packages/legacy-*`(token `packages/legacy-`)、**`src/client/`**(token `src/client`)、`node_modules/(vue|@vue|ant-design-vue|@ant-design)/`(token `node_modules/<命中包>`)。
- 正则统一同时接受 `/` 与 `\\` 两种分隔符(`[/\\]`),不做宽泛的 `client` 子串匹配(避免 `client.jsonc` 之类误判)。
- 导出 `dataIsolationPlugin(): Plugin`,返回对象包含:`name: 'mota-data-isolation'`(OI-1)、`enforce: 'pre'`、`apply: 'build'`、`resolveId(source, importer)` 与 `moduleParsed(id)`。
- `resolveId`:对 `source` 先做 specifier 禁令匹配;命中即调用 `this.error(...)`,错误消息必须以 **`[mota-data-isolation]`** 开头并包含**命中的模块标识**、**导入者**(`importer ?? '<entry>'`)与「data-side build must not depend on render-side modules」的明确说明,使构建立即终止且错误可定位。未命中返回 `null`(交回默认解析)。
- `moduleParsed(id)`:对**解析后的绝对路径**做路径禁令匹配;命中即 `this.error(...)`(消息同样以 `[mota-data-isolation]` 开头,含该绝对路径与其 importers(可由 `this.getModuleInfo(id)?.importers` 提供,若为空则省略))。这一路覆盖经别名解析、传递依赖、`src/client/**` 与 `node_modules` 的真实落点——即使 Vite 别名解析先于用户插件执行,仍能拦截。
- **不得**把数据端包(`@user/data-*`、`@motajs/common`、`@motajs/loader`、`@motajs/types`、`@motajs/animate`、`anon-tokyo`)列入禁令;**不得**对字符串做宽泛的 `client` 子串匹配(避免 `client.jsonc` 之类的误判)。
- `resolveId`:对 `source` 做 specifier 禁令匹配;命中即调用 `this.error(...)`,错误消息以 **`[mota-data-isolation]`** 开头并包含 **`matched: <token>`**、命中的模块标识、**导入者**(`importer ?? '<entry>'`)与「data-side build must not depend on render-side modules」的明确说明。未命中返回 `null`(交回默认解析)。
- `moduleParsed(id)`:对**解析后的绝对路径**做路径禁令匹配;命中即 `this.error(...)`,消息同样以 `[mota-data-isolation]` 开头,包含 **`matched: <token>`**、该绝对路径与其 importers(`this.getModuleInfo(id)?.importers`,为空则省略)。这一路覆盖经别名解析、传递依赖、`src/client/**` 与 `node_modules` 的真实落点——即使 Vite 别名解析先于用户插件执行,仍能拦截;**命中 token 是 Task 2 `.catch` 分类器的契约**(W3)。
- **不得**把数据端包(`@user/data-*`、`@motajs/common`、`@motajs/loader`、`@motajs/anon-tokyo`、`@motajs/types`、`@motajs/animate`)列入禁令;**不得**对字符串做宽泛的 `client` 子串匹配。
- 按 dev.md 为导出函数与其参数写 jsDoc;模块顶层零副作用(只声明常量与函数)。
(2) **新建 `script/vite-plugin-data-isolation.test.ts`:**
- 使用 `node:fs/promises` 的 `mkdtemp`(`os.tmpdir()`)、`mkdir`、`writeFile`、`rm` 与 `node:path`,每个用例独立临时目录,`afterEach` 清理。
- 调用**真实** `vite.build()`:`configFile:false`、`root: <temp>`、`logLevel:'silent'`、`build: { write:false, minify:false, lib: { entry: <temp entry>, formats: ['es'], fileName: 'data' }, copyPublicDir:false }`、`plugins:[dataIsolationPlugin()]`。
- 用例(每个 `it` 前一行注释说明测试内容):
1. **渲染端 specifier 被拒**:entry 导入 `vue`;断言 `build()` rejects,错误消息含 `[mota-data-isolation]`、`vue` 与 `data-side build must not depend on render-side`;为排除「解析失败」干扰,可在 `resolve.alias` 把 `vue` 指到本地 fixture,但仍应先被 `resolveId` 拦截。
2. **渲染端解析路径被拒(传递依赖)**:临时目录内构造 `<temp>/packages-user/client-modules/fake.ts`,entry 相对导入它;断言 rejects 且消息含该路径片段。
3. **`src/client` 渲染目录被拒**:临时目录内构造 `<temp>/src/client/ui/fake.ts`,entry 相对导入它;断言 rejects 且消息含 `src/client` 路径片段(覆盖用户迁移后的真实渲染端目录形态)。
4. **纯数据端通过且产物为 ESM**:entry 导入 `<temp>/packages-user/data-state/fake.ts`(导出常量)并 re-export;断言 `build()` resolve,输出含 chunk 且其 `code` 含 `export` 语句(ESM 产物),不抛错。
5. **数据端路径不误判**:entry 相对导入 `<temp>/packages-user/data-common/fake.ts`;断言通过(证明禁令只覆盖渲染端路径)。
1. **渲染端 specifier 被拒(W2):** entry 写入 `import 'vue';`,**不设置任何 `resolve.alias`**;断言 `build()` rejects,错误消息含 `[mota-data-isolation]`、`matched: vue` 与 `data-side build must not depend on render-side`。
2. **`packages-user/client-*` 解析路径被拒:** 临时目录内构造 `<temp>/packages-user/client-modules/fake.ts`,entry 相对导入它;断言 rejects,消息含该路径片段 **且含 `matched: packages-user/client-`**(证明 `moduleParsed` 路径兜底 + classifier token)。
3. **`src/client` 渲染目录被拒:** 临时目录内构造 `<temp>/src/client/ui/fake.ts`,entry 相对导入它;断言 rejects,消息含 **`matched: src/client`**(覆盖用户迁移后的真实渲染端目录形态)。
4. **别名解析后的渲染端包被拒(classifier token 全覆盖,表驱动):** 依次构造 `<temp>/packages/render/src/x.ts`、`<temp>/packages/render-vue/src/x.ts`、`<temp>/packages/client-base/src/x.ts`、`<temp>/packages/system/src/x.ts`、`<temp>/packages/audio/src/x.ts`、`<temp>/packages/legacy-ui/src/x.ts` 并分别相对导入;每次断言 rejects 且消息含对应 `matched: packages/render` / `matched: packages/render-vue` / `matched: packages/client` / `matched: packages/system` / `matched: packages/audio` / `matched: packages/legacy-`。
5. **纯数据端通过且产物为 ESM:** entry 导入 `<temp>/packages-user/data-state/fake.ts`(导出常量)并 re-export;断言 `build()` resolve,输出含 chunk 且其 `code` 含 `export` 语句(ESM 产物),不抛错。
6. **数据端路径不误判:** entry 相对导入 `<temp>/packages-user/data-common/fake.ts`;断言通过(证明禁令只覆盖渲染端路径)。
- 断言尽量稳定(不依赖 chunk 文件名/顺序),只依赖 rejects/resolve 与错误消息关键字。
- **本测试是「数据端路径不误判」的唯一权威检查(W3):** 插件结构门禁**不再**对 `@user/data-` 做裸 `indexOf` 负向断言(注释 / 白名单会误伤)。
</action>
<behavior>
- entry `import 'vue'` → `build()` rejects;错误消息含 `[mota-data-isolation]`、`vue` 与 `data-side build must not depend on render-side`。
- entry 相对导入 `<temp>/packages-user/client-modules/fake.ts` → rejects;错误消息含该路径(证明 `moduleParsed` 路径兜底生效,且覆盖传递依赖)。
- entry 相对导入 `<temp>/src/client/ui/fake.ts` → rejects;错误消息含 `src/client`(证明迁移后的渲染端 UI 目录被覆盖)。
- entry `import 'vue'` → `build()` rejects;错误消息含 `[mota-data-isolation]`、`matched: vue` 与 `data-side build must not depend on render-side`。
- entry 相对导入 `<temp>/packages-user/client-modules/fake.ts` → rejects;消息含该路径与 `matched: packages-user/client-`。
- entry 相对导入 `<temp>/src/client/ui/fake.ts` → rejects;消息含 `matched: src/client`。
- entry 相对导入 `<temp>/packages/{render,render-vue,client-base,system,audio,legacy-ui}/src/x.ts` → 各自 rejects;消息含对应 `matched:` token(`packages/render` / `packages/render-vue` / `packages/client` / `packages/system` / `packages/audio` / `packages/legacy-`)。
- entry 相对导入 `<temp>/packages-user/data-state/fake.ts` → resolve;输出 chunk `code` 含 `export`(ESM)。
- entry 相对导入 `<temp>/packages-user/data-common/fake.ts` → resolve(不误判数据端)。
</behavior>
<reversibility rating="reversible">仅新增一个插件文件与一个测试文件;`Remove-Item` 两文件即可整体回退,未接线到构建脚本、未改任何既有文件。</reversibility>
<reversibility rating="reversible">仅新增一个插件文件与一个测试文件;删除这两文件即可整体回退,未接线到构建脚本、未改任何既有文件。</reversibility>
<verify>
<automated>pnpm exec vitest run script/vite-plugin-data-isolation.test.ts</automated>
<fails_when>non-zero exit, 或任一断言失败(渲染端 specifier 未被拒 / 渲染端解析路径未被拒 / `src/client` 目录未被拒 / 纯数据端构建失败 / 数据端路径被误判 / 错误消息缺少插件标识、模块标识或导入者)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];const p='script/vite-plugin-data-isolation.ts';if(!fs.existsSync(p))bad.push('plugin file missing');else{const t=fs.readFileSync(p,'utf8');if(!/export\s+function\s+dataIsolationPlugin\s*\(/.test(t))bad.push('dataIsolationPlugin not exported');if(!/enforce\s*:\s*'pre'/.test(t))bad.push('plugin is not enforce:pre');if(t.indexOf('moduleParsed')<0)bad.push('plugin lacks moduleParsed resolved-path check');if(t.indexOf('this.error(')<0)bad.push('plugin does not abort via this.error');if(t.indexOf('mota-data-isolation')<0)bad.push('plugin error tag missing');for(const tok of ['@user/client-','@motajs/render','vue','ant-design-vue']){if(t.indexOf(tok)<0)bad.push('plugin forbidden set missing token: '+tok)}if(!/src\[\\\\\\/\]client/.test(t))bad.push('plugin lacks src/client resolved-path guard');for(const tok of ['@user/data-','@motajs/common']){if(t.indexOf(tok)>=0)bad.push('plugin must not forbid data-side token: '+tok)}}const tp='script/vite-plugin-data-isolation.test.ts';if(!fs.existsSync(tp))bad.push('plugin test file missing');else{const t=fs.readFileSync(tp,'utf8');if(t.indexOf('vite')<0)bad.push('test does not use real vite build');if(t.indexOf('dataIsolationPlugin')<0)bad.push('test does not import the plugin')}if(bad.length>0){console.error('PLUGIN GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK plugin structure: enforce pre + specifier + resolved-path (+ src/client) checks + abort + no data-side false positives')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PLUGIN GATE FAILED"(插件文件缺失 / 未导出 `dataIsolationPlugin` / 非 `enforce: 'pre'` / 缺 `moduleParsed` 路径检查 / 未 `this.error` 终止 / 缺错误标识 / 禁令缺少关键 token 或 `src/client` 守卫 / 误把数据端包列入禁令)</fails_when>
<fails_when>non-zero exit, 或任一断言失败(渲染端 specifier 未被拒 / 渲染端解析路径未被拒 / `src/client` 目录未被拒 / 渲染端别名包未被拒或缺对应 `matched:` token / 纯数据端构建失败 / 数据端路径被误判 / 错误消息缺少插件标识或导入者)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];const p='script/vite-plugin-data-isolation.ts';if(!fs.existsSync(p))bad.push('plugin file missing');else{const t=fs.readFileSync(p,'utf8');if(!/export\s+function\s+dataIsolationPlugin\s*\(/.test(t))bad.push('dataIsolationPlugin not exported');if(!/enforce\s*:\s*'pre'/.test(t))bad.push('plugin is not enforce:pre');if(t.indexOf('moduleParsed')<0)bad.push('plugin lacks moduleParsed resolved-path check');if(t.indexOf('this.error(')<0)bad.push('plugin does not abort via this.error');if(t.indexOf('mota-data-isolation')<0)bad.push('plugin error tag missing');if(t.indexOf('matched:')<0)bad.push('plugin error message lacks matched token');for(const tok of ['@user/client-','@user/entry-client','@motajs/render','@motajs/system','@motajs/audio','@motajs/legacy-','vue','@vue/','ant-design-vue','packages-user/client-','packages/render','packages/system','packages/audio','packages/legacy-','packages/client']){if(t.indexOf(tok)<0)bad.push('plugin forbidden set missing token: '+tok)}}const tp='script/vite-plugin-data-isolation.test.ts';if(!fs.existsSync(tp))bad.push('plugin test file missing');else{const t=fs.readFileSync(tp,'utf8');if(t.indexOf('build(')<0)bad.push('test does not call real vite build');if(t.indexOf('dataIsolationPlugin')<0)bad.push('test does not import the plugin');if(t.indexOf('matched:')<0)bad.push('test does not assert matched token');if(t.indexOf('src/client')<0)bad.push('test does not cover src/client')}if(bad.length>0){console.error('PLUGIN GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK plugin structure: enforce pre + specifier + resolved path + abort + matched token')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PLUGIN GATE FAILED"(插件文件缺失 / 未导出 `dataIsolationPlugin` / 非 `enforce: 'pre'` / 缺 `moduleParsed` 路径检查 / 缺 `this.error` 终止 / 缺错误标签 / 错误消息缺 `matched:` / 禁令缺少关键 token / 测试文件缺失或未用真实 `vite.build()` / 测试未断言 `matched:` 或未覆盖 `src/client`)</fails_when>
</verify>
<acceptance_criteria>
- `script/vite-plugin-data-isolation.ts` 存在并按 OI-1 命名导出 `dataIsolationPlugin`;`enforce: 'pre'` + `resolveId` specifier 检查 + `moduleParsed` 解析路径检查(含 `src/client/**`)+ `this.error(...)` 终止;错误消息带 `[mota-data-isolation]` 标识
- 禁令覆盖 D-94 所列渲染端面(`@user/client-*`、`@user/entry-client`、`@motajs/render*` / `client*` / `system` / `audio` / `legacy-*`、`vue`、`@vue/*`、`ant-design-vue`、`@ant-design/icons-vue` 及其解析路径);未把 `@user/data-*` / `@motajs/common` 等数据端依赖列入
- 运行期单测用**真实 `vite.build()`** 证明五类行为(拒 specifier / 拒解析路径(传递依赖)/ 拒 `src/client` / 纯数据端通过且 ESM / 数据端不误判),且每个 `it` 前有一行注释
- `script/vite-plugin-data-isolation.ts` 存在并按 OI-1 命名导出 `dataIsolationPlugin`;`enforce: 'pre'` + `resolveId` specifier 检查 + `moduleParsed` 解析路径检查(含 `src/client/**`)+ `this.error(...)` 终止;错误消息带 `[mota-data-isolation]` 标识与 `matched: <token>`
- 禁令覆盖 D-94 所列渲染端面(`@user/client-*`、`@user/entry-client`、`@motajs/render*` / `client*` / `system` / `audio` / `legacy-*`、`vue`、`@vue/*`、`ant-design-vue`、`@ant-design/icons-vue` 及其解析路径,含 `packages/render` / `packages/render-vue` / `packages/client*`);未把 `@user/data-*` / `@motajs/common` 等数据端依赖列入
- 运行期单测用**真实 `vite.build()`** 证明六类行为(拒 specifier / 拒 `packages-user/client-*` / 拒 `src/client` / 拒渲染端别名包且 token 正确 / 纯数据端通过并 ESM / 数据端不误判),且每个 `it` 前有一行注释
- 未改任何既有文件;未接线到 `build-game.ts`(留给 Task 2)
</acceptance_criteria>
<done>分离插件与运行期单测就位:真实 Vite 构建在渲染端依赖上被阻断、在纯数据端构建通过并产出 ESM;结构断言全绿;零既有文件改动</done>
<done>分离插件与运行期单测就位:真实 Vite 构建在渲染端依赖上被阻断且错误带 `matched:` token、在纯数据端构建通过并产出 ESM;结构断言全绿;零既有文件改动</done>
</task>
<task type="auto">
<name>Task 2: 构建编排重写与收口 —— `build-resource`(content 压缩/拷贝/字体)+ `build-game`(渲染端 → 数据端 ESM(OI-11 预期失败分类)→ content → LICENSE)+ 删除旧脚本 + npm script / index.html + 分层产物门禁 + SUMMARY</name>
<precondition>Task 1 已完成(`script/vite-plugin-data-isolation.ts` 导出 `dataIsolationPlugin` 并通过单测);Task 0 已闭环(OI-2..OI-11 已获批或按用户指定)。`src/content/**` 共 **23** 个 jsonc,每个恰好一个系统标记;`src/content/font/` 无字体二进制;`public/**` 仍存在(本计划不删,仅阻止其进入 dist);**OI-11 跨端引用链仍未修复时**,数据端构建会被插件按设计终止(预期,不是本计划失败)。</precondition>
<files>script/build-resource.ts, script/build-resource.test.ts, script/build-game.ts, script/build-packages.ts, script/build-lib.ts, package.json, index.html, .planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md</files>
<name>Task 2: 构建编排重写与收口 —— `build-resource`(content 压缩/拷贝/字体)+ `build-game`(清空一次 → 数据端 ESM 硬门禁 → 渲染端条件 BLOCKED → content → LICENSE)+ 删除旧脚本 + npm script / index.html / dev.md 同步 + 产物/范围门禁 + SUMMARY</name>
<precondition>Task 1 已完成(`script/vite-plugin-data-isolation.ts` 导出 `dataIsolationPlugin` 并通过单测);Task 0 已闭环(OI-2..OI-12 已获批或按用户指定,且 baseline 快照已在系统临时目录)。`src/content/**` 共 **23** 个 jsonc,每个恰好一个系统标记;`src/content/font/` 无字体二进制;`public/**` 仍存在(本计划不删,仅阻止其进入 dist);**渲染端两处 stale import 仍未修复**(`src/client/initialize.ts:14` / `src/client/plugin/shop/shop.tsx:9`),渲染端构建可能失败,属用户 `src` WIP。</precondition>
<files>script/build-resource.ts, script/build-resource.test.ts, script/build-game.ts, script/build-packages.ts, script/build-lib.ts, package.json, index.html, dev.md, .planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(**D-93 / D-95 / D-96 / D-97 / D-98 / D-99 / D-100**;D-56)
- script/build-game.ts(旧编排:`logProgress` 步骤表、`buildClient`、`buildData`、`checkClientData`、`getAllChars`、`generateResourceJSON`、`buildGame` 的 toCopy / archiver / compressing / 服务器 exe / `dist.zip`)
- script/build-resource.ts(旧 `splitResource` / `CompressedUsage` / `LoadDataType` / `SplittedResource`)
- script/utils.ts(`formatSize`);script/build-packages.ts / script/build-lib.ts(待删除内容,确认无其它引用)
- package.json(scripts 面);index.html(样式 link 行);src/styles.less(仅 7 行 `#game-draw` 居中样式)
- src/content/**(**23 个 jsonc**:含 `texture/.list.jsonc` / `maps/.list.jsonc` / `defaults/*.jsonc` / `font/.list.jsonc` / `item.ts` / `texture/*.png`)
- packages-user/data-state/src/loader/jsoncProcessor.ts(标记 split);packages-user/data-state/src/loader/loader.ts:70-142(生产态路径);src/constant.ts(`content/core.jsonc` / `content/client.jsonc`);src/data/entry.ts + src/client/ui/constant.ts(**OI-11 跨端引用链**)
- packages/audio/package.json 与 packages/audio/src/{source,decoder}.ts(`audio` chunk 依据);全仓源码中 `ant-design-vue` 无导入的检索结论
- AGENTS.md(禁改既有注释、修改方式须预先声明);dev.md(命名、interface、jsDoc、非空判断、尽量不写 try-catch-finally——用 Promise `.catch` 与现有风格一致)
- package.json(scripts 面);index.html(样式 link 行);src/styles.less(仅 `#game-draw` 居中样式)
- src/content/**(**23 个 jsonc**:含 `texture/.list.jsonc` / `maps/.list.jsonc` / `defaults/*.jsonc` / `font/.list.jsonc`、`item.ts`、`texture/*.png`)
- packages-user/data-state/src/loader/jsoncProcessor.ts(标记 split);packages-user/data-state/src/loader/loader.ts:70-142(生产态路径);src/data/constant.ts(`content/core.jsonc`)/ src/client/constant.ts(`content/client.jsonc`);src/client/initialize.ts 与 src/client/plugin/shop/shop.tsx(两处 stale import)
- packages/audio/package.json 与 packages/audio/src/{source,decoder}.ts(`audio` chunk 依据);全仓源码中 `ant-design-vue` 无导入的检索结果
- AGENTS.md(禁改既有注释、修改方式须预先声明);dev.md(命名、interface、jsDoc、非空判断、尽量不用 try-catch-finally——用 Promise `.catch` 与现有风格一致)
</read_first>
<action>
(0) **前置确认:** 仅按 Task 0 获批口径执行;若 OI-2 命名被调整,以用户指定为准并在 SUMMARY 记录。**本任务不修改 `src/**`。**
(0) **前置确认:** 仅按 Task 0 获批口径执行;若 OI-2 命名被调整,以用户指定为准并写入 SUMMARY。**本任务不修改 `src/**`。**
(1) **重写 `script/build-resource.ts`(命名以 OI-2 为准):** 删除 `splitResource` / `CompressedUsage` / `LoadDataType` / `SplittedResource` / JSZip 与 `RequiredData` / `RequiredIconsData` 引用,改为 content 处理模块:
- 声明并导出 `IContentBuildOptions`:`readonly sourceDir: string`(`src/content`)、`readonly outputDir: string`(`dist/content`)、`readonly chars: string`(fontmin glyph 子集文本)。
- 声明并导出 `IContentBuildOptions`:`readonly sourceDir: string`(`src/content`)、`readonly outputDir: string`(`dist/content`)、**`readonly chars: string`**(fontmin glyph 子集文本,**字符串**而非 `Set`)。
- 声明并导出 `IContentBuildStats`:`readonly files: number`(写入文件数)、`readonly jsonc: number`(压缩的 jsonc 数)、`readonly fonts: number`(fontmin 处理的字体数)。
- 导出 `minifyJSONC(text: string): string`:以系统标记 `'// --- SYSTEM PREFIX END --- //'` 定位;`split` 后:**1 段** = 无标记,对整段 `JSON.stringify(JSON.parse(text))`;**2 段** = 标记存在,输出 `标记 + '\n' + JSON.stringify(JSON.parse(parts[1]))`;**其它段数** = 标记异常,抛出带文件语义的错误。**严禁**丢掉标记行;**不得**改调用方后缀。
- 导出 `minifyJSONC(text: string): string`:以系统标记 `'// --- SYSTEM PREFIX END --- //'` 定位;`split` 后:**1 段** = 无标记,对整体 `JSON.stringify(JSON.parse(text))`;**2 段** = 标记存在,输出 `标记 + '\n' + JSON.stringify(JSON.parse(parts[1]))`;**其它段数** = 标记异常,抛出带文件语义的错误。**严禁**丢掉标记行;**不得**改调用方后缀。
- 导出 `buildContent(options: IContentBuildOptions): Promise<IContentBuildStats>`:递归遍历 `sourceDir`(`readdir` with `withFileTypes`)并在 `outputDir` 内镜像目录结构:
- `*.jsonc` → `minifyJSONC` 后写入同名路径;
- 字体扩展(`.ttf` / `.otf` / `.woff` / `.woff2`)→ 用 `Fontmin` 的 `glyph({ text: options.chars })` 插件压缩到 `outputDir` 对应子目录(保持文件名);
- 其它文件(PNG / `.ts` 等)→ `fs-extra.copy` 原样拷贝;
- `sourceDir/font` 下若**无**字体二进制:跳过 fontmin 并 `process.stdout.write` 告警(含目录),不抛错(OI-4)。
- 按 dev.md 写 jsDoc;模块无顶层副作用;非空判断用 `isNil`(字面量)/ `!`(对象)。
- 按 dev.md 写 jsDoc;模块无顶层副作用;非空判断字面量用 `isNil`、对象用 `!`。
- **注意:** `Fontmin` 的引入从 `build-game.ts` 移到本文件;`build-game.ts` 不再直接引用 fontmin。
(2) **重写 `script/build-game.ts`:**
- **保留**:`DEBUG_BUILD` / `DEBUG_REPLAY`、`ansi`、`logProgress`(外部行为)、`buildClient` / `buildData` 名称、`getAllChars` 名称、`distDir`、错误处理与 `process.exit(1)` 风格、`formatSize` 报告。
- **删除**:`tempDir` / `_temp` 相关、`RESOUCE_SIZE`、`checkClientData` / `ClientDataLevel` / `getFileName` / `getFileSize`、`generateResourceJSON` / `CompressedLoadList*`、`@babel/core` 与 `transformAsync`、`archiver` / `createWriteStream` / `compressing`、`RequiredData` / `RequiredIconsData` 及对 `public/project/{data,icons}.js` 的读取、`toCopy` 旧模板清单、源码压缩包、资源地图 JSON、资源分块拷贝、服务器 exe 拷贝、`dist.zip` 压缩与相关体积告警分支、`Fontmin` 直接引用。
- **`buildClient(outDir)`**:复用 `loadConfigFromFile` + `mergeConfig`;注入 `legacy(...)`(目标浏览器列表与 `renderModernChunks:false` 维持现状,OI-7);`build.outDir = outDir`(即 `distDir`)、`copyPublicDir: false`、`minify: !DEBUG_BUILD`、`emptyOutDir: true`;`rollupOptions.external` 维持 `['@wasm-audio-decoders/opus-ml']`;`output` 维持 `format:'es'` / `entryFileNames` / `chunkFileNames` / `assetFileNames`;`manualChunks` 仅保留 `audio`(移除 `antdv`)。
- **`buildData(outDir, entry)`**:`build.outDir = outDir`(调用方传 `resolve(distDir, 'data')`)、`copyPublicDir:false`、`emptyOutDir:false`、`minify: !DEBUG_REPLAY`、`lib: { entry, fileName: 'data', formats: ['es'] }`(移除 iife 与 `name`)、`plugins: [dataIsolationPlugin()]`。构建成功后记录产物为 `dist/data/data.js`。
- **`getAllChars(distDir)`**:递归读取渲染端产物中的 `.js`(**排除 `dist/data` 与 `dist/content`**)与 `src/content/**/*.jsonc` 的字符集合,返回 `Set<string>`;供 fontmin 使用。此函数不依赖数据端产物,故数据端按预期失败时仍可执行。
- **`buildGame()` 编排(新进度表 4 步:① 构建前准备 ② 构建渲染端 ③ 构建数据端 ④ 处理 content 与收尾):**
1. `ensureDir(distDir)` + `emptyDir(distDir)`;
2. `await buildClient(distDir)`;
3. `const chars = await getAllChars(distDir)`;
4. **数据端(关键:区分预期 OI-11 阻断与本计划错误):** `const message0 = ...` 不需要;直接调用 `buildData(resolve(distDir, 'data'), resolve(process.cwd(), 'src/data.ts'))`,用 Promise `.catch` 接收失败:
- **预期分支(a):** 先把错误文本中的反斜杠归一化为 `/`(`String(reason).replace(/\\\\/g, '/')`),若文本**同时**含 `mota-data-isolation` **与**(`@motajs/render` **或** `src/client`),则视为 **OI-11 预期阻断**:`process.stdout.write` 打印完整错误与该结论,返回 `'pending'`,**不退出**;
- **非预期分支(b/c):** 其它任何错误一律 `process.stderr.write(String(reason))` 并 `process.exit(1)`(不得吞掉)。
5. `const stats = await buildContent({ sourceDir: resolve(process.cwd(), 'src/content'), outputDir: resolve(distDir, 'content'), chars })`(`.catch` 失败即退出);
6. `copy(resolve(process.cwd(), 'LICENSE'), resolve(distDir, 'LICENSE'))`(`.catch` 失败即退出);
7. 输出构建信息:渲染端 dist 体积、content 统计(`files` / `jsonc` / `fonts`);**数据端状态行**——`'pending'` 时输出 `⚠️ 数据端构建:按预期终止(OI-11,待用户修复 src 端跨端引用)`,成功时输出 `✅ 数据端构建:dist/data/data.js(ESM)`;**不再**生成任何压缩包。
- `logProgress` 的步骤表改为上述 4 项;最终汇总前 `ansi.clear` 并打印状态行(数据端 pending 的说明不得被进度勾选覆盖)。
- **删除**:`tempDir` / `_temp` 相关、`RESOUCE_SIZE`、`checkClientData` / `ClientDataLevel` / `getFileName` / `getFileSize`、`generateResourceJSON` / `CompressedLoadList*`、`@babel/core` 与其变换函数、`archiver` / `createWriteStream` / `compressing`、`RequiredData` / `RequiredIconsData` 及对 `public/project/{data,icons}.js` 的读取、`toCopy` 旧模板清单、源码压缩包、资源地图 JSON、资源分块拷贝、服务器 exe 拷贝、`dist.zip` 压缩与相关体积告警分支、`Fontmin` 直接引用。**删除后不得把上述标识符留在注释或字符串里**(结构门禁做负向检查)。
- **`buildClient(outDir)`**:复用 `loadConfigFromFile` + `mergeConfig`;注入 `legacy(...)`(目标浏览器列表与 `renderModernChunks:false` 维持现状,OI-7);`build.outDir = outDir`(即 `distDir`)、`copyPublicDir: false`、`minify: !DEBUG_BUILD`、**`emptyOutDir: false`**;`rollupOptions.external` 维持 `['@wasm-audio-decoders/opus-ml']`;`output` 维持 `format:'es'` / `entryFileNames` / `chunkFileNames` / `assetFileNames`;`manualChunks` 仅保留 `audio`(移除 `antdv`)。
- **`buildData(outDir, entry)`**:`build.outDir = outDir`(调用方传 `resolve(distDir, 'data')`)、`copyPublicDir:false`、**`emptyOutDir: false`**、`minify: !DEBUG_REPLAY`、`lib: { entry, fileName: 'data', formats: ['es'] }`(移除 iife 与 `name`)、`plugins: [dataIsolationPlugin()]`。
- **`getAllChars()`:返回 `Promise<string>`**:递归读取渲染端产物中的 `.js`(**排除 `dist/data` 与 `dist/content`**,若 `dist` 下无 js 则跳过、不抛错)与 `src/content/**/*.jsonc` 的字符集合(`Set<string>`),最终以 `[...chars].join('')` 返回**字符串**;供 fontmin 使用。此函数不依赖数据端产物。
- **`buildGame()` 编排(新进度表 5 步:构建前准备 → 构建数据端 → 构建渲染端 → 处理 content → 收尾):**
1. `await ensureDir(distDir)` + **`await emptyDir(distDir)`(整个流程只执行一次)**。
2. **先构建数据端(独立、硬门禁):** `await buildData(resolve(distDir, 'data'), resolve(process.cwd(), 'src/data.ts')).catch(reason => { ... })`;失败时把 `String(reason)` 中的 Windows 反斜杠归一化为 `/`(**源码单反斜杠正则**),若同时含 `mota-data-isolation` 与任一渲染端 token(`@motajs/render` / `packages/render` / `packages/render-vue` / `packages/client` / `packages/system` / `packages/audio` / `packages/legacy-` / `packages-user/client-` / `src/client`)→ 打印 `OI-11 回归诊断`;否则打印普通数据端失败;**两种情况都置 `dataFailed = true`**(不得吞掉、不得软通过、不得 `process.exit(0)`)。
3. **后构建渲染端(条件分支):** `await buildClient(distDir).catch(reason => { ... })`;失败时打印 `BLOCKED`(说明错误来自用户 `src/**`,含两处 stale import),置 `renderBlocked = true`,**不覆盖** `dist/data`、**不退出**。
4. `const chars = await getAllChars()`(`string`)→ `await buildContent({ sourceDir: resolve(process.cwd(), 'src/content'), outputDir: resolve(distDir, 'content'), chars })`(失败置 `contentFailed`)→ `await copy(resolve(process.cwd(), 'LICENSE'), resolve(distDir, 'LICENSE'))`(失败置 `licenseFailed`)。
5. 输出:渲染端状态(BUILT / BLOCKED)、数据端状态(`dist/data/data.js`(ESM)或 FAILED + 归因)、content 统计(`files` / `jsonc` / `fonts`)、dist 体积;**`dataFailed || contentFailed || licenseFailed` → `process.exit(1)`**;仅 `renderBlocked` 时退出码保持 0(用户 WIP,非本计划失败)。**不生成任何压缩包。**
- `logProgress` 步骤表同步改为上述 5 项;`ansi.clear` 后的状态行不得被进度勾选覆盖。
- 保留模块底部执行块(`buildGame()`);无需导出。
(3) **删除旧脚本:** 删除 `script/build-packages.ts` 与 `script/build-lib.ts`(用 `Remove-Item` / `fs.rm`);确认全仓无其它引用(`Select-String` 检索 `build-packages` / `build-lib`,仅允许 `package.json` 旧 script 行在删除前命中)。
(3) **删除旧脚本:** 删除 `script/build-packages.ts` 与 `script/build-lib.ts`(`Remove-Item` / `fs.rm`);确认全仓无其它引用(检索 `build-packages` / `build-lib`,仅允许 `package.json` 的 script 行在删除前命中)。
(4) **`package.json`:** 删除 `build:packages` 与 `build:lib` 两个 script;按 OI-6 将 `build:game` 调整为 `vue-tsc --noEmit && tsx script/build-game.ts`(**保留 `vue-tsc --noEmit`**,移除 `tsx script/declare.ts` 前置;不删除 `script/declare.ts` 文件)。**不改** `dependencies` / `devDependencies`。
(4) **`package.json`:** 删除 `build:packages` 与 `build:lib` 两个 script;按 OI-6 把 `build:game` 调整为 `vue-tsc --noEmit && tsx script/build-game.ts`(**保留 `vue-tsc --noEmit`**,移除 `tsx script/declare.ts` 前置;不删除 `script/declare.ts` 文件)。**不改** `dependencies` / `devDependencies`。
(5) **`index.html`:** 仅删除旧样式表 `<link id="mota-css" ... href="/styles.css">` 行;保留 DOCTYPE / head 其它 meta / `<div id="game-draw">` / `<canvas id="render-main">` / `<script src="/src/bootstrap.ts" type="module">`。
(6) **`src/**`(零改动):** 按用户裁定「你改 src,我做脚本」,**不修改 `src/bootstrap.ts` 或任何 `src/**` 文件**。D-97 的「样式经入口 JS 引入」由用户后续在 `src/**` 侧完成(OI-5);本任务只在 SUMMARY 记录「用户接线前 `#game-draw` 居中样式可能缺失」的已知缺口。
(6) **`dev.md` 构建表(W5):** 删除 `build:packages` / `build:lib` 两行;`build:game` 行说明更新为「类型检查 + 构建渲染端游戏包与数据端 ESM 包 + content 处理」;**只改这三行,不动其它章节**。
(7) **新增 `script/build-resource.test.ts`(OI-2):** 使用临时目录测试 `minifyJSONC` 与 `buildContent`:
- `minifyJSONC`:输入「`// Type: X` + 标记 + 多行 JSON」→ 输出以标记开头、正文为单行压缩 JSON、`split` 后恰好 2 段、`JSON.parse` 成功、不含 `Type:` 注释;
- `minifyJSONC`:非法 JSON → 抛错;
- `minifyJSONC`:无标记的纯 JSON → 压缩且可解析;
- `buildContent`:临时 `source/content` 放一个 jsonc、一个 PNG(任意字节)与一个空 `font/` 目录 → 输出 jsonc 已压缩、PNG 字节一致、`fonts === 0` 且不抛错。
(7) **`src/**`(零改动):** 按用户裁定「你改 src,我做脚本」,**不修改 `src/bootstrap.ts` 或任何 `src/**` 文件**。D-97 的「样式经入口 JS 引入」由用户后续在 `src/**` 侧完成(OI-5);本任务只在 SUMMARY 记录「用户接线前 `#game-draw` 居中样式可能缺失」的已知缺口。
(8) **新增 `script/build-resource.test.ts`(OI-2):** 使用临时目录测试 `minifyJSONC` 与 `buildContent`。
- `minifyJSONC`:输入「`// Type: X` + 标记 + 多行 JSON」→ 输出以标记开头、正文为单行压缩 JSON、`split` 后恰好 2 段、`JSON.parse` 成功、不再含 `Type:` 注释。
- `minifyJSONC`:非法 JSON → 抛错。
- `minifyJSONC`:无标记的纯 JSON → 压缩且可解析。
- `buildContent`:临时 `source/content` 放一个 jsonc、一个 PNG(任意字节)与一个空 `font/` 目录 → 输出 jsonc 已压缩、PNG 字节一致、`fonts === 0` 且不抛错(`chars` 以 `string` 传入)。
- 每个 `it` 前一行注释。
(8) **实跑与分层门禁:** 先跑单测;再实跑 `pnpm exec tsx script/build-game.ts`:
- **(a)预期 OI-11 阻断:** 退出码 0、日志含预期 pending 说明、`dist/data/data.js` 缺失 → **合规**;继续跑渲染端 + content + LICENSE 产物门禁;
- **(b)本计划错误:** 插件 / `build-resource.ts` / `build-game.ts` / 测试自身出错(含数据端错误**不匹配** OI-11 已知链)→ 必须修复后重跑;
- **(c)范围外用户 WIP:** 渲染端因用户其它未完成改动失败 → 记录完整错误、归因与「未修改范围外文件」,产物门禁标 `BLOCKED` 写入 SUMMARY;**不得**为此修改 `src/**` 或包源码;
- `dist/data/data.js` **条件门禁**:存在 → 必须为 ESM(含 `export`);缺失 → 仅允许在(a)成立时通过,并记录 OI-11 pending。
(9) **实跑与分层门禁:** 先跑单测;再实跑 `pnpm exec tsx script/build-game.ts`。
- **数据端:** `dist/data/data.js` 必须存在且为 ESM(含 `export`);缺失 / 非 ESM → 硬失败(`bad.push` + 非零),**无 pending 软通过**。若失败文本含 `mota-data-isolation` 与渲染端 token,按 OI-11 回归诊断记录,但**仍然判负**。
- **渲染端:** 若 `dist/index.html` 产出 → 断言其不含 `styles.css` link;若缺失 → 记为 **BLOCKED**(用户两处 stale import),不判本计划失败。
- **content / LICENSE:** 无论两端成败都必须尝试产出并在产物门禁中断言。
- **本计划自身错误**(插件 / `build-resource.ts` / `build-game.ts` / 测试)→ 必须修复后重跑;**不得**为通过门禁修改 `src/**` 或包源码。
(9) **`09-21-SUMMARY.md`:** 记录范围、插件扫描口径与错误行为、数据端 ESM 产物(或 OI-11 pending 的完整错误与归因)、content 压缩口径(含 23 个 jsonc、标记保留理由与 `Type:` 注释丢失)、dist 产物清单与排除项(D-97)、删除清单、`manualChunks` / legacy 调整、`index.html` / 样式落点(用户侧 OI-5)、命名获批情形、门禁结果(插件单测 / content 单测 / 结构断言 / **渲染端 + content + LICENSE 产物门禁** / 范围门禁 / 数据端条件门禁)、`open_items` 最终处置、遗留缺口(`item.js`、字体缺失、OI-11 待用户修复 `src` 跨端引用、样式 import 待用户接线)。
(10) **`09-21-SUMMARY.md`:** 记录范围、插件扫描口径与错误行为(含 `matched:` token)、**数据端 ESM 硬门禁结果**、渲染端 BLOCKED 或成功分支与两处 stale import 当前状态、content 压缩口径(23 个 jsonc、标记保留理由、`Type:` 注释丢失)、dist 产物清单与 D-97 排除项、删除清单、`manualChunks` / legacy 调整、`index.html` 与样式落点(用户侧 OI-5)、命名获批情形、门禁结果(插件单测 / content 单测 / 结构断言 / 数据硬门禁 / 条件渲染门禁 / baseline 范围门禁)、`open_items` 最终处置、遗留缺口(`item.js`、字体缺失、OI-11、OI-12、样式 import)。
</action>
<reversibility rating="costly">删除 `build-packages.ts` / `build-lib.ts` 与修改 `package.json` scripts 属仓库内结构性删除(可经 git 恢复);其余为文件重写,`git checkout` 可回退。执行后 `dist/` 布局改变,旧产物形态(资源分块 / `dist.zip`)不再可用。</reversibility>
<verify>
<automated>pnpm exec vitest run script/build-resource.test.ts</automated>
<fails_when>non-zero exit, 或任一断言失败(标记未保留 / 注释未去除 / 非法 JSON 未抛错 / 无标记输入处理错误 / PNG 拷贝字节不一致 / 无字体时抛错)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];const bg=fs.readFileSync('script/build-game.ts','utf8');for(const t of ['transformAsync','archiver','compressing','checkClientData','generateResourceJSON','splitResource','source-code.zip','loadList.json','启动服务.exe','dist.zip','data.iife.js','_temp']){if(bg.indexOf(t)>=0)bad.push('build-game.ts still contains legacy token: '+t)}for(const t of ['copyPublicDir: false','emptyOutDir: false','dataIsolationPlugin','buildContent','LICENSE','resolve(distDir, \'data\')','mota-data-isolation','@motajs/render']){if(bg.indexOf(t)<0)bad.push('build-game.ts missing required token: '+t)}if(!/formats\s*:\s*\['es'\]/.test(bg))bad.push('build-game.ts: data build is not ESM-only');if(!/manualChunks\s*:\s*\{[\s\S]*audio/.test(bg))bad.push('build-game.ts: audio manualChunk missing');if(/antdv\s*:/.test(bg))bad.push('build-game.ts: antdv manualChunk must be removed');for(const p of ['script/build-packages.ts','script/build-lib.ts']){if(fs.existsSync(p))bad.push('legacy script not deleted: '+p)}const pkg=JSON.parse(fs.readFileSync('package.json','utf8'));for(const s of ['build:packages','build:lib']){if(s in pkg.scripts)bad.push('package.json still has script: '+s)}if(!pkg.scripts['build:game'].includes('vue-tsc --noEmit'))bad.push('build:game must keep vue-tsc --noEmit');if(pkg.scripts['build:game'].includes('declare.ts'))bad.push('build:game must drop declare.ts pre-step');const html=fs.readFileSync('index.html','utf8');if(html.indexOf('styles.css')>=0)bad.push('index.html still links the legacy stylesheet');if(html.indexOf('/src/bootstrap.ts')<0)bad.push('index.html entry script missing');const br=fs.readFileSync('script/build-resource.ts','utf8');if(br.indexOf('splitResource')>=0||br.indexOf('JSZip')>=0)bad.push('build-resource.ts still contains split/zip logic');for(const t of ['minifyJSONC','buildContent','SYSTEM PREFIX END']){if(br.indexOf(t)<0)bad.push('build-resource.ts missing '+t)}if(bad.length>0){console.error('BUILD STRUCTURE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK build structure: legacy pipeline removed, ESM data lib wired to isolation plugin, content + LICENSE, legacy scripts/scripts entries removed')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "BUILD STRUCTURE GATE FAILED"(`build-game.ts` 仍含二次压缩 / 旧 zip / 资源地图 / 分块 / 旧模板 token;缺 `copyPublicDir:false`、`emptyOutDir:false`、`dataIsolationPlugin`、`buildContent`、`LICENSE`、`dist/data` 落点、OI-11 分类标识;数据端非 ESM;`audio` chunk 缺失或 `antdv` chunk 未移除;旧脚本未删除;`package.json` 仍含被删 script、`build:game` 丢了 `vue-tsc --noEmit` 或仍含 `declare.ts`;`index.html` 仍有旧样式表 link 或缺入口;`build-resource.ts` 仍含分块/zip 或缺压缩/标记实现)</fails_when>
<fails_when>non-zero exit, 或任一断言失败(标记未保留 / 注释未去除 / 非法 JSON 未抛错 / 无标记输入处理错误 / PNG 拷贝字节不一致 / 无字体时抛错 / `chars` 未以字符串传入)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];const bg=fs.readFileSync('script/build-game.ts','utf8');for(const t of ['transformAsync','archiver','compressing','checkClientData','ClientDataLevel','getFileName','getFileSize','generateResourceJSON','splitResource','source-code.zip','loadList.json','dist.zip','data.iife.js','_temp']){if(bg.indexOf(t)>=0)bad.push('legacy token still present: '+t)}for(const t of ['copyPublicDir: false','emptyOutDir: false','dataIsolationPlugin','buildContent','LICENSE','Promise<string>','getAllChars','mota-data-isolation','@motajs/render','packages/render','packages/render-vue','packages/client','packages/system','packages/audio','packages/legacy-','src/client','String(reason)']){if(bg.indexOf(t)<0)bad.push('missing required token: '+t)}if(!/formats\s*:\s*\['es'\]/.test(bg))bad.push('data build is not ESM-only');if(bg.indexOf('iife')>=0)bad.push('build-game.ts still mentions iife');if(!/manualChunks\s*:\s*\{[\s\S]*audio/.test(bg))bad.push('audio manualChunk missing');if(/antdv\s*:/.test(bg))bad.push('antdv manualChunk must be removed');if(bg.split('emptyOutDir: false').length-1<2)bad.push('both buildClient and buildData must set emptyOutDir:false');const start=bg.indexOf('async function buildGame(');const body=start>=0?bg.slice(start):'';if(start<0)bad.push('buildGame function missing');const di=body.indexOf('await buildData(');const ci=body.indexOf('await buildClient(');if(di<0||ci<0)bad.push('buildGame must await buildData() and await buildClient()');else if(di>ci)bad.push('buildGame must build data BEFORE render');if(body.indexOf('emptyDir(')<0)bad.push('buildGame must clear dist in the prep step');else if(di>=0&&body.indexOf('emptyDir(')>di)bad.push('dist must be cleared before the data build');const br=fs.readFileSync('script/build-resource.ts','utf8');if(br.indexOf('splitResource')>=0||br.indexOf('JSZip')>=0)bad.push('build-resource.ts still has split/zip logic');for(const t of ['minifyJSONC','buildContent','SYSTEM PREFIX END','chars: string','Fontmin.glyph','Promise<IContentBuildStats>']){if(br.indexOf(t)<0)bad.push('build-resource.ts missing '+t)}if(!fs.existsSync('script/build-resource.test.ts'))bad.push('build-resource test missing');for(const p of ['script/build-packages.ts','script/build-lib.ts']){if(fs.existsSync(p))bad.push('legacy script not deleted: '+p)}const pkg=JSON.parse(fs.readFileSync('package.json','utf8'));for(const s of ['build:packages','build:lib']){if(s in pkg.scripts)bad.push('package.json still has script: '+s)}if(!pkg.scripts['build:game'].includes('vue-tsc --noEmit'))bad.push('build:game must keep vue-tsc --noEmit');if(pkg.scripts['build:game'].includes('declare.ts'))bad.push('build:game must drop declare.ts pre-step');const html=fs.readFileSync('index.html','utf8');if(html.indexOf('styles.css')>=0)bad.push('index.html still links the legacy stylesheet');if(html.indexOf('/src/bootstrap.ts')<0)bad.push('index.html entry script missing');const dev=fs.readFileSync('dev.md','utf8');for(const s of ['build:packages','build:lib']){if(dev.indexOf(s)>=0)bad.push('dev.md still lists '+s)}if(dev.indexOf('build:game')<0)bad.push('dev.md must keep the build:game row');if(bad.length>0){console.error('BUILD STRUCTURE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK build structure: data-first order + single dist clear + double emptyOutDir:false + ESM data lib wired to isolation plugin + content/LICENSE + legacy scripts and doc rows removed')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "BUILD STRUCTURE GATE FAILED"(`build-game.ts` 仍含 babel / zip / 资源分块 / 旧模板 token;缺 `copyPublicDir: false`、`emptyOutDir: false`(两处)、`dataIsolationPlugin`、`buildContent`、`LICENSE`、`Promise<string>`、`getAllChars`、`mota-data-isolation` 或分类 token;数据端非 ESM 或仍含 iife;`audio` chunk 缺失或 `antdv` 未移除;**数据端未先于渲染端构建**;**`dist` 未在数据端构建前清空**;`build-resource.ts` 仍含分块/zip 或缺压缩/标记/`chars: string`;测试文件缺失;旧脚本未删除;`package.json` 仍含被删 script、`build:game` 丢了 `vue-tsc --noEmit` 或仍含 `declare.ts`;`index.html` 仍有旧样式表 link 或缺入口;`dev.md` 仍列出被删命令或缺 `build:game` 行)</fails_when>
<automated>pnpm exec tsx script/build-game.ts</automated>
<fails_when>non-zero exit **且** 错误来自本计划文件(`script/vite-plugin-data-isolation.ts` / `script/build-resource.ts` / `script/build-game.ts` / 其测试),或数据端错误不匹配 OI-11 已知链;**数据端因 OI-11 已知跨端引用被插件按设计终止时,构建脚本应吞掉该预期错误并以退出码 0 继续产出渲染端 + content + LICENSE——这不判负**;若错误来自用户 WIP 的范围外文件(渲染端),则记录完整错误、归因与「未修改范围外文件」,标 BLOCKED,不单独判负。</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];const must=['dist/index.html','dist/content/core.jsonc','dist/content/client.jsonc','dist/content/maps/sample0.jsonc','dist/content/maps/.list.jsonc','dist/content/texture/.list.jsonc','dist/content/texture/terrains0.png','dist/LICENSE'];for(const p of must){if(!fs.existsSync(p))bad.push('missing '+p)}const mustNot=['dist/project','dist/main.js','dist/source-code.zip','dist/loadList.json','dist/styles.css','dist/logo.png','dist/editor.html','dist/_docs','dist/_server','dist/libs','dist/data.process.js','dist/data/data.iife.js','dist.zip'];for(const p of mustNot){if(fs.existsSync(p))bad.push('unexpected '+p)}const core=fs.readFileSync('dist/content/core.jsonc','utf8');const M='// --- SYSTEM PREFIX END --- //';const parts=core.split(M);if(parts.length!==2)bad.push('core.jsonc marker not preserved exactly once');else{try{JSON.parse(parts[1])}catch(e){bad.push('core.jsonc body is not valid JSON: '+e.message)}}if(core.indexOf('// Type:')>=0)bad.push('core.jsonc still contains the Type comment');const html=fs.readFileSync('dist/index.html','utf8');if(html.indexOf('styles.css')>=0)bad.push('dist/index.html still links styles.css');const dataPath='dist/data/data.js';if(fs.existsSync(dataPath)){const data=fs.readFileSync(dataPath,'utf8');if(!/export\s*\{/.test(data))bad.push('data.js is not ESM (no export statement)')}else{process.stdout.write('PENDING: dist/data/data.js absent (OI-11 known src cross-import; expected until user fixes src)\n')}if(bad.length>0){console.error('ARTIFACT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK render+content artifacts: dist/index.html (no styles.css) + compressed dist/content (marker preserved, 23 jsonc source) + LICENSE; data.js '+(fs.existsSync(dataPath)?'ESM':'pending OI-11'))"</automated>
<fails_when>non-zero exit, 或 stderr 含 "ARTIFACT GATE FAILED"(渲染端 + content + LICENSE 必需产物缺失 / 旧产物仍存在 / `core.jsonc` 标记未恰好保留一次或正文非合法 JSON / `Type:` 注释仍在 / `dist/index.html` 仍含 `styles.css` / 存在的 `data.js` 非 ESM)</fails_when>
<!-- plan-criteria-allow: R4 - SCOPE GATE 内的 git status 由 node execSync 调用并以 try/catch 捕获失败(失败即非零退出),同行的 `|` 属 JS 正则/字符串,非 shell 管道阶段 -->
<automated>node -e "const {execSync}=require('child_process');let raw;try{raw=execSync('git status --porcelain',{encoding:'utf8'})}catch(e){console.error('SCOPE GATE FAILED :: git status failed :: '+e.message);process.exit(1)}const lines=raw.split(/\r?\n/).filter(Boolean);const allow=new Set(['script/vite-plugin-data-isolation.ts','script/vite-plugin-data-isolation.test.ts','script/build-resource.ts','script/build-resource.test.ts','script/build-game.ts','script/build-packages.ts','script/build-lib.ts','package.json','index.html','.planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md']);const wip=['packages-user/client-base/','packages-user/client-modules/','src/','.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md','.planning/phases/10-docs-restructure/'];const bad=[];for(const l of lines){const p=l.slice(3).trim().replace(/^\u0022|\u0022$/g,'');if(allow.has(p))continue;if(wip.some(w=>p===w||p.startsWith(w)))continue;bad.push(p)}if(bad.length>0){console.error('SCOPE GATE FAILED :: out-of-scope changes :: '+bad.join(' | '));process.exit(1)}console.log('OK scope: only this plan files changed; src/** and user WIP untouched by this plan')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "SCOPE GATE FAILED"(改动了 `files_modified` 与用户 WIP 白名单之外的任何文件,例如 `packages-user/client-base/**` 以外的新渲染端包路径、`packages-user/data-*/**`、`packages/**`、`vite.config.ts`、`script/dev.ts`、`script/declare.ts`、lockfile、其它 `.planning/**`)</fails_when>
<fails_when>non-zero exit(数据端硬门禁失败、content 或 LICENSE 失败、或本计划文件自身出错)——**渲染端因用户两处 stale import 失败并打印 BLOCKED 但退出码为 0 时不算失败**;但若构建脚本吞掉数据端失败并以 0 退出,则视为本计划错误;若错误来自用户 WIP 的范围外文件(渲染端),记录完整错误与归因、不修改范围外文件,BLOCKED 写入 SUMMARY</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];const dataPath='dist/data/data.js';if(!fs.existsSync(dataPath))bad.push('HARD GATE: dist/data/data.js missing');else{const d=fs.readFileSync(dataPath,'utf8');if(!/export\s*\{/.test(d))bad.push('HARD GATE: dist/data/data.js is not ESM (no export statement)')}for(const p of ['dist/content/core.jsonc','dist/content/client.jsonc','dist/content/enemy.jsonc','dist/content/item.jsonc','dist/content/tile.jsonc','dist/content/maps/.list.jsonc','dist/content/maps/sample0.jsonc','dist/content/defaults/tile.jsonc','dist/content/texture/.list.jsonc','dist/content/texture/terrains0.png','dist/content/animation/.list.jsonc','dist/content/audio/.list.jsonc','dist/content/font/.list.jsonc','dist/LICENSE']){if(!fs.existsSync(p))bad.push('missing '+p)}const M='// --- SYSTEM PREFIX END --- //';for(const p of ['dist/content/core.jsonc','dist/content/client.jsonc']){if(!fs.existsSync(p))continue;const t=fs.readFileSync(p,'utf8');const parts=t.split(M);if(parts.length!==2)bad.push(p+' marker must appear exactly once');else{try{JSON.parse(parts[1])}catch(e){bad.push(p+' body is not valid JSON')}}if(t.indexOf('// Type:')>=0)bad.push(p+' still contains the Type comment')}for(const p of ['dist/project','dist/main.js','dist/source-code.zip','dist/loadList.json','dist/styles.css','dist/logo.png','dist/editor.html','dist/_docs','dist/_server','dist/libs','dist/data.process.js','dist/data/data.iife.js','dist.zip']){if(fs.existsSync(p))bad.push('unexpected '+p)}if(fs.existsSync('dist/data')){for(const f of fs.readdirSync('dist/data')){if(f.endsWith('.iife.js')||f.endsWith('.process.js'))bad.push('unexpected dist/data/'+f)}}if(fs.existsSync('dist/index.html')){const h=fs.readFileSync('dist/index.html','utf8');if(h.indexOf('styles.css')>=0)bad.push('dist/index.html still links styles.css')}else{process.stdout.write('BLOCKED: dist/index.html absent (render build failed on user src stale imports: src/client/initialize.ts ../constant, src/client/plugin/shop/shop.tsx @/constant). Render artifact gate skipped; data/content/LICENSE gates still enforced.\n')}if(bad.length>0){console.error('ARTIFACT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK artifacts: data.js ESM + content/** compressed (marker kept, Type comment removed) + LICENSE'+(fs.existsSync('dist/index.html')?' + index.html without styles.css':' (render BLOCKED)'))"</automated>
<fails_when>non-zero exit, 或 stderr 含 "ARTIFACT GATE FAILED"(`dist/data/data.js` 缺失或非 ESM / content 必需产物缺失 / `core.jsonc` 或 `client.jsonc` 标记未恰好保留一次或正文非合法 JSON / `// Type:` 注释仍在 / D-97 所列旧产物仍存在 / `dist/data` 含 iife 或 process 产物 / `dist/index.html` 存在却仍链 `styles.css`)。`dist/index.html` 缺失只打印 BLOCKED,**不判负**</fails_when>
<!-- plan-criteria-allow: R4 - 本门禁内的 git 由 node execSync 调用并以 try/catch 捕获(失败即非零退出),命令中的 | 属于 JS 正则/字符串,不是 shell 管道阶段 -->
<automated>node -e "const fs=require('fs'),os=require('os'),path=require('path'),cp=require('child_process'),crypto=require('crypto');const bp=path.join(os.tmpdir(),'gsd-09-21-baseline.json');if(!fs.existsSync(bp)){console.error('SCOPE GATE FAILED :: baseline snapshot missing ('+bp+'); Task 0 must capture it');process.exit(1)}const base=JSON.parse(fs.readFileSync(bp,'utf8'));const allow=new Set(['script/vite-plugin-data-isolation.ts','script/vite-plugin-data-isolation.test.ts','script/build-resource.ts','script/build-resource.test.ts','script/build-game.ts','script/build-packages.ts','script/build-lib.ts','package.json','index.html','dev.md','.planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md']);const norm=p=>{if(p.charCodeAt(0)===34)p=p.slice(1);if(p.charCodeAt(p.length-1)===34)p=p.slice(0,-1);return p};const hash=p=>{try{return fs.existsSync(p)?crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex'):null}catch(e){return 'ERR'}};const parse=raw=>raw.split(/\r?\n/).filter(Boolean).map(l=>{const p=l.slice(3).trim();const i=p.indexOf(' -> ');return norm(i>=0?p.slice(i+4):p)});let status=[];let srcDiff=[];try{status=parse(cp.execSync('git status --porcelain -uall',{encoding:'utf8'}));srcDiff=cp.execSync('git diff --name-only -- src',{encoding:'utf8'}).split(/\r?\n/).filter(Boolean)}catch(e){console.error('SCOPE GATE FAILED :: git capture failed :: '+e.message);process.exit(1)}const bad=[];for(const p of status){if(allow.has(p))continue;if(Object.prototype.hasOwnProperty.call(base.paths,p)){if(hash(p)!==base.paths[p])bad.push('second-modified vs baseline: '+p);continue}bad.push('new path vs baseline: '+p)}for(const p of srcDiff){if((base.srcDiff||[]).indexOf(p)<0)bad.push('new src diff vs baseline: '+p)}if(bad.length>0){console.error('SCOPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK scope: no NEW or second-modified src/** or 09-CONTEXT.md paths vs baseline; only plan files + pre-existing WIP')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "SCOPE GATE FAILED"(baseline 快照缺失 / git 捕获失败 / 出现相对 baseline 的**新增**路径且不在 `files_modified` 白名单 / 出现相对 baseline 被**二次修改**的路径(哈希变化)/ `git diff --name-only -- src` 出现 baseline 之外的新条目)</fails_when>
</verify>
<acceptance_criteria>
- **(a)预期阻断(不得判负):** 若 OI-11 跨端引用仍未修复,`pnpm exec tsx script/build-game.ts` 退出码 **0**,日志含「数据端构建:按预期终止(OI-11…)」,`dist/data/data.js` 缺失属于预期;构建脚本通过错误文本同时匹配 `mota-data-isolation` 与(`@motajs/render` 或 `src/client`)完成分类,**未吞掉其它数据端错误**
- **(b)本计划错误(必须判负并修复):** 插件 / `build-resource.ts` / `build-game.ts` / 测试自身错误,或数据端错误不匹配 OI-11 已知链 → 构建非零退出或单测失败;必须修复后重跑
- **(c)范围外用户 WIP(记录为 BLOCKED,不改范围外文件):** 渲染端因用户其它未完成改动失败 → `09-21-SUMMARY.md` 记录完整错误、归因与「未修改范围外文件」;不得修 `src/**` 或包源码
- **渲染端 + content + LICENSE 门禁(今天即可通过):** `dist/index.html`(不含 `styles.css`)、`dist/content/core.jsonc` / `client.jsonc` / `maps/sample0.jsonc` / `maps/.list.jsonc` / `texture/.list.jsonc` / `texture/terrains0.png`、`dist/LICENSE` 存在;D-97 所列旧产物不存在;`core.jsonc` 标记恰好保留一次、正文为合法 JSON、`// Type:` 注释已去除
- **数据端条件门禁:** `dist/data/data.js` 存在时必须为 ESM(含 `export`);缺失时仅允许在(a)成立时通过,并记录 OI-11 pending
- `script/build-resource.ts` 已重写为 content 处理模块(`minifyJSONC` / `buildContent`,按 OI-2 命名);不再含分块 / zip / `JSZip`;`build-resource.test.ts` 全绿(标记保留、去注释、非法 JSON 抛错、PNG 原样、无字体跳过)
- `script/build-game.ts` 已重写:渲染端构建到 `dist`(`copyPublicDir:false`、`emptyOutDir:true`、`audio` chunk、无 `antdv` chunk)、数据端 ESM 库构建到 `dist/data`(注入 `dataIsolationPlugin`,`emptyOutDir:false`,OI-11 分类)、`buildContent` 处理 `dist/content`、复制 `LICENSE`、输出体积与数据端状态;旧 pipeline token(babel / archiver / compressing / `checkClientData` / `generateResourceJSON` / `splitResource` / 源码 zip / 资源地图 / 服务器 exe / `dist.zip`)全部移除
- **数据端硬门禁(必过):** `dist/data/data.js` 存在且为 ESM(含 `export`);数据端构建在渲染端之前完成;`dist` 只在准备步骤清空一次;`buildClient` 与 `buildData` 均为 `emptyOutDir: false`;数据端失败(含 OI-11 回归签名)→ 退出码非零,**不存在 pending 软通过或 sentinel 豁免**
- **渲染端条件分支:** `dist/index.html` 存在时断言不含 `styles.css` link;缺失时打印 BLOCKED(用户两处 stale import:`src/client/initialize.ts:14` / `src/client/plugin/shop/shop.tsx:9`)并跳过渲染产物断言,**不判本计划失败**
- **content / LICENSE:** `dist/content/**`(jsonc 压缩且保留系统标记恰好一次、PNG 原样、字体按 OI-4 处理)与 `dist/LICENSE` 在两种分支下均被断言
- `script/build-resource.ts` 已重写为 content 处理模块(`minifyJSONC` / `buildContent` / `chars: string`,按 OI-2 命名);不再含分块 / zip / `JSZip`;`script/build-resource.test.ts` 全绿
- `script/build-game.ts` 已重写:删除全部旧 pipeline 标识符(不得残留在注释/字符串中),`manualChunks` 仅保留 `audio`、legacy 维持现状、`getAllChars` 返回 `Promise<string>`
- `script/build-packages.ts` / `script/build-lib.ts` 已删除;`package.json` 不再含 `build:packages` / `build:lib`,`build:game` 保留 `vue-tsc --noEmit` 且去掉 `declare.ts` 前置
- `index.html` 仅移除旧样式表 link(其余内容保留);**`src/**` 零改动**(样式 import 由用户侧完成,OI-5)
- `git status` 范围门禁全绿(仅 `files_modified` 与用户 WIP 白名单变更)
- `09-21-SUMMARY.md` 已写(范围、插件口径、ESM 产物或 OI-11 pending 归因、content 压缩与标记理由(23 个 jsonc)、产物/排除清单、删除清单、命名获批、门禁结果、`open_items` 最终处置、遗留缺口)
- `index.html` 仅移除旧样式表 link;**`src/**` 零改动**(样式 import 由用户侧完成,OI-5);`dev.md` 构建表同步(删除两行、保留并更新 `build:game`)
- **baseline 范围门禁:** 相对 Task 0 快照无新增、无二次修改的 `src/**` 与 `09-CONTEXT.md` 路径(白名单仅 `files_modified`;不整体放行 `src/`)
- `09-21-SUMMARY.md` 已写(范围、插件口径、数据端硬门禁结果、渲染端分支与 stale import 状态、content 压缩与标记理由、产物/排除清单、删除清单、命名获批、门禁结果、`open_items` 处置、遗留缺口)
</acceptance_criteria>
<done>构建流程完成新引擎适配:数据端 ESM 独立包由模块图硬门禁把关(OI-11 预期终止被正确分类而非误判为计划失败)、content 以压缩形态落到 `dist/content`、渲染端产物不再夹带旧模板内容、旧脚本与旧 pipeline 全部移除、`LICENSE` 保留;运行期单测与结构/范围门禁全绿;**渲染端 + content + LICENSE 产物门禁今天即可通过**;`dist/data/data.js` 条件门禁与 OI-11 处置写入 `09-21-SUMMARY.md`</done>
<done>构建流程完成新引擎适配:数据端先构建且 `dist/data/data.js`(ESM)为硬门禁、content 以压缩形态(保留标记)落到 `dist/content`、`LICENSE` 保留、渲染端按条件分支产出或打印 BLOCKED、旧脚本与旧 pipeline 全部移除、`src/**` 零改动;插件单测 / content 单测 / 结构断言 / 数据硬门禁 / baseline 范围门禁全绿,结果与分支处置写入 `09-21-SUMMARY.md`</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
@ -413,51 +439,51 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
|----------|-------------|
| 写入/删除边界 | 本增量含写操作与两个脚本删除;只允许 `files_modified` 白名单,**`src/**` 零写入** |
| 数据端 ↔ 渲染端 | 数据端独立包不得依赖渲染端;由模块图插件硬门禁(取代体积启发式),覆盖 `src/client/**` |
| content 输入 → dist | `src/content` 的 23 个 jsonc 被 `JSON.parse` 后重新序列化;格式非法会在构建期暴露 |
| content 输入 → dist | `src/content` 的 23 个 jsonc 经 `JSON.parse` 后重新序列化;格式非法会在构建期暴露 |
| 构建产物 → 浏览器 | `dist/content/**` 由加载器按 `BASE_URL` 相对路径读取;标记行是解析契约 |
| 预期失败 vs 真实失败 | OI-11 跨端引用导致的数据端终止是预期;其它数据端错误必须仍使构建失败 |
| 预期失败 vs 真实失败 | OI-11 回归签名只用于归因,数据端失败仍非零;渲染端 BLOCKED 属用户 WIP |
| 用户 WIP 边界 | 工作树含未提交改动;不得回退、不得为通过门禁而修改范围外文件 |
| 依赖边界 | 不新增依赖、不改 lockfile、不改 `package.json` 依赖面 |
| 命名边界 | 新文件/导出/接口及成员须先经 Task 0 报备批准(D-04) |
**威胁级别说明(security_enforcement = ASVS L1,block on high):** 本计划是**本地构建流程重构**,不执行第三方代码、不新增依赖、不暴露网络面;`high` 级(构建产物被篡改 / 依赖边界被破坏 / 预期失败掩盖真实错误)均已给出 `mitigate`。
**威胁级别说明(security_enforcement = ASVS L1,block on high):** 本计划是**本地构建流程重构**,不执行第三方代码、不新增依赖、不暴露网络面;`high` 级(构建产物被污染 / 依赖边界被破坏 / 预期签名掩盖真实错误 / 数据端硬门禁被软化)均已给出 `mitigate`。
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-21-01 | Tampering | 数据端构建被渲染端依赖污染(体积启发式漏判) | high | mitigate | Task 1 插件在 `resolveId`(specifier)+ `moduleParsed`(解析路径,含 `src/client/**`)双路扫描并 `this.error` 终止;真实 `vite.build()` 单测覆盖 specifier / 传递依赖 / `src/client` / 纯数据端通过 / 数据端不误判;Task 2 `BUILD STRUCTURE GATE` 断言插件已注入数据端构建 |
| T-09-21-02 | Tampering | content jsonc 压缩丢失系统标记,导致运行期解析为 `{}` | high | mitigate | `minifyJSONC` 强制保留 `// --- SYSTEM PREFIX END --- //` 恰好一次;单测断言 `split` 后恰好 2 段且正文可 `JSON.parse`;产物门禁对 `dist/content/core.jsonc` 复验;SUMMARY 记录该兼容性约束 |
| T-09-21-03 | Tampering | 把 `public/**` 旧模板内容带入 dist(D-97 违规) | medium | mitigate | `copyPublicDir: false`;不拷贝旧模板清单;产物门禁断言 D-97 所列旧产物不存在 |
| T-09-21-04 | Tampering | 越界写入 / 回退用户 WIP / 误改 `src/**` | medium | mitigate | `files_modified` 白名单 + prohibitions(`src/**` 零改动)+ `git status` 范围门禁(用户 WIP 前缀白名单) |
| T-09-21-05 | Tampering | 误删用户仍需要的脚本或 npm script(不可逆结构删除) | medium | mitigate | 删除范围仅 `build-packages.ts` / `build-lib.ts` 与对应两个 script(D-93 / D-98);Task 0 明示可经 git 恢复;结构门禁只允许这两个文件被删 |
| T-09-21-01 | Tampering | 数据端构建被渲染端依赖污染(体积启发式漏判) | high | mitigate | Task 1 插件 `resolveId`(specifier)+ `moduleParsed`(解析路径,含 `src/client/**`)双路扫描并 `this.error` 终止;真实 `vite.build()` 单测覆盖 specifier / 别名解析包(含 `packages/render` / `packages/render-vue` / `packages/client*`)/ `src/client` / 纯数据端通过 / 数据端不误判;Task 2 结构门禁断言插件已注入数据端构建 |
| T-09-21-02 | Tampering | content jsonc 压缩丢失系统标记,导致运行期解析为 `{}` | high | mitigate | `minifyJSONC` 强制保留 `// --- SYSTEM PREFIX END --- //` 恰好一次;单测断言 `split` 后恰好 2 段且正文可 `JSON.parse`;产物门禁对 `dist/content/core.jsonc` / `client.jsonc` 复验并断言 `// Type:` 注释已去除 |
| T-09-21-03 | Tampering | 从 `public/**` 旧模板内容带进 dist(D-97 违规) | medium | mitigate | `copyPublicDir: false`;不拷贝旧模板清单;产物门禁断言 D-97 所列旧产物不存在 |
| T-09-21-04 | Tampering | 越界写入 / 回退用户 WIP / 误改 `src/**` | medium | mitigate | `files_modified` 白名单 + prohibitions(`src/**` 零改动)+ **baseline 快照范围门禁**(新增或哈希变化的路径一律判负,不整体放行 `src/`) |
| T-09-21-05 | Tampering | 误删用户仍需要的脚本或 npm script(不可逆结构删除) | medium | mitigate | 删除范围仅 `build-packages.ts` / `build-lib.ts` 与对应两个 script(D-93 / D-98);Task 0 明示可经 git 恢复;结构门禁只允许这两个文件缺失 |
| T-09-21-06 | Tampering | 未经批准使用新命名(插件 / 接口 / 成员) | medium | mitigate | Task 0 报备 OI-1 / OI-2;prohibitions 明列「未获批前不得落码」 |
| T-09-21-07 | DoS | OI-11 预期阻断被误判为本计划失败(诱发越界修复),或反过来吞掉真实数据端错误 | high | mitigate | 分类必须同时匹配 `mota-data-isolation` 与(`@motajs/render` 或归一化后的 `src/client`);不匹配的数据端错误一律 `process.exit(1)`;门禁分层:渲染端 + content + LICENSE 必过、`data.js` 条件门禁;范围外失败只记录归因,禁止修改 `src/**` / 包源码 |
| T-09-21-08 | Tampering | 字体缺失导致 fontmin 抛错阻断构建 | medium | mitigate | 无字体二进制时跳过 fontmin 并告警(OI-4);`buildContent` 单测覆盖空 `font/` 目录 |
| T-09-21-09 | Tampering | `manualChunks` 引用不存在的 `ant-design-vue` 造成分块噪声/误解 | low | mitigate | 移除 `antdv` chunk,保留 `audio`;结构门禁断言 |
| T-09-21-07 | DoS | 数据端硬门禁被软通过、或 OI-11 签名吞掉真实数据端错误 | high | mitigate | 分类必须同时匹配 `mota-data-isolation` 与(`@motajs/render` / `packages/render` / `packages/render-vue` / `packages/client` / `packages/system` / `packages/audio` / `packages/legacy-` / `packages-user/client-` / `src/client`);**两条路径都 `dataFailed = true` 并最终非零**;产物门禁对 `dist/data/data.js` 缺失 / 非 ESM 直接 `bad.push`,无 pending 豁免 |
| T-09-21-08 | Tampering | 字体缺失或格式非法导致 fontmin 抛错阻断构建 | medium | mitigate | 无字体二进制时跳过 fontmin 并告警(OI-4);`buildContent` 单测覆盖空 `font/` 目录 |
| T-09-21-09 | Tampering | `manualChunks` 引用不存在的 `ant-design-vue` 造成分块噪声/构建告警 | low | mitigate | 移除 `antdv` chunk、保留 `audio`;结构门禁断言 |
| T-09-21-10 | Information Disclosure | 改动既有注释 / jsDoc(AGENTS.md 禁改) | low | mitigate | prohibitions 明列;仅重写两个构建脚本自身,其它文件保持原注释 |
| T-09-21-11 | Repudiation | 未跑门禁即收口 / 未记录阻断与 `open_items` | low | mitigate | Task 2 运行单测 + 结构 + 产物 + 范围门禁;SUMMARY 记录数据端状态、OI-11 归因与 `open_items` 处置 |
| T-09-21-SC | Tampering | npm/pip/cargo installs | high | mitigate | 不新增依赖、不改 `package.json` 依赖面与 lockfile;范围门禁拒绝 lockfile 变更;如需新增依赖须回 Task 0 报备获批 |
| T-09-21-11 | Repudiation | 未跑门禁即收口 / 未记录阻断与 `open_items` | low | mitigate | Task 2 运行单测 + 结构 + 产物 + 范围门禁;SUMMARY 记录数据端硬门禁结果、渲染端分支与归因、`open_items` 处置 |
| T-09-21-SC | Tampering | npm/pip/cargo installs | high | mitigate | 不新增依赖、不改 `package.json` 依赖面与 lockfile;范围门禁将 lockfile 相对 baseline 的哈希变化判负;如需新增依赖须回 Task 0 报备获批 |
</threat_model>
<verification>
- **Task 0**:预执行状态(旧构建脚本 / `package.json` scripts / `index.html` 旧 link / 加载器标记逻辑就位;`src/content` 共 23 个 jsonc 且标记各恰好一次;新插件与测试文件不存在;报告 OI-11 链是否存在);人工关卡(用户回复「可以执行」;OI-1..OI-11 若有指定按其指定)。
- **Task 1**:`dataIsolationPlugin` 结构(`enforce: 'pre'` + `resolveId` + `moduleParsed` + `this.error` + 禁令集合 + `src/client` 守卫);运行期单测以真实 `vite.build()` 断言「拒渲染端 specifier / 拒渲染端解析路径(传递依赖)/ 拒 `src/client` / 纯数据端通过且 ESM / 数据端不误判」。
- **Task 2**:`build-resource` 单测(标记保留、去注释、非法 JSON 抛错、PNG 原样、无字体跳过);`build-game` / `package.json` / `index.html` / `build-resource` 结构断言(旧 pipeline 移除、ESM 数据端 + 插件注入 + OI-11 分类标识、content + LICENSE、旧脚本删除、scripts 调整、样式 link 移除);`pnpm exec tsx script/build-game.ts` 实跑——**(a)OI-11 预期终止 → 退出码 0 并继续;渲染端 + content + LICENSE 产物门禁必须全绿**;(b)本计划错误必须修复;(c)范围外 WIP 记 `BLOCKED`;`dist/data/data.js` 条件门禁;`git status` 范围门禁。
- **一致性**:`dist/content/**` 落点与 `MotaDataLoader.resolveURL` 生产态一致;jsonc 标记与 `PrefixedJSONCProcessor` 契约一致;`build:game` 与 `script/build-game.ts` 入口一致;`index.html` 去 link 与 OI-5(用户侧样式接线)一致。
- **无全仓门禁**:全程不使用 `pnpm check:type` / `pnpm build` / 全量 `pnpm test:ci` / 全仓 TS 诊断数(用户 WIP 存在历史类型错误)。
- **人工复核**:结论写入 `09-21-SUMMARY.md`(含范围、插件口径、content 压缩与标记理由(23 个 jsonc)、产物/排除清单、删除清单、命名获批、门禁结果、OI-11 预期阻断的完整错误与归因、渲染端 + content + LICENSE 产物结果、`open_items` 最终处置)。
- **Task 0:** 预执行状态(旧构建脚本 / `package.json` scripts / `index.html` 旧 link / `dev.md` 三行构建表 / 加载器标记逻辑就位;`src/content` 共 23 个 jsonc 且标记各恰好一次;插件与测试文件不存在;数据端链路为 data-only);**baseline 快照**(`git status --porcelain -uall` 路径哈希 + `git diff --name-only -- src`)写入系统临时目录;人工关卡(用户回复「可以执行」;OI-1..OI-12 若有指定按其指定)。
- **Task 1:** `dataIsolationPlugin` 结构(`enforce: 'pre'` + `resolveId` + `moduleParsed` + `this.error` + 禁令集合 + `packages/client*` / `src/client` 守卫 + `matched:` token);运行期单测以真实 `vite.build()` 断言「拒渲染端 specifier / 拒渲染端解析路径(含别名包与 token)/ 拒 `src/client` / 纯数据端通过并产出 ESM / 数据端不误判」。
- **Task 2:** `build-resource` 单测(标记保留、去注释、非法 JSON 抛错、PNG 原样、无字体跳过);`build-game` / `package.json` / `index.html` / `build-resource` / `dev.md` 结构断言(旧 pipeline 移除、**数据端先于渲染端**、**`dist` 只清空一次**、两处 `emptyOutDir: false`、ESM 数据端 + 插件注入、content + LICENSE、旧脚本删除、scripts 调整、样式 link 移除、`chars: string` / `Promise<string>`);`pnpm exec tsx script/build-game.ts` 实跑——数据端 ESM **硬门禁**(缺失 / 非 ESM 即非零)、渲染端**条件分支**(存在则断言、缺失则 BLOCKED)、content 与 LICENSE 必过;baseline 范围门禁。
- **一致性:** `dist/content/**` 落点与 `MotaDataLoader.resolveURL` 生产态一致;jsonc 标记与 `PrefixedJSONCProcessor` 契约一致;`build:game` 与 `script/build-game.ts` 入口一致;`index.html` 去 link 与 OI-5(用户侧样式接线)一致。
- **无全仓门禁:** 全程不使用 `pnpm check:type` / `pnpm build` / 全量 `pnpm test:ci` / 全仓 TS 诊断数(用户 WIP 存在历史类型错误)。
- **人工复核:** 结论写入 `09-21-SUMMARY.md`(范围、插件口径、数据端硬门禁结果、渲染端分支与两处 stale import 状态、content 压缩与标记理由(23 个 jsonc)、产物/排除清单、删除清单、命名获批、门禁结果、`open_items` 最终处置)。
</verification>
<success_criteria>
- **D-94:** 数据端构建由模块图插件把关(specifier + 解析路径双路,含 `src/client/**`),命中渲染端模块即 `this.error` 终止;`checkClientData` 及体积启发式已删除;数据端依赖不误判。
- **D-95:** 数据端库模式 `formats: ['es']`,产物 `dist/data/data.js`;无 iife 产物。**OI-11 未修复时该产物允许缺失**,由条件门禁与 SUMMARY 记录。
- **D-96:** `src/content`(23 个 jsonc)→ `dist/content`;jsonc 压缩且**保留系统标记**、后缀仍 `.jsonc`;fontmin 压缩(无字体时跳过告警);不分块、不 zip;`build-resource` 从 `src/content` 读取。
- **D-97:** `copyPublicDir:false`;dist 不含 D-97 所列旧模板产物;`LICENSE` 复制到 `dist/LICENSE`;`index.html` 旧样式表 link 移除;样式 import 由用户侧 `src/**` 完成(OI-5,本计划不改 `src/**`)。
- **D-93 / D-98 / D-99 / D-100:** 两个旧脚本删除、两个 npm script 删除、`build:game` 保留 `vue-tsc --noEmit`;babel 二次压缩移除;`manualChunks` 去 `antdv` 保 `audio`;legacy 插件维持。
- **边界:** 未删 `public/**`;**未改 `src/**`(零改动,含 OI-11 跨端引用链)**;未改数据端/渲染端源码、`vite.config.ts`、`script/dev.ts`、lockfile、`ROADMAP.md` / `STATE.md` / `09-CONTEXT.md` / 既有 PLAN 与 SUMMARY;未回退用户 WIP。
- **预期阻断分层(OI-11):** 数据端按设计终止被记录为 expected pending(**非本计划失败**);分类严格,其它数据端错误仍使构建失败;渲染端 + `dist/content` + `LICENSE` 产物门禁今天即可通过。
- **可运行验证:** 插件单测 + content 单测 + 结构断言 + 渲染端/content/LICENSE 产物门禁 + 范围门禁全绿;数据端条件门禁按 OI-11 状态给出 ESM 或 pending 记录。
- **D-94:** 数据端构建由模块图插件把关(specifier + 解析路径双路,含 `packages/client*` / `src/client/**`),命中渲染端模块即 `this.error` 终止且错误含 `matched:` token;`checkClientData` 及体积启发式已删除;数据端依赖不误判。
- **D-95:** 数据端库模式 `formats: ['es']`,产物 `dist/data/data.js`;无 iife 产物;**数据端先于渲染端构建**,两者均 `emptyOutDir: false`;`dist/data/data.js` 缺失或非 ESM **一律判负**(无预期放行、无 sentinel 豁免)。
- **D-96:** `src/content`(23 个 jsonc)→ `dist/content`;jsonc 压缩**保留系统标记**、后缀仍 `.jsonc`;fontmin 压缩(无字体时跳过告警);不分块、不 zip;`build-resource` 从 `src/content` 读取。
- **D-97:** `copyPublicDir: false`;dist 不含 D-97 所列旧模板产物;`LICENSE` 复制到 `dist/LICENSE`;`index.html` 旧样式表 link 移除;样式 import 由用户侧 `src/**` 完成(OI-5,本计划不改 `src/**`)。
- **D-93 / D-98 / D-99 / D-100:** 两个旧脚本删除、两个 npm script 删除、`dev.md` 构建表同步、`build:game` 保留 `vue-tsc --noEmit`;babel 二次压缩移除;`manualChunks` 去 `antdv` 保 `audio`;legacy 插件维持现状。
- **边界:** 未删 `public/**`;**未改 `src/**`(零改动,含两处 stale import)**;未改数据端/渲染端源码、`vite.config.ts`、`script/dev.ts`、lockfile、`ROADMAP.md` / `STATE.md` / `09-CONTEXT.md` / 既有 PLAN 与 SUMMARY;未回退用户 WIP。
- **门禁分层:** 数据端 ESM 硬门禁必须通过;渲染端成功 → 追加渲染产物断言;渲染端因用户 stale import 失败 → 打印 BLOCKED 并跳过渲染断言(数据端、content、LICENSE 仍断言);**不作出任何无条件通过承诺**。
- **可运行验证:** 插件单测 + content 单测 + 结构断言(顺序 / 清空 / 类型 / token)+ 数据硬门禁 + 条件渲染门禁 + baseline 范围门禁全部就位并跑到确定结论。
</success_criteria>
<output>