docs(09-22): 测试文件统一移到 script/__test__/(第 6 轮修订)

This commit is contained in:
unanmed 2026-10-21 15:25:45 +08:00
parent 4bd1665a78
commit 23202cbcb7

View File

@ -6,10 +6,10 @@ wave: 19
depends_on: [09-21]
files_modified:
- script/build-plugin.ts
- script/build-plugin.test.ts
- script/__test__/build-plugin.test.ts
- script/build-game.ts
- script/build-resource.ts
- script/build-resource.test.ts
- script/__test__/build-resource.test.ts
- .planning/phases/09-minimum-browser-runnable/09-22-SUMMARY.md
autonomous: false
requirements: []
@ -20,7 +20,7 @@ estimate:
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)、验证方式(含对照式双装饰器构建与加固后的 jsDoc 门禁)、已知边界与待批准命名(OI-1 / OI-2 / OI-3),然后停下等待「可以执行」。"
- "**本计划第一个 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/__test__/build-plugin.test.ts` 扩展 + 新增 `script/__test__/build-resource.test.ts`)、逐文件夹的 content 过滤规则(含根目录显式跳过过滤)、修饰器删除口径(TS AST 精确切片、不删 import)、验证方式(含对照式双装饰器构建与加固后的 jsDoc 门禁)、已知边界与待批准命名(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` 的子文件夹全量输出。"
- "**根目录显式跳过清单过滤(INFO-5):** `buildContent` 调用 `processDirectory` 时必须传根标记(`isRoot` / `filter: false`),**根目录本身永不按清单取舍**——根目录全量输出不依赖「当前根目录没有 `.list.jsonc`」这一现状,也不依赖将来是否出现根 `.list.jsonc`;只有**子目录**才做 `.list.jsonc` 检测。"
- "**清单匹配口径(用户裁定 1):** 解析 `.list.jsonc`(去注释后 `JSON.parse`)后递归收集**所有字符串值**,以「**完整文件名**」或「**去扩展名主文件名**」匹配保留(如 `terrains0.png` 命中完整名、`empty` 命中 `empty.jsonc`、`test` 命中 `test.anim`)。"
@ -29,20 +29,20 @@ must_haves:
- "**删除方式必须可靠(用户裁定 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`。"
- "**测试是必需产物,且统一放在 `script/__test__/`(用户裁定):** 扩展 `script/__test__/build-plugin.test.ts`——由现有 `script/build-plugin.test.ts` **移动**而来(`script/build-plugin.test.ts` 不得保留),对被测模块的导入改为 `../build-plugin`,新增 `describe`、不改动既有 `dataIsolationPlugin` 用例;新增 `script/__test__/build-resource.test.ts`(导入 `../build-resource`);主 `vitest` 配置未设 `include`,默认 `**/*.test.ts` 会收集该目录,故 `vitest run` 与 `eslint` 命令一律指向新路径;每个 `it` 前一行注释(AGENTS.md);content 测试用临时 fixture 目录,不触碰 `src/content`。"
- "**验证可运行且范围诚实(Nyquist):** 修饰器删除用「纯函数文本断言 + **对照式双装饰器**真实 `vite.build()`」双证——同一 fixture 必须**导出一个类**、两个方法分别带 `@shouldReplay('...')` 与 `@ignoreReplay('...')`、各自返回**不同**的唯一字面量(`mota_replay_probe` / `mota_ignore_probe`),且 `shouldReplay` / `ignoreReplay` **工厂函数只在 fixture 内本地定义、不得导出**(否则实验组也会保留名字,对照失效)。**对照构建** `plugins: []` 产物必须 `toContain('shouldReplay')`、`toContain('ignoreReplay')`、`toContain('mota_replay_probe')`、`toContain('mota_ignore_probe')` 且两个方法名都在;**实验构建**(带插件)必须 `not.toContain('shouldReplay')`、`not.toContain('ignoreReplay')`、`toContain('mota_replay_probe')`、`toContain('mota_ignore_probe')` 且两个方法名都在(缺任一侧即成空断言);另含字符串/注释不误删、`node_modules` 跳过。content 过滤以 fixture 行为测试证明(`defaults` 全量(即使 `defaults/` 内出现 `.list.jsonc`)、清单引用保留、未引用剔除、无清单子文件夹全量、**根目录即使出现 `.list.jsonc` 也全量**、`.list.jsonc` 压缩去注释、jsonc 去注释)。另加真实 `src/content` 的「清单覆盖率」检查、按函数切片的结构门禁、`build-resource.ts` 的「既有注释零删除」门禁放在 **Task 2**、`build-plugin.ts` 的同口径门禁放在 **Task 1**(两者都与 **Task 0 一次性捕获、写在仓库外 `os.tmpdir()/gsd-09-22-jsdoc-baseline.json` 的注释行集合**逐行比对——**与 git 提交无关、可重复执行**,`gsd-executor` 的逐 task 提交不会让基线漂移;门禁另校验基线注释行数下限与「基线不得已含新标识 `replayDecoratorPlugin` / `removeReplayDecorators`」),以及 `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'`,以及新增的模块级常量 `REPLAY_DECORATORS`(目标装饰器名集合)/ `SCRIPT_EXTENSIONS`(`.ts` / `.tsx` 后缀集合)/ `NODE_MODULES_SEGMENT`(`node_modules` 路径判定))、OI-2(`LIST_FILE_NAME` / `DEFAULTS_DIRECTORY` / `readListNames` / `collectListedNames` / `isListedFile` / `processDirectory` 新增的 `isRoot` 参数)、OI-3(`script/build-resource.test.ts` 文件名)。未获批前不得落码。**若用户在 Task 0 指定了与提议不同的命名,必须先按新命名修订本计划(门禁命令、测试用例与 SUMMARY 关键字同步)并重新获批后执行**——门禁本身按行为、值标记与导出形态断言,不会自动跟随改名。"
- "**命名须先获批(D-04):** OI-1(`removeReplayDecorators` / `replayDecoratorPlugin` / `name: 'mota-replay-decorator'`,以及新增的模块级常量 `REPLAY_DECORATORS`(目标装饰器名集合)/ `SCRIPT_EXTENSIONS`(`.ts` / `.tsx` 后缀集合)/ `NODE_MODULES_SEGMENT`(`node_modules` 路径判定))、OI-2(`LIST_FILE_NAME` / `DEFAULTS_DIRECTORY` / `readListNames` / `collectListedNames` / `isListedFile` / `processDirectory` 新增的 `isRoot` 参数)、OI-3(测试文件位置与文件名:`script/__test__/build-plugin.test.ts`(由 `script/build-plugin.test.ts` 移动)、`script/__test__/build-resource.test.ts`)。未获批前不得落码。**若用户在 Task 0 指定了与提议不同的命名,必须先按新命名修订本计划(门禁命令、测试用例与 SUMMARY 关键字同步)并重新获批后执行**——门禁本身按行为、值标记与导出形态断言,不会自动跟随改名。"
artifacts:
- path: script/build-plugin.ts
provides: "在既有 `dataIsolationPlugin` 之外新增:模块级常量 `REPLAY_DECORATORS` / `SCRIPT_EXTENSIONS` / `NODE_MODULES_SEGMENT`(均为内部常量、不导出)、`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:**导出的类**上两个方法分别带 `@shouldReplay` / `@ignoreReplay` 并各自返回 `mota_replay_probe` / `mota_ignore_probe`,两个工厂函数在 fixture 内**本地定义且不导出**;**不带插件**的对照产物必须含 `shouldReplay` / `ignoreReplay` / 两个唯一字面量 / 两个方法名;带插件产物必须 **不含** `shouldReplay` 与 `ignoreReplay`,且两个唯一字面量与两个方法名**仍在**);每个 `it` 前一行注释"
- path: script/__test__/build-plugin.test.ts
provides: "既有测试**移动至 `script/__test__/`(源 `script/build-plugin.test.ts` 不保留、导入改 `../build-plugin`)后扩展**:新增 `replayDecoratorPlugin` / `removeReplayDecorators` 的 `describe`——纯函数断言(删装饰器、保留字符串与注释中的同名文本、参数含引号/括号整段删除)+ **对照式双装饰器**真实 `vite.build()`(同一 fixture:**导出的类**上两个方法分别带 `@shouldReplay` / `@ignoreReplay` 并各自返回 `mota_replay_probe` / `mota_ignore_probe`,两个工厂函数在 fixture 内**本地定义且不导出**;**不带插件**的对照产物必须含 `shouldReplay` / `ignoreReplay` / 两个唯一字面量 / 两个方法名;带插件产物必须 **不含** `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` 增加 `isRoot` 参数(`buildContent` 传根标记,**根目录永不按清单取舍**);有清单的目录内按清单保留文件、`defaults` 与无清单目录全量输出、`.list.jsonc` 始终压缩输出(命名待批准,OI-2)"
- path: script/build-resource.test.ts
provides: "**新增**行为测试:临时 fixture 验证根文件、**根目录即使存在 `.list.jsonc` 也全量输出**、`defaults/` 全量(**fixture 内 `defaults/.list.jsonc` 只列 `env`,`env.jsonc` 与 `other.jsonc` 仍都输出**,镜像根目录用例)、`maps/` 与 `texture/` 与 `font/` 的清单保留/剔除、**无清单子文件夹全量**、`.list.jsonc` 压缩且剥离整行 `//` 注释(fixture 清单原文带注释)、`item.ts` 原样拷贝;每个 `it` 前一行注释(文件名待批准,OI-3)"
- path: script/__test__/build-resource.test.ts
provides: "**新增**行为测试(位于 `script/__test__/`,被测模块以 `../build-resource` 导入):临时 fixture 验证根文件、**根目录即使存在 `.list.jsonc` 也全量输出**、`defaults/` 全量(**fixture 内 `defaults/.list.jsonc` 只列 `env`,`env.jsonc` 与 `other.jsonc` 仍都输出**,镜像根目录用例)、`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)、命名获批情形、门禁结果(含与 Task 0 捕获基线 `os.tmpdir()/gsd-09-22-jsdoc-baseline.json` 比对的注释零丢失,及该基线的 40 位 `head` SHA)、`estimate.tokens=46000` 为**未校准**估算(`confidence: low`)的声明、`open_items` 处置、已知边界(未端到端跑全量 `build:game`;生产产物不再做录像安全检查;`index.html` 仍引用 `/styles.css`)"
key_links:
@ -67,7 +67,7 @@ must_haves:
- "**行号会漂移:** `build-game.ts` 的门禁按**函数名切片**(`buildClient` < `buildData` < `buildGame` 的定义顺序)而非固定行号。"
prohibitions:
- "不得在用户回复通过 Task 0 关卡之前执行任何任务或派发执行子代理。"
- "**未经批准的命名一律不得使用:** `removeReplayDecorators` / `replayDecoratorPlugin` / `name: 'mota-replay-decorator'` / 新增常量 `REPLAY_DECORATORS` / `SCRIPT_EXTENSIONS` / `NODE_MODULES_SEGMENT`(OI-1)、`LIST_FILE_NAME` / `DEFAULTS_DIRECTORY` / `readListNames` / `collectListedNames` / `isListedFile` / `isRoot` 参数(OI-2)、`script/build-resource.test.ts`(OI-3)。**若用户指定了其它命名,必须先用新命名修订本计划(含门禁命令与测试用例)并重新获批**,不得沿用旧名跑门禁、也不得只改代码不改计划。"
- "**未经批准的命名一律不得使用:** `removeReplayDecorators` / `replayDecoratorPlugin` / `name: 'mota-replay-decorator'` / 新增常量 `REPLAY_DECORATORS` / `SCRIPT_EXTENSIONS` / `NODE_MODULES_SEGMENT`(OI-1)、`LIST_FILE_NAME` / `DEFAULTS_DIRECTORY` / `readListNames` / `collectListedNames` / `isListedFile` / `isRoot` 参数(OI-2)、`script/__test__/build-resource.test.ts`(OI-3)。**若用户指定了其它命名,必须先用新命名修订本计划(含门禁命令与测试用例)并重新获批**,不得沿用旧名跑门禁、也不得只改代码不改计划。"
- "**不得手动删除源码中的 import:** 不得改 `packages-user/**`、`packages/**`、`src/**` 里的任何 import 语句(用户裁定 2 / 3)。"
- "**不得让 `defaults/` 参与清单过滤:** 不得排除、不得按清单取舍、不得跳过压缩。"
- "**不得把清单匹配做成模糊匹配:** 只允许「完整文件名」或「去扩展名主文件名」两种精确匹配;不得用包含/前缀/正则通配去保留文件。"
@ -90,7 +90,7 @@ must_haves:
open_items:
- "**OI-1(修饰器删除插件命名):** 提议导出 `removeReplayDecorators(code: string, fileName: string): string` 与 `replayDecoratorPlugin(): Plugin`,插件 `name: 'mota-replay-decorator'`;并新增**不导出**的模块级常量 `REPLAY_DECORATORS`(目标装饰器名集合 `['shouldReplay', 'ignoreReplay']`)、`SCRIPT_EXTENSIONS`(`['.ts', '.tsx']`)、`NODE_MODULES_SEGMENT`(`node_modules` 路径判定正则)。备选:`stripReplayDecorators` / `removeReplayDecoratorPlugin`、`name: 'mota-replay-decorator-strip'`;常量备选 `DECORATOR_NAMES` / `TS_EXTENSIONS` / `NODE_MODULES_PATTERN`,或**全部改为函数内内联字面量、不新增模块级常量**。**待批准**(D-04)。门禁只断言值标记与导出形态,不绑定这些常量名。"
- "**OI-2(content 过滤内部命名):** 提议模块内常量 `LIST_FILE_NAME`(`.list.jsonc`)、`DEFAULTS_DIRECTORY`(`defaults`),内部函数 `readListNames(listFile: string): Promise<Set<string>>`、`collectListedNames(value: unknown, output: Set<string>): void`、`isListedFile(name: string, names: Set<string>): boolean`,以及 `processDirectory` 新增的 `isRoot: boolean = false` 参数(`buildContent` 传 `true`)。备选:`LIST_NAME` / `LISTED_NAME_CACHE`、`loadListNames` / `pickStrings` / `keepFile`、`isRootCall` / `filter`。**待批准**。既有 `buildContent` / `minifyJSONC` / `IContentBuildOptions` / `IContentBuildStats` 与 `IContentBuildCounter` **不改名**。"
- "**OI-3(测试文件):** 扩展既有 `script/build-plugin.test.ts`;新增 `script/build-resource.test.ts`(与被测文件同名,测试文件按 AGENTS.md 由 AI 维护)。**报备**(如需改名请一并指明)。"
- "**OI-3(测试文件):** 扩展既有 `script/__test__/build-plugin.test.ts`;新增 `script/__test__/build-resource.test.ts`(与被测文件同名,测试文件按 AGENTS.md 由 AI 维护)。**报备**(如需改名请一并指明)。"
- "**已知边界(不在本计划):** ① 未端到端运行全量 `build:game`(渲染端仍有用户 WIP 的 stale import),验证只到单测 + 结构门禁 + 真实 content 清单覆盖率;② 生产构建删掉录像安全检查装饰器属用户裁定 2 的预期结果,运行期不再产生 `@shouldReplay` 的收集信息;③ `index.html` 仍引用 `/styles.css`(09-21 已记录),本计划不改。"
---
<objective>
@ -104,7 +104,7 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
- `script/build-plugin.ts`:在既有 `dataIsolationPlugin` 之外新增一个修饰器删除插件(TS AST 定位 + 逆序切片)与其纯函数。
- `script/build-game.ts`:`buildClient` 与 `buildData` 各注入该插件;其余配置、顺序、产物落点不变。
- `script/build-resource.ts`:`processDirectory` 增加「有 `.list.jsonc` 的目录按清单取舍文件」的判断(`defaults/` 与无清单目录全量、根目录 `isRoot` 跳过过滤、`.list.jsonc` 自身始终压缩输出)。
- 测试:扩展 `script/build-plugin.test.ts`;新增 `script/build-resource.test.ts`(fixture 行为测试)。
- 测试:把现有 `script/build-plugin.test.ts` **移动**至 `script/__test__/build-plugin.test.ts`(导入改 `../build-plugin`)后扩展;新增 `script/__test__/build-resource.test.ts`(fixture 行为测试,导入 `../build-resource`)。
**约束:** 用户裁定 3 / 4 —— 只改构建脚本;不改 `build-lib.ts` / `package.json` / `index.html` / `dev.md` / `src/**` / `public/**` / 数据端与渲染端源码 / `vite.config.ts` / lockfile / `script/dev.ts`;不改构建顺序;不做特殊失败处理。
@ -125,6 +125,7 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
@AGENTS.md
@dev.md
@script/build-plugin.ts
<!-- 插件测试的移动前位置:Task 1 读取后移动至 script/__test__/build-plugin.test.ts -->
@script/build-plugin.test.ts
@script/build-game.ts
@script/build-resource.ts
@ -136,7 +137,7 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报两处修复的范围、content 过滤规则、修饰器删除口径与待批准命名,等待「可以执行」</name>
<precondition>`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` 尚不存在。</precondition>
<precondition>`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-plugin.test.ts` 仍在旧位置 `script/`,`script/__test__/build-plugin.test.ts` 与 `script/__test__/build-resource.test.ts` 尚不存在(`script/__test__/` 目录可能也不存在,由 Task 1 移动时创建)。</precondition>
<files>(不改仓库文件)os.tmpdir()/gsd-09-22-jsdoc-baseline.json(仓库外临时基线,本任务一次性写入)</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-93 / D-96 / D-98;D-03 / D-04)
@ -153,17 +154,17 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
① **要解决的问题:** 见 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`)。
② **范围(最小):** `script/build-plugin.ts`(新增插件 + 纯函数)、`script/build-game.ts`(`buildClient` / `buildData` 各注入一次)、`script/build-resource.ts`(清单过滤)、`script/__test__/build-plugin.test.ts`(**由 `script/build-plugin.test.ts` 移动后扩展**)、`script/__test__/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` 原样),且**根目录显式跳过清单过滤**(传根标记,不依赖「根目录当前没有 `.list.jsonc`」,将来出现也不过滤);`defaults/` 全量输出并压缩(显式豁免清单过滤);`maps` / `texture` / `animation` / `audio` / `font` 只保留被各自 `.list.jsonc` 引用的文件,未引用剔除;`.list.jsonc` 自身始终压缩输出;无清单的子文件夹全量输出。匹配口径 = 去注释 `JSON.parse` 后递归收集**所有字符串**,按「完整文件名」或「去扩展名主文件名」精确匹配(`empty` → `empty.jsonc`、`test` → `test.anim`、`terrains0.png` → 同名文件)。jsonc 压缩沿用 `minifyJSONC` 现有行为:只剥离**整行** `//` 注释(含系统标记行),不改其实现。
④ **修饰器删除口径(用户裁定 2):** 构建时删除**所有** `@shouldReplay(...)` / `@ignoreReplay(...)` 应用(当前 `@shouldReplay` = **69 次应用 / 20 个 `.ts` 文件**;`packages-user/data-common/src/replay/func.ts` 另有 1 行注释含 2 处 `@shouldReplay` 文本,非应用;`@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 构建两次:fixture **导出一个类**,两个方法分别带 `@shouldReplay` / `@ignoreReplay` 并各自返回不同唯一字面量 `mota_replay_probe` / `mota_ignore_probe`,`shouldReplay` / `ignoreReplay` 工厂在 fixture 内**本地定义、不导出**;**对照** `plugins: []` 产物必须含 `shouldReplay`、`ignoreReplay`、两个唯一字面量与两个方法名,**实验组**必须不含 `shouldReplay` / `ignoreReplay`,但两个唯一字面量与两个方法名**仍在**——两侧缺一即空断言)」;`script/build-resource.test.ts` 用临时 fixture 验证根文件、**根目录即使存在 `.list.jsonc` 也全量输出**、`defaults/` 全量(即使 `defaults/` 内存在 `.list.jsonc`)、`maps` / `texture` / `font` 的保留与剔除、**无清单子文件夹全量**、`.list.jsonc` 压缩且剥离整行 `//` 注释、`item.ts` 原样;另加真实 `src/content` 的清单覆盖率检查(每个清单目录内非清单文件都必须被清单命中,防止过滤掉现有素材)、`build-game` 按函数切片的结构门禁(只断言导入绑定与注入位置,不绑定提议命名)、`build-resource.ts` 与 `build-plugin.ts` 的「既有注释零删除」门禁(⓪ 已把两个文件的注释行集合与 `git rev-parse HEAD` 存到仓库外的临时基线;Task 1 / Task 2 各自与**该基线**逐行比对,**与提交无关、可重复执行**,并校验基线注释行数下限防空洞与「基线不得已含 `replayDecoratorPlugin` / `removeReplayDecorators`」),以及 `script/build-*.ts` 范围内的 `eslint`;命令一律 `cmd /c "pnpm exec ..."`,内联 `node -e` 只用单引号字面量。
⑤ **验证方式:** `script/__test__/build-plugin.test.ts`(由 `script/build-plugin.test.ts` **移动**而来、导入改 `../build-plugin`)扩展为「纯函数断言(删装饰器、字符串/注释不误删、带引号与括号的参数整段删除)+ **对照式双装饰器**真实 `vite.build()`(同一 fixture 构建两次:fixture **导出一个类**,两个方法分别带 `@shouldReplay` / `@ignoreReplay` 并各自返回不同唯一字面量 `mota_replay_probe` / `mota_ignore_probe`,`shouldReplay` / `ignoreReplay` 工厂在 fixture 内**本地定义、不导出**;**对照** `plugins: []` 产物必须含 `shouldReplay`、`ignoreReplay`、两个唯一字面量与两个方法名,**实验组**必须不含 `shouldReplay` / `ignoreReplay`,但两个唯一字面量与两个方法名**仍在**——两侧缺一即空断言)」;`script/__test__/build-resource.test.ts` 用临时 fixture 验证根文件、**根目录即使存在 `.list.jsonc` 也全量输出**、`defaults/` 全量(即使 `defaults/` 内存在 `.list.jsonc`)、`maps` / `texture` / `font` 的保留与剔除、**无清单子文件夹全量**、`.list.jsonc` 压缩且剥离整行 `//` 注释、`item.ts` 原样;另加真实 `src/content` 的清单覆盖率检查(每个清单目录内非清单文件都必须被清单命中,防止过滤掉现有素材)、`build-game` 按函数切片的结构门禁(只断言导入绑定与注入位置,不绑定提议命名)、`build-resource.ts` 与 `build-plugin.ts` 的「既有注释零删除」门禁(⓪ 已把两个文件的注释行集合与 `git rev-parse HEAD` 存到仓库外的临时基线;Task 1 / Task 2 各自与**该基线**逐行比对,**与提交无关、可重复执行**,并校验基线注释行数下限防空洞与「基线不得已含 `replayDecoratorPlugin` / `removeReplayDecorators`」),以及 `script/build-*.ts` 与 `script/__test__/*.test.ts` 范围内的 `eslint`(`eslint.config.js` 不忽略该目录);`vitest` 主配置未设 `include`,默认 `**/*.test.ts` 会收集 `script/__test__/` 下的测试,故 `vitest run` 命令一律指向新路径;命令一律 `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'`,以及新增模块级常量 `REPLAY_DECORATORS` / `SCRIPT_EXTENSIONS` / `NODE_MODULES_SEGMENT`——若您不同意新增常量名,我改为函数内内联字面量);OI-2(`LIST_FILE_NAME` / `DEFAULTS_DIRECTORY` / `readListNames` / `collectListedNames` / `isListedFile`,以及 `processDirectory` 新增的 `isRoot` 参数);OI-3(新增测试文件 `script/build-resource.test.ts`、扩展 `script/build-plugin.test.ts`)。未获批前不落码。**若您给出其它命名,我会先修订本计划(含门禁命令与测试用例)再重新汇报**——门禁按行为、值标记与导出形态断言,不随改名自动跟随。
⑦ **待批准命名(须用户确认):** OI-1(`removeReplayDecorators` / `replayDecoratorPlugin` / `name: 'mota-replay-decorator'`,以及新增模块级常量 `REPLAY_DECORATORS` / `SCRIPT_EXTENSIONS` / `NODE_MODULES_SEGMENT`——若您不同意新增常量名,我改为函数内内联字面量);OI-2(`LIST_FILE_NAME` / `DEFAULTS_DIRECTORY` / `readListNames` / `collectListedNames` / `isListedFile`,以及 `processDirectory` 新增的 `isRoot` 参数);OI-3(测试文件统一放在 `script/__test__/`:新增 `script/__test__/build-resource.test.ts`;`script/build-plugin.test.ts` **移动**为 `script/__test__/build-plugin.test.ts`,旧路径不保留、导入改 `../build-plugin`)。未获批前不落码。**若您给出其它命名,我会先修订本计划(含门禁命令与测试用例)再重新汇报**——门禁按行为、值标记与导出形态断言,不随改名自动跟随。
⑧ **不在本增量:** 不改 `src/**`(含改动数据端源码或手删 import)、不改 `packages-user/**`、不改 `script/dev.ts`、不改 `package.json` / `dev.md`、不新增依赖、不改构建顺序与产物落点、不做特殊失败处理。
</action>
@ -187,13 +188,14 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
<verify>
<automated>node -e "const fs=require('fs'),os=require('os'),path=require('path'),cp=require('child_process');const out=path.join(os.tmpdir(),'gsd-09-22-jsdoc-baseline.json');const files=['script/build-plugin.ts','script/build-resource.ts'];const marks=['//','/*','*'];const isComment=l=>marks.some(m=>l.startsWith(m));const bad=[];let sha='';try{sha=cp.execSync('git rev-parse HEAD',{encoding:'utf8'}).trim()}catch(e){bad.push('cannot rev-parse HEAD: '+e.message)}if(sha.length!==40)bad.push('HEAD sha is not 40 chars: '+sha);const fresh={head:sha,capturedAt:new Date().toISOString(),files:{}};for(const f of files){fresh.files[f]=fs.readFileSync(f,'utf8').split(/\r?\n/).map(l=>l.trim()).filter(isComment)}let base=fresh;if(fs.existsSync(out)){const prev=JSON.parse(fs.readFileSync(out,'utf8'));let poisoned=false;for(const f of files){const ls=prev.files?prev.files[f]:undefined;if(!Array.isArray(ls)){poisoned=true;break}for(const l of ls){if(l.indexOf('replayDecoratorPlugin')>-1){poisoned=true;break}if(l.indexOf('removeReplayDecorators')>-1){poisoned=true;break}}if(poisoned)break}if(poisoned)bad.push('existing baseline already contains new identifiers - delete '+out+' and recapture before any edit');else{base=prev;console.log('reuse existing baseline ('+prev.capturedAt+', head '+prev.head+')')}}else{fs.writeFileSync(out,JSON.stringify(fresh,null,2));console.log('captured baseline at '+out+' (head '+sha+')')}for(const f of files){const ls=base.files?base.files[f]:undefined;if(!Array.isArray(ls)){bad.push('baseline missing comment lines for '+f);continue}const min=f==='script/build-plugin.ts'?5:8;if(ls.length<min)bad.push(f+' baseline comment lines too few ('+ls.length+'<'+min+')');console.log(f+' baseline comment lines = '+ls.length)}if(bad.length>0){console.error('JSDOC BASELINE CAPTURE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK jsdoc baseline ready: head '+base.head)"</automated>
<fails_when>non-zero exit,或 stderr 含 "JSDOC BASELINE CAPTURE FAILED":`git rev-parse HEAD` 失败或 SHA 不是 40 位 / 已存在的临时基线已含 `replayDecoratorPlugin` 或 `removeReplayDecorators`(说明它是在编辑之后捕获的,须删除该临时文件并在任何编辑前重跑)/ 某文件基线注释行数低于下限(`build-plugin.ts` 5 行、`build-resource.ts` 8 行)</fails_when>
<automated>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')"</automated>
<fails_when>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` 的系统标记行已变化</fails_when>
<automated>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/__test__/build-resource.test.ts'))bad.push('script/__test__/build-resource.test.ts must not exist yet');if(fs.existsSync('script/__test__/build-plugin.test.ts'))bad.push('script/__test__/build-plugin.test.ts must not exist yet (the move happens in Task 1)');if(!fs.existsSync('script/__test__'))console.log('note: script/__test__ does not exist yet - Task 1 creates it when moving the plugin test');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, script/__test__/*.test.ts absent')"</automated>
<fails_when>non-zero exit,或 stderr 含 "PRESTATE FAILED":必需文件缺失(含移动前的 `script/build-plugin.test.ts`)/ `.jsonc` 数量不是 23 / 某个素材子文件夹缺 `.list.jsonc` / `defaults/` 不是 8 个 jsonc / `script/__test__/build-resource.test.ts` 已存在 / `script/__test__/build-plugin.test.ts` 已存在(说明移动已提前发生,与「Task 1 负责移动」的前提不符)/ `build-plugin.ts` 缺 `dataIsolationPlugin` / `build-game.ts` 已不是 09-21 实现 / 修饰器定义缺失 / `.list.jsonc` 的系统标记行已变化</fails_when>
<human-check>用户明确回复「可以执行」;若就 OI-1 / OI-2 / OI-3 指定了与提议不同的命名,须先按新命名修订本计划(门禁命令、测试用例与 SUMMARY 关键字同步)并重新获批,未修订前不得执行</human-check>
</verify>
<acceptance_criteria>
- 用户回复中明确包含「可以执行」
- 汇报涵盖:两条要解决的问题、最小范围与「其余文件一律不动」清单、content 逐文件夹规则与清单匹配口径、修饰器删除口径(AST + 双端注入 + 不手删 import + `apply: 'build'`)、验证方式、三项已知边界、OI-1 / OI-2 / OI-3 命名
- 已说明测试文件位置:两个测试都放在 `script/__test__/`;`script/build-plugin.test.ts` 被**移动**为 `script/__test__/build-plugin.test.ts`(旧路径不保留、导入改 `../build-plugin`),新增 `script/__test__/build-resource.test.ts`(导入 `../build-resource`)
- 已说明:若用户改命名,须先修订本计划(门禁按行为/导出形态断言,不会自动跟随改名)再执行
- 已说明:`processDirectory` 等既有 jsDoc 逐字保留,新逻辑说明只写在新函数 / 新常量上
- 已说明:修饰器删除的验收依赖「带插件 vs 不带插件」的**双装饰器**对照构建(fixture 导出类、两方法各返回唯一字面量、工厂本地不导出),不依赖 `@shouldReplay` 语法文本
@ -207,12 +209,12 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
<task type="tracer">
<name>Task 1: 修饰器删除插件(`script/build-plugin.ts`)+ 双端注入(`script/build-game.ts`)+ 测试</name>
<precondition>`os.tmpdir()/gsd-09-22-jsdoc-baseline.json` 已由 Task 0 捕获(含 40 位 `head` 与 `script/build-plugin.ts` / `script/build-resource.ts` 的注释行集合;若缺失,先按 Task 0 ⓪ 的捕获命令补捕——此时两个文件尚未被本任务修改)。Task 0 已闭环(用户回复「可以执行」,OI-1 已获批;若用户改了命名,本计划已按新命名修订并重新获批——门禁按导出形态与导入绑定断言,未修订不得执行)。`typescript` 与 `vitest` 可用;`script/build-plugin.ts` 仅含 `dataIsolationPlugin`;`script/build-game.ts` 为 09-21 实现(`buildClient` < `buildData` < `buildGame` 的定义顺序)。</precondition>
<files>script/build-plugin.ts, script/build-plugin.test.ts, script/build-game.ts</files>
<precondition>`os.tmpdir()/gsd-09-22-jsdoc-baseline.json` 已由 Task 0 捕获(含 40 位 `head` 与 `script/build-plugin.ts` / `script/build-resource.ts` 的注释行集合;若缺失,先按 Task 0 ⓪ 的捕获命令补捕——此时两个文件尚未被本任务修改)。Task 0 已闭环(用户回复「可以执行」,OI-1 已获批;若用户改了命名,本计划已按新命名修订并重新获批——门禁按导出形态与导入绑定断言,未修订不得执行)。`typescript` 与 `vitest` 可用;`script/build-plugin.ts` 仅含 `dataIsolationPlugin`;`script/build-game.ts` 为 09-21 实现(`buildClient` < `buildData` < `buildGame` 的定义顺序);插件测试仍在旧位置 `script/build-plugin.test.ts`(本任务负责移动),`script/__test__/` 目录可能尚不存在。</precondition>
<files>script/build-plugin.ts, script/build-game.ts, script/__test__/build-plugin.test.ts(新建;移动源 `script/build-plugin.test.ts` 在 Task 1 内删除)</files>
<read_first>
- .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-plugin.test.ts(移动前的既有测试风格:显式 `import { describe, expect, it } from 'vitest'`、`mkdtemp` fixture、每个 `it` 前一行注释;本任务读取后移动到 `script/__test__/`)
- 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(装饰器写在方法上方的实际形态,含双引号参数的用例)
@ -238,7 +240,7 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
- `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` 前一行注释):
(3) **移动到 `script/__test__/build-plugin.test.ts` 后扩展(用户裁定:所有测试文件放在 `script/__test__/`):** 先把现有 `script/build-plugin.test.ts` 移动到 `script/__test__/build-plugin.test.ts`(`script/__test__/` 不存在则创建;用 `git mv` 或等价移动方式),把对被测模块的导入从 `'./build-plugin'` 改为 `'../build-plugin'`;`script/build-plugin.test.ts` **不得残留**。随后保留既有 `describe('dataIsolationPlugin')` 全部用例不改;新增 `describe` 覆盖(每个 `it` 前一行注释):
- **删 `@shouldReplay('...')`:** 输入含「装饰器 + 方法」的类片段,输出不再含该装饰器调用,且方法名与 `export` 结构仍在。
- **删 `@ignoreReplay('...')`:** 同上。
- **参数整段删除:** 装饰器参数含单引号 / 双引号 / 逗号 / 嵌套括号时整段被删,不残留括号碎片。
@ -258,10 +260,10 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
</behavior>
<reversibility rating="reversible">仅新增两个导出、两处 `plugins` 数组注入与测试用例;`git checkout` 三个文件即整体回退,未改产物落点与构建顺序。</reversibility>
<verify>
<automated>cmd /c "pnpm exec vitest run script/build-plugin.test.ts"</automated>
<automated>cmd /c "pnpm exec vitest run script/__test__/build-plugin.test.ts"</automated>
<fails_when>non-zero exit,或任一断言失败(装饰器未被删 / 参数残留碎片 / 字符串或注释中的同名文本被误删 / 对照产物缺 `shouldReplay` 或 `ignoreReplay` 或某个唯一字面量(token 前提不成立)/ 带插件产物仍含 `shouldReplay` / `ignoreReplay` / 带插件产物丢了 `mota_replay_probe`、`mota_ignore_probe` 或方法名 / 既有 `dataIsolationPlugin` 用例回归)</fails_when>
<automated>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 exported=[...t.matchAll(/export function (\w+)/g)].map(m=>m[1]);if(exported.length<3)bad.push('build-plugin.ts must export dataIsolationPlugin plus two strip APIs, found '+exported.length);if(exported.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','mota_ignore_probe','export class','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 cnt=(s,sub)=>s.split(sub).length-1;const pos='.toContain('+q,neg='.not.toContain('+q;for(const name of ['shouldReplay','ignoreReplay','mota_replay_probe','mota_ignore_probe']){if(cnt(tp,pos+name+q+')')-cnt(tp,neg+name+q+')')<1)bad.push('test must positively assert '+name+' in the control build')}for(const name of ['shouldReplay','ignoreReplay']){if(cnt(tp,neg+name+q+')')<1)bad.push('test must negatively assert '+name+' in the plugin build')}for(const fn of ['shouldReplay','ignoreReplay']){if(tp.indexOf('function '+fn+'(')<0)bad.push('fixture must locally define '+fn);if(tp.indexOf('export function '+fn)>-1)bad.push('fixture must NOT export '+fn)}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||!(s<d&&d<b))bad.push('function order changed; slicing gate needs buildClient < buildData < buildGame');else{const idx=g.indexOf('./build-plugin');if(idx<0)bad.push('build-game.ts must import from ./build-plugin');else{const start=g.lastIndexOf('import',idx),open=g.indexOf('{',start),close=g.indexOf('}',open);const bindings=g.slice(open+1,close).split(',').map(v=>v.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')"</automated>
<fails_when>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_ignore_probe` / 测试缺 `export class` 形态 / 测试缺少不带插件的对照构建(`plugins: []`)/ 测试缺少对 `shouldReplay` / `ignoreReplay` / `mota_replay_probe` / `mota_ignore_probe` 的**正向**断言 / 测试缺少对 `shouldReplay` / `ignoreReplay` 的**负向**断言 / 测试导出了 `shouldReplay` 或 `ignoreReplay` 工厂(对照会失效)/ 函数定义顺序不满足切片前提 / `./build-plugin` 的导入绑定不是「`dataIsolationPlugin` + 恰好 1 个新插件」/ `buildClient` 或 `buildData` 片段缺该新插件 / `buildData` 丢了 `dataIsolationPlugin` / `dataIsolationPlugin` 被接进 `buildClient`)</fails_when>
<automated>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 exported=[...t.matchAll(/export function (\w+)/g)].map(m=>m[1]);if(exported.length<3)bad.push('build-plugin.ts must export dataIsolationPlugin plus two strip APIs, found '+exported.length);if(exported.indexOf('dataIsolationPlugin')<0)bad.push('dataIsolationPlugin export lost');if(fs.existsSync('script/build-plugin.test.ts'))bad.push('old test path must be gone after the move: script/build-plugin.test.ts');const tp=fs.readFileSync('script/__test__/build-plugin.test.ts','utf8');for(const x of ['@shouldReplay','@ignoreReplay','node_modules','mota_replay_probe','mota_ignore_probe','export class','build(','not.toContain','../build-plugin']){if(tp.indexOf(x)<0)bad.push('script/__test__/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 cnt=(s,sub)=>s.split(sub).length-1;const pos='.toContain('+q,neg='.not.toContain('+q;for(const name of ['shouldReplay','ignoreReplay','mota_replay_probe','mota_ignore_probe']){if(cnt(tp,pos+name+q+')')-cnt(tp,neg+name+q+')')<1)bad.push('test must positively assert '+name+' in the control build')}for(const name of ['shouldReplay','ignoreReplay']){if(cnt(tp,neg+name+q+')')<1)bad.push('test must negatively assert '+name+' in the plugin build')}for(const fn of ['shouldReplay','ignoreReplay']){if(tp.indexOf('function '+fn+'(')<0)bad.push('fixture must locally define '+fn);if(tp.indexOf('export function '+fn)>-1)bad.push('fixture must NOT export '+fn)}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||!(s<d&&d<b))bad.push('function order changed; slicing gate needs buildClient < buildData < buildGame');else{const idx=g.indexOf('./build-plugin');if(idx<0)bad.push('build-game.ts must import from ./build-plugin');else{const start=g.lastIndexOf('import',idx),open=g.indexOf('{',start),close=g.indexOf('}',open);const bindings=g.slice(open+1,close).split(',').map(v=>v.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')"</automated>
<fails_when>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_ignore_probe` / 测试缺 `export class` 形态 / 测试缺少不带插件的对照构建(`plugins: []`)/ 测试缺少对 `shouldReplay` / `ignoreReplay` / `mota_replay_probe` / `mota_ignore_probe` 的**正向**断言 / 测试缺少对 `shouldReplay` / `ignoreReplay` 的**负向**断言 / 测试导出了 `shouldReplay` 或 `ignoreReplay` 工厂(对照会失效)/ 函数定义顺序不满足切片前提 / `./build-plugin` 的导入绑定不是「`dataIsolationPlugin` + 恰好 1 个新插件」/ `buildClient` 或 `buildData` 片段缺该新插件 / `buildData` 丢了 `dataIsolationPlugin` / `dataIsolationPlugin` 被接进 `buildClient` / 测试未移动到 `script/__test__/build-plugin.test.ts`(旧路径 `script/build-plugin.test.ts` 仍存在)/ 测试仍导入 `./build-plugin` 而非 `../build-plugin`)</fails_when>
<automated>node -e "const fs=require('fs'),os=require('os'),path=require('path');const f='script/build-plugin.ts';const bad=[];const bp=path.join(os.tmpdir(),'gsd-09-22-jsdoc-baseline.json');let base=null;if(!fs.existsSync(bp))bad.push('jsdoc baseline missing: '+bp+' - Task 0 must capture it before any edit');else{base=JSON.parse(fs.readFileSync(bp,'utf8'));if(typeof base.head!=='string'||base.head.length!==40)bad.push('baseline head is not a 40-char sha')}let baseSet=new Set();if(base){const ls=base.files?base.files[f]:undefined;if(!Array.isArray(ls))bad.push('baseline has no comment lines for '+f);else{baseSet=new Set(ls);for(const l of baseSet){if(l.indexOf('replayDecoratorPlugin')>-1||l.indexOf('removeReplayDecorators')>-1){bad.push('baseline already contains new identifiers (captured after edits?) - delete '+bp+' and recapture before edits');break}}}}if(baseSet.size<5)bad.push(f+' baseline comment lines too few ('+baseSet.size+'<5), comment gate would be vacuous');const curSet=new Set(fs.readFileSync(f,'utf8').split(/\r?\n/).map(l=>l.trim()));const lost=[...baseSet].filter(l=>!curSet.has(l));if(lost.length>0)bad.push(f+' lost '+lost.length+' existing comment/jsDoc line(s), first: '+lost[0]);if(bad.length>0){console.error('PLUGIN COMMENT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK plugin comments: Task-0 baseline ('+base.head.slice(0,8)+') comment lines intact ('+baseSet.size+' lines)')"</automated>
<fails_when>non-zero exit,或 stderr 含 "PLUGIN COMMENT GATE FAILED":`os.tmpdir()/gsd-09-22-jsdoc-baseline.json` 不存在或没有 `script/build-plugin.ts` 的注释行(Task 0 未捕获基线)/ 基线 `head` 不是 40 位 SHA / 基线已含 `replayDecoratorPlugin` 或 `removeReplayDecorators`(基线是在编辑后捕获的,须删除临时文件并在任何编辑前重捕)/ 基线注释行数低于下限 5(门禁可能空洞)/ 基线中的既有注释 / jsDoc 行在当前文件中缺失(被删除或改写)</fails_when>
</verify>
@ -270,21 +272,22 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
- 删除基于 TypeScript AST 的节点范围逆序切片;字符串、注释、非目标装饰器不受影响
- `script/build-game.ts` 在 `buildClient` 与 `buildData` 各注入一次;`buildData` 仍保持 `dataIsolationPlugin` 首位;构建顺序与其余配置未改
- 既有 `dataIsolationPlugin` 的行为与用例零回归
- `script/build-plugin.test.ts` 覆盖「删两类装饰器、参数含引号/括号、字符串不误删、注释不误删、`node_modules` 跳过、**对照式双装饰器** `vite.build()`(fixture 导出类 + 两个方法各带一种装饰器各返回唯一字面量 + 工厂本地不导出;`plugins: []` 对照产物含 `shouldReplay` / `ignoreReplay` / 两个唯一字面量 / 两个方法名,带插件产物不含 `shouldReplay` / `ignoreReplay` 但两个唯一字面量与两个方法名仍在)」;每个 `it` 前一行注释
- `script/__test__/build-plugin.test.ts` 覆盖「删两类装饰器、参数含引号/括号、字符串不误删、注释不误删、`node_modules` 跳过、**对照式双装饰器** `vite.build()`(fixture 导出类 + 两个方法各带一种装饰器各返回唯一字面量 + 工厂本地不导出;`plugins: []` 对照产物含 `shouldReplay` / `ignoreReplay` / 两个唯一字面量 / 两个方法名,带插件产物不含 `shouldReplay` / `ignoreReplay` 但两个唯一字面量与两个方法名仍在)」;每个 `it` 前一行注释
- 插件测试位于 `script/__test__/build-plugin.test.ts`(由 `script/build-plugin.test.ts` 移动而来,导入改为 `../build-plugin`),旧路径 `script/build-plugin.test.ts` 不再存在;`pnpm exec vitest run script/__test__/build-plugin.test.ts` 可收集并通过
- 既有注释未被改写:`dataIsolationPlugin` / `SPECIFIER_BANS` / `PATH_BANS` / `findBan` 的注释逐字保留(由与 **Task 0 捕获的 `os.tmpdir()/gsd-09-22-jsdoc-baseline.json`** 比对的注释门禁证明——**与 git 提交无关,提交后重跑同一门禁仍成立**;基线若已含 `replayDecoratorPlugin` / `removeReplayDecorators` 即门禁自失败并要求在编辑前重捕);新增 jsDoc 只出现在新函数上
- 未手删任何 import;未改 `packages-user/**` / `packages/**` / `src/**` / `script/dev.ts`
</acceptance_criteria>
<done>修饰器删除插件就位并双端注入:数据端与渲染端构建都不再编译 `@shouldReplay` / `@ignoreReplay` 应用(AST 精确切片、字符串与注释无损伤、`node_modules` 跳过),对照构建证明 token 在未处理时确实存在,开发服务行为不变;单测、结构门禁与注释基线门禁全绿,既有隔离插件零回归</done>
<done>修饰器删除插件就位并双端注入:数据端与渲染端构建都不再编译 `@shouldReplay` / `@ignoreReplay` 应用(AST 精确切片、字符串与注释无损伤、`node_modules` 跳过),对照构建证明 token 在未处理时确实存在,开发服务行为不变;插件测试已移动到 `script/__test__/build-plugin.test.ts`(旧路径不残留);单测、结构门禁与注释基线门禁全绿,既有隔离插件零回归</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: content 清单过滤(`script/build-resource.ts`)+ 行为测试 + SUMMARY</name>
<precondition>Task 1 已完成(修饰器插件就位并双端注入);Task 0 已闭环且 `os.tmpdir()/gsd-09-22-jsdoc-baseline.json` 仍存在(含 `script/build-resource.ts` 的注释行集合与 40 位 `head`——本任务的注释门禁与它比对,不随 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` 尚不存在。</precondition>
<files>script/build-resource.ts, script/build-resource.test.ts, .planning/phases/09-minimum-browser-runnable/09-22-SUMMARY.md</files>
<precondition>Task 1 已完成(修饰器插件就位并双端注入);Task 0 已闭环且 `os.tmpdir()/gsd-09-22-jsdoc-baseline.json` 仍存在(含 `script/build-resource.ts` 的注释行集合与 40 位 `head`——本任务的注释门禁与它比对,不随 Task 1 的提交漂移);Task 0 已闭环(OI-2 / OI-3 已获批;若用户改了命名,本计划已按新命名修订并重新获批,未修订不得执行)。`src/content` 的 `animation` / `audio` / `font` / `maps` / `texture` 各有 `.list.jsonc`,`defaults/` 有 8 个 `.jsonc`、无清单;`script/__test__/build-plugin.test.ts` 已由 Task 1 就位;`script/__test__/build-resource.test.ts` 尚不存在。</precondition>
<files>script/build-resource.ts, script/__test__/build-resource.test.ts, .planning/phases/09-minimum-browser-runnable/09-22-SUMMARY.md</files>
<read_first>
- .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` 清理)
- script/__test__/build-plugin.test.ts(fixture 风格:`mkdtemp` / `writeFile` / `afterEach` 清理)
- dev.md(命名、jsDoc、模块无副作用、非空判断约定);AGENTS.md(测试每个 `it` 前一行注释)
</read_first>
<action>
@ -305,7 +308,7 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
- `minifyJSONC`、`buildContent`、`IContentBuildOptions`、`IContentBuildStats`、`IContentBuildCounter` 的名称与签名**一律不改**;**不改 `minifyJSONC` 行为**(仍只按整行 `line.trim().startsWith('//')` 剥离注释,含系统标记行;不剥离行尾注释、不改 `JSON.parse` + `JSON.stringify` 流程);不改字体分支的告警文案。
- 保持 dev.md 约定:字面量非空判断用 `isNil`、对象用 `!`;尽量不用 `?.`。
(2) **新增 `script/build-resource.test.ts`(OI-3):** `mkdtemp` 建临时 fixture,`afterEach` 清理;每个 `it` 前一行注释:
(2) **新增 `script/__test__/build-resource.test.ts`(OI-3;测试目录按用户裁定统一为 `script/__test__/`):** 被测模块以 `../build-resource` 导入;`mkdtemp` 建临时 fixture,`afterEach` 清理;每个 `it` 前一行注释:
- **根目录 jsonc:** `core.jsonc` 含 `// Type: ...` 与系统标记行 → 输出同名 `.jsonc`,`JSON.parse` 成功、文本不含 `//`。
- **根目录即使存在 `.list.jsonc` 也全量输出:** fixture 根放 `.list.jsonc`(内容 `["core"]`)+ `core.jsonc` + `other.jsonc` → `core.jsonc` 与 `other.jsonc` **都存在**(证明根目录 `isRoot` 跳过过滤,root 行为不依赖「当前根目录没有清单」这一现状)。
- **`defaults/` 全量(即使存在清单):** `defaults/` 内放 `.list.jsonc`(内容 `["env"]`)+ `env.jsonc`(含注释)+ `other.jsonc` → `env.jsonc` 与 `other.jsonc` **都输出**且已压缩、`.list.jsonc` 自身也已压缩输出(证明 `isDefaults` 豁免真实生效,不依赖「`defaults/` 当前恰好没有清单」这一现状;与根目录 `isRoot` 用例同构)。
@ -331,27 +334,27 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
</behavior>
<reversibility rating="reversible">`build-resource.ts` 为单文件改造 + 新增一个测试文件;`git checkout` / 删除测试文件即回退,产物落点与构建顺序不变。</reversibility>
<verify>
<automated>cmd /c "pnpm exec vitest run script/build-resource.test.ts"</automated>
<automated>cmd /c "pnpm exec vitest run script/__test__/build-resource.test.ts"</automated>
<fails_when>non-zero exit,或任一断言失败(`defaults/` 被过滤 / 清单引用文件被剔除 / 未引用文件仍被输出 / `.list.jsonc` 被剔除或未压缩 / 根 `.jsonc` 未压成纯 JSON / `item.ts` 拷贝内容不一致 / 统计值与实际输出不一致)</fails_when>
<automated>node -e "const fs=require('fs'),os=require('os'),path=require('path');const bad=[];const f='script/build-resource.ts';const t=fs.readFileSync(f,'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)}const bp=path.join(os.tmpdir(),'gsd-09-22-jsdoc-baseline.json');let base=null;if(!fs.existsSync(bp))bad.push('jsdoc baseline missing: '+bp+' - Task 0 must capture it before any edit');else{base=JSON.parse(fs.readFileSync(bp,'utf8'));if(typeof base.head!=='string'||base.head.length!==40)bad.push('baseline head is not a 40-char sha')}let baseSet=new Set();if(base){const ls=base.files?base.files[f]:undefined;if(!Array.isArray(ls))bad.push('baseline has no comment lines for '+f);else{baseSet=new Set(ls);for(const l of baseSet){if(l.indexOf('.list.jsonc')>-1||l.indexOf('readListNames')>-1){bad.push('baseline already contains new content-filter identifiers (captured after edits?) - delete '+bp+' and recapture before edits');break}}}}if(baseSet.size<8)bad.push(f+' baseline comment lines too few ('+baseSet.size+'<8), comment gate would be vacuous');const curSet=new Set(t.split(/\r?\n/).map(l=>l.trim()));const lost=[...baseSet].filter(l=>!curSet.has(l));if(lost.length>0)bad.push(f+' lost '+lost.length+' existing comment/jsDoc line(s), first: '+lost[0]);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','gsd-09-22-jsdoc-baseline','uncalibrated']){if(sm.indexOf(tok)<0)bad.push('SUMMARY must record token: '+tok)}if(base&&sm.indexOf(base.head)<0)bad.push('SUMMARY must record jsdoc baseline HEAD sha '+base.head)}if(bad.length>0){console.error('CONTENT FILTER GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK content filter: list retention + Task-0 baseline ('+base.head.slice(0,8)+') comments intact ('+baseSet.size+' lines) + SUMMARY tokens')"</automated>
<automated>node -e "const fs=require('fs'),os=require('os'),path=require('path');const bad=[];const f='script/build-resource.ts';const t=fs.readFileSync(f,'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)}const bp=path.join(os.tmpdir(),'gsd-09-22-jsdoc-baseline.json');let base=null;if(!fs.existsSync(bp))bad.push('jsdoc baseline missing: '+bp+' - Task 0 must capture it before any edit');else{base=JSON.parse(fs.readFileSync(bp,'utf8'));if(typeof base.head!=='string'||base.head.length!==40)bad.push('baseline head is not a 40-char sha')}let baseSet=new Set();if(base){const ls=base.files?base.files[f]:undefined;if(!Array.isArray(ls))bad.push('baseline has no comment lines for '+f);else{baseSet=new Set(ls);for(const l of baseSet){if(l.indexOf('.list.jsonc')>-1||l.indexOf('readListNames')>-1){bad.push('baseline already contains new content-filter identifiers (captured after edits?) - delete '+bp+' and recapture before edits');break}}}}if(baseSet.size<8)bad.push(f+' baseline comment lines too few ('+baseSet.size+'<8), comment gate would be vacuous');const curSet=new Set(t.split(/\r?\n/).map(l=>l.trim()));const lost=[...baseSet].filter(l=>!curSet.has(l));if(lost.length>0)bad.push(f+' lost '+lost.length+' existing comment/jsDoc line(s), first: '+lost[0]);if(!fs.existsSync('script/__test__/build-resource.test.ts'))bad.push('script/__test__/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','gsd-09-22-jsdoc-baseline','uncalibrated']){if(sm.indexOf(tok)<0)bad.push('SUMMARY must record token: '+tok)}if(base&&sm.indexOf(base.head)<0)bad.push('SUMMARY must record jsdoc baseline HEAD sha '+base.head)}if(bad.length>0){console.error('CONTENT FILTER GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK content filter: list retention + Task-0 baseline ('+base.head.slice(0,8)+') comments intact ('+baseSet.size+' lines) + SUMMARY tokens')"</automated>
<fails_when>non-zero exit,或 stderr 含 "CONTENT FILTER GATE FAILED"(`build-resource.ts` 缺 `.list.jsonc` / `defaults` / `basename` / `JSON.parse` 等值标记 / 既有导出改成别名或丢失(`minifyJSONC` / `buildContent` / `IContentBuildOptions` / `IContentBuildStats`)/ `chars: string` 丢失 / **`os.tmpdir()/gsd-09-22-jsdoc-baseline.json` 不存在或没有 `script/build-resource.ts` 的注释行(Task 0 未捕获基线)** / 基线 `head` 不是 40 位 SHA / 基线已含 `.list.jsonc` 或 `readListNames`(基线是在编辑后捕获的)/ **基线中的既有注释或 jsDoc 行在当前 `build-resource.ts` 中缺失(被删除或改写)** / 基线注释行数低于下限 8(门禁可能空洞)/ 缺测试文件 / 缺 SUMMARY 或 SUMMARY 未记录 `defaults`、`.list.jsonc`、`node_modules`、`build:game`、`D-96`、`shouldReplay`、`gsd-09-22-jsdoc-baseline`、`uncalibrated`,或未记录基线 `head` SHA)</fails_when>
<automated>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')"</automated>
<fails_when>non-zero exit,或 stderr 含 "CONTENT COVERAGE GATE FAILED":某个素材子文件夹缺 `.list.jsonc`,或该目录内存在既未被完整文件名、也未被去扩展名主文件名命中的现存素材(真实施加过滤后会丢文件)</fails_when>
<automated>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"</automated>
<automated>cmd /c "pnpm exec eslint script/build-plugin.ts script/build-resource.ts script/build-game.ts script/__test__/build-plugin.test.ts script/__test__/build-resource.test.ts"</automated>
<fails_when>non-zero exit:五个改动文件在 `eslint` 下报错(dev.md 要求 eslint 零报错;当前被测的四个既有文件实测为 0 报告,新增代码须维持)</fails_when>
</verify>
<acceptance_criteria>
- `script/build-resource.ts` 按用户裁定 1 实现:根目录照常(且 `isRoot` 显式跳过过滤)、`defaults/` 全量、有清单的子文件夹只保留被引用文件、`.list.jsonc` 自身始终压缩输出、无清单目录全量
- 清单解析走 `minifyJSONC`(去注释)+ `JSON.parse` + 递归收集字符串;匹配仅「完整文件名」与「去扩展名主文件名」两种精确方式;`minifyJSONC` 行为未改(只剥离整行 `//` 注释)
- 既有导出名称与签名(`minifyJSONC` / `buildContent` / `IContentBuildOptions`(`chars: string`)/ `IContentBuildStats` / `IContentBuildCounter`)未改;字体分支与告警文案未改
- `script/build-resource.test.ts` 覆盖 9 类用例(根 jsonc、**根目录存在 `.list.jsonc` 仍全量**、`defaults` 全量(**`defaults/` 内存在 `.list.jsonc` 时仍全量**)、maps 清单(fixture 清单原文含 `//` 注释且输出已剥离)、texture 对象清单、font 清单、无清单子文件夹全量、`item.ts` 原样、统计),每个 `it` 前一行注释
- `script/__test__/build-resource.test.ts`(位于 `script/__test__/`,被测模块以 `../build-resource` 导入)覆盖 9 类用例(根 jsonc、**根目录存在 `.list.jsonc` 仍全量**、`defaults` 全量(**`defaults/` 内存在 `.list.jsonc` 时仍全量**)、maps 清单(fixture 清单原文含 `//` 注释且输出已剥离)、texture 对象清单、font 清单、无清单子文件夹全量、`item.ts` 原样、统计),每个 `it` 前一行注释
- `processDirectory` / `minifyJSONC` / `compressFont` / `buildContent` 与接口成员的既有 jsDoc **逐字未改**(由与 **Task 0 捕获的 `os.tmpdir()/gsd-09-22-jsdoc-baseline.json`** 比对的注释门禁证明——**与 git 提交无关、Task 1 提交后重跑仍成立**);`script/build-plugin.ts` 的既有注释由 **Task 1** 的同口径门禁(同一基线文件)证明;过滤与删除的说明只写在新函数 / 新常量上
- 五个改动文件通过范围内的 `eslint`(`pnpm exec eslint` 零报告)
- 真实 `src/content` 清单覆盖率门禁通过(现有素材无一被过滤掉)
- `09-22-SUMMARY.md` 已写并含 8 个必需关键字、基线 `head` SHA(40 位)、`estimate.tokens=46000` **未校准**(`confidence: low`)的说明与三项已知边界
- 未改 `packages-user/**` / `packages/**` / `src/**` / `package.json` / `index.html` / `dev.md` / `vite.config.ts` / lockfile / `script/dev.ts` / `script/build-lib.ts`
</acceptance_criteria>
<done>content 构建按清单过滤:`defaults/` 与无清单目录全量输出、根目录 `isRoot` 跳过过滤(即使出现 `.list.jsonc` 也不过滤)、5 个素材子文件夹只保留被引用文件、`.list.jsonc` 自身压缩输出、jsonc 仍压成纯 JSON;行为测试、`script/build-resource.ts` 与 Task 0 捕获基线的注释零丢失比对、真实清单覆盖率门禁全绿;范围内的 `eslint` 零报告;SUMMARY 记录两处修复、基线 SHA、未校准估算声明与三项已知边界</done>
<done>content 构建按清单过滤:`defaults/` 与无清单目录全量输出、根目录 `isRoot` 跳过过滤(即使出现 `.list.jsonc` 也不过滤)、5 个素材子文件夹只保留被引用文件、`.list.jsonc` 自身压缩输出、jsonc 仍压成纯 JSON;行为测试(`script/__test__/build-resource.test.ts`)、`script/build-resource.ts` 与 Task 0 捕获基线的注释零丢失比对、真实清单覆盖率门禁全绿;范围内的 `eslint` 零报告(含 `script/__test__/*.test.ts`);SUMMARY 记录两处修复、基线 SHA、未校准估算声明与三项已知边界</done>
</task>
</tasks>
@ -384,9 +387,9 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
</threat_model>
<verification>
- **Task 0:** 预执行状态(09-21 构建脚本为当前实现 / 23 个 jsonc / 5 个清单目录 / `defaults` 8 个 jsonc / 修饰器定义存在 / `script/build-resource.test.ts` 不存在)检查;**jsDoc 基线一次性捕获**(`git rev-parse HEAD` + 两文件注释行集合写入 `os.tmpdir()/gsd-09-22-jsdoc-baseline.json`,幂等复用、行数下限与「不含新标识」自校验);人工关卡(用户回复「可以执行」;OI-1 / OI-2 / OI-3 若被改名须先修订计划再获批);汇报须含基线 SHA、三项已知边界与「既有 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()`:fixture 导出类 + 两个方法分别带 `@shouldReplay` / `@ignoreReplay` 且各返回 `mota_replay_probe` / `mota_ignore_probe` + 两个工厂本地不导出;`plugins: []` 对照产物含 `shouldReplay`、`ignoreReplay`、两个唯一字面量与两个方法名,带插件产物不含 `shouldReplay` / `ignoreReplay` 但两个唯一字面量与两个方法名仍在;结构门禁另行校验正/负向断言都在、工厂未被导出);另加 `script/build-plugin.ts` 的「既有注释零丢失」门禁——**与 Task 0 捕获的 `os.tmpdir()/gsd-09-22-jsdoc-baseline.json` 比对**(**与 git 提交无关、可重复执行**,基线须为 40 位 `head`、行数 ≥5、不含 `replayDecoratorPlugin` / `removeReplayDecorators`)。
- **Task 2:** `script/build-resource.ts` 结构(`.list.jsonc` / `defaults` / `basename` / `JSON.parse` 值标记、既有导出与 `chars: string` 未变、**与 Task 0 捕获的同一基线比对注释行零丢失**,并校验基线行数下限 8 防止门禁空洞);`script/build-resource.test.ts` 行为(根 jsonc、**根目录存在 `.list.jsonc` 仍全量**、`defaults` 全量(含 `defaults/.list.jsonc` 时仍全量)、maps 清单保留与剔除 + 清单输出已剥离 `//`、texture 对象清单、font 清单、无清单子文件夹全量、`item.ts` 原样、统计);真实 `src/content` **清单覆盖率**(5 个清单目录内现存素材必须全部被命中);`eslint`(五个改动文件零报告);SUMMARY 关键字 + 基线 `head` SHA + `estimate.tokens` 未校准声明。
- **Task 0:** 预执行状态(09-21 构建脚本为当前实现 / 23 个 jsonc / 5 个清单目录 / `defaults` 8 个 jsonc / 修饰器定义存在 / `script/build-plugin.test.ts` 仍在旧路径 / `script/__test__/build-plugin.test.ts` 与 `script/__test__/build-resource.test.ts` 都不存在)检查;**jsDoc 基线一次性捕获**(`git rev-parse HEAD` + 两文件注释行集合写入 `os.tmpdir()/gsd-09-22-jsdoc-baseline.json`,幂等复用、行数下限与「不含新标识」自校验);人工关卡(用户回复「可以执行」;OI-1 / OI-2 / OI-3 若被改名须先修订计划再获批);汇报须含基线 SHA、三项已知边界与「既有 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/__test__/build-plugin.test.ts` 行为(删两类装饰器、参数含引号括号、字符串与注释不误删、`node_modules` 跳过、**对照式双装饰器** `vite.build()`:fixture 导出类 + 两个方法分别带 `@shouldReplay` / `@ignoreReplay` 且各返回 `mota_replay_probe` / `mota_ignore_probe` + 两个工厂本地不导出;`plugins: []` 对照产物含 `shouldReplay`、`ignoreReplay`、两个唯一字面量与两个方法名,带插件产物不含 `shouldReplay` / `ignoreReplay` 但两个唯一字面量与两个方法名仍在;结构门禁另行校验正/负向断言都在、工厂未被导出);测试文件位置门禁(`script/__test__/build-plugin.test.ts` 存在且导入 `../build-plugin`,旧路径 `script/build-plugin.test.ts` 不存在);另加 `script/build-plugin.ts` 的「既有注释零丢失」门禁——**与 Task 0 捕获的 `os.tmpdir()/gsd-09-22-jsdoc-baseline.json` 比对**(**与 git 提交无关、可重复执行**,基线须为 40 位 `head`、行数 ≥5、不含 `replayDecoratorPlugin` / `removeReplayDecorators`)。
- **Task 2:** `script/build-resource.ts` 结构(`.list.jsonc` / `defaults` / `basename` / `JSON.parse` 值标记、既有导出与 `chars: string` 未变、**与 Task 0 捕获的同一基线比对注释行零丢失**,并校验基线行数下限 8 防止门禁空洞);`script/__test__/build-resource.test.ts` 行为(根 jsonc、**根目录存在 `.list.jsonc` 仍全量**、`defaults` 全量(含 `defaults/.list.jsonc` 时仍全量)、maps 清单保留与剔除 + 清单输出已剥离 `//`、texture 对象清单、font 清单、无清单子文件夹全量、`item.ts` 原样、统计);真实 `src/content` **清单覆盖率**(5 个清单目录内现存素材必须全部被命中);`eslint`(五个改动文件零报告,含 `script/__test__/` 下两个测试);SUMMARY 关键字 + 基线 `head` SHA + `estimate.tokens` 未校准声明。
- **一致性:** `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`。
@ -397,7 +400,8 @@ Phase 9 · 任务 11 · 第 22 个计划 —— **构建脚本的 content 过滤
- **用户裁定 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 结构 + 与 **Task 0 捕获基线**比对的注释零丢失、真实清单覆盖率、`eslint`、SUMMARY 关键字 + 基线 SHA,以及对照式**双装饰器** `vite.build()` 的正/负向双向断言)全部跑到确定结论;**未端到端运行全量 `build:game` 的事实已写入 SUMMARY**;SUMMARY 同时声明 `estimate.tokens=46000` 为**未校准**估算(`confidence: low`)。
- **测试文件位置(用户裁定):** 两个测试文件都位于 `script/__test__/`——`script/__test__/build-plugin.test.ts`(由 `script/build-plugin.test.ts` 移动而来、导入 `../build-plugin`、旧路径不存在)与 `script/__test__/build-resource.test.ts`(导入 `../build-resource`);主 `vitest` 配置未设 `include`,默认 `**/*.test.ts` 可收集该目录;`eslint` 门禁包含这两个新路径。
- **验证:** 两个测试文件(`script/__test__/*.test.ts`)与六个门禁(插件结构、content 结构 + 与 **Task 0 捕获基线**比对的注释零丢失、真实清单覆盖率、`eslint`、SUMMARY 关键字 + 基线 SHA,以及对照式**双装饰器** `vite.build()` 的正/负向双向断言)全部跑到确定结论;**未端到端运行全量 `build:game` 的事实已写入 SUMMARY**;SUMMARY 同时声明 `estimate.tokens=46000` 为**未校准**估算(`confidence: low`)。
- **注释纪律:** 既有 jsDoc / 注释逐字保留;新增说明只出现在新函数与新常量上(`build-plugin.ts` 与 `build-resource.ts` 都由同一个 **Task 0 捕获的临时基线**证明——门禁在逐 task 提交后重跑仍成立,不依赖「提交前」时序)。
- **已知边界:** 生产产物不再产生录像安全检查信息(预期);`index.html` 仍引用 `/styles.css`(09-21 已记录);`animation` / `audio` / `font` 当前无素材,其过滤规则由测试覆盖。
</success_criteria>