diff --git a/.planning/phases/09-minimum-browser-runnable/09-22-PLAN.md b/.planning/phases/09-minimum-browser-runnable/09-22-PLAN.md new file mode 100644 index 0000000..4c32aa1 --- /dev/null +++ b/.planning/phases/09-minimum-browser-runnable/09-22-PLAN.md @@ -0,0 +1,391 @@ +--- +phase: 09-minimum-browser-runnable +plan: 22 +type: execute +wave: 19 +depends_on: [09-21] +files_modified: + - script/build-plugin.ts + - script/build-plugin.test.ts + - script/build-game.ts + - script/build-resource.ts + - script/build-resource.test.ts + - .planning/phases/09-minimum-browser-runnable/09-22-SUMMARY.md +autonomous: false +requirements: [] +estimate: + tokens: 46000 + raw_tokens: 46000 + tasks: 3 + confidence: low +must_haves: + truths: + - "**本计划第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md / D-03)**:先汇报「任务 11 后续修复 —— 构建脚本的 content 过滤 + 修饰器删除」的要做什么 / 为什么 / 大致怎么做 / 影响范围,列明最小范围(`script/build-plugin.ts` 新增修饰器删除插件、`script/build-game.ts` 注入两处、`script/build-resource.ts` 增加 content 过滤、`script/build-plugin.test.ts` 扩展 + 新增 `script/build-resource.test.ts`)、逐文件夹的 content 过滤规则、修饰器删除口径(TS AST 精确切片、不删 import)、验证方式、已知边界与待批准命名(OI-1 / OI-2 / OI-3),然后停下等待「可以执行」。" + - "**content 结构规则(用户裁定 1 / D-96):** `src/content` → `dist/content`;**根目录文件照常输出**(`.jsonc` 压缩、`item.ts` 原样);**`defaults/` 照常输出并压缩**(不排除、不按清单过滤);**其余子文件夹(`maps` / `texture` / `animation` / `audio` / `font`)只保留被各自 `.list.jsonc` 引用的文件**,未引用的剔除;**`.list.jsonc` 自身压缩输出**;无 `.list.jsonc` 的子文件夹全量输出。" + - "**清单匹配口径(用户裁定 1):** 解析 `.list.jsonc`(去注释后 `JSON.parse`)后递归收集**所有字符串值**,以「**完整文件名**」或「**去扩展名主文件名**」匹配保留(如 `terrains0.png` 命中完整名、`empty` 命中 `empty.jsonc`、`test` 命中 `test.anim`)。" + - "**jsonc 压缩(用户裁定 1 / D-96):** 去掉**所有** `//` 注释压成**纯 JSON**(含系统标记行),后缀**仍为 `.jsonc`**;**不分块、不 zip**;字体仍 fontmin 子集化(无字体二进制时跳过)。" + - "**修饰器删除(用户裁定 2):** 构建时删除**所有** `@shouldReplay(...)` / `@ignoreReplay(...)` 装饰器应用,**数据端构建与渲染端构建都删**(`buildData` 与 `buildClient` 各注入一次);**不得手动删 import**,交给 Vite/ESBuild 依赖分析。" + - "**删除方式必须可靠(用户裁定 2):** 以 **TypeScript AST**(`ts.createSourceFile` + `ts.canHaveDecorators` / `ts.getDecorators`)定位装饰器节点,按 `getStart()` / `getEnd()` **逆序切片删除**;**字符串字面量、注释、以及非目标装饰器中出现的同名文本一律不得被删**;仅处理项目自身 `.ts` / `.tsx`(**跳过 `node_modules`**),不处理 `.vue`。" + - "**构建范围与顺序不变(用户裁定 3 / 4 / D-93):** 只改构建脚本与其测试;**不改** `script/build-lib.ts`、`package.json`、`index.html`、`dev.md`、`src/**`、`public/**`、数据端/渲染端源码(`packages-user/**`、`packages/**`)、`vite.config.ts`、lockfile、`script/dev.ts`;**不改构建顺序**(准备 → 渲染端 → 数据端 → content → LICENSE);**不做特殊失败处理**(无 BLOCKED 分支、无 baseline / 范围门禁、无外部状态检查)。" + - "**开发环境不受影响:** 修饰器删除插件 `apply: 'build'`(`pnpm dev` 仍保留装饰器,`script/dev.ts` 不改);插件 `enforce: 'pre'`,其 `transform` 早于 Vite 的 TS/JSX 转译。" + - "**测试是必需产物:** 扩展 `script/build-plugin.test.ts`(新增 `describe`,不改动既有 `dataIsolationPlugin` 用例)并新增 `script/build-resource.test.ts`;每个 `it` 前一行注释(AGENTS.md);content 测试用临时 fixture 目录,不触碰 `src/content`。" + - "**验证可运行且范围诚实(Nyquist):** 修饰器删除用「纯函数文本断言 + **对照式**真实 `vite.build()`」双证——同一 fixture 在**不带插件**的对照构建里产物必须含 `mota_replay_probe` 与 `shouldReplay` 标记,在**带插件**的构建里必须不含(缺对照即成空断言);另含字符串/注释不误删、`node_modules` 跳过。content 过滤以 fixture 行为测试证明(`defaults` 全量、清单引用保留、未引用剔除、无清单子文件夹全量、`.list.jsonc` 压缩去注释、jsonc 去注释)。另加真实 `src/content` 的「清单覆盖率」检查、按函数切片的结构门禁、`build-resource.ts` 的「既有注释零删除」git diff 门禁,以及 `script/build-*.ts` 范围内的 `eslint`。结构门禁只断言**行为与导出形态**(AST API 标记、`enforce` / `apply`、导入绑定与注入位置),不硬编码提议命名。所有 `node -e` 门禁**只用单引号字面量**(PowerShell 5.1 下 `\"` 会提前闭合字符串,实测确定性 SyntaxError)。命令一律 `cmd /c \"pnpm exec ...\"`(PowerShell 5.1 下裸 `pnpm` 会因 `pnpm.ps1` 的 `PSSecurityException` 失败)。" + - "**命名须先获批(D-04):** OI-1(`removeReplayDecorators` / `replayDecoratorPlugin` / `name: 'mota-replay-decorator'`)、OI-2(`LIST_FILE_NAME` / `DEFAULTS_DIRECTORY` / `readListNames` / `collectListedNames` / `isListedFile`)、OI-3(`script/build-resource.test.ts` 文件名)。未获批前不得落码。**若用户在 Task 0 指定了与提议不同的命名,必须先按新命名修订本计划(门禁命令、测试用例与 SUMMARY 关键字同步)并重新获批后执行**——门禁本身按行为与导出形态断言,不会自动跟随改名。" + artifacts: + - path: script/build-plugin.ts + provides: "在既有 `dataIsolationPlugin` 之外新增:`removeReplayDecorators(code: string, fileName: string): string`(TS AST 定位并逆序切片删除 `@shouldReplay` / `@ignoreReplay` 装饰器)与 `replayDecoratorPlugin(): Plugin`(`name: 'mota-replay-decorator'`、`enforce: 'pre'`、`apply: 'build'`、`transform(code, id)` 仅处理项目内 `.ts` / `.tsx`,跳过 `node_modules`)(命名待批准,OI-1)" + - path: script/build-plugin.test.ts + provides: "既有测试**扩展**:新增 `replayDecoratorPlugin` / `removeReplayDecorators` 的 `describe`——纯函数断言(删装饰器、保留字符串与注释中的同名文本、参数含引号/括号整段删除)+ **对照式**真实 `vite.build()`(同一 fixture:**不带插件**的对照产物必须含 `mota_replay_probe` 与 `shouldReplay` / `ignoreReplay` 标记;带插件产物必须不含这些标记且方法名仍存在);每个 `it` 前一行注释" + - path: script/build-game.ts + provides: "注入:`buildClient` 的 `plugins` 增加 `replayDecoratorPlugin()`(在 `legacy(...)` 之前)、`buildData` 的 `plugins` 变为 `[dataIsolationPlugin(), replayDecoratorPlugin()]`;构建顺序、产物落点、失败处理、其余配置一律不变" + - path: script/build-resource.ts + provides: "content 过滤:`LIST_FILE_NAME`(`.list.jsonc`)、`DEFAULTS_DIRECTORY`(`defaults`)、`readListNames`、`collectListedNames`、`isListedFile`;`processDirectory` 在有清单的目录内按清单保留文件、`defaults` 与无清单目录全量输出、`.list.jsonc` 始终压缩输出(命名待批准,OI-2)" + - path: script/build-resource.test.ts + provides: "**新增**行为测试:临时 fixture 验证根文件、`defaults/` 全量、`maps/` 与 `texture/` 与 `font/` 的清单保留/剔除、**无清单子文件夹全量**、`.list.jsonc` 压缩且剥离 `//` 注释(fixture 清单原文带注释)、`item.ts` 原样拷贝;每个 `it` 前一行注释(文件名待批准,OI-3)" + - path: .planning/phases/09-minimum-browser-runnable/09-22-SUMMARY.md + provides: "执行摘要:content 逐文件夹过滤规则与清单匹配口径、修饰器删除口径(TS AST + 双端注入 + 不删 import)、命名获批情形、门禁结果、`open_items` 处置、已知边界(未端到端跑全量 `build:game`;生产产物不再做录像安全检查;`index.html` 仍引用 `/styles.css`)" + key_links: + - "`replayDecoratorPlugin()`(`script/build-plugin.ts`)↔ `buildClient` 与 `buildData` 的 `plugins` 数组(`script/build-game.ts`)—— 用户裁定 2「双端都删」的唯一执行点" + - "`removeReplayDecorators` 的 AST 切片 ↔ `@shouldReplay('...')` 装饰器文本 —— 只删装饰器节点范围,字符串/注释中的同名文本不会被误删" + - "`replayDecoratorPlugin` 的 `apply: 'build'` ↔ `script/dev.ts` 未改 —— 开发环境仍保留装饰器" + - "`readListNames`(`minifyJSONC` + `JSON.parse` + `collectListedNames`)↔ `isListedFile` 的「完整文件名 / 去扩展名主文件名」双匹配 —— 用户裁定 1 的保留口径" + - "`processDirectory` 的 `.list.jsonc` 检测 ↔ `DEFAULTS_DIRECTORY` 豁免 —— `defaults/` 永远全量输出" + - "`.list.jsonc` 自身始终走 `minifyJSONC` 输出 ↔ 其余文件按清单取舍 —— 清单文件不会因未被自己引用而丢失" + - "`dist/content/**` 落点 ↔ `MotaDataLoader.resolveURL` 的 `content/**` 与 `src/data/constant.ts` 的 `CORE_URL='content/core.jsonc'` —— 目录契约不变" + assumptions: + - "**当前清单已覆盖现有素材:** `texture/.list.jsonc` 引用了 `texture/` 下全部 48 个 PNG(含 `magictower.png`);`maps/.list.jsonc` 引用了全部 5 个地图 jsonc;`animation`(`[\"test\"]`)与 `font`(`[\"testFont.ttf\"]`)目录内无对应素材;`audio/.list.jsonc` 为 `{}`(解析后无字符串),`audio/` 当前为空。故过滤不会减少现有 `dist/content` 的实际素材(由 Task 2 的清单覆盖率门禁证明)。" + - "**PowerShell 5.1 参数传递:** 所有 `node -e` 门禁只用单引号字面量,不出现 `\"`(双引号会提前闭合字符串)、不出现 `$` 与反引号(会被插值 / 转义);确需引号字符时用 `String.fromCharCode(39)` 构造。`cmd /c \"pnpm exec ...\"` 形式保留(裸 `pnpm` 会因 `pnpm.ps1` 的 `PSSecurityException` 失败)。" + - "**计数基线(已实测):** `src/content` 共 23 个 `.jsonc`(5 个根 + 8 个 `defaults` + 5 个地图 + 5 个 `.list.jsonc`)、`texture/` 共 48 个 PNG;`@shouldReplay` 应用共 69 处(20 个 `.ts` 文件,另有 1 处仅出现在 `packages-user/data-common/src/replay/func.ts` 的注释里,不算应用);`@ignoreReplay` 无实际应用。" + - "**`defaults/` 无 `.list.jsonc`:** 现为 8 个 `.jsonc`;按用户裁定 1 显式豁免,即使将来出现清单也全量输出。" + - "**TypeScript AST 可用:** 仓库 `typescript@6.0.3` 为 devDependency;`ts.canHaveDecorators` / `ts.getDecorators` 可用,装饰器位于 `modifiers`;标准装饰器无需开启 `experimentalDecorators` 即可解析(`tsconfig.json` 未开启)。" + - "**插件时序:** `enforce: 'pre'` 的 `transform` 早于 Vite 的 `vite:esbuild` TS 转译与 `@vitejs/plugin-vue-jsx`,因此删装饰器发生在任何编译之前;`apply: 'build'` 保证 `pnpm dev` 不受影响。" + - "**import 由依赖分析处理:** 删除装饰器后 `shouldReplay` / `ignoreReplay` 的绑定变为未使用,由 Rollup/esbuild tree-shaking 移除;**不手动删 import**(用户裁定 2)。" + - "**`transform` 的 `id` 可能带查询串:** 需要在后缀判断前剥离 `?` 之后的部分(Vite 对 `.ts` / `.tsx` 常见 `?` 查询参数)。" + - "**删装饰器后残留空白行无害:** 装饰器独占一行时删除后仅留空行,不改变语法;不额外做格式化。" + - "**fontmin 仅对字体扩展名生效:** `font/` 下当前无字体二进制;测试用非字体扩展名 fixture 验证清单过滤,避免触发 fontmin。" + - "**行号会漂移:** `build-game.ts` 的门禁按**函数名切片**(`buildClient` < `buildData` < `buildGame` 的定义顺序)而非固定行号。" + prohibitions: + - "不得在用户回复通过 Task 0 关卡之前执行任何任务或派发执行子代理。" + - "**未经批准的命名一律不得使用:** `removeReplayDecorators` / `replayDecoratorPlugin` / `name: 'mota-replay-decorator'`(OI-1)、`LIST_FILE_NAME` / `DEFAULTS_DIRECTORY` / `readListNames` / `collectListedNames` / `isListedFile`(OI-2)、`script/build-resource.test.ts`(OI-3)。**若用户指定了其它命名,必须先用新命名修订本计划(含门禁命令与测试用例)并重新获批**,不得沿用旧名跑门禁、也不得只改代码不改计划。" + - "**不得手动删除源码中的 import:** 不得改 `packages-user/**`、`packages/**`、`src/**` 里的任何 import 语句(用户裁定 2 / 3)。" + - "**不得让 `defaults/` 参与清单过滤:** 不得排除、不得按清单取舍、不得跳过压缩。" + - "**不得把清单匹配做成模糊匹配:** 只允许「完整文件名」或「去扩展名主文件名」两种精确匹配;不得用包含/前缀/正则通配去保留文件。" + - "**不得让 `.list.jsonc` 自身被过滤掉:** 它必须始终压缩输出(否则运行期无法再知道清单)。" + - "**不得用正则扫描源码来删装饰器:** 必须用 TypeScript AST 的范围切片,避免误删字符串/注释中的同名文本;不得处理 `node_modules` 内的文件。" + - "**不得让插件进入开发服务:** 必须 `apply: 'build'`;不得改 `script/dev.ts`。" + - "**不得改构建顺序与产物落点:** 渲染端先于数据端;`dist/data.js` + `dist/data/`;`dist/content/`;`LICENSE` 复制;不得重新引入分块 / zip / babel / 旧模板拷贝。" + - "**不得引入特殊失败处理:** 不写 BLOCKED 分支、不写 baseline / 范围门禁、不检查外部文件或仓库状态;构建失败即以非零退出并打印错误。" + - "**不得改范围外文件:** 不改 `script/build-lib.ts`、`package.json`、`index.html`、`dev.md`、`src/**`、`public/**`、`packages-user/**`、`packages/**`、`vite.config.ts`、lockfile、`script/dev.ts`。" + - "**既有注释 / jsDoc 一律逐字保留(AGENTS.md):** `script/build-resource.ts` 中 `processDirectory` / `minifyJSONC` / `compressFont` / `buildContent` 与接口成员的 jsDoc 不得改写或删除;`script/build-plugin.ts` 中 `dataIsolationPlugin`、`SPECIFIER_BANS` / `PATH_BANS` / `findBan` 的注释同理。新逻辑的说明只能写在新函数 / 新常量上。" + - "**不得在 `node -e` 门禁里用 `\\\"` 转义双引号:** PowerShell 5.1 会提前闭合字符串并抛 SyntaxError;内联脚本只允许单引号字面量(需引号字符时用 `String.fromCharCode(39)`),且不得出现 `$` 与反引号。" + - "**不得删掉对照构建:** 不带插件的对照构建缺失时,`not.toContain` 就是空断言(编译产物本就不含 `@shouldReplay` 语法)。" + - "**不得用 `not.toContain('@shouldReplay')` 作为唯一证据:** 必须同时对「对照产物含标记」「实验产物不含标记 + 方法名/literal 仍在」两侧断言。" + - "**不得新增依赖、不改 lockfile。**" + - "**不得伪造门禁:** 不得手写 `dist` 产物冒充构建结果;不得用「全量 `build:game` 已通过」之类的表述掩盖未端到端运行的事实。" + open_items: + - "**OI-1(修饰器删除插件命名):** 提议导出 `removeReplayDecorators(code: string, fileName: string): string` 与 `replayDecoratorPlugin(): Plugin`,插件 `name: 'mota-replay-decorator'`。备选:`stripReplayDecorators` / `removeReplayDecoratorPlugin`、`name: 'mota-replay-decorator-strip'`。**待批准**(D-04)。" + - "**OI-2(content 过滤内部命名):** 提议模块内常量 `LIST_FILE_NAME`(`.list.jsonc`)、`DEFAULTS_DIRECTORY`(`defaults`),内部函数 `readListNames(listFile: string): Promise>`、`collectListedNames(value: unknown, output: Set): void`、`isListedFile(name: string, names: Set): boolean`。备选:`LIST_NAME` / `LISTED_NAME_CACHE`、`loadListNames` / `pickStrings` / `keepFile`。**待批准**。既有 `buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats` 与 `IContentBuildCounter` **不改名**。" + - "**OI-3(测试文件):** 扩展既有 `script/build-plugin.test.ts`;新增 `script/build-resource.test.ts`(与被测文件同名,测试文件按 AGENTS.md 由 AI 维护)。**报备**(如需改名请一并指明)。" + - "**已知边界(不在本计划):** ① 未端到端运行全量 `build:game`(渲染端仍有用户 WIP 的 stale import),验证只到单测 + 结构门禁 + 真实 content 清单覆盖率;② 生产构建删掉录像安全检查装饰器属用户裁定 2 的预期结果,运行期不再产生 `@shouldReplay` 的收集信息;③ `index.html` 仍引用 `/styles.css`(09-21 已记录),本计划不改。" +--- + +Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤 + 修饰器删除**(任务 11 的后续修复),最小范围,只动构建脚本与其测试。 + +**要解决的问题(两处):** +1. **content 未过滤:** `buildContent` 现在把 `src/content` 全量镜像到 `dist/content`,`texture/` 等素材目录里未被清单引用的文件也会被打进产物;用户要求除 `defaults/` 外的素材子文件夹按各自 `.list.jsonc` 只保留被引用的文件。 +2. **录像装饰器进了生产构建:** 数据端源码里大量 `@shouldReplay('...')`(`ignoreReplay` 同族)在构建时仍被编译进产物。用户要求在**数据端与渲染端**构建时全部删除这些装饰器应用,且**不手动删 import**(交给依赖分析)。 + +**做法(最小):** +- `script/build-plugin.ts`:在既有 `dataIsolationPlugin` 之外新增一个修饰器删除插件(TS AST 定位 + 逆序切片)与其纯函数。 +- `script/build-game.ts`:`buildClient` 与 `buildData` 各注入该插件;其余配置、顺序、产物落点不变。 +- `script/build-resource.ts`:`processDirectory` 增加「有 `.list.jsonc` 的目录按清单取舍文件」的判断(`defaults/` 与无清单目录全量、`.list.jsonc` 自身始终压缩输出)。 +- 测试:扩展 `script/build-plugin.test.ts`;新增 `script/build-resource.test.ts`(fixture 行为测试)。 + +**约束:** 用户裁定 3 / 4 —— 只改构建脚本;不改 `build-lib.ts` / `package.json` / `index.html` / `dev.md` / `src/**` / `public/**` / 数据端与渲染端源码 / `vite.config.ts` / lockfile / `script/dev.ts`;不改构建顺序;不做特殊失败处理。 + +**修改方式声明(AGENTS.md):** 本计划执行含文件写入,由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified`(含两个测试文件与 SUMMARY)外无任何写入。 + + + +@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md +@.planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md +@AGENTS.md +@dev.md +@script/build-plugin.ts +@script/build-plugin.test.ts +@script/build-game.ts +@script/build-resource.ts +@packages-user/data-common/src/replay/func.ts +@src/content/texture/.list.jsonc +@src/content/maps/.list.jsonc + + + + + Task 0: 预执行汇报关卡 —— 汇报两处修复的范围、content 过滤规则、修饰器删除口径与待批准命名,等待「可以执行」 + `script/build-plugin.ts` 仅含 `dataIsolationPlugin`;`script/build-resource.ts` 为全量镜像实现(`processDirectory` 无清单过滤);`src/content` 共 23 个 `.jsonc`,`animation` / `audio` / `font` / `maps` / `texture` 各有 `.list.jsonc`、`defaults/` 有 8 个 `.jsonc`;`packages-user/data-common/src/replay/func.ts` 仍导出 `shouldReplay` / `ignoreReplay`,数据端源码中仍有大量 `@shouldReplay` 应用;`script/build-resource.test.ts` 尚不存在。 + (只读汇报,不修改任何文件) + + - .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-93 / D-96 / D-98;D-03 / D-04) + - .planning/phases/09-minimum-browser-runnable/09-21-SUMMARY.md(上游执行结果与已知边界) + - script/build-plugin.ts / script/build-game.ts / script/build-resource.ts(当前实现) + - src/content 下 5 个 `.list.jsonc`(`animation` 为无后缀名数组、`audio` 为 `{}`、`font` 为完整文件名、`maps` 为去扩展名、`texture` 为 `{ textures: [{ source }] }`) + - packages-user/data-common/src/replay/func.ts(`shouldReplay` / `ignoreReplay` 定义) + - AGENTS.md(执行前须获批、命名须先反馈、修改方式须预先声明);dev.md(命名、jsDoc、无副作用、非空判断) + + + **停下并向用户汇报,等待回复「可以执行」后才继续;汇报本身只读。** + + ① **要解决的问题:** 见 objective 的两条(content 未过滤、录像装饰器进了生产构建)。 + + ② **范围(最小):** `script/build-plugin.ts`(新增插件 + 纯函数)、`script/build-game.ts`(`buildClient` / `buildData` 各注入一次)、`script/build-resource.ts`(清单过滤)、`script/build-plugin.test.ts`(扩展)、`script/build-resource.test.ts`(新增)、`09-22-SUMMARY.md`。**其余文件一律不动**(含 `build-lib.ts` / `package.json` / `index.html` / `dev.md` / `src/**` / `public/**` / `packages-user/**` / `packages/**` / `vite.config.ts` / lockfile / `script/dev.ts`)。 + + ③ **content 逐文件夹规则(用户裁定 1):** 根目录文件照常(`.jsonc` 压缩、`item.ts` 原样);`defaults/` 全量输出并压缩(显式豁免清单过滤);`maps` / `texture` / `animation` / `audio` / `font` 只保留被各自 `.list.jsonc` 引用的文件,未引用剔除;`.list.jsonc` 自身始终压缩输出;无清单的子文件夹全量输出。匹配口径 = 去注释 `JSON.parse` 后递归收集**所有字符串**,按「完整文件名」或「去扩展名主文件名」精确匹配(`empty` → `empty.jsonc`、`test` → `test.anim`、`terrains0.png` → 同名文件)。 + + ④ **修饰器删除口径(用户裁定 2):** 构建时删除**所有** `@shouldReplay(...)` / `@ignoreReplay(...)` 应用(当前 `@shouldReplay` 共 **69 处应用 / 20 个文件**,`@ignoreReplay` 无应用),**数据端与渲染端都删**(`buildData` + `buildClient` 各注入一次);用 **TypeScript AST**(`ts.createSourceFile` + `ts.canHaveDecorators` / `ts.getDecorators`)取装饰器节点范围后**逆序切片删除**,字符串 / 注释中的同名文本不删;仅处理项目自身 `.ts` / `.tsx`,跳过 `node_modules`,不处理 `.vue`;**不手动删 import**(交给依赖分析 tree-shaking)。插件 `apply: 'build'`,故 `pnpm dev` 行为不变。 + + ⑤ **验证方式:** `script/build-plugin.test.ts` 扩展为「纯函数断言(删装饰器、字符串/注释不误删、带引号与括号的参数整段删除)+ **对照式**真实 `vite.build()`(同一 fixture 构建两次:不带插件的对照组产物必须含 `mota_replay_probe` 与 `shouldReplay` / `ignoreReplay` 标记,带插件的实验组必须不含这些标记且 `mota_replay_probe` 与方法名仍在)」;`script/build-resource.test.ts` 用临时 fixture 验证根文件、`defaults/` 全量、`maps` / `texture` / `font` 的保留与剔除、**无清单子文件夹全量**、`.list.jsonc` 压缩且剥离 `//` 注释、`item.ts` 原样;另加真实 `src/content` 的清单覆盖率检查(每个清单目录内非清单文件都必须被清单命中,防止过滤掉现有素材)、`build-game` 按函数切片的结构门禁(只断言导入绑定与注入位置,不绑定提议命名)、`build-resource.ts` 的「既有注释零删除」git diff 门禁,以及 `script/build-*.ts` 范围内的 `eslint`;命令一律 `cmd /c "pnpm exec ..."`,内联 `node -e` 只用单引号字面量。 + + ⑥ **已知边界(原样告知):** ① **不端到端运行全量 `build:game`**(渲染端仍有用户 WIP 的 stale import;09-21 已记录),验证只到单测 + 结构门禁 + 真实清单覆盖率;② 生产构建不再有录像安全检查装饰器,属预期结果;③ `index.html` 仍引用 `/styles.css`,本计划不产出、不改;④ `animation` / `audio` / `font` 目录当前无对应素材(清单为空或无文件),过滤不会影响现有产物。 + + ⑦ **待批准命名(须用户确认):** OI-1(`removeReplayDecorators` / `replayDecoratorPlugin` / `name: 'mota-replay-decorator'`);OI-2(`LIST_FILE_NAME` / `DEFAULTS_DIRECTORY` / `readListNames` / `collectListedNames` / `isListedFile`);OI-3(新增测试文件 `script/build-resource.test.ts`、扩展 `script/build-plugin.test.ts`)。未获批前不落码。**若您给出其它命名,我会先修订本计划(含门禁命令与测试用例)再重新汇报**——门禁按行为与导出形态断言,不随改名自动跟随。 + + ⑧ **不在本增量:** 不改 `src/**`(含改动数据端源码或手删 import)、不改 `packages-user/**`、不改 `script/dev.ts`、不改 `package.json` / `dev.md`、不新增依赖、不改构建顺序与产物落点、不做特殊失败处理。 + + 是否按本计划(09-22,最小范围)执行「任务 11 后续修复」——在 `script/build-plugin.ts` 新增基于 TypeScript AST 的 `@shouldReplay` / `@ignoreReplay` 删除插件并注入 `buildClient` 与 `buildData`(不手删 import)、在 `script/build-resource.ts` 增加「除 `defaults/` 外的素材子文件夹按各自 `.list.jsonc` 只保留被引用文件、`.list.jsonc` 自身压缩输出」的过滤、补两个测试文件与 SUMMARY;**仅改上述脚本与测试,其余文件一律不动**,并接受三项已知边界(不端到端跑全量 `build:game`、生产产物无录像安全检查、`/styles.css` 仍旧缺失)。 + + AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。执行期含写操作(3 个脚本 + 2 个测试 + 1 个 SUMMARY),无删除。`texture/.list.jsonc` 已覆盖 `texture/` 下全部 48 个 PNG,`maps/.list.jsonc` 覆盖全部 5 个地图,故本次过滤不会减少现有素材——这一点由 Task 2 的清单覆盖率门禁证明。`@shouldReplay` 应用共 **69 处**(20 个 `.ts` 文件:`packages-user/data-base`、`data-system`、`src/data/plugin/shop`;另有 1 处仅出现在 `packages-user/data-common/src/replay/func.ts` 的注释中,不算应用),`@ignoreReplay` 当前无实际应用(仅定义与注释),插件需同时支持两者。 + + + + + + 回复「可以执行」即可;如需调整命名或规则细节,请在回复中一并指明。 + + node -e "const fs=require('fs'),path=require('path');const bad=[];for(const p of ['script/build-plugin.ts','script/build-plugin.test.ts','script/build-game.ts','script/build-resource.ts','script/build-lib.ts','src/content/core.jsonc','src/content/texture/.list.jsonc','packages-user/data-common/src/replay/func.ts']){if(!fs.existsSync(p))bad.push('missing '+p)}const jc=fs.readdirSync('src/content',{recursive:true}).filter(f=>String(f).endsWith('.jsonc'));if(jc.length!==23)bad.push('expected 23 jsonc, got '+jc.length);for(const d of ['animation','audio','font','maps','texture']){if(!fs.existsSync(path.join('src','content',d,'.list.jsonc')))bad.push('missing list in '+d)}if(fs.readdirSync(path.join('src','content','defaults')).filter(f=>String(f).endsWith('.jsonc')).length!==8)bad.push('defaults must contain 8 jsonc');if(fs.existsSync('script/build-resource.test.ts'))bad.push('build-resource.test.ts must not exist yet');const p=fs.readFileSync('script/build-plugin.ts','utf8');if(p.indexOf('dataIsolationPlugin')<0)bad.push('dataIsolationPlugin missing from build-plugin.ts');const g=fs.readFileSync('script/build-game.ts','utf8');if(g.indexOf('dataIsolationPlugin')<0||g.indexOf('buildContent')<0)bad.push('build-game.ts is not the 09-21 implementation');const rf=fs.readFileSync('packages-user/data-common/src/replay/func.ts','utf8');if(rf.indexOf('export function shouldReplay')<0||rf.indexOf('export function ignoreReplay')<0)bad.push('replay decorator definitions missing');if(fs.readFileSync('src/content/texture/.list.jsonc','utf8').indexOf('// --- SYSTEM PREFIX END --- //')<0)bad.push('list jsonc marker changed');if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK prestate: 09-21 scripts in place, 23 jsonc, 5 list folders, defaults=8, decorator defs present, build-resource.test.ts absent')" + non-zero exit,或 stderr 含 "PRESTATE FAILED":必需文件缺失 / `.jsonc` 数量不是 23 / 某个素材子文件夹缺 `.list.jsonc` / `defaults/` 不是 8 个 jsonc / `script/build-resource.test.ts` 已存在 / `build-plugin.ts` 缺 `dataIsolationPlugin` / `build-game.ts` 已不是 09-21 实现 / 修饰器定义缺失 / `.list.jsonc` 的系统标记行已变化 + 用户明确回复「可以执行」;若就 OI-1 / OI-2 / OI-3 指定了与提议不同的命名,须先按新命名修订本计划(门禁命令、测试用例与 SUMMARY 关键字同步)并重新获批,未修订前不得执行 + + + - 用户回复中明确包含「可以执行」 + - 汇报涵盖:两条要解决的问题、最小范围与「其余文件一律不动」清单、content 逐文件夹规则与清单匹配口径、修饰器删除口径(AST + 双端注入 + 不手删 import + `apply: 'build'`)、验证方式、三项已知边界、OI-1 / OI-2 / OI-3 命名 + - 已说明:若用户改命名,须先修订本计划(门禁按行为/导出形态断言,不会自动跟随改名)再执行 + - 已说明:`processDirectory` 等既有 jsDoc 逐字保留,新逻辑说明只写在新函数 / 新常量上 + - 已说明:修饰器删除的验收依赖「带插件 vs 不带插件」的对照构建,不依赖 `@shouldReplay` 语法文本 + - 已说明生产构建不再有录像安全检查装饰器(属用户裁定 2 的预期结果) + - 已说明 `defaults/` 显式豁免过滤、`.list.jsonc` 自身始终压缩输出 + - 已说明本计划不端到端运行全量 `build:game` + + 用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何仓库文件、未派发任何执行子代理 + + + + Task 1: 修饰器删除插件(`script/build-plugin.ts`)+ 双端注入(`script/build-game.ts`)+ 测试 + Task 0 已闭环(用户回复「可以执行」,OI-1 已获批;若用户改了命名,本计划已按新命名修订并重新获批——门禁按导出形态与导入绑定断言,未修订不得执行)。`typescript` 与 `vitest` 可用;`script/build-plugin.ts` 仅含 `dataIsolationPlugin`;`script/build-game.ts` 为 09-21 实现(`buildClient` < `buildData` < `buildGame` 的定义顺序)。 + script/build-plugin.ts, script/build-plugin.test.ts, script/build-game.ts + + - .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-04;用户裁定 2) + - script/build-plugin.ts(既有 `SPECIFIER_BANS` / `PATH_BANS` / `findBan` / `dataIsolationPlugin` 风格) + - script/build-plugin.test.ts(既有风格:显式 `import { describe, expect, it } from 'vitest'`、`mkdtemp` fixture、每个 `it` 前一行注释) + - script/build-game.ts(`buildClient` / `buildData` 的 `plugins` 数组位置) + - packages-user/data-common/src/replay/func.ts(`shouldReplay(message)` / `ignoreReplay(_msg)` 的调用形态) + - packages-user/data-base/src/map/gameMap.ts(装饰器写在方法上方的实际形态,含双引号参数的用例) + - AGENTS.md(测试注释要求);dev.md(命名、jsDoc、模块无副作用、非空判断约定、限制 `?.`) + + + (1) **`script/build-plugin.ts` 追加(OI-1 获批后命名):** + - 顶部 `import` 区追加 `import ts from 'typescript';`(dev.md:`import` 必须集中在文件首部且中间不插注释/空行;不使用 `import type`)。 + - 模块内声明**不导出**的常量:目标装饰器名集合 `['shouldReplay', 'ignoreReplay']`(`readonly`)、源文件后缀判定(`.ts` / `.tsx`)、`node_modules` 路径判定(同时接受 `/` 与 `\\`)。 + - 导出纯函数 `removeReplayDecorators(code: string, fileName: string): string`: + - 用 `ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true, fileName.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS)` 建立 AST。 + - 深度遍历:对每个节点用 `ts.canHaveDecorators(node)` + `ts.getDecorators(node)` 取装饰器;仅当装饰器表达式是**标识符**(`@shouldReplay`)或**调用表达式**(`@shouldReplay('...')`,callee 为标识符)且标识符文本在目标集合内时,记录 `[decorator.getStart(sourceFile), decorator.getEnd()]`。 + - 按**起点降序**逐个用字符串切片删除,返回结果;无命中时原样返回 `code`(不要 `JSON` 化、不要重排缩进)。 + - 只在新函数上写 jsDoc:说明删除的是「装饰器应用」而非函数定义,且不触碰字符串与注释;`dataIsolationPlugin`、`SPECIFIER_BANS` / `PATH_BANS` / `findBan` 的既有注释**逐字保留**(AGENTS.md 禁改既有注释)。 + - 导出 `replayDecoratorPlugin(): Plugin`: + - 返回 `name: 'mota-replay-decorator'`、`enforce: 'pre'`、`apply: 'build'`、`transform(code, id)`。 + - `transform` 内:先剥离 `id` 的查询串(`id.split('?')[0]`);路径含 `node_modules` 段则返回 `null`;后缀不是 `.ts` / `.tsx` 返回 `null`;调用 `removeReplayDecorators`,结果与输入相同则返回 `null`,否则返回新代码(保持 `map: null` 语义)。 + - 保留既有 `dataIsolationPlugin` 全部行为不变(不动 `SPECIFIER_BANS` / `PATH_BANS` / `findBan`)。 + - 模块顶层零副作用;字面量非空判断用 `isNil`、对象用 `!`。 + + (2) **`script/build-game.ts` 注入(不改顺序与其余配置):** + - 追加 `import { replayDecoratorPlugin } from './build-plugin';`(与既有 `dataIsolationPlugin` 同一导入语句的具名列表)。 + - `buildClient`:`plugins: [replayDecoratorPlugin(), legacy({ ... })]` —— 插件置于 `legacy(...)` **之前**;其余字段(`outDir` / `copyPublicDir` / `emptyOutDir` / `minify` / `rollupOptions` / `manualChunks`)一字不动。 + - `buildData`:`plugins: [dataIsolationPlugin(), replayDecoratorPlugin()]` —— 隔离插件保持在首位;其余字段(`lib.formats` / `entryFileNames` / `chunkFileNames` / `assetFileNames` / `emptyOutDir`)一字不动。 + + (3) **`script/build-plugin.test.ts` 扩展:** 保留既有 `describe('dataIsolationPlugin')` 全部用例不改;新增 `describe` 覆盖(每个 `it` 前一行注释): + - **删 `@shouldReplay('...')`:** 输入含「装饰器 + 方法」的类片段,输出不再含该装饰器调用,且方法名与 `export` 结构仍在。 + - **删 `@ignoreReplay('...')`:** 同上。 + - **参数整段删除:** 装饰器参数含单引号 / 双引号 / 逗号 / 嵌套括号时整段被删,不残留括号碎片。 + - **字符串不误删:** 字符串字面量中出现同名文本(如日志文案)时保持原样。 + - **注释不误删:** 单行注释中出现同名文本时保持原样。 + - **`node_modules` 与非源文件跳过:** 取 `const plugin = replayDecoratorPlugin()` 后,把 `plugin.transform` 归一成可调用函数(函数直接用;对象则取其 `handler`),用 `id` 为 `/node_modules/dep/a.ts` 与 `/a.css` 各调用一次,断言返回值为 `null`(代码未被改写);再以 `/src/a.ts` 调用一次并断言装饰器被删。测试文件内的类型断言按需要书写,测试文件不受 dev.md 的类型约束。 + - **对照式真实 `vite.build()`(W3,否决空断言):** fixture = 「本地定义同形的 `shouldReplay` / `ignoreReplay` 函数 + 一个类,类方法上有 `@shouldReplay('...')` 装饰器,方法体返回唯一字面量 `mota_replay_probe`」。以库模式 ESM 构建**两次**(`configFile:false`、`write:false`、`minify:false`):**对照组** `plugins: []`,**实验组** `plugins: [replayDecoratorPlugin()]`。断言:对照组产物**必须**含 `shouldReplay` 与 `mota_replay_probe`(若对照缺失即用例自失败,证明 token 在未处理时确实存在);实验组产物**不得**含 `shouldReplay` / `ignoreReplay`,且**仍含** `mota_replay_probe` 与方法名。**不得**只断言「产物不含 `@shouldReplay` 语法」——编译产物本来就不会保留该语法,那样等于空断言。 + - 断言只依赖文本 / rejects / resolve 与产物代码关键字,不依赖 chunk 文件名或顺序。 + + + - `removeReplayDecorators` 对 `@shouldReplay('x')` + 方法的类片段 → 输出不含 `@shouldReplay`,方法声明保留。 + - 对 `@ignoreReplay('why')` → 输出不含 `@ignoreReplay`。 + - 对含嵌套括号 / 双引号参数的装饰器 → 整段(到匹配的右括号)被删,无残留 `)` 或字符串碎片。 + - 字符串字面量与注释中的同名文本 → 原样保留。 + - 插件对 `node_modules` 路径与非 `.ts` / `.tsx` 文件 → 不改动(返回 `null`)。 + - 对照构建(`plugins: []`)产物 → 含 `shouldReplay` 与 `mota_replay_probe`;带插件构建产物 → `shouldReplay` / `ignoreReplay` 消失、`mota_replay_probe` 与方法名仍在。 + + 仅新增两个导出、两处 `plugins` 数组注入与测试用例;`git checkout` 三个文件即整体回退,未改产物落点与构建顺序。 + + cmd /c "pnpm exec vitest run script/build-plugin.test.ts" + non-zero exit,或任一断言失败(装饰器未被删 / 参数残留碎片 / 字符串或注释中的同名文本被误删 / 对照产物缺 `shouldReplay` 或 `mota_replay_probe`(token 前提不成立)/ 带插件产物仍含 `shouldReplay` / `ignoreReplay` / 带插件产物丢了 `mota_replay_probe` 或方法名 / 既有 `dataIsolationPlugin` 用例回归) + node -e "const fs=require('fs');const bad=[];const q=String.fromCharCode(39);const t=fs.readFileSync('script/build-plugin.ts','utf8');const need=['ts.createSourceFile','canHaveDecorators','getDecorators','node_modules','shouldReplay','ignoreReplay','dataIsolationPlugin','transform(','code: string','fileName: string','): string','): Plugin','enforce: '+q+'pre'+q,'apply: '+q+'build'+q];for(const x of need){if(t.indexOf(x)<0)bad.push('build-plugin.ts missing: '+x)}const exports=[...t.matchAll(/export function (\w+)/g)].map(m=>m[1]);if(exports.length<3)bad.push('build-plugin.ts must export dataIsolationPlugin plus two strip APIs, found '+exports.length);if(exports.indexOf('dataIsolationPlugin')<0)bad.push('dataIsolationPlugin export lost');const tp=fs.readFileSync('script/build-plugin.test.ts','utf8');for(const x of ['@shouldReplay','@ignoreReplay','node_modules','mota_replay_probe','build(','not.toContain']){if(tp.indexOf(x)<0)bad.push('build-plugin.test.ts missing: '+x)}if(tp.indexOf('plugins: []')<0&&tp.indexOf('plugins:[]')<0)bad.push('test must include a control vite.build without the plugin');const g=fs.readFileSync('script/build-game.ts','utf8');const s=g.indexOf('async function buildClient('),d=g.indexOf('async function buildData('),b=g.indexOf('async function buildGame(');if(s<0||d<0||b<0||!(sv.trim()).filter(Boolean);const extra=bindings.filter(n=>n!=='dataIsolationPlugin');if(bindings.indexOf('dataIsolationPlugin')<0)bad.push('dataIsolationPlugin import lost');if(extra.length!==1)bad.push('exactly one new binding expected from ./build-plugin, got '+extra.length);else{const client=g.slice(s,d),data=g.slice(d,b);if(client.indexOf(extra[0])<0)bad.push('buildClient must inject the replay decorator plugin');if(data.indexOf(extra[0])<0)bad.push('buildData must inject the replay decorator plugin');if(data.indexOf('dataIsolationPlugin')<0)bad.push('buildData must keep dataIsolationPlugin');if(client.indexOf('dataIsolation'+'Plugin')>=0)bad.push('dataIsolationPlugin must stay out of buildClient')}}}if(bad.length>0){console.error('REPLAY PLUGIN GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK replay plugin: AST strip + dual-sided wiring')" + non-zero exit,或 stderr 含 "REPLAY PLUGIN GATE FAILED"(插件缺 AST 定位(`ts.createSourceFile` / `canHaveDecorators` / `getDecorators`)/ 缺 `enforce: 'pre'` 或 `apply: 'build'` / 缺 `node_modules` 跳过 / 导出数少于 3 或丢了 `dataIsolationPlugin` / 缺 `code: string` + `fileName: string` + `): string` 或 `): Plugin` 导出形态 / 测试缺关键用例字样 / 测试缺 `mota_replay_probe` 或缺少不带插件的对照构建(`plugins: []`)/ 函数定义顺序不满足切片前提 / `./build-plugin` 的导入绑定不是「`dataIsolationPlugin` + 恰好 1 个新插件」/ `buildClient` 或 `buildData` 片段缺该新插件 / `buildData` 丢了 `dataIsolationPlugin` / `dataIsolationPlugin` 被接进 `buildClient`) + + + - `script/build-plugin.ts` 新增 `removeReplayDecorators` 与 `replayDecoratorPlugin`(命名按 OI-1 获批结果),`enforce: 'pre'` + `apply: 'build'`,仅处理项目内 `.ts` / `.tsx`,跳过 `node_modules` + - 删除基于 TypeScript AST 的节点范围逆序切片;字符串、注释、非目标装饰器不受影响 + - `script/build-game.ts` 在 `buildClient` 与 `buildData` 各注入一次;`buildData` 仍保持 `dataIsolationPlugin` 首位;构建顺序与其余配置未改 + - 既有 `dataIsolationPlugin` 的行为与用例零回归 + - `script/build-plugin.test.ts` 覆盖「删两类装饰器、参数含引号/括号、字符串不误删、注释不误删、`node_modules` 跳过、**对照式** `vite.build()`(`plugins: []` 对照产物含 `shouldReplay` + `mota_replay_probe`;带插件产物两者皆无装饰器标记且 `mota_replay_probe` 仍在)」;每个 `it` 前一行注释 + - 既有注释未被改写:`dataIsolationPlugin` / `SPECIFIER_BANS` / `PATH_BANS` / `findBan` 的注释逐字保留;新增 jsDoc 只出现在新函数上 + - 未手删任何 import;未改 `packages-user/**` / `packages/**` / `src/**` / `script/dev.ts` + + 修饰器删除插件就位并双端注入:数据端与渲染端构建都不再编译 `@shouldReplay` / `@ignoreReplay` 应用(AST 精确切片、字符串与注释无损伤、`node_modules` 跳过),对照构建证明 token 在未处理时确实存在,开发服务行为不变;单测与结构门禁全绿,既有隔离插件零回归 + + + Task 2: content 清单过滤(`script/build-resource.ts`)+ 行为测试 + SUMMARY + Task 1 已完成(修饰器插件就位并双端注入);Task 0 已闭环(OI-2 / OI-3 已获批;若用户改了命名,本计划已按新命名修订并重新获批,未修订不得执行)。`src/content` 的 `animation` / `audio` / `font` / `maps` / `texture` 各有 `.list.jsonc`,`defaults/` 有 8 个 `.jsonc`、无清单;`script/build-resource.test.ts` 尚不存在。 + script/build-resource.ts, script/build-resource.test.ts, .planning/phases/09-minimum-browser-runnable/09-22-SUMMARY.md + + - .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-96;用户裁定 1) + - script/build-resource.ts(既有 `minifyJSONC` / `compressFont` / `processDirectory` / `buildContent`) + - src/content 下 5 个 `.list.jsonc`(四种形态:无后缀名数组、`{}`、完整文件名数组、`{ textures: [{ source }] }`) + - script/build-plugin.test.ts(fixture 风格:`mkdtemp` / `writeFile` / `afterEach` 清理) + - dev.md(命名、jsDoc、模块无副作用、非空判断约定);AGENTS.md(测试每个 `it` 前一行注释) + + + (1) **`script/build-resource.ts` 增加清单过滤(OI-2 获批后命名):** + - `import` 区把 `basename` 加入既有 `path` 具名导入(dev.md:`import` 集中在文件首部)。 + - 模块内新增**不导出**常量:`LIST_FILE_NAME = '.list.jsonc'`、`DEFAULTS_DIRECTORY = 'defaults'`。 + - 新增内部函数(均写 jsDoc,模块无顶层副作用): + - `readListNames(listFile: string): Promise>`:`readFile` 后用 `minifyJSONC` 去注释,再 `JSON.parse`,交给 `collectListedNames` 收集。 + - `collectListedNames(value: unknown, output: Set): void`:字符串 → `add`;数组 → 逐项递归;非空对象 → 遍历 `Object.values` 递归;其它(数字/布尔/null)忽略。 + - `isListedFile(name: string, names: Set): boolean`:`names.has(name)` 或 `names.has(name 去掉最后一个扩展名后的主名)`;只做这两种精确匹配。 + - **既有 jsDoc 逐字保留(AGENTS.md 禁改既有注释):** `processDirectory` / `minifyJSONC` / `compressFont` / `buildContent` 与接口成员上已有的 jsDoc 不得改写、不得删除、不得换行重排;「按清单过滤」的说明只写到上述新函数与 `LIST_FILE_NAME` / `DEFAULTS_DIRECTORY` 的 jsDoc 上。 + - 改造 `processDirectory(sourceDir, outputDir, chars, counter)`: + - 先 `readdir`;`const isDefaults = basename(sourceDir) === DEFAULTS_DIRECTORY;`;`const listEntry = entries.find(entry => entry.isFile() && entry.name === LIST_FILE_NAME);`;`const names = !isDefaults && listEntry ? await readListNames(join(sourceDir, listEntry.name)) : null;` + - 遍历时:目录 → 照旧递归;文件 → **`.list.jsonc` 自身永不被过滤**(先于过滤判断,走 `.jsonc` 压缩分支);若 `names` 存在且 `!isListedFile(entry.name, names)` → `continue`(不拷贝、不计入 `counter.files`);其余分支保持现状(`.jsonc` → `minifyJSONC`、字体扩展名 → `compressFont`、其它 → `copy`)。 + - **`defaults/` 与无 `.list.jsonc` 的目录行为不变**(全量输出)。 + - `minifyJSONC`、`buildContent`、`IContentBuildOptions`、`IContentBuildStats`、`IContentBuildCounter` 的名称与签名**一律不改**;不改字体分支的告警文案。 + - 保持 dev.md 约定:字面量非空判断用 `isNil`、对象用 `!`;尽量不用 `?.`。 + + (2) **新增 `script/build-resource.test.ts`(OI-3):** `mkdtemp` 建临时 fixture,`afterEach` 清理;每个 `it` 前一行注释: + - **根目录 jsonc:** `core.jsonc` 含 `// Type: ...` 与系统标记行 → 输出同名 `.jsonc`,`JSON.parse` 成功、文本不含 `//`。 + - **`defaults/` 全量:** `defaults/env.jsonc` 含注释且不被任何清单引用 → 仍输出且已压缩。 + - **`maps/` 清单(fixture 清单原文带 `//` 注释):** `.list.jsonc` 写成「`// Type: string[], without .jsonc suffix` + `// --- SYSTEM PREFIX END --- //` + `["keep"]`」三部分,同目录放 `keep.jsonc` 与 `drop.jsonc` → `keep.jsonc` 输出、`drop.jsonc` **不存在**、`.list.jsonc` 输出且 `JSON.parse` 成功、输出文本**不含 `//`**(注释与系统标记行被剥离)。 + - **`texture/` 清单(对象形态 + 完整文件名):** `.list.jsonc` = `{"textures":[{"type":"grid","source":"a.png"}]}` + `a.png` + `b.png` → `a.png` 存在且与源字节一致、`b.png` 不存在。 + - **`font/` 清单:** `.list.jsonc` = `["ok.fake"]` + `ok.fake` + `skip.fake` → `ok.fake` 存在、`skip.fake` 不存在(用非字体扩展名避开 fontmin)。 + - **无清单的非 `defaults` 子文件夹:** `extra/`(无 `.list.jsonc`,含 `free.jsonc` 与 `free.txt`)→ 两者都输出、`free.jsonc` 已压缩(证明「无清单 = 全量」,不是「一律按清单过滤」)。 + - **根目录非 jsonc:** `item.ts` 原样拷贝(内容逐字相等)。 + - **统计:** 返回值 `files` 等于输出目录实际文件数、`jsonc` 等于压缩的 `.jsonc` 数(含 `.list.jsonc`)、`fonts` 为 0。 + - 测试用 `chars: ''`;不触碰仓库 `src/content`。 + + (3) **`09-22-SUMMARY.md`:** 记录范围(只动构建脚本与测试)、content 逐文件夹规则与清单匹配口径、修饰器删除口径(TypeScript AST + 双端注入 + 不手删 import + `apply: 'build'`)、命名获批情形、门禁结果(两个单测 + 对照式 `vite.build()`(含对照组含标记的证据)+ 结构门禁 + 既有注释零删除 + 真实 `src/content` 清单覆盖率 + 范围内的 `eslint`)、`open_items` 处置,并**明确记录已知边界**:未端到端运行全量 `build:game`(渲染端仍有 stale import)、生产构建不再有录像安全检查、`index.html` 仍引用 `/styles.css`。需出现关键字:`defaults`、`.list.jsonc`、`node_modules`、`build:game`、`D-96`、`shouldReplay`。 + + + - 根 `.jsonc` → 输出纯 JSON,后缀仍 `.jsonc`。 + - `defaults/*.jsonc` → 即使无清单引用也输出并压缩。 + - 有 `.list.jsonc` 的目录 → 清单命中(完整名或去扩展名主名)的文件保留,未命中文件不出现在输出。 + - `.list.jsonc` 自身 → 始终输出且已压缩。 + - 无 `.list.jsonc` 的非 `defaults` 子文件夹与根目录 → 全量输出(`.jsonc` 压缩、其它原样)。 + - fixture 的 `.list.jsonc` 原文含 `//` 注释与系统标记行 → 输出可 `JSON.parse` 且不含 `//`。 + - 统计值 → 与实际输出文件数一致。 + + `build-resource.ts` 为单文件改造 + 新增一个测试文件;`git checkout` / 删除测试文件即回退,产物落点与构建顺序不变。 + + cmd /c "pnpm exec vitest run script/build-resource.test.ts" + non-zero exit,或任一断言失败(`defaults/` 被过滤 / 清单引用文件被剔除 / 未引用文件仍被输出 / `.list.jsonc` 被剔除或未压缩 / 根 `.jsonc` 未压成纯 JSON / `item.ts` 拷贝内容不一致 / 统计值与实际输出不一致) + node -e "const fs=require('fs'),cp=require('child_process');const bad=[];const t=fs.readFileSync('script/build-resource.ts','utf8');for(const x of ['.list.jsonc','defaults','basename','JSON.parse','minifyJSONC','buildContent','Fontmin','chars: string','IContentBuildOptions','IContentBuildStats']){if(t.indexOf(x)<0)bad.push('build-resource.ts missing: '+x)}try{const diff=cp.execSync('git diff -U0 -- script/build-resource.ts',{encoding:'utf8'});const removed=diff.split('\n').filter(l=>l.slice(0,1)==='-'&&l.slice(0,3)!=='---');const comments=removed.filter(l=>l.indexOf('*')>=0?true:l.trim().slice(0,2)==='//');if(comments.length>0)bad.push('existing comments/jsDoc of build-resource.ts must stay untouched, removed comment lines: '+comments.length)}catch(e){bad.push('git diff check failed: '+e.message)}if(!fs.existsSync('script/build-resource.test.ts'))bad.push('script/build-resource.test.ts missing');const sp='.planning/phases/09-minimum-browser-runnable/09-22-SUMMARY.md';if(!fs.existsSync(sp))bad.push('09-22-SUMMARY.md missing');else{const sm=fs.readFileSync(sp,'utf8');for(const tok of ['defaults','.list.jsonc','node_modules','build:game','D-96','shouldReplay']){if(sm.indexOf(tok)<0)bad.push('SUMMARY must record token: '+tok)}}if(bad.length>0){console.error('CONTENT FILTER GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK content filter: list retention + jsDoc untouched + SUMMARY tokens')" + non-zero exit,或 stderr 含 "CONTENT FILTER GATE FAILED"(`build-resource.ts` 缺 `.list.jsonc` / `defaults` / `basename` / `JSON.parse` 等标记 / 既有导出改成别名或丢失(`minifyJSONC` / `buildContent` / `IContentBuildOptions` / `IContentBuildStats`)/ `chars: string` 丢失 / **git diff 显示既有注释或 jsDoc 被删除或改写** / 缺测试文件 / 缺 SUMMARY 或 SUMMARY 未记录 `defaults`、`.list.jsonc`、`node_modules`、`build:game`、`D-96`、`shouldReplay`) + node -e "const fs=require('fs'),path=require('path');const bad=[];function names(f){const t=fs.readFileSync(f,'utf8').split(/\r?\n/).filter(l=>!l.trim().startsWith('//')).join('\n');const set=new Set();(function walk(v){if(typeof v==='string')set.add(v);else if(Array.isArray(v))v.forEach(walk);else if(v&&typeof v==='object')Object.values(v).forEach(walk)})(JSON.parse(t));return set}for(const d of ['animation','audio','font','maps','texture']){const dir=path.join('src','content',d);const list=path.join(dir,'.list.jsonc');if(!fs.existsSync(list)){bad.push('missing '+list);continue}const set=names(list);for(const e of fs.readdirSync(dir,{withFileTypes:true})){if(e.isDirectory()||e.name==='.list.jsonc')continue;const stem=e.name.slice(0,e.name.length-path.extname(e.name).length);if(!set.has(e.name)&&!set.has(stem))bad.push('unlisted asset would be dropped: '+path.join(dir,e.name))}}if(bad.length>0){console.error('CONTENT COVERAGE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK content coverage: every existing asset under the 5 list folders is referenced by its list')" + non-zero exit,或 stderr 含 "CONTENT COVERAGE GATE FAILED":某个素材子文件夹缺 `.list.jsonc`,或该目录内存在既未被完整文件名、也未被去扩展名主文件名命中的现存素材(真实施加过滤后会丢文件) + cmd /c "pnpm exec eslint script/build-plugin.ts script/build-resource.ts script/build-game.ts script/build-plugin.test.ts script/build-resource.test.ts" + non-zero exit:五个改动文件在 `eslint` 下报错(dev.md 要求 eslint 零报错;当前被测的四个既有文件实测为 0 报告,新增代码须维持) + + + - `script/build-resource.ts` 按用户裁定 1 实现:根目录照常、`defaults/` 全量、有清单的子文件夹只保留被引用文件、`.list.jsonc` 自身始终压缩输出、无清单目录全量 + - 清单解析走 `minifyJSONC`(去注释)+ `JSON.parse` + 递归收集字符串;匹配仅「完整文件名」与「去扩展名主文件名」两种精确方式 + - 既有导出名称与签名(`minifyJSONC` / `buildContent` / `IContentBuildOptions`(`chars: string`)/ `IContentBuildStats` / `IContentBuildCounter`)未改;字体分支与告警文案未改 + - `script/build-resource.test.ts` 覆盖 8 类用例(根 jsonc、`defaults` 全量、maps 清单(fixture 清单原文含 `//` 注释且输出已剥离)、texture 对象清单、font 清单、无清单子文件夹全量、`item.ts` 原样、统计),每个 `it` 前一行注释 + - `processDirectory` / `minifyJSONC` / `compressFont` / `buildContent` 与接口成员的既有 jsDoc **逐字未改**(由 git diff 门禁证明),过滤说明只写在新函数 / 新常量上 + - 五个改动文件通过范围内的 `eslint`(`pnpm exec eslint` 零报告) + - 真实 `src/content` 清单覆盖率门禁通过(现有素材无一被过滤掉) + - `09-22-SUMMARY.md` 已写并含 6 个必需关键字与三项已知边界 + - 未改 `packages-user/**` / `packages/**` / `src/**` / `package.json` / `index.html` / `dev.md` / `vite.config.ts` / lockfile / `script/dev.ts` / `script/build-lib.ts` + + content 构建按清单过滤:`defaults/` 与无清单目录全量输出、5 个素材子文件夹只保留被引用文件、`.list.jsonc` 自身压缩输出、jsonc 仍压成纯 JSON;行为测试、注释零删除与真实清单覆盖率门禁全绿;范围内的 `eslint` 零报告;SUMMARY 记录两处修复与三项已知边界 + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| `src/content` → `dist/content` | 构建期读取素材并在清单过滤后写入产物;过滤错误会静默丢掉运行期需要的素材 | +| `.list.jsonc` 原文 → 解析 | 去注释后 `JSON.parse`;清单格式非法将在构建期暴露 | +| 项目源码 → 构建产物 | 装饰器删除插件改写 `.ts` / `.tsx` 源码文本;误伤会破坏语义或误删字符串/注释内容 | +| 构建期 ↔ 开发期 | 插件必须只在 `build` 生效,开发服务需保留装饰器 | +| 写入边界 | 只允许 `files_modified` 白名单(4 个脚本 + 2 个测试 + 1 个 SUMMARY) | +| 依赖边界 | 不新增依赖、不改 `package.json` 与 lockfile(`typescript` 已是 devDependency) | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-09-22-01 | Tampering | `processDirectory` 的清单过滤误删运行期素材 | high | mitigate | 匹配限定「完整文件名 / 去扩展名主文件名」两种精确方式;fixture 行为测试断言保留与剔除两侧;另加真实 `src/content` 清单覆盖率门禁(现存素材必须全部被清单命中) | +| T-09-22-02 | Tampering | `.list.jsonc` 自身被过滤或未压缩 | high | mitigate | `.list.jsonc` 判断先于过滤且走 `minifyJSONC` 分支;structure 门禁要求 `LIST_FILE_NAME` 与压缩分支;fixture 断言 `.list.jsonc` 输出且可 `JSON.parse` | +| T-09-22-03 | Tampering | 装饰器删除误伤字符串 / 注释 / 非目标装饰器 | high | mitigate | 用 TypeScript AST 取装饰器节点范围后逆序切片,不做正则扫描;单测覆盖「字符串不误删」「注释不误删」「参数含引号与括号」 | +| T-09-22-04 | Tampering | 装饰器删除进入开发服务(`pnpm dev` 行为漂移) | medium | mitigate | 插件固定 `apply: 'build'`;`script/dev.ts` 不在写入范围;结构门禁断言该字段 | +| T-09-22-05 | Tampering | 插件改写 `node_modules` 内第三方源码 | medium | mitigate | `transform` 先剥离查询串再判 `node_modules` 段与 `.ts` / `.tsx` 后缀;单测覆盖跳过分支 | +| T-09-22-06 | Tampering | 过滤逻辑未真正接线(仍全量输出) | medium | mitigate | fixture 断言未引用文件**不存在**于输出;structure 门禁要求 `isListedFile` / `readListNames` 被使用 | +| T-09-22-07 | Tampering | 越界写入 / 顺手改源码(含手删 import) | medium | mitigate | `files_modified` 白名单 + prohibitions(`src/**`、`packages-user/**`、`packages/**`、`script/dev.ts` 等一律不动;不得手删 import) | +| T-09-22-08 | Tampering | 未经批准使用新命名 | medium | mitigate | Task 0 报备 OI-1 / OI-2 / OI-3;prohibitions 明列未获批前不得落码 | +| T-09-22-09 | Repudiation | 以「构建通过」掩盖未端到端运行的事实 | medium | mitigate | SUMMARY 必须含 `build:game` 字样并写明未端到端运行;structure 门禁校验该关键字 | +| T-09-22-SC | Tampering | npm/pip/cargo installs | high | mitigate | 不新增依赖、不改 `package.json` 依赖面与 lockfile;如需新增依赖须回 Task 0 报备获批 | + + + +- **Task 0:** 预执行状态(09-21 构建脚本为当前实现 / 23 个 jsonc / 5 个清单目录 / `defaults` 8 个 jsonc / 修饰器定义存在 / `script/build-resource.test.ts` 不存在)检查;人工关卡(用户回复「可以执行」;OI-1 / OI-2 / OI-3 若被改名须先修订计划再获批);汇报须含三项已知边界与「既有 jsDoc 逐字保留」的说明。 +- **Task 1:** `script/build-plugin.ts` 结构(**按导出形态断言**:两个新导出 + `dataIsolationPlugin` 保留 / AST 定位标记 / `enforce: 'pre'` / `apply: 'build'` / `node_modules` 跳过,不硬编码提议命名);`build-game` **按函数切片**断言(从 `./build-plugin` 的导入绑定推导新插件名——`dataIsolationPlugin` + 恰好 1 个新绑定,`buildClient` 与 `buildData` 各含该绑定、`buildData` 仍含隔离插件、隔离插件不进 `buildClient`、定义顺序未变);`script/build-plugin.test.ts` 行为(删两类装饰器、参数含引号括号、字符串与注释不误删、`node_modules` 跳过、**对照式** `vite.build()`:`plugins: []` 对照产物含 `shouldReplay` + `mota_replay_probe`,带插件产物不含装饰器标记且 `mota_replay_probe` 仍在)。 +- **Task 2:** `script/build-resource.ts` 结构(`.list.jsonc` / `defaults` / `basename` / `JSON.parse` 标记、既有导出与 `chars: string` 未变、**git diff 证明既有注释零删除**);`script/build-resource.test.ts` 行为(根 jsonc、`defaults` 全量、maps 清单保留与剔除 + 清单输出已剥离 `//`、texture 对象清单、font 清单、无清单子文件夹全量、`item.ts` 原样、统计);真实 `src/content` **清单覆盖率**(5 个清单目录内现存素材必须全部被命中);`eslint`(五个改动文件零报告);SUMMARY 关键字。 +- **一致性:** `dist/content/**` 落点与 `src/data/constant.ts` 的 `content/core.jsonc` 一致;`build-game.ts` 的插件导入路径 `./build-plugin` 与 Task 1 产物一致;`script/build-resource.ts` 被 `build-game.ts` 以 `buildContent` / `IContentBuildOptions` 原名调用。 +- **无全仓门禁:** 不使用 `pnpm check:type` / `pnpm build` / 全量 `pnpm test:ci`(用户 WIP 存在历史类型错误);本计划**不端到端运行全量** `build:game`。 +- **人工复核:** 结论(含三项已知边界)写入 `09-22-SUMMARY.md`。 + + + +- **用户裁定 1(content):** `src/content` → `dist/content`;根目录照常(`.jsonc` 压缩、`item.ts` 原样);`defaults/` 全量输出并压缩;`maps` / `texture` / `animation` / `audio` / `font` 按各自 `.list.jsonc` 只保留被引用文件;`.list.jsonc` 自身压缩输出;所有 jsonc 去净 `//` 注释压成纯 JSON、后缀仍 `.jsonc`;fontmin 保留;不分块、不 zip。 +- **用户裁定 2(修饰器):** 构建时删除所有 `@shouldReplay` / `@ignoreReplay` 应用,数据端与渲染端都删;删除基于 TypeScript AST、不误伤字符串与注释、跳过 `node_modules`;**未手删任何 import**。 +- **用户裁定 3 / 4(边界):** 只改构建脚本与其测试;未改 `build-lib.ts` / `package.json` / `index.html` / `dev.md` / `src/**` / `public/**` / `packages-user/**` / `packages/**` / `vite.config.ts` / lockfile / `script/dev.ts`;未改构建顺序;无特殊失败处理。 +- **D-93 / D-96 / D-98:** 任务 11 的构建脚本继续承担 content 处理;`build-game` 仍是唯一编排入口;未回退 09-21 已落地的数据端 ESM 产物与隔离检测。 +- **验证:** 两个测试文件与六个门禁(插件结构、content 结构 + 注释零删除、真实清单覆盖率、`eslint`、SUMMARY 关键字,以及对照式 `vite.build()` 的双向断言)全部跑到确定结论;**未端到端运行全量 `build:game` 的事实已写入 SUMMARY**。 +- **注释纪律:** 既有 jsDoc / 注释逐字保留;新增说明只出现在新函数与新常量上(`build-resource.ts` 由 git diff 门禁证明)。 +- **已知边界:** 生产产物不再产生录像安全检查信息(预期);`index.html` 仍引用 `/styles.css`(09-21 已记录);`animation` / `audio` / `font` 当前无素材,其过滤规则由测试覆盖。 + + + +Create `.planning/phases/09-minimum-browser-runnable/09-22-SUMMARY.md` when done +