docs(09-21): revision 6 - minimal build-script scope, data.js at dist root, no special failure handling

This commit is contained in:
unanmed 2026-10-20 16:12:21 +08:00
parent f1c0f8b2a4
commit 1a22d25e58

View File

@ -8,142 +8,90 @@ files_modified:
- 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
autonomous: false
requirements: []
estimate:
tokens: 36000
raw_tokens: 36000
tokens: 26000
raw_tokens: 26000
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` 终止 + 错误含命中 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。样式改由入口经 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/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 范围门禁**兜底;`cmd /c \"pnpm exec tsx script/build-game.ts\"` **必须实跑**(本仓 PowerShell 5.1 下裸 `pnpm` 会因 `pnpm.ps1` 的 `PSSecurityException` 失败,故一律经 `cmd /c` 调用),且**构建进程必须在有界时间内显式退出**(见 Task 2 的显式 `process.exit(...)` 要求)。**数据端 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` 仍照常断言。**不存在任何无条件通过承诺。**"
- "**本计划第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md / D-03)**:先汇报「任务 11 · 构建重构(适配新引擎)」的要做什么 / 为什么 / 大致怎么做 / 影响范围,列明最小范围(只动 4 个 script 路径 + 删除 `build-packages.ts` + 可选插件测试)、确认的目标构建流程(准备 → 渲染端 → 数据端 → 隔离检测 → content → LICENSE)、**不改构建顺序**、**不做特殊失败处理(无 BLOCKED、无 baseline 范围门禁、无对外部文件/状态的检查)**、以及待批准命名(OI-1 / OI-2),然后停下等待「可以执行」。"
- "**渲染端(D-97 适配):** 以 `index.html` 为入口(Vite 默认,不显式设置 `rollupOptions.input`,`index.html` 本身不改),按 `vite.config.ts` 常规配置输出到 `dist`(常规 `index.html` + `assets/` / `chunks/`);`copyPublicDir: false`、`emptyOutDir: false`;不再拷贝旧模板产物(`editor.html` / `_server` / `_docs` / `libs` / `logo.png` / `styles.css` / `main.js` / `启动服务.exe`),不再做 `main.js` 的 babel 二次压缩。"
- "**数据端(D-94 / D-95):** 入口 `src/data.ts`,Vite 库模式 **ESM**(`formats: ['es']`);**入口产物固定为 `dist/data.js`(dist 根目录)**,数据端附属产物(chunk / asset)落在**数据端自己的子文件夹 `dist/data/`**,**不得与渲染端的 `assets/`、`chunks/` 混用**;`emptyOutDir: false`(不得清掉已产出的渲染端产物);注入 `dataIsolationPlugin()`。"
- "**隔离检测(D-94):** 新增 Vite 插件扫描数据端模块图(specifier + 解析后路径双路);命中渲染端模块即 `this.error(...)` 终止;错误消息含 `[mota-data-isolation]`、命中模块与 `matched: <token>`;**取代**现有按体积的 `checkClientData` 启发式(`checkClientData` / `ClientDataLevel` / `getFileName` / `getFileSize` 一并删除)。"
- "**content(D-96):** `src/content` → `dist/content`;`*.jsonc` 压成**纯 JSON、去掉所有注释(含 `// --- SYSTEM PREFIX END --- //`)**,后缀仍 `.jsonc`;字体用 fontmin 压缩(无字体二进制时跳过并告警);**不分块、不 zip**;`build-resource` 从 `src/content` 读取素材。"
- "**最终 `dist` 根(D-97):** `index.html` + Vite 常规渲染产物 + `data.js` + `content/` + `LICENSE`;数据端附属在 `dist/data/`;**不产出** `source-code.zip` / `loadList.json` / `dist.zip` / 资源分块 `resource.<hash>.h5data`。"
- "**写入边界:** 只写 `files_modified`;**不改** `script/build-lib.ts`、`package.json`、`index.html`、`dev.md`、`src/**`、`public/**`、`vite.config.ts`、lockfile、`packages*/**`、`script/dev.ts`;不回退用户既有 WIP。"
- "**命名须先获批(D-04):** `script/vite-plugin-data-isolation.ts` / `dataIsolationPlugin` / `name: 'mota-data-isolation'` / `script/vite-plugin-data-isolation.test.ts`(OI-1);`buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats`(OI-2)。未获批前不得落码。"
- "**验证可运行(Nyquist):** 插件单测以**真实 `vite.build()`** 证明「拒渲染端 import / 通过纯数据端」;两个脚本做结构断言(无 `checkClientData`、数据端 `formats: ['es']` 且入口 `data.js` 在根、附属产物在 `dist/data/`、jsonc 去注释、无 zip / 分块、无旧模板拷贝、数据子文件夹与渲染端分离);命令一律 `cmd /c \"pnpm exec ...\"`(PowerShell 5.1 下裸 `pnpm` 会因 `pnpm.ps1` 的 `PSSecurityException` 失败)。"
artifacts:
- path: script/vite-plugin-data-isolation.ts
provides: "新增 Vite 插件 `dataIsolationPlugin`:`enforce: 'pre'` + `resolveId` 拦渲染端 specifier,`moduleParsed` 按解析后绝对路径拦渲染端包(含传递依赖与 `src/client/**`),命中即 `this.error(...)` 终止数据端构建;错误消息含 `[mota-data-isolation]` + 命中模块 + **命中 token(`matched: <token>`)** + importers(命名待批准,OI-1)"
provides: "新增插件 `dataIsolationPlugin`:`enforce: 'pre'` + `apply: 'build'`;`resolveId` 拦渲染端 specifier,`moduleParsed` 按解析后绝对路径兜底;命中即 `this.error(...)`,消息含 `[mota-data-isolation]` / 命中模块 / `matched: <token>`(命名待批准,OI-1)"
- path: script/vite-plugin-data-isolation.test.ts
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)"
provides: "运行期单测:真实 `vite.build()`(`configFile:false`、临时 fixture、`write:false`、lib ESM)验证拒渲染端 specifier(`vue`)、拒渲染端解析路径(`src/client` / `packages-user/client-*`)、通过纯数据端并产出 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 子集压缩字体、其它素材原样拷贝;无字体二进制时跳过并告警)与结果/选项接口(`IContentBuildOptions.chars: string`;命名待批准,OI-2)"
- path: script/build-resource.test.ts
provides: "运行期单测:`minifyJSONC` 保留标记且压缩、去注释、非法 JSON 抛错、无标记纯 JSON 处理;`buildContent` 在临时目录完成 jsonc 压缩 + PNG 原样拷贝 + 无字体时跳过(`chars` 以 `string` 传入);每个 `it` 前一行注释"
provides: "重写为 content 处理模块:`minifyJSONC`(按行去掉 `//` 注释后 `JSON.parse` + `JSON.stringify`,输出纯 JSON)、`buildContent`(递归 `src/content` → `dist/content`:jsonc 压缩、fontmin glyph 子集、其它素材原样拷贝;无字体时跳过告警)与 `IContentBuildOptions`(`chars: string`)/ `IContentBuildStats`"
- path: script/build-game.ts
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` 等旧逻辑"
provides: "重写编排:清空/创建 `dist` 与 `_temp` 一次 → 渲染端(`index.html` 入口 → `dist`)→ 数据端(库模式 ESM,入口 `dist/data.js`、附属 `dist/data/`)→ `getAllChars` → `buildContent` → 复制 `LICENSE`;删除旧 pipeline(babel / zip / 分块 / 旧模板拷贝 / 资源地图 / `checkClientData`)"
- path: script/build-packages.ts
provides: "**删除**(D-93 / D-98)"
- path: script/build-lib.ts
provides: "**删除**(D-93 / D-98)"
- path: package.json
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);不改其它章节"
provides: "**删除**(D-93 / D-98;`build-lib.ts` 不动)"
- path: .planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md
provides: "本计划执行摘要:范围、插件扫描口径与错误行为(含命中 token)、content 压缩口径(含标记保留这一兼容性约束)、dist 产物清单与排除项、删除清单、`dev.md` 同步、`open_items` 最终处置、门禁结果(渲染端 BLOCKED 分支或成功分支)、数据端 ESM 硬门禁结果、渲染端两处 stale import 的当前状态、**D-97「样式经入口 JS 引入」不由本计划交付的显式声明(`src/styles.less` 全仓无 import 点;移除 link 后、用户接线前 `dist` 缺 `#game-draw` 居中样式,OI-5;不静默降级)**、baseline 范围门禁结果"
provides: "执行摘要:范围、插件扫描口径与错误行为、数据端产物落点(`dist/data.js` + `dist/data/`)、content 压缩口径(纯 JSON、无注释)、dist 产物与排除清单、删除清单、命名获批情形、门禁结果、`open_items` 处置"
key_links:
- "`dataIsolationPlugin`(`script/vite-plugin-data-isolation.ts`)↔ `buildData` 的 `plugins` 数组(`script/build-game.ts`)—— D-94 的唯一执行点;取代 `checkClientData` 体积启发式"
- "插件 `this.error` 错误文本(含 `mota-data-isolation`、命中模块与**命中 token**)↔ `buildGame` 的数据端 `.catch` 分类 —— 归一化后(`String(reason)` 上把 Windows 反斜杠替换为 `/`,**源码单反斜杠正则**)同时匹配插件标识与已知渲染端 token(`@motajs/render` / `@motajs/client` / `@motajs/system` / `@motajs/audio` / `@motajs/legacy-` / `@user/client-` / `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 / `{}`"
- "`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)"
- "`dataIsolationPlugin`(`script/vite-plugin-data-isolation.ts`)↔ `buildData` 的 `plugins` 数组(`script/build-game.ts`)—— D-94 的唯一执行点,取代 `checkClientData` 体积启发式"
- "`buildData` 的 `build.lib.fileName: 'data'` + `rollupOptions.output.entryFileNames: 'data.js'` ↔ `dist/data.js`(dist 根)—— D-95 入口产物契约"
- "`buildData` 的 `chunkFileNames` / `assetFileNames` 前缀 `data/` ↔ `dist/data/` —— 数据端附属产物与渲染端 `assets/`、`chunks/` 分离"
- "`buildClient` 不设 `rollupOptions.input` ↔ Vite 默认以 `index.html` 为入口 —— 渲染端输出常规 `dist/index.html` + `assets/` / `chunks/`"
- "`minifyJSONC` 的按行去注释 ↔ D-96「压成纯 JSON、去掉所有注释(含系统标记行)」;后缀仍 `.jsonc`"
- "`getAllChars` 返回 `Promise<string>` ↔ `IContentBuildOptions.chars: string` ↔ `Fontmin.glyph({ text })` —— 类型一致,不得让 `Set<string>` 流入"
- "`dist/content/**` ↔ `MotaDataLoader.resolveURL` 的 `content/**` 相对路径与 `src/data/constant.ts` 的 `CORE_URL='content/core.jsonc'` —— 目录落点契约"
assumptions:
- "**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,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` 导入(仅旧 `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` 不一致。"
- "**行号可能漂移:** 本计划引用行号为规划日实测;门禁以「文件存在 + 结构/行为断言」为准,不硬编码行号。"
- "**构建顺序维持现状:** 准备(清空/创建 `dist`、`_temp`)→ 渲染端 → 数据端;与现有 `build-game.ts` 的 client → data 顺序一致。数据端构建必须 `emptyOutDir: false`,否则会清掉渲染端产物。"
- "**`src/content` 共 23 个 `.jsonc`:** 均为「前缀注释(`// Type: ...` / `// --- SYSTEM PREFIX END --- //`)+ 严格 JSON」;注释只以整行 `//` 形式出现,故按行过滤可安全转成纯 JSON。"
- "**jsonc 输出为纯 JSON(无注释、无标记行):** 按本条执行;运行期 `PrefixedJSONCProcessor` 依赖标记行的适配不在本计划范围(不改 `src/**` / `packages-user/**`)。"
- "**字体:** `src/content/font/` 仅有 `.list.jsonc`(引用 `testFont.ttf`),无字体二进制;fontmin 无源可压时跳过并告警,不阻断构建。"
- "**渲染端可能构建失败:** `src/client/initialize.ts:14`(`'../constant'`)与 `src/client/plugin/shop/shop.tsx:9`(`'@/constant'`)仍指向已删除的 `src/constant.ts`,属用户 `src` WIP。按本条**不做特殊失败处理**:构建失败即失败,不由本计划的脚本层做 BLOCKED 分支;本计划的验证不实跑全量构建。"
- "**`build-lib.ts` 保持不动:** 仅删除 `build-packages.ts`;`package.json` 不改,故 `build:packages` script 会指向已删除文件(按本条接受)。"
- "**`manualChunks` 维持现状:** 当前 `antdv` + `audio` 两段不在本次适配范围,不改。"
- "**行号可能漂移:** 门禁以「文件存在 + 结构/行为断言」为准,不硬编码行号。"
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 回归诊断,最终退出码仍为非零。**`buildGame` 必须在每个分支显式 `process.exit(...)` 终止进程**(先 flush stdout;不得依赖事件循环自然退出——Vite/esbuild 的 handle 会让进程在渲染端构建失败后存活至超时)。"
- "**不得删除 `public/**`:** D-97 的「整体删除」由用户执行;本计划只通过 `copyPublicDir: false` 阻止其进入 `dist`。"
- "**写入边界(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 回归诊断;渲染端若因用户两处 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` 做负向检查,残留会被判负;并对**裸 `iife`** 做负向检查——`formats: ['iife']`、`data.iife.js`、甚至注释里解释「不用 iife」的说明都不得出现在 `script/build-game.ts` 中(要与该门禁口径一致,改写说明请放在本 PLAN / SUMMARY,不写进源码注释)。"
- "不得在用户回复通过 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`(OI-2)。"
- "**构建顺序不得倒置:** 必须先渲染端、后数据端;`buildData` 必须 `emptyOutDir: false`(不得清掉已产出的渲染端产物)。"
- "**数据端附属产物不得混入渲染端目录:** 不得让 chunk / asset 落到 `assets/` 或 `chunks/`;入口必须是 `dist/data.js`(根),不得改成 `dist/data/data.js`。"
- "**jsonc 必须去净注释:** 不得保留 `// --- SYSTEM PREFIX END --- //` 或 `// Type: ...`;不得改后缀(仍 `.jsonc`)。"
- "**不得保留旧 pipeline:** 不得把 `checkClientData` / `ClientDataLevel` / `getFileName` / `getFileSize` / `generateResourceJSON` / `splitResource` / `transformAsync` / `archiver` / `compressing` / `source-code.zip` / `loadList.json` / `dist.zip` / 旧模板文件名写进源码(含注释与字符串)。"
- "**不得改范围外文件:** 不改 `script/build-lib.ts`、`package.json`、`index.html`、`dev.md`、`src/**`、`public/**`、`vite.config.ts`、lockfile、`packages*/**`、`script/dev.ts`;不改既有注释 / jsDoc(重写两个脚本自身的注释属重写范围)。"
- "**不得新增依赖、不改 lockfile。**"
- "**不得引入特殊失败处理:** 不写 BLOCKED 分支、不写 baseline / 范围门禁、不检查外部文件或仓库状态;构建失败即以非零退出并打印错误。"
- "**不得伪造门禁:** 不得手写 `dist` 产物或用手工文件冒充构建结果。"
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: 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(全量构建的可验证性 = 数据端硬门禁 + 渲染端条件分层):** 当前实测:**数据端闭包无渲染端导入**,`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/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 仍硬断言)。"
- "**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`。**待批准**(D-04;测试文件按 AGENTS.md 免审批,仍一并报备)。"
- "**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`)。备选:`processContent` / `compressJSONC`。**待批准**。"
---
<objective>
Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎)**。
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/**` 相对路径读取,渲染端/数据端已分家。继续沿用旧脚本既无法跑通新数据,也无法保证「数据端独立包不得引用渲染端」。
**要解决的问题:** 现有 `script/build-game.ts` / `script/build-resource.ts` 面向旧引擎与旧模板:数据端产出 iife、用体积启发式 `checkClientData` 猜「是否引用了渲染端」、`main.js` 做 babel 二次压缩、资源按 2MB 分块并 zip、拷贝 `editor.html` / `_server` / `_docs` / `libs` / `logo.png` / `styles.css` / `启动服务.exe`、打包 `source-code.zip`、生成 `loadList.json`、再整体压 `dist.zip`。新引擎的数据落在 `src/content`,渲染端与数据端分家。
**目标(D-93..D-100):**
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);同步 `dev.md` 构建表(W5)。
6. **D-100** 移除 `main.js` 二次压缩;`manualChunks` 按实际依赖微调(`antdv` 已无源码引用 → 移除;`audio` 保留);legacy 插件维持现状。
**确认的目标构建流程(不得改序):**
1. 准备:清空 / 创建 `dist`、`_temp`。
2. 渲染端:以 `index.html` 为入口(`index.html` 本身不改),按 Vite 常规配置输出到 `dist`(常规 `index.html` + `assets/` / `chunks/`)。
3. 数据端:入口 `src/data.ts`,库模式 **ESM**;入口产物固定 **`dist/data.js`(dist 根)**;附属产物放**数据端自己的子文件夹**(如 `dist/data/`),**不得与渲染端 `assets/`、`chunks/` 混用**。
4. 隔离检测:构建数据端时用 Vite 插件扫描模块图,命中渲染端即 `this.error` 终止;**取代** `checkClientData`。
5. 内容:`src/content` → `dist/content`;jsonc 压成**纯 JSON、去掉所有注释(含系统标记行)**,后缀仍 `.jsonc`;字体 fontmin 压缩;**不分块、不 zip**。
6. `LICENSE` 复制到 `dist`。
7. 不产出旧模板内容与 `source-code.zip` / `loadList.json` / `dist.zip`;移除 `main.js` babel 步骤。
8. 最终 `dist` 根:`index.html` + Vite 常规渲染产物 + `data.js` + `content/` + `LICENSE`;数据端附属在 `dist/data/`。
**关键兼容性约束(本计划对 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 待确认)。
**约束:** 不改构建顺序;只做上述适配;不做特殊失败处理(无 BLOCKED、无 baseline/范围门禁、无对外部文件或状态的检查);构建失败就失败。
**当前实测状态(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`)= 预执行汇报关卡**:汇报要做什么 / 为什么 / 大致怎么做 / 影响范围(含插件扫描口径与命中 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]`(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` / `dev.md` 构建表、写 SUMMARY),将由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 所列路径外无任何写入,**`src/**` 零改动**。
**修改方式声明(AGENTS.md):** 本计划执行含文件写入与删除,由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 外无任何写入。
</objective>
<execution_context>
@ -157,280 +105,198 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
@.planning/STATE.md
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
@AGENTS.md
@dev.md
@script/build-game.ts
@script/build-resource.ts
@script/build-packages.ts
@script/build-lib.ts
@package.json
@index.html
@vite.config.ts
@index.html
@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/bootstrap.ts
@src/content/core.jsonc
@packages-user/data-state/src/loader/jsoncProcessor.ts
@packages-user/data-state/src/loader/loader.ts
</context>
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<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>
<name>Task 0: 预执行汇报关卡 —— 汇报最小范围、目标构建流程与待批准命名,等待「可以执行」</name>
<precondition>`script/build-game.ts` / `script/build-resource.ts` 仍为旧引擎实现(含 `checkClientData`、babel、zip、资源分块、旧模板拷贝);`script/build-packages.ts` 与 `script/build-lib.ts` 均存在;`index.html` 含 `/styles.css` link 与 `/src/bootstrap.ts` module script;`vite.config.ts` 提供 `base: './'` 与 `@motajs/*` / `@user/*` / `@dp/*` / `@/` 别名;`src/content/**` 共 23 个 `.jsonc`、`src/content/font/` 无字体二进制;`script/vite-plugin-data-isolation.ts` 与测试文件尚不存在。</precondition>
<files>(只读汇报,不修改任何文件)</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);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 --porcelain -uall`(用户 WIP 面)
- AGENTS.md(执行前须获批、命名须先反馈、修改方式须预先声明、禁改既有注释);dev.md(命名规则、对象类型须声明 interface、模块无副作用、非空判断约定、jsDoc 规范)
- .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)
- script/build-game.ts(`buildClient` / `buildData` / `checkClientData` / `getAllChars` / `generateResourceJSON` / `buildGame`)
- script/build-resource.ts(`splitResource` 分块 zip 逻辑);script/build-packages.ts(待删除)
- index.html(`<link href='/styles.css'>` 与 `/src/bootstrap.ts`);vite.config.ts(别名 / `base`)
- src/data.ts;src/content/core.jsonc 与 src/content/font/.list.jsonc(注释 + 严格 JSON 形态)
- AGENTS.md(执行前须获批、命名须先反馈、修改方式须预先声明)
</read_first>
<action>
**停下并向用户汇报,等待用户回复「可以执行」后才继续;汇报本身只读(不修改仓库文件),但必须先捕获 baseline 快照。**
**停下并向用户汇报,等待回复「可以执行」后才继续;汇报本身只读。**
① **要解决的问题:** 旧构建脚本面向旧引擎与旧模板(依赖 `public/project`、数据端 iife、体积启发式、`main.js` 二次压缩、资源分块 zip、旧模板拷贝、源码 zip、`loadList.json`、`dist.zip`),无法适配新引擎的 `src/content` 数据与双端分离。
① **要解决的问题:** 旧构建脚本面向旧引擎(iife 数据端、体积启发式、babel 二次压缩、资源分块 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` 不改;同步 `dev.md` 构建命令表(W5)。
② **范围(最小):** 新增 `script/vite-plugin-data-isolation.ts`(+ 可选 `script/vite-plugin-data-isolation.test.ts`);重写 `script/build-game.ts` 与 `script/build-resource.ts`;删除 `script/build-packages.ts`;写 `09-21-SUMMARY.md`。**`build-lib.ts` / `package.json` / `index.html` / `dev.md` / `src/**` / `public/**` / `vite.config.ts` / lockfile / `packages*/**` / `script/dev.ts` 全部不动。**
③ **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`。
③ **目标构建流程(原样告知,不得改序):** 准备(清空/创建 `dist`、`_temp`)→ 渲染端(`index.html` 入口,常规输出到 `dist`)→ 数据端(`src/data.ts`,库模式 ESM,入口 `dist/data.js`,附属产物 `dist/data/`)→ 隔离检测(插件 `this.error`)→ content(`src/content` → `dist/content`,jsonc 纯 JSON 去净注释、后缀 `.jsonc`、fontmin、不分块不 zip)→ `LICENSE` 复制。最终 `dist` 根 = `index.html` + Vite 常规渲染产物 + `data.js` + `content/` + `LICENSE`。
④ **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`,因此**渲染端失败不会删除数据端产物**。
④ **隔离检测口径:** 插件 `enforce: 'pre'` + `apply: 'build'`;`resolveId` 拦 specifier(`@user/client-*`、`@user/entry-client`、`@motajs/render*`、`@motajs/client*`、`@motajs/system`、`@motajs/audio`、`@motajs/legacy-*`、`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(...)`,错误含 `[mota-data-isolation]` / 命中模块 / `matched: <token>`。`checkClientData` / `ClientDataLevel` / `getFileName` / `getFileSize` 一并删除。
⑤ **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,每个恰好一个标记。
⑤ **content 口径:** jsonc 按行去掉 `//` 注释后 `JSON.parse` + `JSON.stringify`,输出纯 JSON(**含系统标记行在内的所有注释都去掉**),后缀仍 `.jsonc`;字体 fontmin glyph 子集(无字体二进制时跳过告警);不分块、不 zip。
⑥ **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。**D-97 的「样式经入口 JS 引入」不由本计划交付:** `src/styles.less` 当前全仓无 import 点(仅 `index.html` 的旧 link 引用),按用户裁定「你改 src,我做脚本」本计划不改 `src/**`;因此移除 link 后、用户接线前 `dist` 缺 `#game-draw` 居中样式(OI-5),该缺口须**显式记入 SUMMARY**,不得描述为已交付。
⑥ **失败处理口径(原样告知):** **不做特殊失败处理** —— 无 BLOCKED 分支、无 baseline / 范围门禁、无对外部文件或仓库状态的检查;构建失败即打印错误并非零退出。本计划**不实跑全量构建**作为门禁(渲染端仍有两处用户 `src` WIP 的 stale import)。
⑦ **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` 说明**。
⑦ **待批准命名(须用户确认):** OI-1(插件文件 / `dataIsolationPlugin` / `name: 'mota-data-isolation'` / 测试文件);OI-2(`buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats`)。未获批前不落码。
⑧ **拟新增命名(须报备批准):** `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('')`)。**
⑧ **验证方式:** 插件单测以真实 `vite.build()` 证明「拒渲染端 import / 通过纯数据端」;两个脚本做结构断言(无 `checkClientData`、数据端 `formats: ['es']` 且入口 `data.js` 在根、附属产物 `dist/data/`、jsonc 去注释、无 zip / 分块、无旧模板拷贝、数据子文件夹与渲染端分离);命令一律 `cmd /c "pnpm exec ..."`。
⑨ **待确认项:** 标记保留与 `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)。
⑩ **当前数据端引用链(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`。
⑪ **渲染端两处 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/**`**,须在汇报中说明修复属用户任务。
⑫ **门禁分层(必须原样告知):** **数据端先行且独立:`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 在任何构建结果下都会尝试产出)。**不存在任何无条件通过承诺。**
⑬ **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 被豁免。
⑭ **产出文件:** 新增 `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/**`。**
⑮ **不在本增量:** 不删 `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。
⑯ **已知阻断(须一并告知并记入 SUMMARY):** ① 数据端**当前无已知阻断**(历史 OI-11 链路已解决);② 渲染端可能因两处 stale import 失败(用户 WIP,用户修复);③ 若渲染端仍因用户其它 WIP 失败,记录完整错误与归因,**不得**修改范围外文件;④ **样式缺口(D-97 未尽项)**:`src/styles.less` 全仓无 import 点,移除 `index.html` 的 `styles.css` link 后,用户接线前 `dist` 缺 `#game-draw` 居中样式(OI-5),本计划**不交付**「样式经入口 JS 引入」,须显式写明。
⑰ **验证方式(PowerShell 5.1 口径:一律经 `cmd /c` 调用,裸 `pnpm` 会因 `pnpm.ps1` 的 `PSSecurityException` 失败):** Task 1 = `cmd /c "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 = `cmd /c "pnpm exec vitest run script/build-resource.test.ts"` + 构建脚本结构断言(含 **数据先于渲染的调用顺序**、**`dist` 只清空一次**、**两处 `emptyOutDir: false`**、**显式 `process.exit(...)` 终止**、`getAllChars` 返回 `Promise<string>`、`dev.md` 构建表)+ 产物门禁(**数据端 ESM 硬断言** / 渲染端条件分支)+ **baseline 范围门禁** + `cmd /c "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 给出指定,按指定执行。
⑨ **不在本增量:** 不改 `src/**`(含两处 stale import);不改 `package.json`(故 `build:packages` 会指向已删除文件);不改 `dev.md` 构建表;不删 `public/**`;不新增依赖;不改 `manualChunks` / legacy 配置。
</action>
<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>
<decision>是否按本计划(09-21,最小范围)执行「任务 11 · 构建重构」——新增数据端隔离 Vite 插件并注入数据端库模式 ESM 构建(入口 `dist/data.js`、附属 `dist/data/`)、重写 `build-resource`(`src/content` → `dist/content`,jsonc 纯 JSON 去净注释、fontmin、不分块不 zip)、重写 `build-game`(准备 → 渲染端 → 数据端 → content → LICENSE;去掉 babel / zip / 分块 / 旧模板拷贝)、删除 `build-packages.ts`;**仅改上述脚本,其余文件一律不动**。</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` 不匹配——属既有缺口,本计划只记录不改。**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/**`**,故渲染端产物门禁是条件分支,但**绝不能拖累数据端硬门禁**。**baseline 由 Task 0 在执行时捕获,不预置并发文件清单:** 规划日实测 `git status --porcelain -uall` 仅含 `.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md`、`.planning/phases/10-docs-restructure/10-CONTEXT.md` 与 `10-13..10-20-PLAN.md`、`pnpm-lock.yaml` 的既有改动(`index.html` / `packages-user/**` / `src/**` 当前无改动);执行时以实际快照为准,提交时须逐条核对暂存面、只暂存本计划 `files_modified`。行号为规划日实测、可能漂移,门禁以结构/行为断言为准。
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。执行期含写操作与一个脚本删除。`src/content` 共 23 个 jsonc,均为「前缀注释 + 严格 JSON」,按行去注释即可安全转成纯 JSON。渲染端两处 stale import 仍存在(`src/client/initialize.ts:14` / `src/client/plugin/shop/shop.tsx:9`),属用户 WIP,本计划不修、也不据此设置构建门禁。
</context>
<options>
<option id="execute-default">
<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>
<name>可以执行(按本计划默认口径:OI-1 / OI-2 采用提议命名,构建流程与失败处理按上述口径)</name>
<pros>最小改动面把旧 pipeline 换成新引擎产物;隔离检查由体积启发式升级为模块图硬报错;数据端入口与附属产物落点明确且与渲染端分离;content 压缩口径明确;验证全部可运行</pros>
<cons>会删除 `build-packages.ts`(`package.json` 不改,故 `build:packages` 失效);引入若干待批准命名</cons>
</option>
<option id="revise">
<name>修订 OI-1..OI-12 的任一取向后重新汇报</name>
<pros>避免带着未确认的命名 / 样式落点 / 前置链 / 标注口径落码</pros>
<name>修订 OI-1 / OI-2 命名或任一流程细节后重新汇报</name>
<pros>避免带着未确认的命名落码</pros>
<cons>须修订 PLAN.md 后重新汇报</cons>
</option>
</options>
<resume-signal>回复「可以执行」即可(默认采用 OI-1..OI-12 的提议口径)。如需调整任一 `open_items`,请在回复中一并指明。</resume-signal>
<resume-signal>回复「可以执行」即可;如需调整命名或流程细节,请在回复中一并指明。</resume-signal>
<verify>
<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>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['script/build-game.ts','script/build-resource.ts','script/build-packages.ts','script/build-lib.ts','index.html','vite.config.ts','src/data.ts','src/content/core.jsonc','src/content/texture/.list.jsonc','src/content/font/.list.jsonc','packages-user/data-state/src/loader/jsoncProcessor.ts']){if(!fs.existsSync(p))bad.push('missing '+p)}const jc=fs.readdirSync('src/content',{recursive:true}).filter(f=>String(f).endsWith('.jsonc')).length;if(jc!==23)bad.push('expected 23 jsonc, got '+jc);if(fs.existsSync('script/vite-plugin-data-isolation.ts'))bad.push('plugin file must not exist yet');const bg=fs.readFileSync('script/build-game.ts','utf8');if(bg.indexOf('checkClientData')<0)bad.push('build-game.ts is not the legacy implementation');if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK prestate: legacy scripts in place, 23 jsonc, plugin absent')"</automated>
<fails_when>non-zero exit,或 stderr 含 "PRESTATE FAILED":必需文件缺失 / jsonc 数量不是 23 / 插件文件已存在 / `build-game.ts` 已不是旧实现</fails_when>
<human-check>用户明确回复「可以执行」(若就 OI-1 / OI-2 给出指定,按指定执行)</human-check>
</verify>
<acceptance_criteria>
- 用户回复中明确包含「可以执行」
- 汇报内容涵盖:要解决的问题、范围(两个脚本重构 + 两个脚本删除 + 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` 构建表将同步删除这两个命令**
- 汇报中已**显式说明 D-97 的未尽项**:「样式经入口 JS 引入」**不由本计划交付**(`src/styles.less` 全仓无 import 点;移除 link 后、用户于 `src/**` 接线前,`dist` 缺 `#game-draw` 居中样式,OI-5),并承诺写入 SUMMARY,不静默降级
- 汇报中已说明**构建必须在每个分支显式 `process.exit(...)` 终止**(先 flush stdout),不得依赖事件循环自然退出(渲染端失败时 Vite/esbuild 残留 handle 会使进程挂到超时),且所有验证命令一律经 `cmd /c "pnpm exec ..."` 调用(PowerShell 5.1 下裸 `pnpm` 触发 `PSSecurityException`)
- 汇报中已原样告知**关卡性质**(必须停下等待;不因触发方式或 `yolo` / `auto_advance` 自动放行;回复「可以执行」即通过)
- 汇报涵盖:要解决的问题、最小范围与「其余文件一律不动」清单、目标构建流程 8 步与「不改构建顺序」、隔离检测口径(specifier + 解析路径 + `matched:` token + 取代 `checkClientData`)、数据端 ESM 入口 `dist/data.js` 与附属 `dist/data/`、content 纯 JSON 去净注释、最终 `dist` 清单与排除项、**不做特殊失败处理(无 BLOCKED / 无 baseline 门禁 / 无外部状态检查)**、OI-1 / OI-2 命名、验证方式
- 已说明删除 `build-packages.ts` 而 `package.json` 不改(`build:packages` 将失效)
- 已说明本计划不改 `src/**`,渲染端两处 stale import 由用户处理,且不作为本计划门禁
</acceptance_criteria>
<done>用户明确回复「可以执行」;baseline 快照已写入系统临时目录(含路径哈希与 `src` diff 清单);在此之前未执行任何任务、未修改或删除任何仓库文件、未派发任何执行子代理</done>
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改或删除任何仓库文件、未派发任何执行子代理</done>
</task>
<task type="tracer">
<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>
<name>Task 1: 数据端隔离插件 `dataIsolationPlugin` + 真实 `vite.build()` 单测</name>
<precondition>Task 0 已闭环(用户回复「可以执行」,OI-1 已获批或按用户指定)。`vite` 与 `vitest` 可用;`script/vite-plugin-data-isolation.ts` 与测试文件尚不存在。</precondition>
<files>script/vite-plugin-data-isolation.ts, script/vite-plugin-data-isolation.test.ts</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(**D-94**;D-04 / D-56)
- vite.config.ts(别名:`@motajs/*` → `packages/*/src`、`@user/*` → `packages-user/*/src`、`@dp/*`、`@/*`;`base: './'`)
- packages-user/client-modules/package.json / packages-user/client-base/package.json / packages-user/client-system/package.json(渲染端包面)
- packages-user/data-state/package.json / packages-user/data-common/package.json / packages-user/data-base/package.json / packages-user/data-system/package.json(数据端包面,用于避免误判)
- script/check-data-circular.test.ts(既有 `script/*.test.ts` 的 vitest 风格:显式 `import { describe, expect, it } from 'vitest'`;每个 `it` 前一行注释)
- vitest 配置位于 vite.config.ts 的 `test` 段(`testTimeout: 30000`)
- AGENTS.md(测试文件规则:每个 `it` 前一行注释;命名须先获批);dev.md(命名规范、jsDoc、模块无副作用、非空判断约定)
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-94;D-04)
- vite.config.ts(`@motajs/*` / `@user/*` / `@dp/*` / `@/` 别名,`base: './'`)
- script/check-data-circular.test.ts(既有 `script/*.test.ts` 风格:显式 `import { describe, expect, it } from 'vitest'`;每个 `it` 前一行注释)
- AGENTS.md(测试文件:每个 `it` 前一行注释);dev.md(命名、jsDoc、模块无副作用、非空判断约定)
</read_first>
<action>
(1) **新建 `script/vite-plugin-data-isolation.ts`(命名以 OI-1 获批结果为准):**
- 自 `vite` 导入**类型** `Plugin`(`import { Plugin } from 'vite'`,按 dev.md 不使用 `import type`)。
- 模块内声明两组模块级只读常量(不导出,避免额外命名面),每个条目都是 `{ 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]`** 开头并包含 **`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;模块顶层零副作用(只声明常量与函数)。
- 自 `vite` 导入类型 `Plugin`(按 dev.md 不使用 `import type`)。
- 模块内声明两组不导出的只读常量,每项 `{ test: RegExp, token: string }`:
- **specifier 禁令**:`@user/client-`(token `@user/client-`)、`@user/entry-client`、`@motajs/render`、`@motajs/client`、`@motajs/system`、`@motajs/audio`、`@motajs/legacy-`、`vue` 与 `vue/*`(token `vue`)、`@vue/`、`ant-design-vue`、`@ant-design/icons-vue`。
- **解析路径禁令**:`packages-user/client-`、`packages/render`、`packages/render-vue`、`packages/client`、`packages/system`、`packages/audio`、`packages/legacy-`、`src/client`、`node_modules/(vue|@vue|ant-design-vue|@ant-design)`。
- 正则同时接受 `/` 与 `\\`(`[/\\]`);**不做**宽泛的 `client` 子串匹配(避免误判 `client.jsonc`)。
- 导出 `dataIsolationPlugin(): Plugin`,返回 `name: 'mota-data-isolation'`、`enforce: 'pre'`、`apply: 'build'`、`resolveId(source, importer)`、`moduleParsed(id)`。
- `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(...)`,消息同格式(含 `matched: <token>` 与 `this.getModuleInfo(id)?.importers`)。这一路覆盖别名解析、传递依赖、`src/client/**` 与 `node_modules` 的真实落点。
- **不得**把数据端依赖(`@user/data-*`、`@motajs/common`、`@motajs/loader`、`@motajs/anon-tokyo`、`@motajs/types`、`@motajs/animate`)列入禁令。
- 为导出函数与参数写 jsDoc;模块顶层零副作用。
(2) **新建 `script/vite-plugin-data-isolation.test.ts`:**
- 使用 `node:fs/promises` 的 `mkdtemp`(`os.tmpdir()`)、`mkdir`、`writeFile`、`rm` 与 `node:path`,每个用例独立临时目录,`afterEach` 清理。
- 用 `node:fs/promises`(`mkdtemp` / `mkdir` / `writeFile` / `rm`)与 `node:os` / `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 被拒(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` 负向断言(注释 / 白名单会误伤)。
- 用例(每个 `it` 前一行注释):
1. **拒渲染端 specifier:** entry `import 'vue';`,**不设任何 `resolve.alias`**;断言 rejects,消息含 `[mota-data-isolation]`、`matched: vue` 与 `data-side build must not depend on render-side`。
2. **拒 `src/client` 渲染目录:** 构造 `<temp>/src/client/ui/fake.ts` 并相对导入;断言 rejects 且消息含 `matched: src/client`。
3. **拒 `packages-user/client-*` 解析路径:** 构造 `<temp>/packages-user/client-modules/fake.ts` 并相对导入;断言 rejects 且消息含 `matched: packages-user/client-`。
4. **纯数据端通过且产物为 ESM:** 构造 `<temp>/packages-user/data-common/fake.ts`(导出常量)并 re-export;断言 `build()` resolve,输出 chunk 的 `code` 含 `export`(ESM),不抛错。
- 断言只依赖 rejects / resolve 与错误消息关键字,不依赖 chunk 文件名或顺序。
</action>
<behavior>
- 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(不误判数据端)。
- entry `import 'vue'` → rejects;消息含 `[mota-data-isolation]`、`matched: vue`、`data-side build must not depend on render-side`。
- entry 导入 `<temp>/src/client/ui/fake.ts` → rejects;消息含 `matched: src/client`。
- entry 导入 `<temp>/packages-user/client-modules/fake.ts` → rejects;消息含 `matched: packages-user/client-`。
- entry 导入 `<temp>/packages-user/data-common/fake.ts` → resolve;输出 chunk `code` 含 `export`。
</behavior>
<reversibility rating="reversible">仅新增一个插件文件与一个测试文件;删除这两文件即可整体回退,未接线到构建脚本、未改任何既有文件。</reversibility>
<reversibility rating="reversible">仅新增一个插件文件与一个测试文件;删除它们即整体回退,未接线到构建脚本、未改既有文件。</reversibility>
<verify>
<automated>cmd /c "pnpm exec vitest run script/vite-plugin-data-isolation.test.ts"</automated>
<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>
<fails_when>non-zero exit,或任一断言失败(渲染端 specifier 未被拒 / `src/client` 未被拒 / `packages-user/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('moduleParsed path check missing');if(t.indexOf('this.error(')<0)bad.push('does not abort via this.error');if(t.indexOf('mota-data-isolation')<0)bad.push('error tag missing');if(t.indexOf('matched:')<0)bad.push('error message lacks matched token');for(const tok of ['@user/client-','@user/entry-client','@motajs/render','@motajs/client','@motajs/system','@motajs/audio','@motajs/legacy-','vue','@vue/','ant-design-vue','@ant-design/icons-vue','packages-user/client-','packages/render','packages/render-vue','packages/client','packages/system','packages/audio','packages/legacy-','src/client']){if(t.indexOf(tok)<0)bad.push('forbidden set missing: '+tok)}}const tp='script/vite-plugin-data-isolation.test.ts';if(!fs.existsSync(tp))bad.push('test file missing');else{const s=fs.readFileSync(tp,'utf8');if(s.indexOf('build(')<0)bad.push('test does not call real vite build');if(s.indexOf('dataIsolationPlugin')<0)bad.push('test does not import the plugin');if(s.indexOf('matched:')<0)bad.push('test does not assert matched token');if(s.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: 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()`)</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]` 标识与 `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` 前有一行注释
- 插件按 OI-1 命名导出 `dataIsolationPlugin`;`enforce: 'pre'` + `resolveId` + `moduleParsed` + `this.error`;错误消息含 `[mota-data-isolation]` 与 `matched: <token>`
- 禁令覆盖渲染端 specifier 与解析路径(含 `packages/render*` / `packages/client*` / `src/client`);未把数据端依赖列入
- 单测以真实 `vite.build()` 证明拒 `vue`、拒 `src/client`、拒 `packages-user/client-*`、通过纯数据端并产出 ESM;每个 `it` 前一行注释
- 未改任何既有文件;未接线到 `build-game.ts`(留给 Task 2)
</acceptance_criteria>
<done>分离插件与运行期单测就位:真实 Vite 构建在渲染端依赖上被阻断且错误带 `matched:` token、在纯数据端构建通过并产出 ESM;结构断言全绿;零既有文件改动</done>
<done>插件与运行期单测就位:渲染端依赖被阻断且错误带 `matched:` token,纯数据端构建通过并产出 ESM;结构断言全绿;零既有文件改动</done>
</task>
<task type="auto">
<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>
<name>Task 2: 重写 `build-resource` 与 `build-game`、删除 `build-packages.ts`、写 SUMMARY</name>
<precondition>Task 1 已完成(`dataIsolationPlugin` 可用并通过单测);Task 0 已闭环(OI-2 已获批或按用户指定)。`src/content` 共 23 个 jsonc;`src/content/font/` 无字体二进制;`public/**` 仍存在(不删,仅通过 `copyPublicDir: false` 阻止进入 dist);`script/build-lib.ts` 保持原样。</precondition>
<files>script/build-resource.ts, script/build-game.ts, script/build-packages.ts, .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(仅 `#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` 与现有风格一致)
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-93 / D-95 / D-96 / D-97 / D-98 / D-99 / D-100)
- script/build-game.ts(旧编排:`buildClient` / `buildData` / `checkClientData` / `getAllChars` / `buildGame` 的 babel / archiver / compressing / 旧模板拷贝 / `dist.zip`)
- script/build-resource.ts(旧 `splitResource` 分块 zip);script/utils.ts(`formatSize`)
- index.html(入口 HTML,读而不改);src/content/**(23 个 jsonc 的注释形态);package.json(只读:`type: module` 决定 es 产物扩展名为 `.js`)
- AGENTS.md(禁改既有注释;修改方式须预先声明);dev.md(命名、interface、jsDoc、非空判断约定)
</read_first>
<action>
(0) **前置确认:** 仅按 Task 0 获批口径执行;若 OI-2 命名被调整,以用户指定为准并写入 SUMMARY。**本任务不修改 `src/**`。**
(0) **只按 Task 0 获批口径执行;`src/**`、`package.json`、`index.html`、`dev.md`、`vite.config.ts`、`script/build-lib.ts` 一律不动。**
(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 子集文本,**字符串**而非 `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]))`;**其它段数** = 标记异常,抛出带文件语义的错误。**严禁**丢掉标记行;**不得**改调用方后缀。
- 导出 `buildContent(options: IContentBuildOptions): Promise<IContentBuildStats>`:递归遍历 `sourceDir`(`readdir` with `withFileTypes`)并在 `outputDir` 内镜像目录结构:
(1) **重写 `script/build-resource.ts`(命名以 OI-2 为准):** 删除 `splitResource` / `CompressedUsage` / `LoadDataType` / `ResourceInfo` / `SplittedResource` / `ResourceContent` / `ResourcePath` / JSZip 与 `./types` 引用,改为 content 处理模块:
- 导出 `IContentBuildOptions`:`readonly sourceDir: string`(`src/content`)、`readonly outputDir: string`(`dist/content`)、`readonly chars: string`(fontmin glyph 文本,**字符串**)。
- 导出 `IContentBuildStats`:`readonly files: number`、`readonly jsonc: number`、`readonly fonts: number`。
- 导出 `minifyJSONC(text: string): string`:按行过滤掉 `line.trim().startsWith('//')` 的行(去掉 `// Type: ...` 与 `// --- SYSTEM PREFIX END --- //` 等全部注释),`join('\n')` 后 `JSON.stringify(JSON.parse(body))` 返回**单行纯 JSON**;非法 JSON 直接抛错。**不得**保留标记行,**不得**改后缀。
- 导出 `buildContent(options: IContentBuildOptions): Promise<IContentBuildStats>`:递归遍历 `sourceDir`(`readdir` + `withFileTypes`),在 `outputDir` 镜像目录结构:
- `*.jsonc` → `minifyJSONC` 后写入同名路径;
- 字体扩展(`.ttf` / `.otf` / `.woff` / `.woff2`)→ 沿用旧写法(`script/build-game.ts:475-482`):`const fontmin = new Fontmin();` + **`Fontmin.glyph({ text: options.chars })`** 插件,`fontmin.src(...).dest(...).use(plugin).runAsync()` 压缩到 `outputDir` 对应子目录(保持文件名)。**结构门禁按 `Fontmin` 与 `glyph` 两个独立字面量检查(不要求连写成 `Fontmin.glyph`),但 `.glyph({ text: options.chars })` 必须收到 `options.chars` 字符串**;
- 字体(`.ttf` / `.otf` / `.woff` / `.woff2`)→ `new Fontmin()` + `Fontmin.glyph({ text: options.chars })`,`src(...).dest(...).use(plugin).runAsync()`,保持文件名;
- 其它文件(PNG / `.ts` 等)→ `fs-extra.copy` 原样拷贝;
- `sourceDir/font` 下若**无**字体二进制:跳过 fontmin 并 `process.stdout.write` 告警(含目录),不抛错(OI-4)。
- 按 dev.md 写 jsDoc;模块无顶层副作用;非空判断字面量用 `isNil`、对象用 `!`。
- **注意:** `Fontmin` 的引入从 `build-game.ts` 移到本文件;`build-game.ts` 不再直接引用 fontmin。
- 无字体二进制时跳过 fontmin 并 `process.stdout.write` 告警,不抛错。
- 统计并返回 `files` / `jsonc` / `fonts`;按 dev.md 写 jsDoc;模块无顶层副作用;字面量非空判断用 `isNil`、对象用 `!`。
(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` 与其变换函数、`archiver` / `createWriteStream` / `compressing`、`RequiredData` / `RequiredIconsData` 及对 `public/project/{data,icons}.js` 的读取、`toCopy` 旧模板清单、源码压缩包、资源地图 JSON、资源分块拷贝、服务器 exe 拷贝、`dist.zip` 压缩与相关体积告警分支、`Fontmin` 直接引用(移至 `build-resource.ts`)、**`formats: ['iife']` 与任何 `iife` 字样**。**删除后不得把上述标识符留在注释或字符串里**(结构门禁对以上 token **及裸 `iife`** 做负向检查;说明性文字放本 PLAN / SUMMARY,不写进源码)。
- **`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'] }`(`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` / `@motajs/client` / `@motajs/system` / `@motajs/audio` / `@motajs/legacy-` / `@user/client-` / `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 体积;随后**先 flush stdout**(`await new Promise(resolve => process.stdout.write('', resolve))`),再**无条件显式终止进程**:`process.exit(dataFailed || contentFailed || licenseFailed ? 1 : 0)`。**每个分支都必须到达这条显式退出**(数据端 / content / LICENSE 任一失败 → 1;仅 `renderBlocked` → 0),**不得依赖事件循环自然退出**——实测渲染端构建失败时 node/tsx 会因 Vite/esbuild 残留 handle 存活 7s+,构建会挂到超时。**不生成任何压缩包。**
- `logProgress` 步骤表同步改为上述 5 项;`ansi.clear` 后的状态行不得被进度勾选覆盖。
- 保留模块底部执行块(`buildGame()`);无需导出。
- **保留**:`DEBUG_BUILD` / `DEBUG_REPLAY`、`ansi`、`logProgress`(步骤表改为:`1. 构建前准备` / `2. 构建渲染端` / `3. 构建数据端` / `4. 处理 content` / `5. 收尾`)、`buildClient` / `buildData` / `getAllChars` 名称、`distDir`、`formatSize` 报告。
- **删除**:`RESOUCE_SIZE`、`checkClientData` / `ClientDataLevel` / `getFileName` / `getFileSize`、`generateResourceJSON` / `CompressedLoadList*`、`@babel/core` 与其变换、`archiver` / `createWriteStream` / `compressing`、`RequiredData` / `RequiredIconsData` 与 `public/project/*` 读取、`toCopy` 旧模板清单、源码 zip、资源地图 JSON、资源分块拷贝、`启动服务.exe` 拷贝、`dist.zip`、直接引用 `Fontmin`(移至 `build-resource.ts`)。
- **`buildClient(outDir)`**:复用 `loadConfigFromFile` + `mergeConfig`;注入 `legacy(...)`(目标浏览器列表与 `renderModernChunks:false` 维持现状);`build.outDir = outDir`(`dist`)、`copyPublicDir: false`、`minify: !DEBUG_BUILD`、`emptyOutDir: false`;**不设置 `rollupOptions.input`**(默认以 `index.html` 为入口);`rollupOptions.external` 维持 `['@wasm-audio-decoders/opus-ml']`;`output` 维持 `format:'es'` / `entryFileNames` / `chunkFileNames: 'chunks/...'` / `assetFileNames: 'assets/...'`;`manualChunks` **维持现状,不改**。
- **`buildData(outDir, entry)`**:`build.outDir = outDir`(调用方传 `distDir`)、`copyPublicDir: false`、**`emptyOutDir: false`**、`minify: !DEBUG_REPLAY`、`lib: { entry, fileName: 'data', formats: ['es'] }`;`rollupOptions.output`:`entryFileNames: 'data.js'`、`chunkFileNames: 'data/[name].[hash].js'`、`assetFileNames: 'data/[name].[hash][extname]'` —— **入口落在 `dist/data.js`,附属产物落在 `dist/data/`**;`plugins: [dataIsolationPlugin()]`。
- **`getAllChars()`:返回 `Promise<string>`**:读取 `dist` 下渲染端 `.js`(**跳过 `dist/data` 与 `dist/content`**)与 `src/content/**/*.jsonc` 的字符集(`Set<string>`),以 `[...chars].join('')` 返回字符串;无匹配文件时返回空字符串,不抛错。
- **`buildGame()` 编排(顺序固定,不得倒置):**
1. `await ensureDir(distDir)` + `await ensureDir(tempDir)` + `await emptyDir(distDir)` + `await emptyDir(tempDir)`(整个流程只做一次)。
2. **渲染端:** `await buildClient(distDir)`。
3. **数据端:** `await buildData(distDir, resolve(process.cwd(), 'src/data.ts'))`。
4. `const chars = await getAllChars()` → `await buildContent({ sourceDir: resolve(process.cwd(), 'src/content'), outputDir: resolve(distDir, 'content'), chars })`。
5. `await copy(resolve(process.cwd(), 'LICENSE'), resolve(distDir, 'LICENSE'))`;输出渲染端 / 数据端产物状态、content 统计与 `dist` 体积。
- **失败处理保持简单:** 顶层执行块 `buildGame().catch(reason => { process.stderr.write(String(reason)); process.exit(1); })`;不写 BLOCKED 分支、不做重试或分类、不检查外部文件或仓库状态、不生成任何压缩包。
(3) **删除旧脚本:** 删除 `script/build-packages.ts` 与 `script/build-lib.ts`(`Remove-Item` / `fs.rm`);确认全仓无其它引用(检索 `build-packages` / `build-lib`,仅允许 `package.json` 的 script 行在删除前命中)。
(3) **删除 `script/build-packages.ts`**(`build-lib.ts` 保持不动)。
(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) **`dev.md` 构建表(W5):** 删除 `build:packages` / `build:lib` 两行;`build:game` 行说明更新为「类型检查 + 构建渲染端游戏包与数据端 ESM 包 + content 处理」;**只改这三行,不动其它章节**。
(7) **`src/**`(零改动):** 按用户裁定「你改 src,我做脚本」,**不修改 `src/bootstrap.ts` 或任何 `src/**` 文件**。**D-97 的「样式经入口 JS 引入」本计划不交付**——`src/styles.less` 当前全仓无 import 点,本计划只移除 `index.html` 的 link;在用户于 `src/**` 侧接线(OI-5)之前,`dist` **缺少 `#game-draw` 居中样式**,该缺口必须**显式写入 SUMMARY**,不得描述为已交付或静默降级。
(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` 前一行注释。
(9) **实跑与分层门禁:** 先跑单测;再实跑 `cmd /c "pnpm exec tsx script/build-game.ts"`(该进程必须经显式 `process.exit(...)` 在**有界时间内**返回;挂起到超时即判负)。
- **数据端:** `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/**` 或包源码。
(10) **`09-21-SUMMARY.md`:** 记录范围、插件扫描口径与错误行为(含 `matched:` token)、**数据端 ESM 硬门禁结果**、渲染端 BLOCKED 或成功分支与两处 stale import 当前状态、content 压缩口径(23 个 jsonc、标记保留理由、`Type:` 注释丢失)、dist 产物清单与 D-97 排除项、删除清单、`manualChunks` / legacy 调整、`index.html` 去 link、**D-97「样式经入口 JS 引入」不由本计划交付的显式声明**(`src/styles.less` 全仓无 import 点,用户接线前 `dist` 缺 `#game-draw` 居中样式,OI-5)、命名获批情形、门禁结果(插件单测 / content 单测 / 结构断言 / 数据硬门禁 / 条件渲染门禁 / baseline 范围门禁 / **构建进程显式退出**)、`open_items` 最终处置、遗留缺口(`item.js`、字体缺失、OI-11、OI-12、样式 import)。
(4) **`09-21-SUMMARY.md`:** 记录范围(只动脚本)、插件扫描口径与错误行为、数据端产物落点(`dist/data.js` + `dist/data/`)、content 压缩口径(纯 JSON、去净注释、后缀 `.jsonc`)、最终 `dist` 清单与排除项、删除清单(`build-packages.ts`;`build-lib.ts` 未动;`package.json` 未改,`build:packages` 已失效)、命名获批情形、门禁结果(插件单测 + 两个脚本结构断言)、`open_items` 处置、已知边界(渲染端两处 stale import 属用户 WIP;未实跑全量构建)。
</action>
<reversibility rating="costly">删除 `build-packages.ts` / `build-lib.ts` 与修改 `package.json` scripts 属仓库内结构性删除(可经 git 恢复);其余为文件重写,`git checkout` 可回退。执行后 `dist/` 布局改变,旧产物形态(资源分块 / `dist.zip`)不再可用。</reversibility>
<reversibility rating="costly">删除 `build-packages.ts` 属仓库内结构性删除(可经 git 恢复);两个脚本为重写,`git checkout` 可回退;执行后 `dist` 布局改变,旧产物形态(资源分块 / `dist.zip`)不再可用。</reversibility>
<verify>
<automated>cmd /c "pnpm exec vitest run script/build-resource.test.ts"</automated>
<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','@motajs/client','@motajs/system','@motajs/audio','@motajs/legacy-','@user/client-','packages/render','packages/render-vue','packages/client','packages/system','packages/audio','packages/legacy-','packages-user/client-','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 must not mention iife at all (code, strings or comments; D-95 requires ESM-only)');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');if(body.indexOf('dataFailed')<0)bad.push('buildGame must track dataFailed');if(body.indexOf('process.exit(')<0)bad.push('buildGame must end with an explicit process.exit(...)');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` 行;**`buildGame` 未以显式 `process.exit(...)` 终止(或未跟踪 `dataFailed`)**)</fails_when>
<automated>cmd /c "pnpm exec tsx script/build-game.ts"</automated>
<fails_when>non-zero exit(数据端硬门禁失败、content 或 LICENSE 失败、或本计划文件自身出错)——**渲染端因用户两处 stale import 失败并打印 BLOCKED 但退出码为 0 时不算失败**;但若构建脚本吞掉数据端失败并以 0 退出,则视为本计划错误;若错误来自用户 WIP 的范围外文件(渲染端),记录完整错误与归因、不修改范围外文件,BLOCKED 写入 SUMMARY;**构建进程未在有界时间内返回(挂起 / 超时)一律判负**——`buildGame` 必须在每个分支显式 `process.exit(dataFailed || contentFailed || licenseFailed ? 1 : 0)`(先 flush stdout),不得依赖事件循环自然退出</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>
<automated>node -e "const fs=require('fs');const bad=[];const t=fs.readFileSync('script/build-game.ts','utf8');for(const x of ['checkClientData','ClientDataLevel','getFileName','getFileSize','splitResource','generateResourceJSON','transformAsync','archiver','compressing','source-code.zip','loadList.json','dist.zip','editor.html','logo.png','styles.css','启动服务.exe','_server','_docs','libs']){if(t.indexOf(x)>=0)bad.push('legacy token: '+x)}if(t.indexOf('dataIsolationPlugin')<0)bad.push('missing dataIsolationPlugin');if(t.indexOf('buildContent')<0)bad.push('missing buildContent');if(t.indexOf('LICENSE')<0)bad.push('missing LICENSE copy');if(t.indexOf('copyPublicDir: false')<0)bad.push('missing copyPublicDir:false');if(t.indexOf('emptyOutDir: false')<0)bad.push('missing emptyOutDir:false (data build would wipe render output)');if(!/formats\s*:\s*\['es'\]/.test(t))bad.push('data build is not ESM-only');if(t.indexOf('entryFileNames: \'data.js\'')<0)bad.push('data entry must be data.js at dist root');if(t.indexOf('chunkFileNames: \'data/')<0||t.indexOf('assetFileNames: \'data/')<0)bad.push('data aux output must live in dist/data/');if(t.indexOf('src/content')<0||t.indexOf('src/data.ts')<0)bad.push('missing content/data entry paths');if(!/Promise<string>/.test(t))bad.push('getAllChars must return Promise<string>');const s=t.indexOf('async function buildGame(');const b=s>=0?t.slice(s):'';if(!b)bad.push('buildGame missing');const c=b.indexOf('await buildClient(');const d=b.indexOf('await buildData(');if(c<0||d<0)bad.push('buildClient/buildData not awaited');else if(c>d)bad.push('render must build before data');if(b.indexOf('emptyDir(')<0||b.indexOf('emptyDir(')>c)bad.push('dist must be cleared before the render build');if(bad.length>0){console.error('BUILD GAME GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK build-game: render then data, data.js at root, dist/data/ aux, no legacy pipeline')"</automated>
<fails_when>non-zero exit,或 stderr 含 "BUILD GAME GATE FAILED"(仍含旧 pipeline token / 缺 `dataIsolationPlugin`、`buildContent`、`LICENSE`、`copyPublicDir: false`、`emptyOutDir: false` / 数据端非 ESM / 入口不是 `data.js` 在根 / 附属产物不在 `dist/data/` / 缺 `src/content` 或 `src/data.ts` / `getAllChars` 非 `Promise<string>` / 数据端先于渲染端构建 / 未在渲染端构建前清空 `dist`)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];const t=fs.readFileSync('script/build-resource.ts','utf8');for(const x of ['splitResource','JSZip','jszip','CompressedUsage','LoadDataType','SplittedResource','RequiredData','SYSTEM PREFIX']){if(t.indexOf(x)>=0)bad.push('legacy token: '+x)}for(const x of ['minifyJSONC','buildContent','IContentBuildOptions','IContentBuildStats','JSON.parse','JSON.stringify','Fontmin','glyph','.jsonc']){if(t.indexOf(x)<0)bad.push('missing: '+x)}if(!/chars\s*:\s*string/.test(t))bad.push('chars must be string');if(!/startsWith\s*\(\s*'\/\/'\s*\)/.test(t))bad.push('jsonc comment stripping by line missing');if(!/\.jsonc/.test(t))bad.push('jsonc suffix handling missing');if(bad.length>0){console.error('BUILD RESOURCE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK build-resource: pure JSON minify + fontmin, no zip/split')"</automated>
<fails_when>non-zero exit,或 stderr 含 "BUILD RESOURCE GATE FAILED"(仍含分块 / zip / 旧类型 token / 仍保留 `SYSTEM PREFIX` / 缺 `minifyJSONC`、`buildContent`、接口、`JSON.parse` + `JSON.stringify`、fontmin / `chars` 非 `string` / 缺按行去 `//` 注释)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];if(fs.existsSync('script/build-packages.ts'))bad.push('script/build-packages.ts must be deleted');if(!fs.existsSync('script/build-lib.ts'))bad.push('script/build-lib.ts must stay untouched');if(fs.existsSync('script/build-resource.test.ts'))bad.push('build-resource.test.ts is out of scope');if(!fs.existsSync('.planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md'))bad.push('09-21-SUMMARY.md missing');if(bad.length>0){console.error('FILES GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK files: build-packages deleted, build-lib untouched, SUMMARY written')"</automated>
<fails_when>non-zero exit,或 stderr 含 "FILES GATE FAILED"(`build-packages.ts` 未删除 / `build-lib.ts` 被删或被改 / 新增了范围外测试文件 / SUMMARY 缺失)</fails_when>
</verify>
<acceptance_criteria>
- **数据端硬门禁(必过):** `dist/data/data.js` 存在且为 ESM(含 `export`);数据端构建在渲染端之前完成;`dist` 只在准备步骤清空一次;`buildClient` 与 `buildData` 均为 `emptyOutDir: false`;数据端失败(含 OI-11 回归签名)→ 退出码非零,**不存在 pending 软通过或 sentinel 豁免**;`buildGame` 在**每个分支**显式 `process.exit(...)` 后返回,构建进程**不得挂起到超时**
- **渲染端条件分支:** `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/**` 零改动**;`dev.md` 构建表同步(删除两行、保留并更新 `build:game`);**D-97「样式经入口 JS 引入」本计划不交付**(`src/styles.less` 全仓无 import 点,OI-5;用户接线前 `dist` 缺 `#game-draw` 居中样式,须显式写入 SUMMARY,不得静默降级)
- **baseline 范围门禁:** 相对 Task 0 快照无新增、无二次修改的 `src/**` 与 `09-CONTEXT.md` 路径(白名单仅 `files_modified`;不整体放行 `src/`)
- `09-21-SUMMARY.md` 已写(范围、插件口径、数据端硬门禁结果、渲染端分支与 stale import 状态、content 压缩与标记理由、产物/排除清单、删除清单、命名获批、门禁结果、**构建进程显式退出**、**D-97「样式经入口 JS 引入」不由本计划交付的显式声明(OI-5 缺口)**、`open_items` 处置、遗留缺口)
- `script/build-resource.ts` 重写为 content 模块:`minifyJSONC` 按行去掉 `//` 注释后输出单行纯 JSON(无系统标记行、后缀仍 `.jsonc`);`buildContent` 递归镜像 `src/content` → `dist/content`,jsonc 压缩、字体 fontmin glyph、其它原样拷贝、无字体时跳过告警;接口 `chars: string`;不再含分块 / zip / `JSZip`
- `script/build-game.ts` 重写:准备(`dist` + `_temp` 清空一次)→ `await buildClient(distDir)`(`index.html` 默认入口、`copyPublicDir: false`、`emptyOutDir: false`)→ `await buildData(distDir, src/data.ts)`(`formats: ['es']`、`entryFileNames: 'data.js'`、chunk/asset 前缀 `data/`、`emptyOutDir: false`、注入 `dataIsolationPlugin`)→ `getAllChars`(`Promise<string>`)→ `buildContent` → 复制 `LICENSE`;旧 pipeline 标识符全部移除
- `script/build-packages.ts` 已删除;`script/build-lib.ts`、`package.json`、`index.html`、`dev.md`、`src/**`、`public/**`、`vite.config.ts`、lockfile、`packages*/**`、`script/dev.ts` 均未改动
- 无特殊失败处理:无 BLOCKED 分支、无 baseline / 范围门禁、无外部文件或仓库状态检查;构建失败即打印错误并非零退出
- 结构与行为断言全绿(插件单测、`build-game` 结构、`build-resource` 结构、文件清单)
- `09-21-SUMMARY.md` 已写(范围、插件口径、数据端产物落点、content 口径、dist 清单与排除项、删除清单与 `build:packages` 失效说明、命名获批、门禁结果、`open_items` 处置、已知边界)
</acceptance_criteria>
<done>构建流程完成新引擎适配:数据端先构建且 `dist/data/data.js`(ESM)为硬门禁、content 以压缩形态(保留标记)落到 `dist/content`、`LICENSE` 保留、渲染端按条件分支产出或打印 BLOCKED、旧脚本与旧 pipeline 全部移除、`src/**` 零改动;插件单测 / content 单测 / 结构断言 / 数据硬门禁 / baseline 范围门禁全绿,结果与分支处置写入 `09-21-SUMMARY.md`</done>
<done>构建脚本完成新引擎适配:渲染端输出常规 `dist`,数据端产出 `dist/data.js`(ESM)与 `dist/data/` 附属产物,content 以纯 JSON 落到 `dist/content`,`LICENSE` 保留;旧 pipeline 与 `build-packages.ts` 移除;结构断言与插件单测全绿,结果写入 `09-21-SUMMARY.md`</done>
</task>
</tasks>
@ -439,53 +305,45 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
| Boundary | Description |
|----------|-------------|
| 写入/删除边界 | 本增量含写操作与两个脚本删除;只允许 `files_modified` 白名单,**`src/**` 零写入** |
| 数据端 ↔ 渲染端 | 数据端独立包不得依赖渲染端;由模块图插件硬门禁(取代体积启发式),覆盖 `src/client/**` |
| 写入/删除边界 | 含写操作与一个脚本删除;只允许 `files_modified` 白名单,`src/**` 零写入 |
| 数据端 ↔ 渲染端 | 数据端独立包不得依赖渲染端;由模块图插件硬报错(取代体积启发式) |
| content 输入 → dist | `src/content` 的 23 个 jsonc 经 `JSON.parse` 后重新序列化;格式非法会在构建期暴露 |
| 构建产物 → 浏览器 | `dist/content/**` 由加载器按 `BASE_URL` 相对路径读取;标记行是解析契约 |
| 预期失败 vs 真实失败 | OI-11 回归签名只用于归因,数据端失败仍非零;渲染端 BLOCKED 属用户 WIP |
| 用户 WIP 边界 | 工作树含未提交改动;不得回退、不得为通过门禁而修改范围外文件 |
| 依赖边界 | 不新增依赖、不改 lockfile、不改 `package.json` 依赖面 |
| 命名边界 | 新文件/导出/接口及成员须先经 Task 0 报备批准(D-04) |
**威胁级别说明(security_enforcement = ASVS L1,block on high):** 本计划是**本地构建流程重构**,不执行第三方代码、不新增依赖、不暴露网络面;`high` 级(构建产物被污染 / 依赖边界被破坏 / 预期签名掩盖真实错误 / 数据端硬门禁被软化)均已给出 `mitigate`。
| 数据端与渲染端产物目录 | `dist/data.js` + `dist/data/` 必须与 `dist/assets/`、`dist/chunks/` 分离 |
| 依赖边界 | 不新增依赖、不改 lockfile、不改 `package.json` |
| 命名边界 | 新文件 / 导出 / 接口须先经 Task 0 报备批准(D-04) |
## 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 / 别名解析包(含 `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` / `@motajs/client` / `@motajs/system` / `@motajs/audio` / `@motajs/legacy-` / `@user/client-` / `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 豁免;`buildGame` 每个分支显式 `process.exit(...)`,避免渲染端失败后被残留 handle 挂到超时 |
| 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 记录数据端硬门禁结果、渲染端分支与归因、`open_items` 处置 |
| T-09-21-SC | Tampering | npm/pip/cargo installs | high | mitigate | 不新增依赖、不改 `package.json` 依赖面与 lockfile;范围门禁将 lockfile 相对 baseline 的哈希变化判负;如需新增依赖须回 Task 0 报备获批 |
| T-09-21-01 | Tampering | 数据端构建被渲染端依赖污染(体积启发式漏判) | high | mitigate | 插件 `resolveId`(specifier)+ `moduleParsed`(解析路径,含 `src/client/**`)双路扫描并 `this.error` 终止;真实 `vite.build()` 单测覆盖拒 `vue` / 拒 `src/client` / 拒 `packages-user/client-*` / 纯数据端通过 |
| T-09-21-02 | Tampering | 数据端产物写入 `dist` 时清空渲染端产物 | high | mitigate | `buildData` 强制 `emptyOutDir: false`;结构断言检查;`dist` 只在准备步骤清空一次,且先渲染后数据 |
| T-09-21-03 | Tampering | 数据端附属产物混入渲染端 `assets/` / `chunks/` | medium | mitigate | `chunkFileNames` / `assetFileNames` 前缀 `data/`;入口 `entryFileNames: 'data.js'`;结构断言检查两处前缀 |
| T-09-21-04 | Tampering | jsonc 压缩残留注释或标记行 | medium | mitigate | `minifyJSONC` 按行过滤 `//` 后 `JSON.parse` + `JSON.stringify`;结构断言禁止 `SYSTEM PREFIX` 与要求 `startsWith('//')` 过滤 |
| T-09-21-05 | Tampering | 从 `public/**` / 旧模板带进 dist(D-97 违规) | medium | mitigate | `copyPublicDir: false`;删除旧模板拷贝;结构断言禁止 `editor.html` / `logo.png` / `styles.css` / `启动服务.exe` / `_server` / `_docs` / `libs` |
| T-09-21-06 | Tampering | 越界写入 / 改范围外文件 | medium | mitigate | `files_modified` 白名单 + prohibitions(`build-lib.ts` / `package.json` / `index.html` / `dev.md` / `src/**` 等一律不动);文件清单断言 |
| T-09-21-07 | Tampering | 未经批准使用新命名 | medium | mitigate | Task 0 报备 OI-1 / OI-2;prohibitions 明列「未获批前不得落码」 |
| T-09-21-08 | Tampering | 字体缺失或格式非法导致 fontmin 抛错 | low | mitigate | 无字体二进制时跳过并告警;`buildContent` 仅在遇到字体文件时才调用 fontmin |
| T-09-21-SC | Tampering | npm/pip/cargo installs | high | mitigate | 不新增依赖、不改 `package.json` 依赖面与 lockfile;如需新增依赖须回 Task 0 报备获批 |
</threat_model>
<verification>
- **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>`);`cmd /c "pnpm exec tsx script/build-game.ts"` 实跑(**必须经显式 `process.exit(...)` 在有界时间内退出**)——数据端 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` 最终处置)。
- **Task 0:** 预执行状态(旧构建脚本 / 23 个 jsonc / 插件文件不存在)检查;人工关卡(用户回复「可以执行」;OI-1 / OI-2 如有指定按指定)。
- **Task 1:** `dataIsolationPlugin` 结构(`enforce: 'pre'` + `resolveId` + `moduleParsed` + `this.error` + 禁令集合 + `matched:` token);真实 `vite.build()` 单测(拒 `vue` / 拒 `src/client` / 拒 `packages-user/client-*` / 纯数据端通过并产出 ESM)。
- **Task 2:** `build-game` 结构断言(无旧 pipeline token;渲染端先于数据端;`dist` 在渲染前清空一次;数据端 ESM 且入口 `data.js` 在根、附属 `dist/data/`;`copyPublicDir: false` / `emptyOutDir: false`;`buildContent` + `LICENSE`;`getAllChars` 返回 `Promise<string>`);`build-resource` 结构断言(无分块 / zip;`minifyJSONC` 按行去注释 + `JSON.parse` / `JSON.stringify`;fontmin;`chars: string`);文件清单断言(`build-packages.ts` 已删、`build-lib.ts` 未动、SUMMARY 已写)。
- **一致性:** `dist/content/**` 落点与 `src/data/constant.ts` 的 `content/core.jsonc` 一致;数据端 `data.js` 扩展名与 `package.json` 的 `type: module` 一致。
- **无全仓门禁:** 不使用 `pnpm check:type` / `pnpm build` / 全量 `pnpm test:ci`(用户 WIP 存在历史类型错误);本计划不实跑全量构建。
- **人工复核:** 结论写入 `09-21-SUMMARY.md`。
</verification>
<success_criteria>
- **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 移除;**「样式经入口 JS 引入」不由本计划交付**——`src/styles.less` 全仓无 import 点,本计划不改 `src/**`(OI-5),用户接线前 `dist` 缺 `#game-draw` 居中样式,缺口须显式记录。
- **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 范围门禁全部就位并跑到确定结论。
- **D-94:** 数据端构建由模块图插件把关(specifier + 解析路径双路),命中即 `this.error` 终止且错误含 `matched:` token;`checkClientData` 及体积启发式已删除。
- **D-95:** 数据端库模式 `formats: ['es']`,入口产物 `dist/data.js`(dist 根),附属产物 `dist/data/`,与渲染端 `assets/` / `chunks/` 分离;`emptyOutDir: false`。
- **D-96:** `src/content`(23 个 jsonc)→ `dist/content`;jsonc 压成纯 JSON、去掉所有注释(含系统标记行)、后缀仍 `.jsonc`;fontmin 压缩(无字体时跳过告警);不分块、不 zip。
- **D-97:** `copyPublicDir: false`;dist 不含旧模板产物;`LICENSE` 复制到 `dist/LICENSE`;`index.html` 以默认入口产出常规渲染产物且文件本身不改。
- **D-93 / D-98 / D-99 / D-100:** `build-packages.ts` 删除;`build-game` 承担渲染端 + 数据端 + content;移除 `main.js` babel 二次压缩;不改 `package.json` / `dev.md` / `build-lib.ts`。
- **边界:** 仅 `files_modified` 内的脚本与 SUMMARY 被写入;未改 `src/**` / `public/**` / `vite.config.ts` / lockfile / `packages*/**` / `script/dev.ts`;未回退用户 WIP。
- **验证:** 插件单测 + 两个脚本结构断言 + 文件清单断言全部就位并跑到确定结论;无特殊失败处理与外部状态检查。
</success_criteria>
<output>