docs(09-21): revision 8 - rename isolation plugin to user-specified build-plugin.ts

This commit is contained in:
unanmed 2026-10-20 17:12:10 +08:00
parent 69e44b823b
commit 453839cc07

View File

@ -5,8 +5,8 @@ type: execute
wave: 18
depends_on: [09-19, 09-20]
files_modified:
- script/vitePluginDataIsolation.ts
- script/vitePluginDataIsolation.test.ts
- script/build-plugin.ts
- script/build-plugin.test.ts
- script/build-resource.ts
- script/build-game.ts
- script/build-packages.ts
@ -20,8 +20,8 @@ estimate:
confidence: low
must_haves:
truths:
- "**本计划第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md / D-03)**:先汇报「任务 11 · 构建重构(适配新引擎)」的要做什么 / 为什么 / 大致怎么做 / 影响范围,列明最小范围(新增 `script/vitePluginDataIsolation.ts` 及其**必需**测试 `script/vitePluginDataIsolation.test.ts`、重写 2 个 script、删除 `build-packages.ts`)、确认的目标构建流程(准备 → 渲染端 → 数据端 → 隔离检测 → content → LICENSE)、**不改构建顺序**、**不做特殊失败处理(无 BLOCKED、无 baseline 范围门禁、无对外部文件/状态的检查)**、三项已知边界(jsonc 去净注释与 `PrefixedJSONCProcessor` error 66 的冲突、`index.html` 仍引用 `/styles.css` 且本计划不产出该文件、本计划不端到端运行全量 `build:game`)、以及待批准命名(OI-1 / OI-2),然后停下等待「可以执行」。"
- "**新文件名必须小驼峰(dev.md「代码文件名:小驼峰」):** 插件文件固定为 `script/vitePluginDataIsolation.ts`,测试文件固定为 `script/vitePluginDataIsolation.test.ts`;**不得**创建 `script/vite-plugin-data-isolation.ts` / `script/vite-plugin-data-isolation.test.ts`(连字符命名只用于 Markdown 文档)。测试是**必需产物**,不是可选项。"
- "**本计划第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md / D-03)**:先汇报「任务 11 · 构建重构(适配新引擎)」的要做什么 / 为什么 / 大致怎么做 / 影响范围,列明最小范围(新增 `script/build-plugin.ts` 及其**必需**测试 `script/build-plugin.test.ts`、重写 2 个 script、删除 `build-packages.ts`)、确认的目标构建流程(准备 → 渲染端 → 数据端 → 隔离检测 → content → LICENSE)、**不改构建顺序**、**不做特殊失败处理(无 BLOCKED、无 baseline 范围门禁、无对外部文件/状态的检查)**、三项已知边界(jsonc 去净注释与 `PrefixedJSONCProcessor` error 66 的冲突、`index.html` 仍引用 `/styles.css` 且本计划不产出该文件、本计划不端到端运行全量 `build:game`)、以及待批准命名(OI-1 / OI-2),然后停下等待「可以执行」。"
- "**新文件名由用户显式指定(D-04):** 插件文件固定为 `script/build-plugin.ts`,测试文件固定为 `script/build-plugin.test.ts`(**用户明确选定该文件名,覆盖 dev.md「代码文件名:小驼峰」规则**);**不得**改成其它名字(旧提议名 `script/vitePluginDataIsolation.ts` / `script/vitePluginDataIsolation.test.ts` 不得使用)。测试是**必需产物**,不是可选项。"
- "**`moduleParsed` 签名(W2):** 使用 Rollup 的 `ModuleParsedHook` 形态 `moduleParsed(info: ModuleInfo)`(**不是** `moduleParsed(id: string)`),按 `info.id` 做解析路径匹配;错误消息中的导入者通过 `this.getModuleInfo(info.id)` 读取 `importers`。"
- "**结构门禁按函数切片(W3):** `build-game.ts` 的门禁先按函数切出 `buildClient` / `buildData` / `buildGame` 片段,再在各自片段内断言——`buildData` 片段必须含 `copyPublicDir: false`、`emptyOutDir: false`、`dataIsolationPlugin`、`formats: ['es']`、`entryFileNames: 'data.js'`、`chunkFileNames: 'data/`、`assetFileNames: 'data/`;`buildClient` 片段单独断言 `copyPublicDir: false` 且**不含** `dataIsolationPlugin`;配置放错位置必须导致门禁失败。"
- "**渲染端(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 二次压缩。"
@ -30,23 +30,23 @@ must_haves:
- "**content(D-96):** `src/content` → `dist/content`;`*.jsonc` 压成**纯 JSON、去掉所有注释(含 `// --- SYSTEM PREFIX END --- //`)**,后缀仍 `.jsonc`;字体用 fontmin 压缩(无字体二进制时跳过并告警);**不分块、不 zip**;`build-resource` 从 `src/content` 读取素材。**已知边界(INFO-1):** 去净注释会移除运行期 `PrefixedJSONCProcessor` 依赖的标记行(其 `split` 结果不是 2 段时报 error 66);`packages-user/**` 的 loader 适配经用户确认**不在本计划范围**,只作为已知后续项记入 Task 0 汇报与 SUMMARY,**不得**因此保留注释或改 loader。"
- "**最终 `dist` 根(D-97):** `index.html` + Vite 常规渲染产物 + `data.js` + `content/` + `LICENSE`;数据端附属在 `dist/data/`;**不产出** `source-code.zip` / `loadList.json` / `dist.zip` / 资源分块 `resource.<hash>.h5data`。**已知边界(INFO-2):** `index.html` 仍含 `<link href='/styles.css'>` 且本计划不产出 `styles.css`;该文件与样式接线由用户处理,事实须记入 Task 0 汇报与 SUMMARY。"
- "**写入边界:** 只写 `files_modified`;**不改** `script/build-lib.ts`、`package.json`、`index.html`、`dev.md`、`src/**`、`public/**`、`packages-user/**`、`vite.config.ts`、lockfile、`packages*/**`、`script/dev.ts`;不回退用户既有 WIP。"
- "**命名须先获批(D-04):** `script/vitePluginDataIsolation.ts` / `dataIsolationPlugin` / `name: 'mota-data-isolation'` / `script/vitePluginDataIsolation.test.ts`(OI-1);`buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats`(OI-2)。未获批前不得落码。"
- "**命名须先获批(D-04):** `script/build-plugin.ts` / `dataIsolationPlugin` / `name: 'mota-data-isolation'` / `script/build-plugin.test.ts`(OI-1);`buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats`(OI-2)。未获批前不得落码。"
- "**验证可运行(Nyquist)且范围诚实(INFO-4):** 插件单测(**必需**)以**真实 `vite.build()`** 证明「拒渲染端 import / 通过纯数据端」;两个脚本做**按函数切片**的结构断言(无 `checkClientData`、数据端 `formats: ['es']` 且入口 `data.js` 在根、附属产物在 `dist/data/`、jsonc 去注释、无 zip / 分块、无旧模板拷贝、数据子文件夹与渲染端分离);SUMMARY 明确记录**未端到端运行全量 `build:game`**(只做插件单测 + 结构断言);命令一律 `cmd /c \"pnpm exec ...\"`(PowerShell 5.1 下裸 `pnpm` 会因 `pnpm.ps1` 的 `PSSecurityException` 失败)。"
artifacts:
- path: script/vitePluginDataIsolation.ts
provides: "新增插件(文件名小驼峰,dev.md):`dataIsolationPlugin(): Plugin`,`enforce: 'pre'` + `apply: 'build'`;`resolveId` 拦渲染端 specifier,`moduleParsed(info: ModuleInfo)` 按 `info.id` 兜底,错误消息经 `this.getModuleInfo(info.id)` 读取 `importers`;命中即 `this.error(...)`,消息含 `[mota-data-isolation]` / 命中模块 / `matched: <token>`(命名待批准,OI-1)"
- path: script/vitePluginDataIsolation.test.ts
- path: script/build-plugin.ts
provides: "新增插件(文件名由用户显式指定,覆盖 dev.md 小驼峰规则):`dataIsolationPlugin(): Plugin`,`enforce: 'pre'` + `apply: 'build'`;`resolveId` 拦渲染端 specifier,`moduleParsed(info: ModuleInfo)` 按 `info.id` 兜底,错误消息经 `this.getModuleInfo(info.id)` 读取 `importers`;命中即 `this.error(...)`,消息含 `[mota-data-isolation]` / 命中模块 / `matched: <token>`(命名待批准,OI-1)"
- path: script/build-plugin.test.ts
provides: "**必需**运行期单测:真实 `vite.build()`(`configFile:false`、临时 fixture、`write:false`、lib ESM)验证拒渲染端 specifier(`vue`)、拒渲染端解析路径(`src/client` / `packages-user/client-*`,走 `moduleParsed(info.id)`)、通过纯数据端并产出 ESM;每个 `it` 前一行注释(AGENTS.md)"
- path: script/build-resource.ts
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: "重写编排:清空/创建 `dist` 与 `_temp` 一次 → 渲染端(`index.html` 入口 → `dist`)→ 数据端(库模式 ESM,入口 `dist/data.js`、附属 `dist/data/`,注入 `./vitePluginDataIsolation` 的 `dataIsolationPlugin`)→ `getAllChars` → `buildContent` → 复制 `LICENSE`;删除旧 pipeline(babel / zip / 分块 / 旧模板拷贝 / 资源地图 / `checkClientData`)"
provides: "重写编排:清空/创建 `dist` 与 `_temp` 一次 → 渲染端(`index.html` 入口 → `dist`)→ 数据端(库模式 ESM,入口 `dist/data.js`、附属 `dist/data/`,注入 `./build-plugin` 的 `dataIsolationPlugin`)→ `getAllChars` → `buildContent` → 复制 `LICENSE`;删除旧 pipeline(babel / zip / 分块 / 旧模板拷贝 / 资源地图 / `checkClientData`)"
- path: script/build-packages.ts
provides: "**删除**(D-93 / D-98;`build-lib.ts` 不动)"
- path: .planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md
provides: "执行摘要:范围、插件扫描口径与错误行为(含 `moduleParsed(info: ModuleInfo)`)、数据端产物落点(`dist/data.js` + `dist/data/`)、content 压缩口径(纯 JSON、无注释)、dist 产物与排除清单、删除清单、命名获批情形、门禁结果、`open_items` 处置、**已知后续项**(INFO-1 `PrefixedJSONCProcessor` error 66 冲突与 loader 适配不在范围;INFO-2 `index.html` 仍引用 `/styles.css` 且不产出)与**验证边界**(INFO-4 未端到端运行全量 `build:game`)"
key_links:
- "`dataIsolationPlugin`(`script/vitePluginDataIsolation.ts`)↔ `buildData` 的 `plugins` 数组(`script/build-game.ts`,`import { dataIsolationPlugin } from './vitePluginDataIsolation'`)—— D-94 的唯一执行点,取代 `checkClientData` 体积启发式"
- "`dataIsolationPlugin`(`script/build-plugin.ts`)↔ `buildData` 的 `plugins` 数组(`script/build-game.ts`,`import { dataIsolationPlugin } from './build-plugin'`)—— D-94 的唯一执行点,取代 `checkClientData` 体积启发式"
- "`moduleParsed(info: ModuleInfo)` 的 `info.id` ↔ `this.getModuleInfo(info.id)` 的 `importers` —— 错误消息可追溯到引入渲染端模块的上游"
- "`buildData` 的 `build.lib.fileName: 'data'` + `rollupOptions.output.entryFileNames: 'data.js'` ↔ `dist/data.js`(dist 根)—— D-95 入口产物契约"
- "`buildData` 的 `chunkFileNames` / `assetFileNames` 前缀 `data/` ↔ `dist/data/` —— 数据端附属产物与渲染端 `assets/`、`chunks/` 分离"
@ -67,8 +67,8 @@ must_haves:
- "**行号可能漂移:** 门禁以「文件存在 + 结构/行为断言」为准,不硬编码行号;`build-game.ts` 的门禁按**函数名切片**而非固定行号。"
prohibitions:
- "不得在用户回复通过 Task 0 关卡之前执行任何任务或派发执行子代理。"
- "**未经批准的命名一律不得使用:** `script/vitePluginDataIsolation.ts` / `dataIsolationPlugin` / `mota-data-isolation` / `script/vitePluginDataIsolation.test.ts`(OI-1)与 `buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats`(OI-2)。"
- "**新文件不得用连字符命名:** 不得创建 `script/vite-plugin-data-isolation.ts` 或 `script/vite-plugin-data-isolation.test.ts`(dev.md 要求代码文件名为小驼峰)。"
- "**未经批准的命名一律不得使用:** `script/build-plugin.ts` / `dataIsolationPlugin` / `mota-data-isolation` / `script/build-plugin.test.ts`(OI-1)与 `buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats`(OI-2)。"
- "**新文件须用用户指定的文件名:** 插件文件只能是 `script/build-plugin.ts`,测试只能是 `script/build-plugin.test.ts`(用户显式指定,覆盖 dev.md 小驼峰规则);不得改用旧提议名 `script/vitePluginDataIsolation.ts` / `script/vitePluginDataIsolation.test.ts` 或其它变体。"
- "**`moduleParsed` 不得改回字符串签名:** 必须为 `moduleParsed(info: ModuleInfo)` 并用 `info.id` 匹配;不得写成 `moduleParsed(id: string)`。"
- "**构建顺序不得倒置:** 必须先渲染端、后数据端;`buildData` 必须 `emptyOutDir: false`(不得清掉已产出的渲染端产物)。"
- "**数据端附属产物不得混入渲染端目录:** 不得让 chunk / asset 落到 `assets/` 或 `chunks/`;入口必须是 `dist/data.js`(根),不得改成 `dist/data/data.js`。"
@ -79,7 +79,7 @@ must_haves:
- "**不得引入特殊失败处理:** 不写 BLOCKED 分支、不写 baseline / 范围门禁、不检查外部文件或仓库状态;构建失败即以非零退出并打印错误。"
- "**不得伪造门禁:** 不得手写 `dist` 产物或用手工文件冒充构建结果;不得用「全量 `build:game` 已通过」之类的表述掩盖未端到端运行的事实(INFO-4)。"
open_items:
- "**OI-1(插件命名):** 提议新增 `script/vitePluginDataIsolation.ts`(**小驼峰,dev.md 强制**),导出 `dataIsolationPlugin(): Plugin`,插件 `name: 'mota-data-isolation'`;**必需**测试文件 `script/vitePluginDataIsolation.test.ts`。备选:`script/buildPluginDataIsolation.ts` / `forbidRenderDependencies`。**待批准**(D-04;测试文件按 AGENTS.md 免审批,仍一并报备)。"
- "**OI-1(插件命名):** **文件名由用户显式指定:** `script/build-plugin.ts`(连字符命名,**用户明确选定,覆盖 dev.md「代码文件名小驼峰」规则**),导出 `dataIsolationPlugin(): Plugin`,插件 `name: 'mota-data-isolation'`;**必需**测试文件 `script/build-plugin.test.ts`。备选(未采用):`script/buildPluginDataIsolation.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`。**待批准**。"
- "**已知后续项(不在本计划):** ① INFO-1 —— `dist/content/**/*.jsonc` 去净注释后,运行期 `PrefixedJSONCProcessor` 会因缺少 `prefixSplit` 标记而报 error 66,需另行适配 loader(经用户确认本计划不做);② INFO-2 —— `index.html` 的 `/styles.css` link 与样式接线由用户处理。"
---
@ -98,7 +98,7 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
7. 不产出旧模板内容与 `source-code.zip` / `loadList.json` / `dist.zip`;移除 `main.js` babel 步骤。
8. 最终 `dist` 根:`index.html` + Vite 常规渲染产物 + `data.js` + `content/` + `LICENSE`;数据端附属在 `dist/data/`。
**约束:** 不改构建顺序;只做上述适配;不做特殊失败处理(无 BLOCKED、无 baseline/范围门禁、无对外部文件或状态的检查);构建失败就失败。**新代码文件名一律小驼峰**(dev.md),插件与测试文件同理。
**约束:** 不改构建顺序;只做上述适配;不做特殊失败处理(无 BLOCKED、无 baseline/范围门禁、无对外部文件或状态的检查);构建失败就失败。**插件与测试文件名由用户显式指定为 `script/build-plugin.ts` / `script/build-plugin.test.ts`(覆盖 dev.md 的小驼峰文件名规则);其余新代码文件名一律小驼峰**(dev.md)。
**修改方式声明(AGENTS.md):** 本计划执行含文件写入与删除,由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 外无任何写入。
</objective>
@ -127,7 +127,7 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<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/` 无字体二进制;`packages-user/data-state/src/loader/jsoncProcessor.ts` 仍按 `prefixSplit` 切分且报 error 66;`script/vitePluginDataIsolation.ts` 与 `script/vitePluginDataIsolation.test.ts` 尚不存在。</precondition>
<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/` 无字体二进制;`packages-user/data-state/src/loader/jsoncProcessor.ts` 仍按 `prefixSplit` 切分且报 error 66;`script/build-plugin.ts` 与 `script/build-plugin.test.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)
@ -136,14 +136,14 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
- 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 形态)
- packages-user/data-state/src/loader/jsoncProcessor.ts(`PrefixedJSONCProcessor`:`prefixSplit` 切分、非 2 段报 error 66)
- AGENTS.md(执行前须获批、命名须先反馈、修改方式须预先声明);dev.md(代码文件名小驼峰)
- AGENTS.md(执行前须获批、命名须先反馈、修改方式须预先声明);dev.md(命名与注释约定;用户已显式指定插件文件名)
</read_first>
<action>
**停下并向用户汇报,等待回复「可以执行」后才继续;汇报本身只读。**
① **要解决的问题:** 旧构建脚本面向旧引擎(iife 数据端、体积启发式、babel 二次压缩、资源分块 zip、旧模板拷贝、源码 zip、`loadList.json`、`dist.zip`),无法适配新引擎的 `src/content` 与双端分离。
② **范围(最小):** 新增 `script/vitePluginDataIsolation.ts`(**小驼峰文件名**)及其**必需**测试 `script/vitePluginDataIsolation.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/**` / `packages-user/**` / `vite.config.ts` / lockfile / `packages*/**` / `script/dev.ts` 全部不动。**
② **范围(最小):** 新增 `script/build-plugin.ts`(**用户显式指定的文件名**)及其**必需**测试 `script/build-plugin.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/**` / `packages-user/**` / `vite.config.ts` / lockfile / `packages*/**` / `script/dev.ts` 全部不动。**
③ **目标构建流程(原样告知,不得改序):** 准备(清空/创建 `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`。
@ -155,13 +155,13 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
⑥ **失败处理与验证边界(原样告知):** **不做特殊失败处理** —— 无 BLOCKED 分支、无 baseline / 范围门禁、无对外部文件或仓库状态的检查;构建失败即打印错误并非零退出。**INFO-4:** 本计划**不端到端运行全量 `build:game`**(渲染端仍有两处用户 `src` WIP 的 stale import),验证只做插件单测 + 两个脚本的结构断言,该事实须写入 SUMMARY,不得表述为「全量构建已通过」。
⑦ **待批准命名(须用户确认):** OI-1(`script/vitePluginDataIsolation.ts` / `dataIsolationPlugin` / `name: 'mota-data-isolation'` / **必需**测试 `script/vitePluginDataIsolation.test.ts`,文件名小驼峰);OI-2(`buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats`)。未获批前不落码。
⑦ **待批准命名(须用户确认):** OI-1(`script/build-plugin.ts` / `dataIsolationPlugin` / `name: 'mota-data-isolation'` / **必需**测试 `script/build-plugin.test.ts`,文件名由用户显式指定,覆盖 dev.md 小驼峰规则);OI-2(`buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats`)。未获批前不落码。
⑧ **验证方式:** 插件单测(必需)以真实 `vite.build()` 证明「拒渲染端 import / 通过纯数据端」;两个脚本做**按函数切片**的结构断言(无 `checkClientData`、数据端 `formats: ['es']` 且入口 `data.js` 在根、附属产物 `dist/data/`、jsonc 去注释、无 zip / 分块、无旧模板拷贝、数据子文件夹与渲染端分离、`moduleParsed(info: ModuleInfo)` 签名);命令一律 `cmd /c "pnpm exec ..."`。
⑨ **不在本增量:** 不改 `src/**`(含两处 stale import);不改 `packages-user/**`(含 `PrefixedJSONCProcessor` 适配);不改 `index.html` / 不产出 `styles.css`;不改 `package.json`(故 `build:packages` 会指向已删除文件);不改 `dev.md` 构建表;不删 `public/**`;不新增依赖;不改 `manualChunks` / legacy 配置。
</action>
<decision>是否按本计划(09-21,最小范围)执行「任务 11 · 构建重构」——新增数据端隔离 Vite 插件(`script/vitePluginDataIsolation.ts`,`moduleParsed(info: ModuleInfo)`)并注入数据端库模式 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`;**仅改上述脚本,其余文件一律不动**;并接受三项已知边界(jsonc 标记行移除与 `PrefixedJSONCProcessor` error 66、`/styles.css` 404、不端到端跑全量构建)。</decision>
<decision>是否按本计划(09-21,最小范围)执行「任务 11 · 构建重构」——新增数据端隔离 Vite 插件(`script/build-plugin.ts`,`moduleParsed(info: ModuleInfo)`)并注入数据端库模式 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`;**仅改上述脚本,其余文件一律不动**;并接受三项已知边界(jsonc 标记行移除与 `PrefixedJSONCProcessor` error 66、`/styles.css` 404、不端到端跑全量构建)。</decision>
<context>
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。执行期含写操作与一个脚本删除。`src/content` 共 23 个 jsonc,均为「前缀注释 + 严格 JSON」,按行去注释即可安全转成纯 JSON(但会与 `PrefixedJSONCProcessor` 的标记行契约冲突,属已知后续项)。渲染端两处 stale import 仍存在(`src/client/initialize.ts:14` / `src/client/plugin/shop/shop.tsx:9`),属用户 WIP,本计划不修、也不据此设置构建门禁。
</context>
@ -179,8 +179,8 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
</options>
<resume-signal>回复「可以执行」即可;如需调整命名或流程细节,请在回复中一并指明。</resume-signal>
<verify>
<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);for(const p of ['script/vitePluginDataIsolation.ts','script/vitePluginDataIsolation.test.ts']){if(fs.existsSync(p))bad.push('must not exist yet: '+p)}const bg=fs.readFileSync('script/build-game.ts','utf8');if(bg.indexOf('checkClientData')<0)bad.push('build-game.ts is not the legacy implementation');const jp=fs.readFileSync('packages-user/data-state/src/loader/jsoncProcessor.ts','utf8');if(jp.indexOf('prefixSplit')<0||jp.indexOf('66')<0)bad.push('jsoncProcessor no longer matches INFO-1 (prefixSplit / error 66)');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, jsoncProcessor marker-split confirmed')"</automated>
<fails_when>non-zero exit,或 stderr 含 "PRESTATE FAILED":必需文件缺失 / jsonc 数量不是 23 / 插件文件(小驼峰命名)已存在 / `build-game.ts` 已不是旧实现 / `jsoncProcessor.ts` 不再按 `prefixSplit` + error 66 工作(INFO-1 前提变化)</fails_when>
<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);for(const p of ['script/build-plugin.ts','script/build-plugin.test.ts','script/vitePluginDataIsolation.ts','script/vitePluginDataIsolation.test.ts']){if(fs.existsSync(p))bad.push('must not exist yet: '+p)}const bg=fs.readFileSync('script/build-game.ts','utf8');if(bg.indexOf('checkClientData')<0)bad.push('build-game.ts is not the legacy implementation');const jp=fs.readFileSync('packages-user/data-state/src/loader/jsoncProcessor.ts','utf8');if(jp.indexOf('prefixSplit')<0||jp.indexOf('66')<0)bad.push('jsoncProcessor no longer matches INFO-1 (prefixSplit / error 66)');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, jsoncProcessor marker-split confirmed')"</automated>
<fails_when>non-zero exit,或 stderr 含 "PRESTATE FAILED":必需文件缺失 / jsonc 数量不是 23 / 插件文件(用户指定的 `script/build-plugin.ts`)已存在 / `build-game.ts` 已不是旧实现 / `jsoncProcessor.ts` 不再按 `prefixSplit` + error 66 工作(INFO-1 前提变化)</fails_when>
<human-check>用户明确回复「可以执行」(若就 OI-1 / OI-2 给出指定,按指定执行)</human-check>
</verify>
<acceptance_criteria>
@ -196,18 +196,18 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
</task>
<task type="tracer">
<name>Task 1: 数据端隔离插件 `dataIsolationPlugin`(`script/vitePluginDataIsolation.ts`)+ 真实 `vite.build()` 单测</name>
<precondition>Task 0 已闭环(用户回复「可以执行」,OI-1 已获批或按用户指定)。`vite` 与 `vitest` 可用;`script/vitePluginDataIsolation.ts` 与 `script/vitePluginDataIsolation.test.ts` 尚不存在(也不存在连字符命名的同名文件)。</precondition>
<files>script/vitePluginDataIsolation.ts, script/vitePluginDataIsolation.test.ts</files>
<name>Task 1: 数据端隔离插件 `dataIsolationPlugin`(`script/build-plugin.ts`)+ 真实 `vite.build()` 单测</name>
<precondition>Task 0 已闭环(用户回复「可以执行」,OI-1 已获批或按用户指定)。`vite` 与 `vitest` 可用;`script/build-plugin.ts` 与 `script/build-plugin.test.ts` 尚不存在(旧提议名 `script/vitePluginDataIsolation.ts` / `script/vitePluginDataIsolation.test.ts` 亦不存在)。</precondition>
<files>script/build-plugin.ts, script/build-plugin.test.ts</files>
<read_first>
- .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` 前一行注释)
- script/build-game.ts 顶部(`import { OutputAsset, OutputChunk, RollupOutput } from 'rollup'` —— 类型正例导入,非 `import type`)
- AGENTS.md(测试文件:每个 `it` 前一行注释);dev.md(代码文件名小驼峰、命名、jsDoc、模块无副作用、非空判断约定、限制 `?.`)
- AGENTS.md(测试文件:每个 `it` 前一行注释);dev.md(命名、jsDoc、模块无副作用、非空判断约定、限制 `?.`)
</read_first>
<action>
(1) **新建 `script/vitePluginDataIsolation.ts`**(文件名小驼峰,dev.md 强制;导出命名以 OI-1 获批结果为准):
(1) **新建 `script/build-plugin.ts`**(**用户显式指定的文件名,覆盖 dev.md 小驼峰规则**;导出命名以 OI-1 获批结果为准):
- 自 `vite` 导入 `Plugin`,自 `rollup` 导入 `ModuleInfo`;两者均为普通 `import`(dev.md 不使用 `import type`,与 `build-game.ts` 既有写法一致)。
- 模块内声明两组不导出的只读常量,每项 `{ 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`。
@ -219,7 +219,7 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
- **不得**把数据端依赖(`@user/data-*`、`@motajs/common`、`@motajs/loader`、`@motajs/anon-tokyo`、`@motajs/types`、`@motajs/animate`)列入禁令。
- 为导出函数与参数写 jsDoc;模块顶层零副作用。
(2) **新建 `script/vitePluginDataIsolation.test.ts`**(**必需产物**,不得标注为可选):
(2) **新建 `script/build-plugin.test.ts`**(**必需产物**,不得标注为可选):
- 用 `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` 前一行注释):
@ -237,30 +237,30 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
</behavior>
<reversibility rating="reversible">仅新增一个插件文件与一个测试文件;删除它们即整体回退,未接线到构建脚本、未改既有文件。</reversibility>
<verify>
<automated>cmd /c "pnpm exec vitest run script/vitePluginDataIsolation.test.ts"</automated>
<automated>cmd /c "pnpm exec vitest run script/build-plugin.test.ts"</automated>
<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/vitePluginDataIsolation.ts';if(!fs.existsSync(p))bad.push('plugin file missing (must be camelCase)');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(!/moduleParsed\s*\(\s*info\s*:\s*ModuleInfo\s*\)/.test(t))bad.push('moduleParsed must take ModuleInfo (not a string id)');if(t.indexOf('info.id')<0)bad.push('moduleParsed must match on info.id');if(t.indexOf('getModuleInfo(info.id)')<0)bad.push('error message must read importers via getModuleInfo(info.id)');if(t.indexOf('importers')<0)bad.push('error message lacks importers');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/vitePluginDataIsolation.test.ts';if(!fs.existsSync(tp))bad.push('test file missing (required, camelCase)');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(s.indexOf('packages-user/client-')<0)bad.push('test does not cover packages-user/client-*')}for(const old of ['script/vite-plugin-data-isolation.ts','script/vite-plugin-data-isolation.test.ts']){if(fs.existsSync(old))bad.push('kebab-case file must not exist: '+old)}if(bad.length>0){console.error('PLUGIN GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK plugin: camelCase files, enforce pre, moduleParsed(ModuleInfo), specifier + resolved path, abort via this.error, matched token')"</automated>
<fails_when>non-zero exit,或 stderr 含 "PLUGIN GATE FAILED"(插件文件缺失或非小驼峰 / 未导出 `dataIsolationPlugin` / 非 `enforce: 'pre'` / `moduleParsed` 不是 `(info: ModuleInfo)` 签名 / 未按 `info.id` 匹配 / 未用 `getModuleInfo(info.id)` 取 `importers` / 缺 `this.error` / 缺错误标签或 `matched:` / 禁令缺关键 token / 测试文件缺失或未用真实 `vite.build()` / 存在连字符命名的同名文件)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];const p='script/build-plugin.ts';if(!fs.existsSync(p))bad.push('plugin file missing (user-specified name script/build-plugin.ts)');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(!/moduleParsed\s*\(\s*info\s*:\s*ModuleInfo\s*\)/.test(t))bad.push('moduleParsed must take ModuleInfo (not a string id)');if(t.indexOf('info.id')<0)bad.push('moduleParsed must match on info.id');if(t.indexOf('getModuleInfo(info.id)')<0)bad.push('error message must read importers via getModuleInfo(info.id)');if(t.indexOf('importers')<0)bad.push('error message lacks importers');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/build-plugin.test.ts';if(!fs.existsSync(tp))bad.push('test file missing (required, user-specified name script/build-plugin.test.ts)');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(s.indexOf('packages-user/client-')<0)bad.push('test does not cover packages-user/client-*')}for(const old of ['script/vitePluginDataIsolation.ts','script/vitePluginDataIsolation.test.ts']){if(fs.existsSync(old))bad.push('legacy plugin filename must not exist: '+old)}if(bad.length>0){console.error('PLUGIN GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK plugin: user-specified filenames, enforce pre, moduleParsed(ModuleInfo), specifier + resolved path, abort via this.error, matched token')"</automated>
<fails_when>non-zero exit,或 stderr 含 "PLUGIN GATE FAILED"(插件文件缺失或文件名不符用户指定(`script/build-plugin.ts`) / 未导出 `dataIsolationPlugin` / 非 `enforce: 'pre'` / `moduleParsed` 不是 `(info: ModuleInfo)` 签名 / 未按 `info.id` 匹配 / 未用 `getModuleInfo(info.id)` 取 `importers` / 缺 `this.error` / 缺错误标签或 `matched:` / 禁令缺关键 token / 测试文件缺失或未用真实 `vite.build()` / 存在旧提议名 `script/vitePluginDataIsolation.*` 文件)</fails_when>
</verify>
<acceptance_criteria>
- 新文件名为 `script/vitePluginDataIsolation.ts` 与 `script/vitePluginDataIsolation.test.ts`(小驼峰,dev.md);**不存在**连字符命名的同名文件;测试为必需产物
- 新文件名为 `script/build-plugin.ts` 与 `script/build-plugin.test.ts`(**用户显式指定,覆盖 dev.md 小驼峰规则**);**不存在**旧提议名 `script/vitePluginDataIsolation.ts` / `script/vitePluginDataIsolation.test.ts`;测试为必需产物
- 插件按 OI-1 命名导出 `dataIsolationPlugin`;`enforce: 'pre'` + `resolveId` + `moduleParsed(info: ModuleInfo)` + `this.error`;错误消息含 `[mota-data-isolation]` 与 `matched: <token>`
- `moduleParsed` 按 `info.id` 匹配,导入者经 `this.getModuleInfo(info.id)` 读取 `importers`;未写成字符串 id 签名
- 禁令覆盖渲染端 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>插件与运行期单测就位:文件名小驼峰、`moduleParsed(info: ModuleInfo)` 按 `info.id` 拦截且错误带 `matched:` token 与 `importers`,纯数据端构建通过并产出 ESM;结构断言全绿;零既有文件改动</done>
<done>插件与运行期单测就位:文件名按用户指定(`script/build-plugin.ts` / `script/build-plugin.test.ts`)、`moduleParsed(info: ModuleInfo)` 按 `info.id` 拦截且错误带 `matched:` token 与 `importers`,纯数据端构建通过并产出 ESM;结构断言全绿;零既有文件改动</done>
</task>
<task type="auto">
<name>Task 2: 重写 `build-resource` 与 `build-game`、删除 `build-packages.ts`、写 SUMMARY</name>
<precondition>Task 1 已完成(`script/vitePluginDataIsolation.ts` 与 `script/vitePluginDataIsolation.test.ts` 就位且单测通过);Task 0 已闭环(OI-2 已获批或按用户指定)。`src/content` 共 23 个 jsonc;`src/content/font/` 无字体二进制;`public/**` 仍存在(不删,仅通过 `copyPublicDir: false` 阻止进入 dist);`script/build-lib.ts` 保持原样。</precondition>
<precondition>Task 1 已完成(`script/build-plugin.ts` 与 `script/build-plugin.test.ts` 就位且单测通过);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)
- script/build-game.ts(旧编排:`buildClient` / `buildData` / `checkClientData` / `getAllChars` / `buildGame` 的 babel / archiver / compressing / 旧模板拷贝 / `dist.zip`)
- script/build-resource.ts(旧 `splitResource` 分块 zip);script/utils.ts(`formatSize`)
- script/vitePluginDataIsolation.ts(Task 1 产物:导出 `dataIsolationPlugin`)
- script/build-plugin.ts(Task 1 产物:导出 `dataIsolationPlugin`)
- index.html(入口 HTML,读而不改;仍含 `/styles.css` link —— INFO-2);src/content/**(23 个 jsonc 的注释形态);package.json(只读:`type: module` 决定 es 产物扩展名为 `.js`)
- AGENTS.md(禁改既有注释;修改方式须预先声明);dev.md(命名、interface、jsDoc、非空判断约定)
</read_first>
@ -279,7 +279,7 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
- 统计并返回 `files` / `jsonc` / `fonts`;按 dev.md 写 jsDoc;模块无顶层副作用;字面量非空判断用 `isNil`、对象用 `!`。
(2) **重写 `script/build-game.ts`:**
- **新增导入:** `import { dataIsolationPlugin } from './vitePluginDataIsolation';`(文件名小驼峰,与 Task 1 产物一致)。
- **新增导入:** `import { dataIsolationPlugin } from './build-plugin';`(文件名按用户指定,与 Task 1 产物一致)。
- **保留**:`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`)。
- **函数定义顺序(门禁按函数切片,必须遵守):** 自上而下保持 `async function buildClient(` → `async function buildData(` → (`getAllChars` 等辅助)→ `async function buildGame(`;顺序被改动会导致切片取空、门禁失败。
@ -303,16 +303,16 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
</action>
<reversibility rating="costly">删除 `build-packages.ts` 属仓库内结构性删除(可经 git 恢复);两个脚本为重写,`git checkout` 可回退;执行后 `dist` 布局改变,旧产物形态(资源分块 / `dist.zip`)不再可用。</reversibility>
<verify>
<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('vitePluginDataIsolation')<0)bad.push('must import dataIsolationPlugin from ./vitePluginDataIsolation');if(t.indexOf('buildContent')<0)bad.push('missing buildContent');if(t.indexOf('LICENSE')<0)bad.push('missing LICENSE copy');if(!/Promise<string>/.test(t))bad.push('getAllChars must return Promise<string>');if(t.indexOf('src/content')<0||t.indexOf('src/data.ts')<0)bad.push('missing content/data entry paths');const s=t.indexOf('async function buildClient(');const d=t.indexOf('async function buildData(');const g=t.indexOf('async function buildGame(');if(s<0||d<0||g<0||!(s<d&&d<g))bad.push('function order must be buildClient < buildData < buildGame (gate slices by function)');const client=s<0||d<0?'':t.slice(s,d);const data=d<0||g<0?'':t.slice(d,g);const game=g<0?'':t.slice(g);if(client.indexOf('copyPublicDir: false')<0)bad.push('buildClient slice missing copyPublicDir:false');if(client.indexOf('emptyOutDir: false')<0)bad.push('buildClient slice missing emptyOutDir:false');if(client.indexOf('dataIsolationPlugin')>=0)bad.push('dataIsolationPlugin must not be wired into buildClient');if(data.indexOf('copyPublicDir: false')<0)bad.push('buildData slice missing copyPublicDir:false');if(data.indexOf('emptyOutDir: false')<0)bad.push('buildData slice missing emptyOutDir:false');if(data.indexOf('dataIsolationPlugin')<0)bad.push('buildData slice missing dataIsolationPlugin');if(!/formats\s*:\s*\['es'\]/.test(data))bad.push('buildData slice is not ESM-only');if(!/entryFileNames\s*:\s*'data\.js'/.test(data))bad.push('buildData entry must be data.js at dist root');if(!/chunkFileNames\s*:\s*'data\//.test(data))bad.push('buildData aux chunks must live in dist/data/');if(!/assetFileNames\s*:\s*'data\//.test(data))bad.push('buildData aux assets must live in dist/data/');if(/chunkFileNames\s*:\s*'chunks\//.test(data)||/assetFileNames\s*:\s*'assets\//.test(data))bad.push('buildData must not write into render folders');const c=game.indexOf('await buildClient(');const dd=game.indexOf('await buildData(');if(c<0||dd<0)bad.push('buildClient/buildData not awaited in buildGame');else if(c>dd)bad.push('render must build before data');const ed=game.indexOf('emptyDir(');if(ed<0||ed>c)bad.push('dist must be cleared before the render build');if(game.indexOf('buildContent')<0)bad.push('buildGame must call buildContent');if(game.indexOf('LICENSE')<0)bad.push('buildGame must copy LICENSE');if(bad.length>0){console.error('BUILD GAME GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK build-game: function-scoped gates - 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 / 未从 `./vitePluginDataIsolation` 引入插件 / 缺 `buildContent`、`LICENSE` / `getAllChars` 非 `Promise<string>` / 缺 `src/content` 或 `src/data.ts` / 函数定义顺序不满足切片前提 / `buildClient` 片段缺 `copyPublicDir: false` 或 `emptyOutDir: false` / 插件被错接到 `buildClient` / `buildData` 片段缺 `copyPublicDir: false`、`emptyOutDir: false`、`dataIsolationPlugin`、`formats: ['es']`、`entryFileNames: 'data.js'`、`chunkFileNames: 'data/`、`assetFileNames: 'data/` / `buildData` 写进 `chunks/` 或 `assets/` / 数据端先于渲染端构建 / 未在渲染端构建前清空 `dist`)</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('build-plugin')<0)bad.push('must import dataIsolationPlugin from ./build-plugin');if(t.indexOf('buildContent')<0)bad.push('missing buildContent');if(t.indexOf('LICENSE')<0)bad.push('missing LICENSE copy');if(!/Promise<string>/.test(t))bad.push('getAllChars must return Promise<string>');if(t.indexOf('src/content')<0||t.indexOf('src/data.ts')<0)bad.push('missing content/data entry paths');const s=t.indexOf('async function buildClient(');const d=t.indexOf('async function buildData(');const g=t.indexOf('async function buildGame(');if(s<0||d<0||g<0||!(s<d&&d<g))bad.push('function order must be buildClient < buildData < buildGame (gate slices by function)');const client=s<0||d<0?'':t.slice(s,d);const data=d<0||g<0?'':t.slice(d,g);const game=g<0?'':t.slice(g);if(client.indexOf('copyPublicDir: false')<0)bad.push('buildClient slice missing copyPublicDir:false');if(client.indexOf('emptyOutDir: false')<0)bad.push('buildClient slice missing emptyOutDir:false');if(client.indexOf('dataIsolationPlugin')>=0)bad.push('dataIsolationPlugin must not be wired into buildClient');if(data.indexOf('copyPublicDir: false')<0)bad.push('buildData slice missing copyPublicDir:false');if(data.indexOf('emptyOutDir: false')<0)bad.push('buildData slice missing emptyOutDir:false');if(data.indexOf('dataIsolationPlugin')<0)bad.push('buildData slice missing dataIsolationPlugin');if(!/formats\s*:\s*\['es'\]/.test(data))bad.push('buildData slice is not ESM-only');if(!/entryFileNames\s*:\s*'data\.js'/.test(data))bad.push('buildData entry must be data.js at dist root');if(!/chunkFileNames\s*:\s*'data\//.test(data))bad.push('buildData aux chunks must live in dist/data/');if(!/assetFileNames\s*:\s*'data\//.test(data))bad.push('buildData aux assets must live in dist/data/');if(/chunkFileNames\s*:\s*'chunks\//.test(data)||/assetFileNames\s*:\s*'assets\//.test(data))bad.push('buildData must not write into render folders');const c=game.indexOf('await buildClient(');const dd=game.indexOf('await buildData(');if(c<0||dd<0)bad.push('buildClient/buildData not awaited in buildGame');else if(c>dd)bad.push('render must build before data');const ed=game.indexOf('emptyDir(');if(ed<0||ed>c)bad.push('dist must be cleared before the render build');if(game.indexOf('buildContent')<0)bad.push('buildGame must call buildContent');if(game.indexOf('LICENSE')<0)bad.push('buildGame must copy LICENSE');if(bad.length>0){console.error('BUILD GAME GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK build-game: function-scoped gates - 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 / 未从 `./build-plugin` 引入插件 / 缺 `buildContent`、`LICENSE` / `getAllChars` 非 `Promise<string>` / 缺 `src/content` 或 `src/data.ts` / 函数定义顺序不满足切片前提 / `buildClient` 片段缺 `copyPublicDir: false` 或 `emptyOutDir: false` / 插件被错接到 `buildClient` / `buildData` 片段缺 `copyPublicDir: false`、`emptyOutDir: false`、`dataIsolationPlugin`、`formats: ['es']`、`entryFileNames: 'data.js'`、`chunkFileNames: 'data/`、`assetFileNames: 'data/` / `buildData` 写进 `chunks/` 或 `assets/` / 数据端先于渲染端构建 / 未在渲染端构建前清空 `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=[];for(const p of ['script/vitePluginDataIsolation.ts','script/vitePluginDataIsolation.test.ts']){if(!fs.existsSync(p))bad.push('missing '+p)}for(const p of ['script/vite-plugin-data-isolation.ts','script/vite-plugin-data-isolation.test.ts']){if(fs.existsSync(p))bad.push('kebab-case file must not exist: '+p)}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');const sp='.planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md';if(!fs.existsSync(sp))bad.push('09-21-SUMMARY.md missing');else{const sm=fs.readFileSync(sp,'utf8');for(const tok of ['PrefixedJSONCProcessor','66','styles.css','build:game']){if(sm.indexOf(tok)<0)bad.push('SUMMARY must record known boundary token: '+tok)}}if(bad.length>0){console.error('FILES GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK files: camelCase plugin present, build-packages deleted, build-lib untouched, SUMMARY records INFO-1/2/4')"</automated>
<fails_when>non-zero exit,或 stderr 含 "FILES GATE FAILED"(小驼峰插件/测试文件缺失 / 存在连字符命名文件 / `build-packages.ts` 未删除 / `build-lib.ts` 被删或被改 / 新增了范围外测试文件 / SUMMARY 缺失或未记录 INFO-1(`PrefixedJSONCProcessor`、`66`)、INFO-2(`styles.css`)、INFO-4(`build:game`))</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['script/build-plugin.ts','script/build-plugin.test.ts']){if(!fs.existsSync(p))bad.push('missing '+p)}for(const p of ['script/vitePluginDataIsolation.ts','script/vitePluginDataIsolation.test.ts']){if(fs.existsSync(p))bad.push('legacy plugin filename must not exist: '+p)}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');const sp='.planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md';if(!fs.existsSync(sp))bad.push('09-21-SUMMARY.md missing');else{const sm=fs.readFileSync(sp,'utf8');for(const tok of ['PrefixedJSONCProcessor','66','styles.css','build:game']){if(sm.indexOf(tok)<0)bad.push('SUMMARY must record known boundary token: '+tok)}}if(bad.length>0){console.error('FILES GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK files: user-specified plugin filenames present, build-packages deleted, build-lib untouched, SUMMARY records INFO-1/2/4')"</automated>
<fails_when>non-zero exit,或 stderr 含 "FILES GATE FAILED"(用户指定的插件/测试文件缺失 / 存在旧提议名 `script/vitePluginDataIsolation.*` 文件 / `build-packages.ts` 未删除 / `build-lib.ts` 被删或被改 / 新增了范围外测试文件 / SUMMARY 缺失或未记录 INFO-1(`PrefixedJSONCProcessor`、`66`)、INFO-2(`styles.css`)、INFO-4(`build:game`))</fails_when>
</verify>
<acceptance_criteria>
- `script/build-resource.ts` 重写为 content 模块:`minifyJSONC` 按行去掉 `//` 注释后输出单行纯 JSON(无系统标记行、后缀仍 `.jsonc`);`buildContent` 递归镜像 `src/content` → `dist/content`,jsonc 压缩、字体 fontmin glyph、其它原样拷贝、无字体时跳过告警;接口 `chars: string`;不再含分块 / zip / `JSZip`
- `script/build-game.ts` 重写:从 `./vitePluginDataIsolation` 引入 `dataIsolationPlugin`;准备(`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-game.ts` 重写:从 `./build-plugin` 引入 `dataIsolationPlugin`;准备(`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/**`、`packages-user/**`、`vite.config.ts`、lockfile、`packages*/**`、`script/dev.ts` 均未改动
- 无特殊失败处理:无 BLOCKED 分支、无 baseline / 范围门禁、无外部文件或仓库状态检查;构建失败即打印错误并非零退出
- 结构与行为断言全绿(插件单测、`build-game` 的**按函数切片**结构断言、`build-resource` 结构断言、文件清单断言)
@ -332,7 +332,7 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
| content 输入 → dist | `src/content` 的 23 个 jsonc 经 `JSON.parse` 后重新序列化;格式非法会在构建期暴露 |
| 数据端与渲染端产物目录 | `dist/data.js` + `dist/data/` 必须与 `dist/assets/`、`dist/chunks/` 分离 |
| 依赖边界 | 不新增依赖、不改 lockfile、不改 `package.json` |
| 命名边界 | 新文件 / 导出 / 接口须先经 Task 0 报备批准(D-04);新文件名须小驼峰(dev.md) |
| 命名边界 | 新文件 / 导出 / 接口须先经 Task 0 报备批准(D-04);插件与测试文件名由用户显式指定为 `script/build-plugin.ts` / `script/build-plugin.test.ts`(覆盖 dev.md 小驼峰规则) |
## STRIDE Threat Register
@ -344,17 +344,17 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
| 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/**` / `packages-user/**` 等一律不动);文件清单断言 |
| T-09-21-07 | Tampering | 未经批准使用新命名 / 连字符文件名 | medium | mitigate | Task 0 报备 OI-1 / OI-2;prohibitions 明列「未获批前不得落码」与小驼峰要求;文件清单断言禁止连字符命名文件存在 |
| T-09-21-07 | Tampering | 未经批准使用新命名 / 改用非用户指定文件名 | medium | mitigate | Task 0 报备 OI-1 / OI-2;prohibitions 明列「未获批前不得落码」与「插件/测试文件只能是用户指定的 `script/build-plugin.ts` / `script/build-plugin.test.ts`」;文件清单断言校验用户指定文件名存在且旧提议名文件不存在 |
| T-09-21-08 | Tampering | 字体缺失或格式非法导致 fontmin 抛错 | low | mitigate | 无字体二进制时跳过并告警;`buildContent` 仅在遇到字体文件时才调用 fontmin |
| T-09-21-09 | Repudiation | 以「构建通过」掩盖未端到端运行的事实(INFO-4) | medium | mitigate | SUMMARY 必须含 `build:game` 字样并写明未端到端运行;文件清单断言校验该字样;prohibitions 禁止此类表述 |
| T-09-21-SC | Tampering | npm/pip/cargo installs | high | mitigate | 不新增依赖、不改 `package.json` 依赖面与 lockfile;如需新增依赖须回 Task 0 报备获批 |
</threat_model>
<verification>
- **Task 0:** 预执行状态(旧构建脚本 / 23 个 jsonc / 小驼峰插件文件不存在 / `jsoncProcessor` 仍按 `prefixSplit` + error 66 工作)检查;人工关卡(用户回复「可以执行」;OI-1 / OI-2 如有指定按指定);汇报须含 INFO-1 / INFO-2 / INFO-4 三项边界。
- **Task 1:** `dataIsolationPlugin` 结构(小驼峰文件名、`enforce: 'pre'`、`resolveId`、`moduleParsed(info: ModuleInfo)` 按 `info.id`、`getModuleInfo(info.id)` 的 `importers`、`this.error`、禁令集合、`matched:` token);真实 `vite.build()` 单测(拒 `vue` / 拒 `src/client` / 拒 `packages-user/client-*` / 纯数据端通过并产出 ESM)。
- **Task 2:** `build-game` **按函数切片**结构断言(切出 `buildClient` / `buildData` / `buildGame` 后分别断言:`buildClient` 含 `copyPublicDir: false` + `emptyOutDir: false` 且不含插件;`buildData` 含 `copyPublicDir: false`、`emptyOutDir: false`、`dataIsolationPlugin`、`formats: ['es']`、`entryFileNames: 'data.js'`、`chunkFileNames: 'data/`、`assetFileNames: 'data/` 且不含 `chunks/` / `assets/` 前缀;`buildGame` 中先 `emptyDir` 再 `await buildClient(` 再 `await buildData(`,并调用 `buildContent` 与复制 `LICENSE`;全文件无旧 pipeline token、`getAllChars` 返回 `Promise<string>`);`build-resource` 结构断言(无分块 / zip;`minifyJSONC` 按行去注释 + `JSON.parse` / `JSON.stringify`;fontmin;`chars: string`);文件清单断言(小驼峰插件与测试存在、无连字符同名文件、`build-packages.ts` 已删、`build-lib.ts` 未动、SUMMARY 已写且含 INFO-1/2/4 关键字)。
- **一致性:** `dist/content/**` 落点与 `src/data/constant.ts` 的 `content/core.jsonc` 一致;数据端 `data.js` 扩展名与 `package.json` 的 `type: module` 一致;`build-game.ts` 的插件导入路径 `./vitePluginDataIsolation` 与 Task 1 产物文件名一致。
- **Task 0:** 预执行状态(旧构建脚本 / 23 个 jsonc / 用户指定的插件文件(`script/build-plugin.ts`)不存在 / `jsoncProcessor` 仍按 `prefixSplit` + error 66 工作)检查;人工关卡(用户回复「可以执行」;OI-1 / OI-2 如有指定按指定);汇报须含 INFO-1 / INFO-2 / INFO-4 三项边界。
- **Task 1:** `dataIsolationPlugin` 结构(用户指定的文件名 `script/build-plugin.ts`、`enforce: 'pre'`、`resolveId`、`moduleParsed(info: ModuleInfo)` 按 `info.id`、`getModuleInfo(info.id)` 的 `importers`、`this.error`、禁令集合、`matched:` token);真实 `vite.build()` 单测(拒 `vue` / 拒 `src/client` / 拒 `packages-user/client-*` / 纯数据端通过并产出 ESM)。
- **Task 2:** `build-game` **按函数切片**结构断言(切出 `buildClient` / `buildData` / `buildGame` 后分别断言:`buildClient` 含 `copyPublicDir: false` + `emptyOutDir: false` 且不含插件;`buildData` 含 `copyPublicDir: false`、`emptyOutDir: false`、`dataIsolationPlugin`、`formats: ['es']`、`entryFileNames: 'data.js'`、`chunkFileNames: 'data/`、`assetFileNames: 'data/` 且不含 `chunks/` / `assets/` 前缀;`buildGame` 中先 `emptyDir` 再 `await buildClient(` 再 `await buildData(`,并调用 `buildContent` 与复制 `LICENSE`;全文件无旧 pipeline token、`getAllChars` 返回 `Promise<string>`);`build-resource` 结构断言(无分块 / zip;`minifyJSONC` 按行去注释 + `JSON.parse` / `JSON.stringify`;fontmin;`chars: string`);文件清单断言(用户指定的 `script/build-plugin.ts` 与 `script/build-plugin.test.ts` 存在、旧提议名 `script/vitePluginDataIsolation.*` 文件不存在、`build-packages.ts` 已删、`build-lib.ts` 未动、SUMMARY 已写且含 INFO-1/2/4 关键字)。
- **一致性:** `dist/content/**` 落点与 `src/data/constant.ts` 的 `content/core.jsonc` 一致;数据端 `data.js` 扩展名与 `package.json` 的 `type: module` 一致;`build-game.ts` 的插件导入路径 `./build-plugin` 与 Task 1 产物文件名一致。
- **无全仓门禁:** 不使用 `pnpm check:type` / `pnpm build` / 全量 `pnpm test:ci`(用户 WIP 存在历史类型错误);本计划**不端到端运行全量** `build:game`。
- **人工复核:** 结论(含 INFO-1 / INFO-2 / INFO-4 已知边界)写入 `09-21-SUMMARY.md`。
</verification>
@ -365,7 +365,7 @@ Phase 9 · 任务 11 · 第 21 个计划 —— **构建重构(适配新引擎
- **D-96:** `src/content`(23 个 jsonc)→ `dist/content`;jsonc 压成纯 JSON、去掉所有注释(含系统标记行)、后缀仍 `.jsonc`;fontmin 压缩(无字体时跳过告警);不分块、不 zip。**已知后续项已记录:** 去净注释与 `PrefixedJSONCProcessor`(error 66)的冲突不在本计划修复。
- **D-97:** `copyPublicDir: false`;dist 不含旧模板产物;`LICENSE` 复制到 `dist/LICENSE`;`index.html` 以默认入口产出常规渲染产物且文件本身不改;**已知边界已记录:** `index.html` 仍引用 `/styles.css` 且本计划不产出该文件。
- **D-93 / D-98 / D-99 / D-100:** `build-packages.ts` 删除;`build-game` 承担渲染端 + 数据端 + content;移除 `main.js` babel 二次压缩;不改 `package.json` / `dev.md` / `build-lib.ts`。
- **命名(dev.md):** `script/vitePluginDataIsolation.ts` / `script/vitePluginDataIsolation.test.ts` 均为小驼峰;不存在连字符命名同名文件。
- **命名(用户指定):** `script/build-plugin.ts` / `script/build-plugin.test.ts`——文件名由用户显式指定,**覆盖 dev.md 的小驼峰文件名规则**;不存在旧提议名 `script/vitePluginDataIsolation.ts` / `script/vitePluginDataIsolation.test.ts`。
- **边界:** 仅 `files_modified` 内的脚本与 SUMMARY 被写入;未改 `src/**` / `public/**` / `packages-user/**` / `vite.config.ts` / lockfile / `packages*/**` / `script/dev.ts`;未回退用户 WIP。
- **验证:** 插件单测(必需)+ 按函数切片的结构断言 + 文件清单断言全部就位并跑到确定结论;无特殊失败处理与外部状态检查;**未端到端运行全量 `build:game` 的事实已写入 SUMMARY**。
</success_criteria>