docs(09): 新增 render-anim 类型骨架计划与摘要

This commit is contained in:
AncTe committed 2026-11-07 20:13:37 +08:00
1 parent 81a795677e
commit 7bf9fe98f9
4 files changed
+1399

No files matched your search

@@ -0,0 +1,479 @@
---
phase: 09-minimum-browser-runnable
plan: 52
type: execute
# 编号说明:09-01..09-51 的计划均已占用,本计划使用下一个空闲编号 09-52。
# depends_on 为空且 wave 为 1:只新建 packages/render-anim 下的文件,与任何既有计划都没有文件重叠或依赖。
wave: 1
depends_on: []
files_modified:
- packages/render-anim/package.json
- packages/render-anim/src/types.ts
- packages/render-anim/src/index.ts
- .planning/phases/09-minimum-browser-runnable/09-52-SUMMARY.md
autonomous: false
requirements: []
estimate:
# 未校准估算(confidence: low),SUMMARY 须声明。
# 成本主要在读取既有类型 / 工具链约定、写 3 个小文件、以及 prettier / eslint / 结构门禁的往返。
tokens: 30000
raw_tokens: 30000
tasks: 5
confidence: low
must_haves:
truths:
- "**第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(D-03 / AGENTS.md)**:汇报本次要新建的包、三个文件的精确形态、类型骨架逐成员清单、jsDoc 方案、dev.md 合规点、OI-1 已按用户裁定 (B) 落为 `export const enum AnimationBlendMode` 的记录、影响范围、验证形态、编号说明与不提交声明;然后停下等待用户恰好回复「可以执行」。"
- "**只交付类型骨架**:`packages/render-anim` 下只新增 `package.json`、`src/types.ts`、`src/index.ts`;不存在 `RenderAnimater` / `parseRenderAnimation` 的任何实现,`types.ts` 内没有 `class` / `function` / 箭头函数 / `let` / `export default`(无实现、无默认导出);`const` / `enum` 仅出现在 `export const enum AnimationBlendMode` 这一处声明(枚举成员为隐式数值,不写显式初始化)。"
- "**类型骨架与用户给定内容逐字一致**:`types.ts` 导出 7 个类型——1 个 `export const enum AnimationBlendMode`(成员 `Normal` / `Add`,隐式数值 0 / 1)与 6 个接口 `IAnimationBitmap`、`IAnimationSound`、`IAnimationObject`、`IAnimationFrame`、`IAnimationAnimaterData`、`IAnimationData`;成员名、成员顺序、类型、`readonly`、可选性与骨架完全相同;`ITexture` / `ITextureAnimater` 由 `@motajs/render` 普通引入(不使用 `import type`)。"
- "**`index.ts` 与 `package.json` 保持最小**:`index.ts` 只有一行 `export * from './types';`,不转发 `@motajs/render` 的符号;`package.json` 只有 `name` 与 `dependencies['@motajs/render'] = 'workspace:*'` 两个顶层键,没有 `version` / `private` / `type` / `main` / `module` / `types` / `exports` / `scripts`。"
- "**dev.md 合规**:三个文件均为 UTF-8 无 BOM + CRLF + 4 空格缩进、无制表符;`types.ts` 第一行是 `import`;每个公开成员(含枚举成员 `Normal` / `Add`)在源头(`types.ts`)有单行中文 jsDoc,接口与枚举本身不写 jsDoc;无 `any` / `as` / `?.` / `!` / `import type` / 多行 `/* */` 注释 / 复述代码的注释。"
- "**验证只读静态**:prettier `--check`、eslint(两个 ts 文件)、node 结构 / 内容门禁全部通过;可选类型门禁只判 `packages/render-anim` 相关诊断(全仓既有类型错误不视为本次失败);不运行测试、构建、dev server 或浏览器。"
- "**范围受控**:除 `files_modified` 的 4 个路径外零改动;09-50 / 09-51 的计划与摘要哈希逐字不变;不运行 `pnpm install`;不执行任何 git 写命令,0 次提交。"
artifacts:
- path: packages/render-anim/package.json
provides: "`name: @motajs/render-anim` 与 `dependencies: { @motajs/render: workspace:* }` 两个键,4 空格缩进 + CRLF"
- path: packages/render-anim/src/types.ts
provides: "7 个导出类型(1 个 `export const enum AnimationBlendMode` + 6 个接口)的完整类型骨架,成员级中文 jsDoc,`ITexture` / `ITextureAnimater` 从 `@motajs/render` 普通引入"
- path: packages/render-anim/src/index.ts
provides: "唯一的 `export * from './types';`"
- path: .planning/phases/09-minimum-browser-runnable/09-52-SUMMARY.md
provides: "逐文件最终形态、Task 1 记录的类型 / 命名方案、各门禁真实结果、跳过项声明、未校准估算、0 次提交、编号说明与待确认项"
key_links:
- "`ITexture` / `ITextureAnimater` ← `@motajs/render` 根导出 ← `packages/render/src/index.ts` → `assets/index.ts` → `assets/types.ts`;解析路径只来自根 `tsconfig.json` 的 `@motajs/*` → `./packages/*/src`,不得为让导入生效新增包级 `tsconfig.json`"
- "`IAnimationData.animater` 的泛型实参 `IAnimationAnimaterData` ↔ 后续 `RenderAnimater implements ITextureAnimater<IAnimationAnimaterData>`(本计划不实现,只固定泛型形状)"
- "`types.ts` 的 `readonly` 数组 ↔ 后续解析器对外返回的只读视图;实现阶段不得把 `objects` / `bitmaps` / `frames` / `sound` 改成可变数组"
- "`index.ts` 的单一 `export *` ↔ `@motajs/render` / `@motajs/animate` 的包导出习惯;星号导出不得跨包转发 `@motajs/render` 的类型"
- "`package.json` 的 `workspace:*` ↔ `pnpm-workspace.yaml` 的 `packages/*`;本计划不安装依赖,锁文件变更由用户后续决定"
assumptions:
- "**工作树基线(规划日实测)**:`git status --porcelain -uall -- packages/render-anim` 输出为空,`packages/render-anim` 目录不存在;09-50 / 09-51 的计划与摘要已提交(HEAD 为 `42a3d41`)。"
- "**解析与配置基线(规划日实测)**:根 `tsconfig.json` 的 `include` 已覆盖 `packages/**/*.ts`,`paths` 已把 `@motajs/*` 映射到 `./packages/*/src`;因此新包无需新增或修改任何 tsconfig / eslint / prettier 配置。"
- "**包清单范式(规划日实测)**:`packages/render/package.json` 只有 `name`;`packages/audio/package.json` 是 `name` + `dependencies`(`workspace:*`)的既有范式;新包按后者书写。"
- "**工具链(规划日实测)**:`node` v24.21.0;`node_modules/prettier/bin/prettier.cjs`、`node_modules/eslint/bin/eslint.js`、`node_modules/vue-tsc/bin/vue-tsc.js`、`node_modules/typescript/bin/tsc` 均存在;仓库根 `.prettierrc` 为 `printWidth: 80` / `tabWidth: 4` / `singleQuote: true` / `arrowParens: avoid` / `trailingComma: none` / `endOfLine: crlf`;`core.autocrlf=true`。"
- "**本 run 无 RESEARCH / 无 VALIDATION**:本阶段无 requirement ID(`requirements: []`),Nyquist 不适用;spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)。"
prohibitions:
- "不得在用户恰好回复「可以执行」之前执行任何任务或派发执行子代理(D-03 / AGENTS.md)。"
- "不得实现 `RenderAnimater` / `parseRenderAnimation`,不得写任何 class、function、测试、示例、demo 或入口逻辑。"
- "不得新增 `packages/render-anim/tsconfig.json`、构建脚本、`exports` / `main` / `module` / `types` / `version` / `private` / `sideEffects` 等字段;用户只要求 name + workspace 依赖。"
- "不得修改 09-50 / 09-51 的计划与摘要、其它包、根配置(`tsconfig.json` / `eslint.config.js` / `.prettierrc` / `pnpm-workspace.yaml`)、锁文件、`STATE.md`、`ROADMAP.md`、`09-CONTEXT.md`。"
- "不得运行 `pnpm install` / 构建 / 测试 / dev server / 浏览器;不得执行任何 git 写命令(`add` / `commit` / `stash` / `checkout` / `restore` / `reset`),本计划全程 0 次提交。"
- "不得改写用户给定的类型名、成员名、成员顺序、成员类型、`readonly` 与可选性;不得新增任何导出符号或成员;不得把 `AnimationBlendMode` 改回字符串字面量联合,不得给枚举成员显式初始化、改成员名或调整 `Normal` → `Add` 顺序。"
- "不得出现 `import type`、`any`、`as`、非 jsDoc 的多行注释、解释下一行代码的注释,或改写既有注释(本计划不触碰任何既有文件)。"
open_items:
- "**OI-1(已裁定:用户选择 B):** `AnimationBlendMode` 由字符串字面量联合 `'normal' | 'add'` 改为 `export const enum AnimationBlendMode`(成员 `Normal` / `Add`,隐式数值 0 / 1,成员 jsDoc 分别为「普通混合」「加算混合」),符合 dev.md「不使用字符串作为键或特殊标识符(如枚举值、事件名、状态名等),应使用枚举代替」。`IAnimationData.blend` 类型不变,仍为 `AnimationBlendMode`。Task 0 只记录该裁定,不再询问 (A)/(B)。"
- "**OI-2(只记录):** `IAnimationSound.name` 为 `string`,属 dev.md 允许的「字符串类型的 id 别名 / 文件路径」一类,保留。"
- "**OI-3(只记录):** 用户将自行审阅 / 修改类型骨架后再进入实现;本计划不实现、不测试、不宣称任何运行时行为。`RenderAnimater` 与 `parseRenderAnimation(buffer: ArrayBuffer): Promise<IAnimationData>` 另立后续计划。"
- "**OI-4(只记录):** 成员(含枚举成员 `Normal` / `Add`)jsDoc 使用单行中文风格(与 `packages/render/src/assets/types.ts` 一致);接口与枚举本身按 dev.md 不写 jsDoc。如需英文注释或接口级说明,请指示。"
- "**OI-5(只记录):** `estimate` 未校准(`confidence: low`);本计划未运行任何构建或测试,因此不做运行时可用性主张。"
---
<objective>
Phase 9 · 第 52 个计划(新增的独立增量)—— **为即将新增的包 `@motajs/render-anim` 建立类型声明骨架**,让用户先审阅 / 修改公开类型形状,再进入实现阶段。
**要解决的问题:** 动画渲染需要一个新的包承载动画数据与动画器,但公开类型的字段、只读性、可选性与泛型形状需要先由用户确认。若直接写实现,后续调整类型会连带返工实现与调用方。因此本计划只落类型骨架,不写任何实现。
**做法(锁定设计,仅类型):**
- `packages/render-anim/package.json`:只声明 `name` 与 `dependencies['@motajs/render'] = 'workspace:*'`。
- `packages/render-anim/src/types.ts`:只放用户给定的 7 个导出类型——1 个 `export const enum AnimationBlendMode`(成员 `Normal` / `Add`,隐式数值,成员 jsDoc「普通混合」「加算混合」)与 6 个接口(`IAnimationBitmap` / `IAnimationSound` / `IAnimationObject` / `IAnimationFrame` / `IAnimationAnimaterData` / `IAnimationData`),成员级中文 jsDoc,`ITexture` / `ITextureAnimater` 从 `@motajs/render` 普通引入。
- `packages/render-anim/src/index.ts`:只 `export * from './types';`。
- 已确认的设计语义只记录、不实现:浏览器专用;`RenderAnimater` 将实现 `ITextureAnimater<IAnimationAnimaterData>`(`once` / `cycled` 返回 `Generator<ITextureRenderable>`,帧时序 / 循环由外部驱动);逐帧惰性合成,产出的可渲染对象在下一次 `.next()` 前有效;使用 `MotaOffscreenCanvas2D` 并调用 `setHD(false)` / `setAntiAliasing(false)`,不直接调浏览器 canvas API;空帧产出透明小图;帧尺寸可不同,每帧自带 `width` / `height` / `originX` / `originY`;`blend` 以 `AnimationBlendMode` 存于文件并由解析数据暴露,动画器不决定混合;`sound` 由宿主播放;解析函数为 `parseRenderAnimation(buffer: ArrayBuffer): Promise<IAnimationData>`。
**约束:** 只写 `files_modified` 的 4 个路径;不新增依赖安装、不改任何既有文件与配置;不提交代码;不做浏览器 / 测试验证,只做只读静态门禁。(D-01 增量派发 / D-02 计划累积追加 / D-04 命名先反馈 / D-05 默认不逐阶段验证 / D-06 范围由用户界定。)
**修改方式声明(AGENTS.md):** 本计划的全部文件写入(新建 3 个包文件、写 1 个 SUMMARY)由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 外无任何写入。
**可见跳过:** spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)。本阶段无 requirement ID,不创建 RESEARCH.md / VALIDATION.md,Nyquist 验证不适用。
</objective>
## 本计划产出物(Artifacts this plan produces)
| 类型 | 名称 / 路径 | 说明 |
|------|-------------|------|
| 新建(包清单) | `packages/render-anim/package.json` | `name` + `@motajs/render` 的 `workspace:*` 依赖,仅两个键 |
| 新建(类型) | `packages/render-anim/src/types.ts` | 1 个 `export const enum` + 6 个接口 + 成员级 jsDoc,无任何实现 |
| 新建(导出) | `packages/render-anim/src/index.ts` | 仅 `export * from './types';` |
| 交付 | `.planning/phases/09-minimum-browser-runnable/09-52-SUMMARY.md` | 执行摘要、门禁结果、跳过项、未校准估算、0 次提交 |
## 类型骨架逐成员对照(Task 2 的唯一依据)
| 导出类型 | 成员(顺序固定) | 成员 jsDoc 语义(中文单行) |
|----------|------------------|------------------------------|
| `AnimationBlendMode`(`export const enum`) | `Normal` | 普通混合(隐式数值 0) |
| | `Add` | 加算混合(隐式数值 1) |
| `IAnimationBitmap` | `readonly x: number` | 位图在纹理中的横坐标 |
| | `readonly y: number` | 位图在纹理中的纵坐标 |
| | `readonly width: number` | 位图宽度 |
| | `readonly height: number` | 位图高度 |
| `IAnimationSound` | `readonly name: string` | 音效名称 |
| | `readonly volume?: number` | 播放音量 |
| | `readonly pitch?: number` | 播放音高 |
| `IAnimationObject` | `readonly index: number` | 使用的位图索引 |
| | `readonly x: number` | 相对帧原点的横坐标 |
| | `readonly y: number` | 相对帧原点的纵坐标 |
| | `readonly scale: number` | 缩放比例 |
| | `readonly opacity: number` | 不透明度 |
| | `readonly mirror: number` | 镜像方向 |
| | `readonly rotate: number` | 旋转角度 |
| `IAnimationFrame` | `readonly width: number` | 帧宽度 |
| | `readonly height: number` | 帧高度 |
| | `readonly originX: number` | 帧原点的横坐标 |
| | `readonly originY: number` | 帧原点的纵坐标 |
| | `readonly objects: readonly IAnimationObject[]` | 帧内的绘制对象 |
| | `readonly sound?: readonly IAnimationSound[]` | 帧内播放的音效 |
| `IAnimationAnimaterData` | `readonly bitmaps: readonly IAnimationBitmap[]` | 位图列表 |
| | `readonly frames: readonly IAnimationFrame[]` | 帧列表 |
| `IAnimationData` | `readonly texture: ITexture` | 合成帧使用的纹理 |
| | `readonly animater: ITextureAnimater<IAnimationAnimaterData>` | 动画器 |
| | `readonly blend: AnimationBlendMode` | 混合模式 |
**`AnimationBlendMode` 声明形态(OI-1 B):** `export const enum AnimationBlendMode {`,随后 4 空格缩进的 `/** 普通混合 */` 与 `Normal,`,再随后 4 空格缩进的 `/** 加算混合 */` 与 `Add`(末成员不带尾逗号),最后顶格 `}`。成员为隐式数值(`Normal` = 0、`Add` = 1),不写显式初始化;声明本身不写 jsDoc;成员顺序固定为 `Normal` → `Add`。`IAnimationData.blend` 仍为 `AnimationBlendMode`。
## 覆盖审计(Multi-Source Coverage Audit)
| 源 | 条目 | 覆盖位置 |
|----|------|----------|
| GOAL(ROADMAP §Phase 9) | 「可在浏览器中运行并验证的最小版本」伞目标 | 本增量只建立类型契约,不新增可运行能力;SUMMARY 如实记录 |
| REQ | 本阶段无 requirement ID(`requirements: []`) | Nyquist 不适用 |
| RESEARCH | 本 run 无 RESEARCH.md | 不适用 |
| CONTEXT D-01 | 增量派发,一个任务一个计划 | 本计划即该派发的唯一计划,不拆分 |
| CONTEXT D-02 | 计划可累积追加 | plan 52,排在 09-01..09-51 之后(编号说明见 frontmatter) |
| CONTEXT D-03 | 执行前必须先汇报并等待「可以执行」 | Task 0(`blocking-human`),`autonomous: false` |
| CONTEXT D-04 | 新命名必须先反馈获批 | 类型 / 成员名全部由用户提供,Task 0 声明不新增任何未获批名称;OI-1 已按用户裁定 (B) 定为 `export const enum` |
| CONTEXT D-05 | 默认不逐阶段验证 | 本计划仍做只读静态门禁,但明确不做运行时验证 |
| CONTEXT D-06 | 范围由用户界定,不擅自扩大 | `files_modified` 4 项 + Task 4 范围 / 哈希闸门 |
| 用户设计 | 新包 `@motajs/render-anim`,依赖 `@motajs/render`(`workspace:*`) | Task 2(`package.json`) |
| 用户设计 | 只建 `package.json` / `src/types.ts` / `src/index.ts`,只含类型 | Task 2 + Task 3(无实现门禁) |
| 用户设计 | 1 个枚举 + 6 个接口与逐成员形状(名称、类型、`readonly`、可选性) | 「类型骨架逐成员对照」+ Task 2 + Task 3 内容门禁 |
| 用户设计 | `ITexture` / `ITextureAnimater` 来自 `@motajs/render` | Task 2(普通引入)+ Task 3 |
| 用户设计 | 已确认的运行时设计只记录、不实现 | objective + OI-3 + SUMMARY |
| 用户设计 | 验证只读静态,不跑游戏 / 浏览器 / 测试 | Task 3 / Task 4 + `<verification>` |
| 用户设计 | `autonomous: false`,Task 0 汇报并等待「可以执行」 | Task 0 |
| 用户裁定 OI-1 | `AnimationBlendMode` 用 `export const enum`(成员 `Normal` / `Add`) | 「类型骨架逐成员对照」+ Task 0 ⑤ 记录 + Task 2 / Task 3 门禁 |
| 用户设计 | 不提交、不越界、不改 09-50 / 09-51 | prohibitions + Task 4 范围 / 哈希闸门 |
<execution_context>
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
@AGENTS.md
@dev.md
@tsconfig.json
@.prettierrc
@eslint.config.js
@pnpm-workspace.yaml
@packages/render/package.json
@packages/render/src/index.ts
@packages/render/src/assets/types.ts
@packages/render/src/assets/texture.ts
@packages/render/src/core/canvas2d.ts
@packages/audio/package.json
@packages/animate/src/index.ts
</context>
<tasks>
<task type="checkpoint:human-verify" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报新包、三个文件形态、类型骨架(含 OI-1 B 的 `export const enum AnimationBlendMode`)、dev.md 合规、影响范围、验证形态,等待「可以执行」</name>
<precondition>`packages/render-anim` 目录尚不存在;根 `tsconfig.json` 的 `include` 覆盖 `packages/**/*.ts` 且 `paths` 已映射 `@motajs/*`;`node_modules` 内 prettier / eslint / vue-tsc / typescript 均存在;`node` v24.21.0。</precondition>
<files>无(本任务只读,不写任何文件;汇报后停下等待「可以执行」)</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-01 增量派发、D-03 汇报关卡、D-04 命名批准、D-06 范围界定)
- AGENTS.md(执行前须获批、命名先反馈、既有注释不得修改、最小修改也走 GSD、不派发额外子代理)
- dev.md(CRLF、4 空格、首行 import、禁止 `import type`、无 `any`、成员 jsDoc 在源头、字符串标识符规则、文件命名)
- packages/render/src/assets/types.ts(`ITexture` / `ITextureAnimater` / `ITextureRenderable` 的既有形状与 jsDoc 风格)
- packages/render/package.json 与 packages/audio/package.json(包清单范式)
- 根 tsconfig.json(`@motajs/*` 路径映射)与 .prettierrc / eslint.config.js
</read_first>
<acceptance_criteria>
- 已向用户逐条汇报 ①–⑫,包含三个文件的逐字形态、1 个 `export const enum AnimationBlendMode`(成员 `Normal` / `Add`)+ 6 个接口的逐成员名称 / 类型 / `readonly` / 可选性,以及 `IAnimationData extends IAnimationAnimaterData`。
- 已把 `readonly` 修饰、成员类型、可选性与 `extends` 关系列为显式批准项,未以「名称级骨架一致」代替成员级确认。
- 已向用户报告 OI-1 的最终裁定:用户选择 (B),`AnimationBlendMode` 使用 `export const enum`(成员 `Normal` / `Add`,隐式数值,成员 jsDoc 为「普通混合」/「加算混合」),`IAnimationData.blend` 仍为 `AnimationBlendMode`;本轮不再询问 (A)/(B)。
- 用户针对本修订版明确回复「可以执行」后才进入 Task 1–4;OI-1 的 (B) 裁定已在规划期由用户消息「B,可以执行」记录,本关卡只做汇报与执行批准。
</acceptance_criteria>
<what-built>
一份待批准的执行计划(`09-52-PLAN.md`):新建 `@motajs/render-anim` 包的**类型声明骨架**(3 个文件),不写任何实现;用户审阅 / 修改类型后再进入实现阶段。
</what-built>
<action>
**停下并向用户汇报,等待其恰好回复「可以执行」后才继续;本任务不写任何文件、不建目录、不派发执行子代理。**
① **要做什么 / 为什么:** 为即将新增的包 `@motajs/render-anim` 建立公开类型契约,先让用户确认字段、只读性、可选性与泛型形状,避免后续实现返工。本计划**只写类型**,不实现 `RenderAnimater`、不实现 `parseRenderAnimation`。
② **精确文件形态(3 个新文件,逐字):**
- `packages/render-anim/package.json`:只有两个顶层键,4 空格缩进 + CRLF + 末尾换行——`name` 为 `@motajs/render-anim`;`dependencies` 为 `{ "@motajs/render": "workspace:*" }`。不写 `version` / `private` / `type` / `main` / `module` / `types` / `exports` / `scripts`。
- `packages/render-anim/src/types.ts`:第一行是 `import { ITexture, ITextureAnimater } from '@motajs/render';`(普通引入,随后一个空行);之后按固定顺序导出 `export const enum AnimationBlendMode`(成员 `Normal` / `Add`,隐式数值,末成员不带尾逗号)、`IAnimationBitmap`、`IAnimationSound`、`IAnimationObject`、`IAnimationFrame`、`IAnimationAnimaterData`、`IAnimationData`;成员名 / 顺序 / 类型 / `readonly` / 可选性与用户骨架完全相同(对照表见计划正文「类型骨架逐成员对照」);**汇报时必须附完整逐成员清单(所属类型 / 成员名 / 类型 / `readonly` / 可选性 / `extends`),不得只报 7 个类型名**;枚举成员与接口成员上方各一行中文 jsDoc(`普通混合` / `加算混合`),枚举与接口本身不写 jsDoc;文件内无 `class` / `function` / 箭头函数 / `let` / `export default` / 其它运行时语句,`const` 仅出现在 `export const enum AnimationBlendMode` 声明处。
- `packages/render-anim/src/index.ts`:只有一行 `export * from './types';`(CRLF 结尾),不转发 `@motajs/render` 的符号。
③ **已确认但本计划不实现的设计(只记录):** 浏览器专用;`RenderAnimater` 将实现 `ITextureAnimater<IAnimationAnimaterData>`(`once` / `cycled` 返回 `Generator<ITextureRenderable>`,帧时序与循环由外部驱动);逐帧惰性合成,产出的可渲染对象在下一次 `.next()` 前有效,下一次恢复或 `return` 时清理上一帧;使用 `MotaOffscreenCanvas2D` 并调用 `setHD(false)` / `setAntiAliasing(false)`,不直接调浏览器 canvas API;空帧产出透明小图;帧尺寸可不同,每帧自带 `width` / `height` / `originX` / `originY`;`blend` 以 `AnimationBlendMode` 存于文件并由解析数据暴露,动画器不决定混合;`sound` 由宿主播放;解析函数为 `parseRenderAnimation(buffer: ArrayBuffer): Promise<IAnimationData>`。
④ **dev.md 合规点(逐条):** UTF-8 无 BOM + CRLF + 4 空格、无制表符;`types.ts` 第一行必须是 `import`,import 之间不夹注释与空行;不使用 `import type`;不使用 `any` / `as` / `?.` / `!`;所有成员显式类型并加 `readonly`;枚举成员与接口成员 jsDoc 在源头(`types.ts`)且为单行中文;接口与枚举本身不写 jsDoc(dev.md 不建议);不使用非 jsDoc 的多行注释;不写复述代码的注释;文件名为小驼峰;`AnimationBlendMode` 使用 `export const enum` 且成员为隐式数值,符合 dev.md「不使用字符串作为键或特殊标识符(如枚举值、事件名、状态名等),应使用枚举代替」;公开类型集中在 `types.ts`;`index.ts` 只做同目录星号导出,不跨包转发。
⑤ **OI-1 已裁定为 (B)(记录,不再询问):** `AnimationBlendMode` 使用 `export const enum`,成员为 `Normal`(jsDoc「普通混合」)与 `Add`(jsDoc「加算混合」),4 空格缩进、隐式数值、末成员不带尾逗号,声明本身不写 jsDoc;`IAnimationData.blend` 仍为 `AnimationBlendMode`。这正是 dev.md「不使用字符串作为键或特殊标识符(如枚举值、事件名、状态名等),应使用枚举代替」的要求,原有字符串字面量联合的例外不再成立。另 `IAnimationSound.name: string` 属允许的字符串 id 一类(OI-2),保留。
⑥ **命名与成员级内容:** 类型名与成员名全部由用户提供,本计划不新增任何用户未提供的名称;jsDoc 不引入新标识符。若需改名,请在批准前说明。以下成员级内容同样须逐项批准,不得仅以「7 个名称一致」视为通过:`readonly` 修饰、成员类型(`number` / `string` / `readonly` 数组 / `ITexture` / `ITextureAnimater<IAnimationAnimaterData>` / `AnimationBlendMode`)、可选性(`volume?` / `pitch?` / `sound?`)、`IAnimationData extends IAnimationAnimaterData` 与 `AnimationBlendMode` 的 `export const enum` 形态(成员序列 `Normal` → `Add`),一律以计划正文「类型骨架逐成员对照」为准。
⑦ **影响范围:** 只新增 `packages/render-anim/package.json`、`packages/render-anim/src/types.ts`、`packages/render-anim/src/index.ts`,以及执行后的 `09-52-SUMMARY.md`;不改任何既有文件 / 配置 / 其它包 / 09-50 与 09-51 的计划与摘要;不运行 `pnpm install`,锁文件不变;不新增包级 tsconfig(根配置已覆盖并已映射 `@motajs/*`)。
⑧ **验证形态(只读静态):** prettier `--check`(3 个文件)、eslint(2 个 ts 文件)、node 结构 / 内容门禁(CRLF、无制表符、1 个 `export const enum`(成员 `Normal` / `Add`)+ 6 个接口、逐成员片段、逐接口成员归属、无 `import type` / `any` / `class` / `function` / `let` / 箭头函数 / `export default`,且 `const` / `enum` 仅出现在 `AnimationBlendMode` 声明处、`index.ts` 唯一内容、`package.json` 仅两个键);可选类型门禁只过滤 `packages/render-anim` 的诊断,全仓既有类型错误与 `vue-tsc` 非 0 退出码属既有基线、不视为本次失败。任何 prettier / eslint 失败都由 Task 2 修正,Task 3 只上报并重跑,不改源码。**不运行测试、构建、dev server 或浏览器;不做运行时主张。**
⑨ **不提交:** 全程 0 次提交,禁止任何 git 写命令;改动留在工作树供审阅。
⑩ **编号说明(须报备):** 派发编号为 09-52 / 波次 1;`09-01`..`09-51` 的计划均已占用,本计划不删除、不覆盖任何既有记录。
⑪ **待确认项:** OI-1 **已裁定为 (B)**(`export const enum AnimationBlendMode`),本轮不再询问;OI-4(jsDoc 语言与接口级注释)、OI-5(估算未校准);OI-3(实现阶段另立计划)。
⑫ **已知限制:** 本计划不验证类型能否通过全仓类型检查(仓库存在大量既有 / 预期类型错误);不验证任何运行时行为;SUMMARY 将如实声明未运行项。
</action>
<verify>用户针对本修订版恰好回复「可以执行」后才进入 Task 1–4;OI-1 已按用户此前的「B,可以执行」裁定为 (B),本任务只汇报 `export const enum AnimationBlendMode` 形态并请求执行批准,不再询问 (A)/(B)。在此之前不执行 Task 1 / Task 2 / Task 3 / Task 4 的任何一步,也不派发执行子代理。</verify>
<done>已完整汇报 ①–⑫(含三个文件逐字形态、1 个 `export const enum` + 6 个接口的逐成员类型 / `readonly` / 可选性、已确认但暂不实现的设计、dev.md 合规点、OI-1 B 的裁定记录、命名、影响范围、验证形态、不提交、编号、待确认项、已知限制),并收到用户针对本修订版的「可以执行」回复。</done>
<how-to-verify>
逐条核对 ①–⑫ 是否与你的意图一致,特别是:② 的 `package.json` 仅两个键与 `index.ts` 单行导出;⑤ 的 OI-1 已裁定为 (B) 的 `export const enum` 形态(成员 `Normal` / `Add`);⑥ 的成员级 `readonly`、成员类型、可选性与 `extends` 逐项确认;④ 的 jsDoc 方案;⑦ 的范围;⑧ 的「只读静态、可选类型门禁」口径;⑩ 的编号说明。
</how-to-verify>
<resume-signal>OI-1 已按你的「B,可以执行」裁定为 (B)(`export const enum AnimationBlendMode`),无需再次选择;请在确认本修订版(尤其是 ② 的枚举形态、⑤ 的 (B) 记录、④ 的 jsDoc、⑧ 的验证口径)后,回复恰好四个字「可以执行」。任何修改意见请直接说明。</resume-signal>
</task>
<task type="auto">
<name>Task 1: 读取既有类型与工具链约定,记录精确的类型 / 命名方案与执行基线(只读)</name>
<files>无(本任务只读;不写仓库内任何文件)</files>
<read_first>
- AGENTS.md(执行前获批、命名先反馈、既有注释不得修改、最小修改也走 GSD、GSD 流程豁免范围)
- dev.md(CRLF、4 空格、首行 import、禁止 `import type`、无 `any`、成员 jsDoc 在源头、字符串标识符规则)
- packages/render/src/assets/types.ts(`ITexture` / `ITextureAnimater<T>` / `ITextureRenderable` 的既有形状与 jsDoc 风格)
- packages/render/src/assets/texture.ts(确认 `ITexture` 是接口而非类)
- packages/render/src/core/canvas2d.ts(`MotaOffscreenCanvas2D.setHD` / `setAntiAliasing` 签名)
- packages/render/package.json 与 packages/audio/package.json(包清单范式)
- tsconfig.json、.prettierrc、eslint.config.js(`@motajs/*` 路径映射与格式化 / lint 配置)
- 09-50-PLAN.md、09-50-SUMMARY.md、09-51-PLAN.md、09-51-SUMMARY.md、09-52-PLAN.md(哈希基线对象)
</read_first>
<action>
仅在收到用户恰好回复「可以执行」之后执行。本任务不写仓库内文件,只读取并记录,供 Task 2 落码、Task 4 写入 SUMMARY。
1. 读取并记录以下事实(逐条写进执行笔记,Task 4 汇总进 SUMMARY):
- `AGENTS.md`:执行前须获批、命名先反馈、不得修改既有注释、最小修改也走 GSD、GSD 流程豁免范围。
- `dev.md`:CRLF、4 空格、模块无副作用、不转发导出、无类型导入、import 必须在首行且中间不夹注释 / 空行、命名规则、注释规范(成员 jsDoc 在源头、接口 / 枚举本身不写、单行注释 `// ` 后一个空格、不得复述代码)、类型规范(无必要 `any`、显式成员类型、合理 `readonly`、对象类型单独 `interface`、公开接口放 `types.ts`)、字符串标识符规则。
- `packages/render/src/assets/types.ts`:`ITexture`、`ITextureAnimater<T>`(`once` / `cycled` 返回 `Generator<ITextureRenderable, void>`)、`ITextureRenderable` 的既有形状与成员 jsDoc 风格(单行、中文、无句号)。
- `packages/render/src/assets/texture.ts`:`export class Texture implements ITexture` 的存在,确认 `ITexture` 是接口而非类。
- `packages/render/src/core/canvas2d.ts`:确认 `MotaOffscreenCanvas2D` 的 `setHD(hd)` / `setAntiAliasing(anti)` 方法签名(仅供 SUMMARY 记录后续实现约定,本计划不调用)。
- `packages/render/package.json`(只有 `name`)与 `packages/audio/package.json`(`name` + `dependencies` 的 `workspace:*` 范式)。
- 根 `tsconfig.json`:`include` 覆盖 `packages/**/*.ts`,`paths['@motajs/*'] = ['./packages/*/src']`,`strict: true`,`isolatedModules: true`,`noEmit: true`。
- `.prettierrc`(`printWidth: 80` / `tabWidth: 4` / `singleQuote: true` / `arrowParens: avoid` / `trailingComma: none` / `endOfLine: crlf`)与 `eslint.config.js`(typescript-eslint + prettier 插件)。
2. 记录**精确类型 / 命名方案**:三个文件的逐行结构、`types.ts` 的 1 个 `export const enum AnimationBlendMode` + 6 个接口与逐成员名称 / 类型 / `readonly` / 可选性(以计划正文「类型骨架逐成员对照」为准)、每个成员的单行中文 jsDoc 文本(含枚举成员「普通混合」/「加算混合」)、`package.json` 的两个键与缩进、`index.ts` 的唯一一行。方案中不得出现用户未提供的公开名称。
3. 采集**范围基线**:`%TEMP%` **不是文件系统路径**,基线文件的实际位置由 Node 的 `process.env.TEMP` 解析;后续所有门禁都写作 `(process.env.TEMP||'.')`,Windows 下 `TEMP` 始终存在,两处解析到同一目录。用下面这条 `node` 命令把当前 `git status --porcelain -uall` 的完整输出(先统一为 LF)写入 `gsd-09-52-status.txt`(UTF-8 无 BOM),并回显绝对路径:`node -e "const{execSync}=require('child_process'),fs=require('fs');const out=execSync('git status --porcelain -uall',{encoding:'utf8'}).replace(/\r\n/g,'\n');const p=(process.env.TEMP||'.')+'/gsd-09-52-status.txt';fs.writeFileSync(p,out,{encoding:'utf8'});console.log(p);"`。该基线用于 Task 4 做集合差,因为工作树可能已存在 09-51 的未提交改动。写入后必须立刻断言:基线文件内容与当前 `git status --porcelain -uall` 完全一致(见 `<verify>` 第 3 条),否则重新采集;不得用 `.trim()` 之外的方式放宽比较。
4. 采集**哈希基线**:对 `.planning/phases/09-minimum-browser-runnable/09-50-PLAN.md`、`09-50-SUMMARY.md`、`09-51-PLAN.md`、`09-51-SUMMARY.md`、`09-52-PLAN.md` 逐个执行 `git hash-object`,把 `路径 哈希` 写入同一 `process.env.TEMP` 目录下的 `gsd-09-52-hashes.txt`(UTF-8 无 BOM),命令:`node -e "const{execSync}=require('child_process'),fs=require('fs');const files=['.planning/phases/09-minimum-browser-runnable/09-50-PLAN.md','.planning/phases/09-minimum-browser-runnable/09-50-SUMMARY.md','.planning/phases/09-minimum-browser-runnable/09-51-PLAN.md','.planning/phases/09-minimum-browser-runnable/09-51-SUMMARY.md','.planning/phases/09-minimum-browser-runnable/09-52-PLAN.md'];const lines=files.map(f=>f+' '+execSync('git hash-object '+f,{encoding:'utf8'}).trim());const p=(process.env.TEMP||'.')+'/gsd-09-52-hashes.txt';fs.writeFileSync(p,lines.join('\n')+'\n',{encoding:'utf8'});console.log(p);console.log(lines.join('\n'));"`(每行恰为「无空格路径 + 空格 + 40 位小写十六进制」)。把 `09-52-PLAN.md` 纳入基线,是为了证明执行期间计划文件本身未被改写(它不在 `files_modified` 内)。
5. 确认 `packages/render-anim` 目录当前不存在、`git status --porcelain -uall -- packages/render-anim` 输出为空。任一命令非 0 退出或输出与预期不符,必须停下上报,不得继续 Task 2。
</action>
<verify>
<automated>node -e "const{execSync}=require('child_process');const out=execSync('git status --porcelain -uall -- packages/render-anim',{encoding:'utf8'});if(out.trim()){console.error(out);process.exit(1);}console.log('OK: render-anim scope empty');"</automated>
<fails_when>`git status --porcelain -uall -- packages/render-anim` 输出非空(打印该输出,脚本退出码 1)。</fails_when>
<automated>node -e "const fs=require('fs');const p=(process.env.TEMP||'.')+'/gsd-09-52-hashes.txt';const s=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n').trim().split('\n').filter(Boolean);const ok=s.length===5&&s.every(l=>{const p=l.split(' ');return p.length===2&&p[1].length===40&&!/[^0-9a-f]/.test(p[1]);});if(!ok){console.error(s.join('\n'));process.exit(1);}console.log(s.join('\n'));"</automated>
<fails_when>哈希基线不是 5 行,或任一行不是「无空格路径 + 40 位小写十六进制」,脚本打印内容并以 1 退出。</fails_when>
<automated>node -e "const{execSync}=require('child_process'),fs=require('fs');const p=(process.env.TEMP||'.')+'/gsd-09-52-status.txt';const base=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n').trim();const now=execSync('git status --porcelain -uall',{encoding:'utf8'}).replace(/\r\n/g,'\n').trim();if(base!==now){console.error('baseline drift');process.exit(1);}console.log('OK: status baseline matches current tree');"</automated>
<fails_when>基线文件与当前 `git status --porcelain -uall` 不一致(打印 `baseline drift`,退出码 1)。</fails_when>
</verify>
<acceptance_criteria>
- 已读取并在执行笔记中记录 `read_first` 列出的全部文件事实(规则、既有类型、包清单范式、根配置、格式化 / lint 配置)。
- `process.env.TEMP` 目录下的 `gsd-09-52-status.txt`(与门禁 `(process.env.TEMP||'.')` 解析一致,不是 `%TEMP%` 字面量)已生成,且 Task 1 结束时与当前 `git status --porcelain -uall` 完全一致。
- `process.env.TEMP` 目录下的 `gsd-09-52-hashes.txt`(与门禁同一路径解析)恰有 5 行,覆盖 09-50-PLAN / 09-50-SUMMARY / 09-51-PLAN / 09-51-SUMMARY / 09-52-PLAN,每行「无空格路径 + 空格 + 40 位哈希」。
- `packages/render-anim` 在 `git status --porcelain -uall -- packages/render-anim` 下为空。
- 已记录 1 个 `export const enum AnimationBlendMode` + 6 个接口与逐成员方案(名称 / 类型 / `readonly` / 可选性),且未新增用户未提供的公开名称。
</acceptance_criteria>
<done>已读取并记录 dev.md / AGENTS.md 的适用规则、`@motajs/render` 的既有类型与包清单范式、根 tsconfig 与格式化配置;已记录 1 个 `export const enum AnimationBlendMode` + 6 个接口与逐成员方案;`process.env.TEMP` 目录下的 `gsd-09-52-status.txt` 与 `gsd-09-52-hashes.txt`(与门禁 `(process.env.TEMP||'.')` 解析一致,未使用不可作为路径的 `%TEMP%` 字面量)均已生成且内容完整(哈希基线含 `09-52-PLAN.md`,共 5 条);`packages/render-anim` 范围为空。</done>
</task>
<task type="auto">
<name>Task 2: 新建 `@motajs/render-anim` 的三个类型骨架文件</name>
<files>packages/render-anim/package.json, packages/render-anim/src/types.ts, packages/render-anim/src/index.ts</files>
<read_first>
- `.planning/phases/09-minimum-browser-runnable/09-52-PLAN.md` 的「类型骨架逐成员对照」与 `<objective>`(唯一内容依据;本任务不新增任何对外名称)
- `packages/render/src/assets/types.ts`(`ITexture` / `ITextureAnimater<T>` 的导入来源与成员 jsDoc 风格)
- `packages/audio/package.json`(`name` + `dependencies` 两键的 `workspace:*` 包清单范式)
- `.prettierrc` 与根 `tsconfig.json`(CRLF、4 空格、`@motajs/*` 路径映射)
</read_first>
<action>
仅在 Task 1 完成且收到「可以执行」之后执行。用 Write 工具直接创建文件(不得使用 `cat << 'EOF'` / here-string / `Set-Content` 拼内容);三个文件均为 UTF-8 无 BOM、CRLF 换行、4 空格缩进、无制表符、末尾保留换行。Write 工具传入的内容字符串必须显式包含 `\r\n`,不得依赖编辑器或 `core.autocrlf` 自动归一化。
**格式化授权(仅限本任务):** 三个文件写完后,本任务可以对且仅对这三个包文件运行 `node node_modules/prettier/bin/prettier.cjs --write packages/render-anim/package.json packages/render-anim/src/types.ts packages/render-anim/src/index.ts` 做格式归一化;这是本计划唯一被授权执行 `prettier --write` 的任务。Task 3 严格只读,任何 prettier 或 eslint 失败都必须回到本任务修正(只改这三个包文件,不得加 `eslint-disable`、不得改 eslint 配置),再由 Task 3 重跑全部门禁。
**1. `packages/render-anim/package.json`**:只有两个顶层键,顺序为 `name`、`dependencies`。`name` 的值是 `@motajs/render-anim`;`dependencies` 只有一个条目,键为 `@motajs/render`、值为 `workspace:*`。不写 `version` / `private` / `type` / `main` / `module` / `types` / `exports` / `scripts` / `devDependencies` / `peerDependencies`;不加注释(JSON 不允许)。
**2. `packages/render-anim/src/types.ts`**:
- 第 1 行必须是 `import { ITexture, ITextureAnimater } from '@motajs/render';`(普通引入,禁止 `import type`);第 2 行空行;其后再写类型。文件内不得有第二段 import。
- 按固定顺序写出 7 个导出:`export const enum AnimationBlendMode`(成员 `Normal` / `Add`,隐式数值,声明与成员均不带显式初始化)、`IAnimationBitmap`、`IAnimationSound`、`IAnimationObject`、`IAnimationFrame`、`IAnimationAnimaterData`、`IAnimationData`(`extends IAnimationAnimaterData`)。类型之间空一行。
- `AnimationBlendMode` 逐字形态:顶格 `export const enum AnimationBlendMode {`;下一行 4 空格缩进 `/** 普通混合 */`;下一行 4 空格缩进 `Normal,`;下一行 4 空格缩进 `/** 加算混合 */`;下一行 4 空格缩进 `Add`(末成员不带尾逗号);最后顶格 `}`。成员顺序固定为 `Normal` → `Add`,不得增删成员或改写 jsDoc 文本,声明本身不写 jsDoc。
- 每个类型的成员名称、成员顺序、成员类型、`readonly`、可选性必须与计划正文「类型骨架逐成员对照」**完全一致**,不得增删成员、不得调整顺序、不得把可选改成必填或反之、不得把只读数组改成可变数组。
- 枚举成员与接口成员上方各写一行中文 jsDoc:`/** … */`,单行、结尾不加句号、只说明该成员含义(枚举成员为「普通混合」/「加算混合」,接口成员语义见对照表),不得出现英文符号堆砌、不得复述代码、不得假设使用场景。枚举与接口本身不写 jsDoc(dev.md 不建议给枚举 / 接口本身写注释)。
- 文件内不得出现 `class` / `function` / 箭头函数 / `let` / 默认导出 / 其它运行时语句 / `any` / `as` / `?.` / `!` / 多行 `/* */` 注释;`const` / `enum` 只允许出现在 `export const enum AnimationBlendMode` 这一处声明,不得有第二个枚举或常量。
**3. `packages/render-anim/src/index.ts`**:全文只有一行 `export * from './types';`(CRLF 结尾)。不得转发 `@motajs/render` 的任何符号,不得写默认导出、不得写第二行内容、不得加注释。
本任务只创建上述 3 个文件;不得创建 `tsconfig.json`、测试、示例、`README`、实现文件或任何目录内其它文件。
</action>
<verify>
<automated>node -e "const fs=require('fs');for(const f of ['packages/render-anim/package.json','packages/render-anim/src/types.ts','packages/render-anim/src/index.ts']){if(!fs.existsSync(f)){console.error('missing '+f);process.exit(1);}}console.log('OK: 3 files exist');"</automated>
<fails_when>任一文件不存在(打印 `missing <路径>`,退出码 1)。</fails_when>
<automated>node node_modules/prettier/bin/prettier.cjs --check packages/render-anim/package.json packages/render-anim/src/types.ts packages/render-anim/src/index.ts</automated>
<fails_when>输出不是 `All matched files use Prettier code style!`,或退出码非 0(prettier 列出待格式化文件)。</fails_when>
<automated>node -e "const fs=require('fs');const files=['packages/render-anim/package.json','packages/render-anim/src/types.ts','packages/render-anim/src/index.ts'];let bad=0;for(const f of files){const s=fs.readFileSync(f,'utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;const tabs=(s.match(/\t/g)||[]).length;const ok=lf===crlf&&tabs===0;if(!ok){bad++;}console.log((ok?'OK ':'FAIL')+' '+f+' lf='+lf+' crlf='+crlf+' tabs='+tabs);}process.exit(bad?1:0);"</automated>
<fails_when>任一行以 `FAIL` 开头(存在非 CRLF 换行或制表符),脚本以非 0 退出。</fails_when>
</verify>
<acceptance_criteria>
- 三个包文件均由 `gsd-executor` 用 Write 工具创建,Write 内容显式使用 `\r\n`;未使用 here-doc / here-string / `Set-Content` 拼内容。
- `prettier --write` 仅在需要时对这三个包文件运行,未触碰任何其它文件;任何 prettier / eslint 失败都由本任务在 `files_modified` 内修正;Task 3 不再执行任何写操作。
- 三个文件均为 CRLF、无制表符、末尾保留换行;`types.ts` 首行是普通 `import`。
- `package.json` 只有 `name` 与 `dependencies['@motajs/render'] = 'workspace:*'` 两个顶层键;`index.ts` 只有 `export * from './types';`。
- `types.ts` 的成员名 / 顺序 / 类型 / `readonly` / 可选性与「类型骨架逐成员对照」逐项一致。
</acceptance_criteria>
<done>三个文件已按骨架创建:`package.json` 仅 `name` + `@motajs/render: workspace:*`;`types.ts` 含 1 个 `export const enum AnimationBlendMode`(成员 `Normal` / `Add`)+ 6 个接口、成员形状与对照表逐字一致、每个成员有单行中文 jsDoc、首行为普通 import;`index.ts` 仅一行 `export * from './types';`;三个文件均为 CRLF 且 prettier `--check` 通过。</done>
</task>
<task type="auto">
<name>Task 3: dev.md 合规与内容门禁(只读静态检查)</name>
<files>无(本任务只读;不修改任何文件)</files>
<read_first>
- `.planning/phases/09-minimum-browser-runnable/09-52-PLAN.md` 的「类型骨架逐成员对照」(成员顺序、接口归属、类型、`readonly`、可选性的唯一判定依据)
- `packages/render-anim/src/types.ts`、`packages/render-anim/src/index.ts`、`packages/render-anim/package.json`(待检查对象)
- `dev.md`(CRLF、4 空格、首行 import、无 `import type` / `any`、成员 jsDoc 在源头、无实现体)
- `.prettierrc` 与 `eslint.config.js`(门禁使用的实际配置)
</read_first>
<action>
只运行只读检查;任何一项失败都必须回到 Task 2 修正后重跑全部检查,不得通过放宽检查、删除断言或忽略文件来「变绿」。本任务**严格只读**:不运行 `prettier --write`、不写任何文件。
1. **格式门禁(prettier)**:运行 `node node_modules/prettier/bin/prettier.cjs --check packages/render-anim/package.json packages/render-anim/src/types.ts packages/render-anim/src/index.ts`,必须退出码 0 且输出 `All matched files use Prettier code style!`。失败时**回到 Task 2** 修正(格式归一化由 Task 2 的 `prettier --write` 授权执行),本任务不得运行 `--write`、不得手改文件或格式规则;Task 2 修正后由本任务重跑全部检查。
2. **lint 门禁(eslint)**:运行 `node node_modules/eslint/bin/eslint.js packages/render-anim/src/types.ts packages/render-anim/src/index.ts`,必须退出码 0、无输出。失败时**本任务不修改任何源码**:把 eslint 的原始输出(文件、行号、规则名、说明)逐字记录并上报,然后**回到 Task 2** 修正(只改 `files_modified` 内的三个包文件,不得添加 `eslint-disable`、不得改 eslint 配置);Task 2 修正后由本任务从第 1 步起重跑全部门禁。
3. **换行与缩进门禁**:运行 `node -e "const fs=require('fs');const files=['packages/render-anim/package.json','packages/render-anim/src/types.ts','packages/render-anim/src/index.ts'];let bad=0;for(const f of files){const s=fs.readFileSync(f,'utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;const tabs=(s.match(/\t/g)||[]).length;const ok=lf===crlf&&tabs===0;if(!ok){bad++;}console.log((ok?'OK ':'FAIL')+' '+f+' lf='+lf+' crlf='+crlf+' tabs='+tabs);}process.exit(bad?1:0);"`,必须三行均为 `OK` 且退出码 0(每个换行都是 CRLF、无制表符)。
4. **内容与结构门禁**:运行 `node -e "const fs=require('fs');const t=fs.readFileSync('packages/render-anim/src/types.ts','utf8');const i=fs.readFileSync('packages/render-anim/src/index.ts','utf8');const p=JSON.parse(fs.readFileSync('packages/render-anim/package.json','utf8'));const fail=[];const names=['IAnimationBitmap','IAnimationSound','IAnimationObject','IAnimationFrame','IAnimationAnimaterData','IAnimationData'];for(const n of names){if(!new RegExp('export interface '+n+'\\b').test(t)){fail.push('missing '+n);}}const em=t.replace(/\r\n/g,'\n').match(/export const enum AnimationBlendMode \{\n([\s\S]*?)\n\}/);if(!em){fail.push('missing enum AnimationBlendMode');}else{const enumBody=em[1].split('\n').map(l=>l.trim()).filter(l=>l.length&&!l.startsWith('/**')&&!l.startsWith('*')&&!l.startsWith('//')).join(' ');if(enumBody!=='Normal, Add'){fail.push('enum members: '+enumBody);}}const snippets=['readonly x: number;','readonly y: number;','readonly width: number;','readonly height: number;','readonly name: string;','readonly volume?: number;','readonly pitch?: number;','readonly index: number;','readonly scale: number;','readonly opacity: number;','readonly mirror: number;','readonly rotate: number;','readonly originX: number;','readonly originY: number;','readonly objects: readonly IAnimationObject[];','readonly sound?: readonly IAnimationSound[];','readonly bitmaps: readonly IAnimationBitmap[];','readonly frames: readonly IAnimationFrame[];','readonly texture: ITexture;','readonly animater: ITextureAnimater<IAnimationAnimaterData>;','readonly blend: AnimationBlendMode;'];for(const s of snippets){if(t.indexOf(s)===-1){fail.push('missing snippet '+s);}}if(t.indexOf('import { ITexture, ITextureAnimater } from')!==0){fail.push('import not first');}if(/\bimport\s+type\b/.test(t)){fail.push('import type');}if(/\bany\b/.test(t)){fail.push('any');}if(/\bclass\b/.test(t)){fail.push('class');}if(/\bfunction\b/.test(t)){fail.push('function');}if(t.indexOf('=>')!==-1){fail.push('arrow');}const noEnum=t.replace(/export const enum AnimationBlendMode \{[\s\S]*?\n\}/,'');if(/\bconst\b/.test(noEnum)){fail.push('const');}if(/\blet\b/.test(t)){fail.push('let');}if(/\benum\b/.test(noEnum)){fail.push('enum');}if(/export\s+default/.test(t)){fail.push('export default');}const ic=i.replace(/\s/g,'');if(ic.indexOf('export*from')!==0||ic.indexOf('export',1)!==-1||ic.indexOf('./types')===-1){fail.push('index content');}if(Object.keys(p).sort().join(',')!=='dependencies,name'){fail.push('package keys');}if(p.name!=='@motajs/render-anim'){fail.push('package name');}if(!p.dependencies||p.dependencies['@motajs/render']!=='workspace:*'){fail.push('package dep');}console.log(fail.length?'FAIL\n'+fail.join('\n'):'OK');process.exit(fail.length?1:0);"`,必须输出 `OK` 且退出码 0。该门禁覆盖:1 个 `export const enum AnimationBlendMode`(成员 `Normal` / `Add` 及顺序)+ 6 个接口存在、25 个成员片段逐字存在、首行 import、无 `import type` / `any` / `class` / `function` / 箭头函数,且 `const` / `enum` 仅出现在枚举声明处、`index.ts` 只有唯一星号导出、`package.json` 只有两个键且依赖正确。
5. **逐接口成员门禁(把内容门禁从「片段存在」提升为「成员归属正确」)**:运行 `<verify>` 中的逐接口 node 命令。它按 `export interface <名称> ... { ... }` 逐块解析,把每个块的成员行序列与期望序列逐字比对;成员被放进错误接口、顺序变化、`readonly` / `?` / 类型变化都会使 `got !== want` 并判失败。接口数、`IAnimationData extends IAnimationAnimaterData`、以及 `AnimationBlendMode` 的 `export const enum` 声明与成员序列 `Normal` → `Add` 也在此断言。
6. **人工逐条复核(只读,且是绑定判定)**:打开 `types.ts`,逐成员对照计划正文「类型骨架逐成员对照」,确认成员顺序、接口归属、`readonly`、`?`、数组只读性、`extends` 关系、泛型实参全部一致,并确认 `AnimationBlendMode` 是 `export const enum`、成员恰为 `Normal` / `Add`(隐式数值、无显式初始化、末成员无尾逗号);确认每个成员都有且只有一行 jsDoc;确认没有任何实现体、没有 `export default`、没有多余空行或注释。打开 `index.ts` 与 `package.json` 逐字确认。**本步骤的结论是绑定判定**:必须写入执行笔记与 SUMMARY;任一成员与对照表不一致即判本任务失败,不得以「7 个名称一致」或「片段存在」放行。
7. **可选类型门禁(跨平台 node 过滤,不使用 PowerShell 专有的 `Select-String`)**:如需类型检查,运行 `<verify>` 中的 node 命令。它捕获 `vue-tsc --noEmit` 的 stdout + stderr(非 0 退出码被捕获,不直接判失败),只用 `packages/render-anim` 过滤结果判定:0 行诊断视为通过,出现任一条即失败。全仓既有 / 预期类型错误与 `vue-tsc` 非 0 退出码属既有基线,不得视为本次失败,也不得因此修改其它文件。若未运行该门禁,SUMMARY 必须写明「类型检查未运行」,不得宣称类型检查通过。
本任务不运行测试、构建、dev server、浏览器或包管理命令;**严格只读**:不运行 `prettier --write`、不写任何文件、不修改源码。任何 prettier / eslint / 内容失败都必须回到 Task 2 修正,再由本任务重跑全部门禁;本任务对 eslint 失败只上报原始输出并停止,绝不就地改源码或加 `eslint-disable`。
</action>
<verify>
<automated>node node_modules/prettier/bin/prettier.cjs --check packages/render-anim/package.json packages/render-anim/src/types.ts packages/render-anim/src/index.ts</automated>
<fails_when>输出不是 `All matched files use Prettier code style!`,或退出码非 0(prettier 列出待格式化文件)。</fails_when>
<automated>node node_modules/eslint/bin/eslint.js packages/render-anim/src/types.ts packages/render-anim/src/index.ts</automated>
<fails_when>eslint 打印任一 error / warning,或退出码非 0。</fails_when>
<automated>node -e "const fs=require('fs');const files=['packages/render-anim/package.json','packages/render-anim/src/types.ts','packages/render-anim/src/index.ts'];let bad=0;for(const f of files){const s=fs.readFileSync(f,'utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;const tabs=(s.match(/\t/g)||[]).length;const ok=lf===crlf&&tabs===0;if(!ok){bad++;}console.log((ok?'OK ':'FAIL')+' '+f+' lf='+lf+' crlf='+crlf+' tabs='+tabs);}process.exit(bad?1:0);"</automated>
<fails_when>任一行以 `FAIL` 开头(存在非 CRLF 换行或制表符),脚本以非 0 退出。</fails_when>
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('packages/render-anim/src/types.ts','utf8');const i=fs.readFileSync('packages/render-anim/src/index.ts','utf8');const p=JSON.parse(fs.readFileSync('packages/render-anim/package.json','utf8'));const fail=[];const names=['IAnimationBitmap','IAnimationSound','IAnimationObject','IAnimationFrame','IAnimationAnimaterData','IAnimationData'];for(const n of names){if(!new RegExp('export interface '+n+'\\b').test(t)){fail.push('missing '+n);}}const em=t.replace(/\r\n/g,'\n').match(/export const enum AnimationBlendMode \{\n([\s\S]*?)\n\}/);if(!em){fail.push('missing enum AnimationBlendMode');}else{const enumBody=em[1].split('\n').map(l=>l.trim()).filter(l=>l.length&&!l.startsWith('/**')&&!l.startsWith('*')&&!l.startsWith('//')).join(' ');if(enumBody!=='Normal, Add'){fail.push('enum members: '+enumBody);}}const snippets=['readonly x: number;','readonly y: number;','readonly width: number;','readonly height: number;','readonly name: string;','readonly volume?: number;','readonly pitch?: number;','readonly index: number;','readonly scale: number;','readonly opacity: number;','readonly mirror: number;','readonly rotate: number;','readonly originX: number;','readonly originY: number;','readonly objects: readonly IAnimationObject[];','readonly sound?: readonly IAnimationSound[];','readonly bitmaps: readonly IAnimationBitmap[];','readonly frames: readonly IAnimationFrame[];','readonly texture: ITexture;','readonly animater: ITextureAnimater<IAnimationAnimaterData>;','readonly blend: AnimationBlendMode;'];for(const s of snippets){if(t.indexOf(s)===-1){fail.push('missing snippet '+s);}}if(t.indexOf('import { ITexture, ITextureAnimater } from')!==0){fail.push('import not first');}if(/\bimport\s+type\b/.test(t)){fail.push('import type');}if(/\bany\b/.test(t)){fail.push('any');}if(/\bclass\b/.test(t)){fail.push('class');}if(/\bfunction\b/.test(t)){fail.push('function');}if(t.indexOf('=>')!==-1){fail.push('arrow');}const noEnum=t.replace(/export const enum AnimationBlendMode \{[\s\S]*?\n\}/,'');if(/\bconst\b/.test(noEnum)){fail.push('const');}if(/\blet\b/.test(t)){fail.push('let');}if(/\benum\b/.test(noEnum)){fail.push('enum');}if(/export\s+default/.test(t)){fail.push('export default');}const ic=i.replace(/\s/g,'');if(ic.indexOf('export*from')!==0||ic.indexOf('export',1)!==-1||ic.indexOf('./types')===-1){fail.push('index content');}if(Object.keys(p).sort().join(',')!=='dependencies,name'){fail.push('package keys');}if(p.name!=='@motajs/render-anim'){fail.push('package name');}if(!p.dependencies||p.dependencies['@motajs/render']!=='workspace:*'){fail.push('package dep');}console.log(fail.length?'FAIL\n'+fail.join('\n'):'OK');process.exit(fail.length?1:0);"</automated>
<fails_when>脚本打印 `FAIL` 并列出缺失项(缺失枚举 / 接口 / 成员片段、枚举成员不是 `Normal, Add`、首行不是 import、出现 `import type` / `any` / `class` / `function` / 箭头 / `let` / `export default`、`const` / `enum` 出现在 `AnimationBlendMode` 声明之外、`index.ts` 内容不符或 `package.json` 键 / 依赖不符),退出码 1。</fails_when>
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('packages/render-anim/src/types.ts','utf8').replace(/\r\n/g,'\n');const exp={IAnimationBitmap:{ext:null,m:['readonly x: number;','readonly y: number;','readonly width: number;','readonly height: number;']},IAnimationSound:{ext:null,m:['readonly name: string;','readonly volume?: number;','readonly pitch?: number;']},IAnimationObject:{ext:null,m:['readonly index: number;','readonly x: number;','readonly y: number;','readonly scale: number;','readonly opacity: number;','readonly mirror: number;','readonly rotate: number;']},IAnimationFrame:{ext:null,m:['readonly width: number;','readonly height: number;','readonly originX: number;','readonly originY: number;','readonly objects: readonly IAnimationObject[];','readonly sound?: readonly IAnimationSound[];']},IAnimationAnimaterData:{ext:null,m:['readonly bitmaps: readonly IAnimationBitmap[];','readonly frames: readonly IAnimationFrame[];']},IAnimationData:{ext:'IAnimationAnimaterData',m:['readonly texture: ITexture;','readonly animater: ITextureAnimater<IAnimationAnimaterData>;','readonly blend: AnimationBlendMode;']}};const re=/export interface (\w+)(?: extends (\w+))? \{([\s\S]*?)\n\}/g;const seen={};let mm;while((mm=re.exec(t))){seen[mm[1]]={ext:mm[2]||null,body:mm[3]};}const fail=[];for(const n of Object.keys(exp)){const s=seen[n];if(!s){fail.push('missing interface '+n);continue;}if((s.ext||null)!==exp[n].ext){fail.push(n+' extends '+(s.ext||'none')+' != '+String(exp[n].ext));}const got=s.body.split('\n').map(l=>l.trim()).filter(l=>l.length&&!l.startsWith('/**')&&!l.startsWith('*')&&!l.startsWith('//')).join(' ');const want=exp[n].m.join(' ');if(got!==want){fail.push(n+' members: '+got);}}if(Object.keys(seen).length!==6){fail.push('interface count '+Object.keys(seen).length);}const em=/export const enum AnimationBlendMode \{\n([\s\S]*?)\n\}/.exec(t);if(!em){fail.push('AnimationBlendMode enum missing');}else{const gotEnum=em[1].split('\n').map(l=>l.trim()).filter(l=>l.length&&!l.startsWith('/**')&&!l.startsWith('*')&&!l.startsWith('//')).join(' ');if(gotEnum!=='Normal, Add'){fail.push('AnimationBlendMode members: '+gotEnum);}}console.log(fail.length?'FAIL\n'+fail.join('\n'):'OK: per-interface skeleton matches');process.exit(fail.length?1:0);"</automated>
<fails_when>脚本打印 `FAIL` 并列出接口级差异(成员落在错误接口、顺序 / `readonly` / `?` / 类型不符、接口数不是 6、`extends` 不符,或 `AnimationBlendMode` 不是 `export const enum`、成员序列不是 `Normal, Add`),退出码 1。</fails_when>
<automated>node -e "const{execSync}=require('child_process');let out='';try{out=execSync('node node_modules/vue-tsc/bin/vue-tsc.js --noEmit',{encoding:'utf8',maxBuffer:67108864,stdio:['ignore','pipe','pipe']});}catch(e){out=String(e.stdout||'')+String(e.stderr||'');}const hits=out.split(/\r?\n/).filter(l=>/packages[\\\/]render-anim[\\\/]/.test(l));console.log(hits.length?hits.join('\n'):'OK: no render-anim diagnostics');process.exit(hits.length?1:0);"</automated>
<fails_when>过滤结果出现任一 `packages/render-anim` 诊断(打印诊断并以 1 退出);其它文件的诊断与 `vue-tsc` 非 0 退出码不参与判定。</fails_when>
</verify>
<acceptance_criteria>
- prettier `--check`、eslint、CRLF / 制表符门禁、内容结构门禁(含 `let` / `export default` 负向检查,以及 `const` / `enum` 仅允许出现在 `AnimationBlendMode` 声明处)、逐接口成员门禁(含枚举成员序列)均以退出码 0 完成。
- 逐接口门禁证明每个成员位于「类型骨架逐成员对照」指定的接口内,且接口数、顺序、类型、`readonly`、`?` 与 `extends` 完全一致;错放成员会使门禁非 0。
- 人工逐成员复核是绑定判定,结论已写入执行笔记与 SUMMARY,未以名称级一致或片段存在代替类型级一致。
- 本任务未写任何文件、未修改源码、未运行 `prettier --write`;eslint 失败一律按「上报原始输出 → 回到 Task 2 修正 → 重跑全部门禁」处理,未就地改源码、未加 `eslint-disable`、未改 eslint 配置;类型门禁的「已运行(0 行 render-anim 诊断)/ 未运行」状态已记录。
</acceptance_criteria>
<done>prettier `--check`、eslint、CRLF / 制表符门禁、内容结构门禁(含 `let` / `export default` 负向检查,以及 `const` / `enum` 仅允许出现在 `AnimationBlendMode` 声明处)与逐接口成员门禁(含枚举成员序列)全部通过;期间任何 prettier / eslint / 内容失败均已回到 Task 2 修正后由本任务重跑全部门禁,本任务自身零写入(未改源码、未加 `eslint-disable`、未改 eslint 配置);已逐成员人工复核并形成绑定结论;类型门禁的「运行 / 未运行」状态已记录,且未运行测试、构建、dev server 或浏览器。</done>
</task>
<task type="auto">
<name>Task 4: 范围 / 哈希闸门自检并写 `09-52-SUMMARY.md`</name>
<files>.planning/phases/09-minimum-browser-runnable/09-52-SUMMARY.md</files>
<read_first>
- `.planning/phases/09-minimum-browser-runnable/09-52-PLAN.md` 的 `<success_criteria>` 与「覆盖审计」(SUMMARY 必须覆盖的条目)
- `process.env.TEMP` 目录下的 `gsd-09-52-status.txt` 与 `gsd-09-52-hashes.txt`(Task 1 采集的范围 / 哈希基线;与门禁 `(process.env.TEMP||'.')` 解析一致)
- Task 1 / Task 2 / Task 3 的执行笔记与各门禁真实输出
- `09-50-PLAN.md`、`09-50-SUMMARY.md`、`09-51-PLAN.md`、`09-51-SUMMARY.md`、`09-52-PLAN.md`(哈希闸门对象)
</read_first>
<action>
只写 `09-52-SUMMARY.md`,不修改任何代码文件。
1. **范围闸门**:运行 `node -e "const{execSync}=require('child_process'),fs=require('fs');const base=fs.readFileSync((process.env.TEMP||'.')+'/gsd-09-52-status.txt','utf8').split(/\r?\n/).filter(l=>l.length>0);const cur=execSync('git status --porcelain -uall',{encoding:'utf8'}).split(/\r?\n/).filter(l=>l.length>0);const expected=['?? packages/render-anim/package.json','?? packages/render-anim/src/index.ts','?? packages/render-anim/src/types.ts'];const summary='?? .planning/phases/09-minimum-browser-runnable/09-52-SUMMARY.md';const added=cur.filter(l=>base.indexOf(l)===-1);const removed=base.filter(l=>cur.indexOf(l)===-1);const extra=added.filter(l=>expected.indexOf(l)===-1&&l!==summary);const missing=expected.filter(l=>added.indexOf(l)===-1);console.log('added:\n'+added.join('\n'));console.log('removed:\n'+removed.join('\n'));console.log('extra:\n'+extra.join('\n'));console.log('missing:\n'+missing.join('\n'));process.exit(extra.length+missing.length+removed.length?1:0);"`。要求:`added` 只包含 3 个包文件;若 SUMMARY 已写入,则允许再多 1 条 `?? .planning/phases/09-minimum-browser-runnable/09-52-SUMMARY.md`(写入前后均可运行)。`extra` 与 `missing` 为空、`removed` 为空(既有未提交改动一条未少)。任何不一致都必须停下上报,不得用 `.trim()` 或子串匹配放宽。
2. **哈希闸门**:重新对 `09-50-PLAN.md` / `09-50-SUMMARY.md` / `09-51-PLAN.md` / `09-51-SUMMARY.md` / `09-52-PLAN.md` 执行 `git hash-object`,与 `process.env.TEMP` 目录下的 `gsd-09-52-hashes.txt`(与门禁 `(process.env.TEMP||'.')` 同一解析)逐行比对,必须完全一致。把 `09-52-PLAN.md` 纳入闸门,用于证明执行期间计划文件本身未被改写。命令见 `<verify>` 第 3 条(成功时输出 `OK: 5 files unchanged`)。
3. **写 `09-52-SUMMARY.md`**(UTF-8 无 BOM、CRLF、4 空格缩进、Markdown 文件名连字符命名),内容至少包含:
- **本计划范围**:只建 `@motajs/render-anim` 的类型骨架;不实现 `RenderAnimater` / `parseRenderAnimation`;不跑测试 / 浏览器 / 构建;0 次提交;09-52 / 波次 1 的编号说明。
- **逐文件最终形态**:`package.json` 的两个键;`types.ts` 的 1 个 `export const enum AnimationBlendMode` + 6 个接口与逐成员清单(名称 / 类型 / `readonly` / 可选性)、jsDoc 方案、首行 import;`index.ts` 的唯一一行。
- **Task 1 记录的类型 / 命名方案**:`@motajs/render` 的既有类型、根 tsconfig 的路径映射、包清单范式、命名与 jsDoc 结论。
- **门禁真实结果**:prettier / eslint / CRLF / 内容结构 / 逐接口成员门禁的实际输出(逐字粘贴关键行);类型门禁「已运行(0 行诊断)」或「未运行」二选一,不得含糊;范围闸门与哈希闸门的实际输出。
- **未做 / 跳过项**:不运行测试、构建、dev server、浏览器;不做运行时主张;不运行 `pnpm install`;不执行 git 写命令。
- **设计记录(未实现)**:浏览器专用、`ITextureAnimater<IAnimationAnimaterData>` 形状、逐帧惰性合成与下一次 `.next()` 前有效、`MotaOffscreenCanvas2D` + `setHD(false)` / `setAntiAliasing(false)`、空帧透明小图、帧尺寸可变、`blend` 由文件数据暴露、`sound` 由宿主播放、`parseRenderAnimation(buffer: ArrayBuffer): Promise<IAnimationData>`。
- **待确认项**:OI-1 的最终裁定(规划期已定 (B):`export const enum AnimationBlendMode`,成员 `Normal` / `Add`)、OI-2、OI-3、OI-4、OI-5。
- **估算**:`confidence: low`,未校准。
4. 不写 `09-52-RESEARCH.md` / `09-52-VALIDATION.md`;不改 `STATE.md` / `ROADMAP.md` / `09-CONTEXT.md`。
</action>
<verify>
<automated>node -e "const fs=require('fs');const f='.planning/phases/09-minimum-browser-runnable/09-52-SUMMARY.md';const s=fs.readFileSync(f,'utf8');const need=['09-52','render-anim','prettier','eslint','类型门禁','0 次提交'];const miss=need.filter(n=>s.indexOf(n)===-1);if(miss.length){console.error('missing '+miss.join(','));process.exit(1);}console.log('OK: summary present');"</automated>
<fails_when>SUMMARY 缺少任一必需关键词(打印 `missing <列表>`,退出码 1),或文件不存在(抛出异常并以非 0 退出)。</fails_when>
<automated>node -e "const{execSync}=require('child_process'),fs=require('fs');const base=fs.readFileSync((process.env.TEMP||'.')+'/gsd-09-52-status.txt','utf8').split(/\r?\n/).filter(l=>l.length>0);const cur=execSync('git status --porcelain -uall',{encoding:'utf8'}).split(/\r?\n/).filter(l=>l.length>0);const expected=['?? packages/render-anim/package.json','?? packages/render-anim/src/index.ts','?? packages/render-anim/src/types.ts'];const summary='?? .planning/phases/09-minimum-browser-runnable/09-52-SUMMARY.md';const added=cur.filter(l=>base.indexOf(l)===-1);const removed=base.filter(l=>cur.indexOf(l)===-1);const extra=added.filter(l=>expected.indexOf(l)===-1&&l!==summary);const missing=expected.filter(l=>added.indexOf(l)===-1);console.log('added:\n'+added.join('\n'));console.log('removed:\n'+removed.join('\n'));console.log('extra:\n'+extra.join('\n'));console.log('missing:\n'+missing.join('\n'));process.exit(extra.length+missing.length+removed.length?1:0);"</automated>
<fails_when>`extra` / `missing` / `removed` 任一非空(打印对应列表,退出码 1),或 `added` 中出现 3 个包文件与可选 1 条 SUMMARY 之外的路径。</fails_when>
<automated>node -e "const{execSync}=require('child_process'),fs=require('fs');const base=fs.readFileSync((process.env.TEMP||'.')+'/gsd-09-52-hashes.txt','utf8').replace(/\r\n/g,'\n').trim().split('\n').filter(Boolean);const now=base.map(l=>{const f=l.split(' ')[0];return f+' '+execSync('git hash-object '+f,{encoding:'utf8'}).trim();});const bad=now.filter((l,k)=>l!==base[k]);console.log(now.join('\n'));if(bad.length){console.error('CHANGED:\n'+bad.join('\n'));process.exit(1);}if(now.length!==5){console.error('COUNT '+now.length);process.exit(1);}console.log('OK: 5 files unchanged');"</automated>
<fails_when>任一路径的哈希与基线不一致(打印 `CHANGED:` 与差异行,退出码 1),或基线行数不是 5(打印 `COUNT <n>`,退出码 1)。</fails_when>
</verify>
<acceptance_criteria>
- 范围闸门 `added` 恰为 3 个包文件,另可有 1 条 `09-52-SUMMARY.md`;`extra` / `missing` / `removed` 为空。
- 哈希闸门确认 `09-52-PLAN.md`、09-50-PLAN / SUMMARY、09-51-PLAN / SUMMARY 共 5 个文件逐字不变(计划文件在执行期间不可变)。
- `09-52-SUMMARY.md` 含范围、逐文件最终形态、Task 1 方案、各门禁真实结果(含类型门禁运行状态)、未做项、设计记录、待确认项与未校准估算。
- 全程未执行 git 写命令,0 次提交;除 SUMMARY 外未写任何其它文件。
</acceptance_criteria>
<done>范围闸门 `added` 只含 3 个包文件(SUMMARY 写入后允许再加 1 条)、`extra` / `missing` / `removed` 为空;`09-52-PLAN.md` 与 09-50 / 09-51 四个文件共 5 个哈希逐字不变;`09-52-SUMMARY.md` 已写入并包含范围、逐文件形态、Task 1 方案、各门禁真实结果(含类型门禁运行状态)、未做项、设计记录、待确认项与未校准估算;0 次提交。</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 无 | 本计划只新增类型声明与包清单,不引入运行时代码、网络 / 存储 / 用户输入处理,也不安装任何依赖 |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-52-01 | Tampering | `packages/render-anim/package.json` 的依赖 | low | mitigate | 依赖仅为同仓库工作区包 `@motajs/render`(`workspace:*`),不引入 registry 包;本计划不运行 `pnpm install`,锁文件不变 |
| T-52-02 | Information Disclosure | `packages/render-anim/src/types.ts` | low | accept | 只声明公开类型骨架,无密钥、无运行时数据;文件内容由用户审阅 |
| T-52-SC | Tampering | npm / pnpm 安装 | high | mitigate | 本计划禁止任何包管理安装命令;若后续实现阶段需要新增 registry 依赖,须先过包合法性审计并在 Task 0 汇报 |
</threat_model>
<verification>
- **格式**:`node node_modules/prettier/bin/prettier.cjs --check packages/render-anim/package.json packages/render-anim/src/types.ts packages/render-anim/src/index.ts` 退出码 0。
- **lint**:`node node_modules/eslint/bin/eslint.js packages/render-anim/src/types.ts packages/render-anim/src/index.ts` 退出码 0、无输出。
- **换行 / 缩进**:Task 3 第 3 步 node 门禁三行 `OK`(全 CRLF、无制表符)。
- **内容 / 结构**:Task 3 第 4 步 node 门禁输出 `OK`(1 个 `export const enum AnimationBlendMode`(成员 `Normal` / `Add`)+ 6 个接口、25 个成员片段、首行 import、无 `import type` / `any` / `class` / `function` / `let` / 箭头函数 / `export default`,且 `const` / `enum` 仅出现在枚举声明处、`index.ts` 唯一导出、`package.json` 仅两个键)。
- **逐接口成员**:Task 3 第 5 步 node 门禁输出 `OK: per-interface skeleton matches`(成员归属、顺序、类型、`readonly`、`?`、接口数、`extends` 与 `AnimationBlendMode` 枚举成员序列 `Normal` → `Add` 全部一致)。
- **人工复核**:`types.ts` 逐成员(含枚举成员 `Normal` / `Add`)与「类型骨架逐成员对照」一致,每个成员一行 jsDoc,无实现体;复核是绑定判定。
- **类型(可选)**:node 过滤 `vue-tsc --noEmit` 输出后 `packages/render-anim` 诊断 0 行;全仓既有错误与退出码不参与判定;未运行时 SUMMARY 必须声明。
- **范围**:Task 4 范围闸门 `added` 恰为 3 个包文件,另可有 1 条 `09-52-SUMMARY.md`(最多 4 条);`extra` / `missing` / `removed` 为空。
- **哈希**:09-52-PLAN.md、09-50 / 09-51 的计划与摘要共 5 个文件哈希不变。
- **明确不做**:不运行测试、构建、dev server、浏览器;不运行 `pnpm install`;不执行任何 git 写命令。
</verification>
<success_criteria>
- [ ] Task 0 已完整汇报 OI-1 的裁定(用户选择 (B):`export const enum AnimationBlendMode`,成员 `Normal` / `Add`),并等待用户针对本修订版回复「可以执行」后才继续;OI-1 的裁定已在规划期由「B,可以执行」记录,本轮不再询问 (A)/(B)。
- [ ] `packages/render-anim/package.json` 存在且只有 `name` 与 `dependencies['@motajs/render'] = 'workspace:*'` 两个键。
- [ ] `packages/render-anim/src/types.ts` 存在,导出 7 个类型(1 个 `export const enum AnimationBlendMode` + 6 个接口),成员名 / 顺序 / 类型 / `readonly` / 可选性与用户骨架完全一致。
- [ ] `types.ts` 首行为普通 `import { ITexture, ITextureAnimater } from '@motajs/render';`,无 `import type`、无 `any`、无 `class` / `function` / `let` / 箭头函数 / `export default`,且 `const` / `enum` 仅出现在 `export const enum AnimationBlendMode` 声明处(内容门禁自动判定)。
- [ ] 每个公开成员(含枚举成员 `Normal` / `Add`)在 `types.ts` 源头有单行中文 jsDoc;接口与枚举本身无 jsDoc。
- [ ] `packages/render-anim/src/index.ts` 只有 `export * from './types';`。
- [ ] 三个文件均为 CRLF + 4 空格 + 无制表符;prettier `--check` 与 eslint 通过;逐接口成员门禁通过(成员归属与类型级形状一致)。
- [ ] 未创建 `tsconfig.json`、测试、示例、实现文件或任何其它文件;`prettier --write` 只可能由 Task 2 对三个包文件执行,任何 prettier / eslint 失败都由 Task 2 修正后由 Task 3 重跑全部门禁,Task 3 未写任何文件、未改源码。
- [ ] `09-52-SUMMARY.md` 已写入,包含门禁真实结果、类型门禁运行状态、未做项、设计记录、待确认项与未校准估算。
- [ ] 范围闸门与哈希闸门通过;09-52-PLAN.md、09-50 / 09-51 未被修改;0 次提交。
</success_criteria>
<output>
Create `.planning/phases/09-minimum-browser-runnable/09-52-SUMMARY.md` when done
</output>
@@ -0,0 +1,181 @@
---
phase: 09-minimum-browser-runnable
plan: 52
subsystem: render-anim
tags: [typescript, types-only, render-anim, enum, animation]
requires: []
provides:
- "新包 @motajs/render-anim 的 package.json(name + @motajs/render workspace:*)"
- "1 个 export const enum AnimationBlendMode(Normal / Add)与 6 个动画数据接口"
- "src/index.ts 的单一星号导出"
affects: [09-minimum-browser-runnable, render-anim]
actuals:
# chars/4 上限值,只统计本计划新建的 3 个包文件(2201 字节);SUMMARY 自身不计入
tokens: 550
tasks: 5
commits: 0
tech-stack:
added: []
patterns:
- "类型骨架先行:公开类型由用户审阅后再另立实现计划"
- "枚举替代字符串标识符:AnimationBlendMode 使用 export const enum 且成员为隐式数值"
- "同目录星号导出,不跨包转发 @motajs/render 的符号"
key-files:
created:
- packages/render-anim/package.json
- packages/render-anim/src/types.ts
- packages/render-anim/src/index.ts
modified: []
key-decisions:
- "OI-1 按用户裁定 (B):AnimationBlendMode 由字符串字面量联合改为 export const enum AnimationBlendMode(成员 Normal / Add,隐式数值 0 / 1)"
- "IAnimationData 通过 extends IAnimationAnimaterData 复用 bitmaps / frames,不重复声明成员"
- "只落类型骨架,RenderAnimater 与 parseRenderAnimation 留给后续实现计划"
patterns-established:
- "新包的 package.json 只写 name 与 workspace 依赖,不写 version / private / type / exports / scripts"
requirements-completed: []
duration: 9min
completed: 2026-11-07
status: complete
---
# Phase 9 Plan 52: 新建 `@motajs/render-anim` 的类型骨架
**只新增 `package.json` / `src/types.ts` / `src/index.ts` 三个文件,落下 1 个 `export const enum AnimationBlendMode`(`Normal` / `Add`)与 6 个动画数据接口,不含任何实现。**
## Performance
- **Duration:** 约 9 min
- **Started:** 2026-11-07T13:02:00Z
- **Completed:** 2026-11-07T13:11:20Z
- **Tasks:** 5(Task 0 为已获「可以执行」的汇报关卡;Task 1–4 为执行任务)
- **Files created:** 3 个包文件,另有本 SUMMARY
## 本计划范围
- 只建 `@motajs/render-anim` 的类型骨架;**不实现** `RenderAnimater`、**不实现** `parseRenderAnimation`。
- 不跑测试、构建、dev server、浏览器;不做任何运行时主张。
- 不运行 `pnpm install`,锁文件不变;不新增包级 `tsconfig.json` 或任何配置。
- **0 次提交**:全程未执行任何 git 写命令,改动留在工作树供审阅。
- 编号说明:派发编号 09-52 / 波次 1;`09-01`..`09-51` 均已占用,本计划不删除、不覆盖任何既有记录。
## Task 0 执行前汇报关卡(已满足)
| 项 | 内容 |
|----|------|
| 关卡类型 | `checkpoint:human-verify` |
| Gate | `blocking-human` |
| 汇报对象 | 三个文件的逐字形态、1 个 `export const enum` + 6 个接口的逐成员清单、dev.md 合规点、影响范围、只读静态验证口径、编号说明、不提交声明 |
| OI-1 裁定 | 用户选择 **(B)**:`AnimationBlendMode` 改为 `export const enum AnimationBlendMode`(成员 `Normal` / `Add`,隐式数值 0 / 1,成员 jsDoc 为「普通混合」/「加算混合」) |
| 用户回复 | 恰好「可以执行」四个字(针对本修订版) |
| 结论 | 关卡通过,进入 Task 1–4;未再询问 (A)/(B) |
**说明:** 本仓库与 GSD 既有约定中不存在独立的「approval 文件」;计划正文也明确 Task 0「不写任何文件」,且本次执行被限定只能写 4 个路径。因此该结构化批准记录落在本 SUMMARY,不额外创建文件。
## 逐文件最终形态
### `packages/render-anim/package.json`
只有两个顶层键,顺序为 `name`、`dependencies`;`dependencies` 只有一个条目 `@motajs/render: workspace:*`:
```json
{
"name": "@motajs/render-anim",
"dependencies": {
"@motajs/render": "workspace:*"
}
}
```
无 `version` / `private` / `type` / `main` / `module` / `types` / `exports` / `scripts`;113 字节,6 行,全 CRLF。
### `packages/render-anim/src/types.ts`
首行为普通引入 `import { ITexture, ITextureAnimater } from '@motajs/render';`(未使用 `import type`),随后一个空行,再按固定顺序导出 7 个类型。成员级 jsDoc 为单行中文,接口与枚举本身不写 jsDoc。2062 字节,76 行,全 CRLF。
| 导出类型 | 成员(顺序固定) |
|----------|------------------|
| `AnimationBlendMode`(`export const enum`) | `Normal`(普通混合,隐式 0)、`Add`(加算混合,隐式 1) |
| `IAnimationBitmap` | `readonly x: number`、`readonly y: number`、`readonly width: number`、`readonly height: number` |
| `IAnimationSound` | `readonly name: string`、`readonly volume?: number`、`readonly pitch?: number` |
| `IAnimationObject` | `readonly index: number`、`readonly x: number`、`readonly y: number`、`readonly scale: number`、`readonly opacity: number`、`readonly mirror: number`、`readonly rotate: number` |
| `IAnimationFrame` | `readonly width: number`、`readonly height: number`、`readonly originX: number`、`readonly originY: number`、`readonly objects: readonly IAnimationObject[]`、`readonly sound?: readonly IAnimationSound[]` |
| `IAnimationAnimaterData` | `readonly bitmaps: readonly IAnimationBitmap[]`、`readonly frames: readonly IAnimationFrame[]` |
| `IAnimationData extends IAnimationAnimaterData` | `readonly texture: ITexture`、`readonly animater: ITextureAnimater<IAnimationAnimaterData>`、`readonly blend: AnimationBlendMode` |
`AnimationBlendMode` 声明形态(OI-1 B):顶格 `export const enum AnimationBlendMode {`,成员为 4 空格缩进的单行 jsDoc + `Normal,` / `Add`(末成员无尾逗号),最后顶格 `}`;无显式初始化,无声明级 jsDoc。
### `packages/render-anim/src/index.ts`
全文只有一行 `export * from './types';`,不转发 `@motajs/render` 的符号。26 字节,1 行,CRLF 结尾。
## Task 1 记录的类型 / 命名方案(只读采集)
- `packages/render/src/assets/types.ts`:`ITexture` 为接口(`Texture` 类 `implements ITexture`);`ITextureAnimater<T>` 的 `once(texture, data)` / `cycled(texture, data)` 均返回 `Generator<ITextureRenderable, void>`;成员 jsDoc 风格为单行中文、结尾不加句号。
- `packages/render/src/index.ts`:`export * from './assets' | './core' | './style' | './types' | './utils'`,因此 `ITexture` / `ITextureAnimater` 可从 `@motajs/render` 根导出取得。
- `packages/render/src/core/canvas2d.ts`:`MotaOffscreenCanvas2D` 提供 `setHD(hd: boolean)` 与 `setAntiAliasing(anti: boolean)`(仅供后续实现记录,本计划未调用)。
- 包清单范式:`packages/render/package.json` 只有 `name`;`packages/audio/package.json` 为 `name` + `dependencies`(`workspace:*`)。新包按后者书写。
- 根 `tsconfig.json`:`include` 覆盖 `packages/**/*.ts`,`paths['@motajs/*'] = ['./packages/*/src']`,`strict: true`、`isolatedModules: true`、`noEmit: true`,因此无需为新包新增 tsconfig。
- `.prettierrc`:`printWidth: 80` / `tabWidth: 4` / `singleQuote: true` / `arrowParens: avoid` / `trailingComma: none` / `endOfLine: crlf`;`eslint.config.js` 为 `typescript-eslint` + `eslint-plugin-prettier/recommended`。
- 命名与 jsDoc 结论:类型名 / 成员名全部由用户提供,未新增任何未获批的公开名称;`IAnimationSound.name: string` 属 dev.md 允许的字符串 id 别名一类(OI-2)。
## 门禁真实结果
| 门禁 | 命令要点 | 真实输出 | 结果 |
|------|----------|----------|------|
| 格式 | `node node_modules/prettier/bin/prettier.cjs --check packages/render-anim/{package.json,src/types.ts,src/index.ts}` | `Checking formatting...` / `All matched files use Prettier code style!`,退出码 0 | 通过 |
| lint | `node node_modules/eslint/bin/eslint.js packages/render-anim/src/types.ts packages/render-anim/src/index.ts` | 无任何输出,退出码 0 | 通过 |
| 换行 / 缩进 | Task 3 第 3 步 node 门禁 | `OK packages/render-anim/package.json lf=6 crlf=6 tabs=0` / `OK packages/render-anim/src/types.ts lf=76 crlf=76 tabs=0` / `OK packages/render-anim/src/index.ts lf=1 crlf=1 tabs=0` | 通过 |
| 内容 / 结构 | Task 3 第 4 步 node 门禁(枚举 + 6 接口 + 21 条成员片段(覆盖 25 个成员)+ 首行 import + 负向检查 + index / package 校验) | `OK`,退出码 0 | 通过 |
| 逐接口成员 | Task 3 第 5 步 node 门禁(按接口块逐字比对成员序列、`extends`、接口数、枚举成员序列) | `OK: per-interface skeleton matches`,退出码 0 | 通过 |
| 类型门禁 | node 过滤 `vue-tsc --noEmit` 输出,只看 `packages/render-anim` 诊断 | **已运行**:`OK: no render-anim diagnostics`(0 行诊断);全仓既有类型错误与 `vue-tsc` 非 0 退出码不参与判定 | 通过 |
| 范围闸门(SUMMARY 写入前) | Task 4 第 1 步 node 门禁 | `added:` 恰为 `?? packages/render-anim/package.json` / `?? packages/render-anim/src/index.ts` / `?? packages/render-anim/src/types.ts`;`removed` / `extra` / `missing` 均为空 | 通过 |
| 哈希闸门 | Task 4 第 2 步 node 门禁 | `09-50-PLAN 1575237b…`、`09-50-SUMMARY 9283ff05…`、`09-51-PLAN a83f21cb…`、`09-51-SUMMARY 257483e7…`、`09-52-PLAN 5fdcef8a…`;`OK: 5 files unchanged` | 通过 |
人工逐成员复核(绑定判定):`types.ts` 的成员顺序、接口归属、`readonly`、可选性(`volume?` / `pitch?` / `sound?`)、只读数组、`extends IAnimationAnimaterData`、泛型实参 `ITextureAnimater<IAnimationAnimaterData>` 与计划正文「类型骨架逐成员对照」逐项一致;`AnimationBlendMode` 为 `export const enum`,成员恰为 `Normal` → `Add`(隐式数值、无显式初始化、末成员无尾逗号);每个成员有且只有一行 jsDoc;无实现体、无 `export default`。
## 未做 / 跳过项
- 未运行任何测试、构建、dev server 或浏览器;不做运行时可用性主张。
- 未运行 `pnpm install`;锁文件未变(依赖仅为同仓库工作区包)。
- 未执行任何 git 写命令(`add` / `commit` / `stash` / `checkout` / `restore` / `reset`),**0 次提交**。
- 未创建 `09-52-RESEARCH.md` / `09-52-VALIDATION.md`;未改动 `STATE.md` / `ROADMAP.md` / `09-CONTEXT.md`。
- 本 run 无 RESEARCH / 无 VALIDATION:`requirements: []`,Nyquist 不适用;spec-less probe fallback skipped(phase has no requirement IDs to probe)。
## 设计记录(未实现)
- 浏览器专用;`RenderAnimater` 将实现 `ITextureAnimater<IAnimationAnimaterData>`(`once` / `cycled` 返回 `Generator<ITextureRenderable>`,帧时序与循环由外部驱动)。
- 逐帧惰性合成;产出的可渲染对象在下一次 `.next()` 前有效,下一次恢复或 `return` 时清理上一帧。
- 使用 `MotaOffscreenCanvas2D` 并调用 `setHD(false)` / `setAntiAliasing(false)`,不直接调用浏览器 canvas API。
- 空帧产出透明小图;帧尺寸可不同,每帧自带 `width` / `height` / `originX` / `originY`。
- `blend` 以 `AnimationBlendMode` 存于文件并由解析数据暴露,动画器不决定混合;`sound` 由宿主播放。
- 解析函数为 `parseRenderAnimation(buffer: ArrayBuffer): Promise<IAnimationData>`。
## 偏差与问题
1. **[Rule 3 - Blocking] Write 工具把换行归一化为 LF**:写入后测得 `crlf=0`,会使 prettier `--check`(`endOfLine: crlf`)失败。处理方式:写入后运行一次 Node 脚本,把 3 个包文件按 `/\r?\n/` → `\r\n` 统一为 CRLF,内容其余部分不变;随后 CRLF 门禁与 prettier `--check` 均通过。未新增或删除任何字符。
2. Task 0 的批准记录落在本 SUMMARY(见上节说明),未创建独立的 approval 文件——计划正文要求 Task 0 不写文件,本次执行也仅允许写 3 个包文件与本 SUMMARY。
除上述两点外,计划按原文执行。
## 待确认项
- **OI-1(已裁定)**:`AnimationBlendMode` 使用 `export const enum`,成员 `Normal` / `Add`,隐式数值;`IAnimationData.blend` 类型不变。
- **OI-2(仅记录)**:`IAnimationSound.name: string` 属允许的字符串 id 别名一类,保留。
- **OI-3(仅记录)**:类型骨架由用户自行审阅 / 修改后再进入实现;`RenderAnimater` 与 `parseRenderAnimation(buffer: ArrayBuffer): Promise<IAnimationData>` 另立后续计划。
- **OI-4(仅记录)**:成员(含枚举成员)jsDoc 使用单行中文风格;接口与枚举本身按 dev.md 不写 jsDoc。如需英文注释或接口级说明,请指示。
- **OI-5(仅记录)**:`estimate` 未校准(`confidence: low`),本计划未运行构建或测试。
## 估算
- **估算:** `confidence: low`,未校准(计划 `estimate.tokens: 30000`)。
- **实际(actuals):** 新建包文件 2201 字节,约 550 tokens(chars/4);任务 5 个;提交 0 次。
## Self-Check: PASSED
- 3 个包文件均存在:`packages/render-anim/package.json`(113 B)、`src/types.ts`(2062 B)、`src/index.ts`(26 B)。
- `files_modified` 之外零改动(范围闸门 `extra` / `missing` / `removed` 均为空)。
- 0 次提交;工作树保留全部改动。
---
*Phase: 09-minimum-browser-runnable*
*Completed: 2026-11-07*
@@ -0,0 +1,536 @@
---
phase: 09-minimum-browser-runnable
plan: 53
type: execute
# 编号说明:09-01..09-52 的计划均已占用,本计划使用下一个空闲编号 09-53。
# depends_on 09-52:本计划修改 09-52 已创建(未提交)的 packages/render-anim/src/types.ts;无其它文件重叠。
wave: 2
depends_on:
- 09-52
files_modified:
- packages/render-anim/src/types.ts
- .planning/phases/09-minimum-browser-runnable/09-53-SUMMARY.md
autonomous: false
requirements: []
estimate:
# 未校准估算(confidence: low),SUMMARY 须声明。
# 成本主要在读取既有类型 / 09-52 记录、做一次精确块替换、以及 prettier / eslint / 结构门禁的往返。
tokens: 30000
raw_tokens: 30000
tasks: 5
confidence: low
must_haves:
truths:
- "**第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md / D-03)**:汇报本次修改的精确前后 diff、为什么拆分(用户否决 `data.animater.once(texture, data)` 的组合设计)、dev.md 合规点、命名、影响范围、验证形态与不提交声明;然后停下等待用户恰好回复「可以执行」。"
- "**只改一个源码文件**:仅 `packages/render-anim/src/types.ts` 被修改;`packages/render-anim/src/index.ts`、`packages/render-anim/package.json` 的哈希与 Task 1 基线一致;09-50 / 09-51 / 09-52 的计划与摘要共 6 个文件哈希不变;`09-53-PLAN.md` 自身在执行期间不被改写。"
- "**新类型逐字落地**:`types.ts` 导出 `IAnimationParsedData`(成员顺序固定为 `readonly texture: ITexture`、`readonly bitmaps: readonly IAnimationBitmap[]`、`readonly frames: readonly IAnimationFrame[]`、`readonly blend: AnimationBlendMode`)与 `export type IAnimationAnimater = ITextureAnimater<IAnimationParsedData>;`;解析数据中不存在 `animater` 成员。"
- "**旧类型彻底移除**:`IAnimationAnimaterData` 与 `IAnimationData` 两个接口及其全部成员、注释、`extends` 关系不再出现在 `types.ts` 中;不存在任何 `animater` 成员或 `ITextureAnimater<IAnimationAnimaterData>` 泛型实参。"
- "**只交付类型声明**:`types.ts` 内没有 `class` / `function` / `let` / 箭头函数 / `export default`;`const` 与 `enum` 仅出现在唯一的 `export const enum AnimationBlendMode` 声明处;不实现 `RenderAnimater`,不实现 `parseRenderAnimation`。"
- "**dev.md 合规**:UTF-8 无 BOM + CRLF + 4 空格缩进、无制表符;首行是普通 `import`,无 `import type`、无 `any`;`IAnimationParsedData` 每个成员前有一行中文 jsDoc(沿用既有注释文本),类型别名本身不写 jsDoc;第 1–61 行逐字不变由 `gsd-09-53-prefix.txt` 的 sha256 前缀哈希机械证明;四个成员 jsDoc 逐字等于「合成帧使用的纹理」/「位图列表」/「帧列表」/「混合模式」,成员与注释的关联、枚举注释(`普通混合` / `加算混合`)均为精确文本断言,改写注释即失败;未改写任何既有注释。"
- "**验证只读静态**:prettier `--check`、eslint、CRLF / BOM / 制表符门禁、逐接口成员门禁(含逐成员 jsDoc 精确文本)、第 1–61 行前缀哈希与枚举注释门禁、类型别名 / 旧类型移除 / 无实现 / 无 `any` / 无 `import type` 门禁全部通过;类型门禁先以 `vue-tsc --version` 哨兵证明检查器可启动、再断言 `--noEmit` 进程正常结束(`status` 非 `null`),只过滤 `packages/render-anim` 的诊断,全仓既有类型错误与 `vue-tsc` 非 0 退出码不参与判定;不运行测试、构建、dev server 或浏览器。"
- "**范围受控且不提交**:除 `files_modified` 的 `types.ts` 与 `09-53-SUMMARY.md` 外零改动;不运行 `pnpm install`;不执行任何 git 写命令,0 次提交。"
artifacts:
- path: packages/render-anim/src/types.ts
provides: "拆分后的解析数据类型 `IAnimationParsedData`(texture / bitmaps / frames / blend)与独立动画器类型别名 `IAnimationAnimater`,不含 `animater` 成员,不含任何实现"
- path: .planning/phases/09-minimum-browser-runnable/09-53-SUMMARY.md
provides: "修改前后 diff、逐成员最终形态、Task 1 基线、各门禁真实结果、跳过项、未校准估算、0 次提交与待确认项"
key_links:
- "`IAnimationAnimater` ← `ITextureAnimater<IAnimationParsedData>` ← `@motajs/render` 根导出 ← `packages/render/src/assets/types.ts`;`once(texture, data)` / `cycled(texture, data)` 的 `data` 参数现在是一个不含 `animater` 的纯解析数据对象"
- "`IAnimationParsedData` ↔ 后续 `RenderAnimater implements IAnimationAnimater` 与 `parseRenderAnimation(buffer: ArrayBuffer): Promise<IAnimationParsedData>`(本计划只固定类型,不实现)"
- "`src/index.ts` 的 `export * from './types';` ↔ 新类型从包根直接可见;本计划不改 `index.ts`,只验证其哈希不变"
- "`IAnimationParsedData` 的 `readonly` 数组 ↔ 后续解析器对外返回的只读视图;实现阶段不得把 `bitmaps` / `frames` 改成可变数组"
assumptions:
- "**工作树基线(规划日实测)**:`packages/render-anim/` 已由 09-52 创建但尚未提交,`git status --porcelain -uall -- packages/render-anim` 为 `?? package.json` / `?? src/index.ts` / `?? src/types.ts` 三条;`types.ts` 当前仍是被用户否决的组合设计(`IAnimationAnimaterData` + `IAnimationData`)。"
- "**09-52 已执行**:`09-52-SUMMARY.md` 的 `status: complete`,本计划在其产物上做类型拆分,不重新创建包、不改 `package.json` / `index.ts`。"
- "**`@motajs/render` 类型基线(规划日实测)**:`ITextureAnimater<T>` 声明 `once(texture: ITexture, data: T): Generator<ITextureRenderable, void>` 与 `cycled(texture: ITexture, data: T): Generator<ITextureRenderable, void>`;`ITexture` 是接口(`Texture` 类实现之);两者均可从 `@motajs/render` 根导出取得。"
- "**工具链(规划日实测)**:`node` v24.x;`node_modules/prettier/bin/prettier.cjs`、`node_modules/eslint/bin/eslint.js`、`node_modules/vue-tsc/bin/vue-tsc.js` 均存在;`.prettierrc` 为 `printWidth: 80` / `tabWidth: 4` / `singleQuote: true` / `arrowParens: avoid` / `trailingComma: none` / `endOfLine: crlf`。"
- "**本 run 无 RESEARCH / 无 VALIDATION**:本阶段无 requirement ID(`requirements: []`),Nyquist 不适用;spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)。"
prohibitions:
- "不得在用户恰好回复「可以执行」之前执行任何任务或派发执行子代理(AGENTS.md / D-03)。"
- "不得修改 `packages/render-anim/src/index.ts`、`packages/render-anim/package.json` 或 `packages/render-anim` 之外的任何源码;不得新增文件(SUMMARY 除外)。"
- "不得修改 09-50 / 09-51 / 09-52 的计划与摘要、`09-CONTEXT.md`、`STATE.md`、`ROADMAP.md`、根配置、锁文件或其它包。"
- "不得实现 `RenderAnimater` / `parseRenderAnimation`,不得写 class、function、测试、示例或入口逻辑;不得把类型别名展开成重复的接口成员。"
- "不得改写用户给定的类型名、成员名、成员顺序、成员类型与 `readonly`;不得保留 `IAnimationAnimaterData` / `IAnimationData`,不得让 `IAnimationParsedData` 包含 `animater` 成员。"
- "不得修改第 1–61 行的任何既有注释或成员;新接口成员只能沿用既有注释文本(`合成帧使用的纹理` / `位图列表` / `帧列表` / `混合模式`),不得改写措辞;`动画器` 注释随其成员与所属接口一并移除。"
- "不得出现 `import type`、`any`、`as`、非 jsDoc 的多行注释、复述代码的注释或新英文注释;不得改 `AnimationBlendMode` 的成员与顺序。"
- "不得运行 `pnpm install` / 构建 / 测试 / dev server / 浏览器;不得执行任何 git 写命令(`add` / `commit` / `stash` / `checkout` / `restore` / `reset`),本计划全程 0 次提交。"
open_items:
- "**OI-1(已获用户批准)**:公开名称固定为 `IAnimationParsedData` 与 `IAnimationAnimater`;解析数据成员固定为 `texture` / `bitmaps` / `frames` / `blend`。本计划不新增任何其它公开名称。"
- "**OI-2(只记录)**:四个成员注释文本从 09-52 的既有注释原样沿用;`动画器` 注释随 `IAnimationData.animater` 成员一并移除。若用户希望改写措辞,请在 Task 0 批准前说明。"
- "**OI-3(只记录,不实现)**:后续 `RenderAnimater` 实现 `IAnimationAnimater`(即 `ITextureAnimater<IAnimationParsedData>`),`parseRenderAnimation(buffer: ArrayBuffer): Promise<IAnimationParsedData>`;本计划不写任何实现体。"
- "**OI-4(只记录)**:`estimate` 未校准(`confidence: low`);本计划不运行构建或测试,因此不做运行时可用性主张。"
- "**OI-5(只记录)**:09-52 的产物仍未提交;本计划不提交任何内容,改动留在工作树供审阅。"
---
<!-- planner-discipline-allow: IAnimationAnimaterData -->
<!-- planner-discipline-allow: IAnimationData -->
<!-- planner-discipline-allow: IAnimationParsedData -->
<!-- planner-discipline-allow: IAnimationAnimater -->
<!-- planner-discipline-allow: animater -->
<!-- planner-discipline-allow: RenderAnimater -->
<!-- planner-discipline-allow: parseRenderAnimation -->
<objective>
Phase 9 · 第 53 个计划(09-52 的修订增量)—— 把 `@motajs/render-anim` 的公开类型从「解析数据内含动画器」拆分为「纯解析数据 + 独立动画器类型」。
**要解决的问题:** 09-52 落下的 `IAnimationData` 同时包含解析数据与 `animater` 成员,且 `animater` 的泛型实参又是 `IAnimationAnimaterData`(与解析数据成员相同)。用户审阅后否决:`data.animater.once(texture, data)` 会把同一个对象既当作动画器自身的数据又当作参数传入,语义不合理。用户批准的拆分设计是:解析数据不再包含动画器;动画器是独立的 `ITextureAnimater<IAnimationParsedData>` 类型别名。
**做法(锁定设计,仅类型):**
- `packages/render-anim/src/types.ts`:删除 `IAnimationAnimaterData` 与 `IAnimationData`;新增 `IAnimationParsedData`(`texture` / `bitmaps` / `frames` / `blend`,成员顺序与用户给定一致)与 `export type IAnimationAnimater = ITextureAnimater<IAnimationParsedData>;`。
- 保持 `AnimationBlendMode`(`export const enum`,`Normal` / `Add`)、`IAnimationBitmap`、`IAnimationSound`、`IAnimationObject`、`IAnimationFrame` 及第 1–61 行逐字不变。
- `packages/render-anim/src/index.ts` 与 `package.json` **只验证、不修改**(哈希不变)。
- 本计划不实现 `RenderAnimater` / `parseRenderAnimation`;实现另立后续计划。
**约束:** 只写 `files_modified` 的 2 个路径;不改任何其它源码 / 配置 / 计划;不提交;不做浏览器 / 测试验证,只做只读静态门禁。(D-01 增量派发 / D-02 计划累积追加 / D-03 执行前汇报 / D-04 命名先反馈 / D-05 默认不逐阶段验证 / D-06 范围由用户界定。)
**修改方式声明(AGENTS.md):** 执行期间的 `types.ts` 修改与 `09-53-SUMMARY.md` 写入全部由**派发子代理(`gsd-executor`)执行**,非内联执行;本计划文件自身由规划流程(`gsd-planner`)创建。除 `files_modified` 外无任何写入。
**可见跳过:** spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)。本阶段无 requirement ID,不创建 RESEARCH.md / VALIDATION.md,Nyquist 验证不适用。
</objective>
## 本计划产出物(Artifacts this plan produces)
| 类型 | 名称 / 路径 | 说明 |
|------|-------------|------|
| 修改(类型) | `packages/render-anim/src/types.ts` | 删除两个旧接口,新增 `IAnimationParsedData` 与 `IAnimationAnimater` 类型别名;无任何实现 |
| 交付 | `.planning/phases/09-minimum-browser-runnable/09-53-SUMMARY.md` | 前后 diff、逐成员形态、门禁真实结果、跳过项、未校准估算、0 次提交 |
| 只验证不修改 | `packages/render-anim/src/index.ts` | 保持 `export * from './types';`,哈希与 Task 1 基线一致 |
| 只验证不修改 | `packages/render-anim/package.json` | 保持 `name` + `@motajs/render: workspace:*`,哈希与 Task 1 基线一致 |
| 只验证不修改 | 09-50 / 09-51 / 09-52 的计划与摘要 | 共 6 个文件哈希逐字不变 |
## 修改前后逐字对照(Task 2 的唯一依据)
### 修改前(`types.ts` 第 62–76 行,逐字)
```ts
export interface IAnimationAnimaterData {
/** 位图列表 */
readonly bitmaps: readonly IAnimationBitmap[];
/** 帧列表 */
readonly frames: readonly IAnimationFrame[];
}
export interface IAnimationData extends IAnimationAnimaterData {
/** 合成帧使用的纹理 */
readonly texture: ITexture;
/** 动画器 */
readonly animater: ITextureAnimater<IAnimationAnimaterData>;
/** 混合模式 */
readonly blend: AnimationBlendMode;
}
```
### 修改后(逐字)
```ts
export interface IAnimationParsedData {
/** 合成帧使用的纹理 */
readonly texture: ITexture;
/** 位图列表 */
readonly bitmaps: readonly IAnimationBitmap[];
/** 帧列表 */
readonly frames: readonly IAnimationFrame[];
/** 混合模式 */
readonly blend: AnimationBlendMode;
}
export type IAnimationAnimater = ITextureAnimater<IAnimationParsedData>;
```
**第 1–61 行保持逐字不变:** 首行普通引入 `import { ITexture, ITextureAnimater } from '@motajs/render';`、`AnimationBlendMode` 枚举、`IAnimationBitmap`、`IAnimationSound`、`IAnimationObject`、`IAnimationFrame` 全部不动。新块的四个成员注释文本沿用旧块中的既有注释,不新增、不改写注释;`动画器` 注释随 `IAnimationData.animater` 成员一并移除。
## 新类型逐成员对照(Task 3 的绑定判定依据)
| 导出类型 | 成员(顺序固定) | 成员 jsDoc(沿用既有文本) |
|----------|------------------|----------------------------|
| `IAnimationParsedData`(新增、`export interface`) | `readonly texture: ITexture` | 合成帧使用的纹理 |
| | `readonly bitmaps: readonly IAnimationBitmap[]` | 位图列表 |
| | `readonly frames: readonly IAnimationFrame[]` | 帧列表 |
| | `readonly blend: AnimationBlendMode` | 混合模式 |
| `IAnimationAnimater`(新增、`export type`) | — | 类型别名本身不写 jsDoc(dev.md 不建议) |
| `AnimationBlendMode`(不变) | `Normal` / `Add` | 普通混合 / 加算混合 |
| `IAnimationBitmap`(不变) | `x` / `y` / `width` / `height` | 保持 09-52 的既有注释 |
| `IAnimationSound`(不变) | `name` / `volume?` / `pitch?` | 保持 09-52 的既有注释 |
| `IAnimationObject`(不变) | `index` / `x` / `y` / `scale` / `opacity` / `mirror` / `rotate` | 保持 09-52 的既有注释 |
| `IAnimationFrame`(不变) | `width` / `height` / `originX` / `originY` / `objects` / `sound?` | 保持 09-52 的既有注释 |
**删除清单:** `IAnimationAnimaterData`(含 `bitmaps` / `frames` 两个成员与两条注释)、`IAnimationData`(含 `texture` / `animater` / `blend` 三个成员与三条注释、`extends IAnimationAnimaterData` 关系)。删除后 `types.ts` 的导出恰为:1 个 `export const enum` + 5 个 `export interface` + 1 个 `export type`。
## 覆盖审计(Multi-Source Coverage Audit)
| 源 | 条目 | 覆盖位置 |
|----|------|----------|
| GOAL(ROADMAP §Phase 9) | 「可在浏览器中运行并验证的最小版本」伞目标 | 本增量只调整类型契约,不新增可运行能力;SUMMARY 如实记录 |
| REQ | 本阶段无 requirement ID(`requirements: []`) | Nyquist 不适用 |
| RESEARCH | 本 run 无 RESEARCH.md | 不适用 |
| CONTEXT D-01 | 增量派发,一个任务一个计划 | 本计划即该派发的唯一计划,不拆分 |
| CONTEXT D-02 | 计划可累积追加 | plan 53,排在 09-01..09-52 之后(编号说明见 frontmatter) |
| CONTEXT D-03 | 执行前必须先汇报并等待「可以执行」 | Task 0(`blocking-human`),`autonomous: false` |
| CONTEXT D-04 | 新命名必须先反馈获批 | 两个新类型名与四个成员名均由用户提供,Task 0 声明不新增任何未获批名称 |
| CONTEXT D-05 | 默认不逐阶段验证 | 本计划仍做只读静态门禁,但明确不做运行时验证 |
| CONTEXT D-06 | 范围由用户界定,不擅自扩大 | `files_modified` 2 项 + Task 4 范围 / 哈希闸门 |
| 用户设计 | 解析数据不含动画器;`data.animater.once(texture, data)` 不合理 | `<objective>` + Task 0 + Task 2 删除 `animater` 成员 |
| 用户设计 | `IAnimationParsedData` 含 `texture` / `bitmaps` / `frames` / `blend` | 「新类型逐成员对照」+ Task 2 + Task 3 逐接口门禁 |
| 用户设计 | `IAnimationAnimater = ITextureAnimater<IAnimationParsedData>` | 修改后块 + Task 2 + Task 3 类型别名门禁 |
| 用户设计 | 删除 `IAnimationAnimaterData` 与 `IAnimationData` | 删除清单 + Task 2 + Task 3 旧类型移除门禁 |
| 用户设计 | `index.ts` / `package.json` 只验证、不修改 | Task 1 哈希基线 + Task 3 / Task 4 哈希闸门 |
| 用户设计 | 只做类型声明,不实现 `RenderAnimater` / 解析器 | Task 2 / Task 3 无实现门禁 |
| 用户设计 | 验证只读静态,不跑游戏 / 浏览器 / 测试;不提交 | Task 3 / Task 4 + `<verification>` + 0 次提交声明 |
<execution_context>
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
@AGENTS.md
@dev.md
@packages/render-anim/src/types.ts
@packages/render-anim/src/index.ts
@packages/render-anim/package.json
@.planning/phases/09-minimum-browser-runnable/09-52-PLAN.md
@.planning/phases/09-minimum-browser-runnable/09-52-SUMMARY.md
@packages/render/src/assets/types.ts
@tsconfig.json
@.prettierrc
@eslint.config.js
</context>
<tasks>
<task type="checkpoint:human-verify" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报精确前后 diff、拆分原因、dev.md 合规、影响范围与验证形态,等待恰好「可以执行」</name>
<precondition>`packages/render-anim/src/types.ts` 存在且当前为 09-52 的组合设计(含 `IAnimationAnimaterData` 与 `IAnimationData`);`packages/render-anim/src/index.ts` 与 `package.json` 存在且未被本计划修改;`09-52-SUMMARY.md` 状态为 complete;`node_modules` 内 prettier / eslint / vue-tsc 均存在。</precondition>
<files>无(本任务只读,不写任何文件;汇报后停下等待「可以执行」)</files>
<read_first>
- `.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md`(D-01 增量派发、D-03 汇报关卡、D-04 命名批准、D-06 范围界定)
- `AGENTS.md`(执行前须获批、命名先反馈、既有注释不得修改、最小修改也走 GSD、不派发额外子代理)
- `dev.md`(CRLF、4 空格、首行 import、禁止 `import type`、无 `any`、成员 jsDoc 在源头、不得复述代码、公开接口放 `types.ts`)
- `packages/render-anim/src/types.ts`(当前第 1–61 行与第 62–76 行)
- `packages/render-anim/src/index.ts`、`packages/render-anim/package.json`(只验证对象)
- `09-52-PLAN.md`、`09-52-SUMMARY.md`(被否决设计的来源与 09-52 已落地内容)
- `packages/render/src/assets/types.ts`(`ITexture` / `ITextureAnimater<T>` 的 `(texture, data)` 签名)
</read_first>
<acceptance_criteria>
- 已向用户逐条汇报 ①–⑨,包含修改前(第 62–76 行)与修改后(新块)的逐字 diff,而不是只报两个类型名。
- 已说明拆分原因:原 `IAnimationData` 内含 `animater`,且其泛型实参又是与解析数据相同的 `IAnimationAnimaterData`,导致 `data.animater.once(texture, data)` 语义不合理;用户已批准新拆分。
- 已把 `IAnimationParsedData` 的四个成员(名称 / 类型 / `readonly` / 顺序 / 注释文本)、`IAnimationAnimater` 的逐字别名、删除清单(两个旧接口 + `animater` 成员 + `动画器` 注释)列为显式批准项。
- 已报告 dev.md 合规点、影响范围(只改 `types.ts` + 写 SUMMARY;`index.ts` / `package.json` / 09-50 / 09-51 / 09-52 不变;不安装、不提交)、验证形态(只读静态 + 过滤类型门禁;不跑测试 / 浏览器 / 构建)。
- 用户针对本修订版明确回复「可以执行」后才进入 Task 1–4;在此之前不执行任何任务、不派发执行子代理。
</acceptance_criteria>
<what-built>
一份待批准的执行计划(`09-53-PLAN.md`):在 09-52 的类型骨架上做**纯类型拆分**——解析数据 `IAnimationParsedData` 不再包含动画器,动画器改为独立类型别名 `IAnimationAnimater = ITextureAnimater<IAnimationParsedData>`;删除 `IAnimationAnimaterData` 与 `IAnimationData`;不写任何实现。
</what-built>
<action>
**停下并向用户汇报,等待其恰好回复「可以执行」后才继续;本任务不写任何文件、不建目录、不派发执行子代理。**
① **要做什么 / 为什么:** 09-52 落下的设计把解析数据与动画器合在 `IAnimationData` 中,且 `animater` 的泛型实参就是解析数据本身的形状。用户指出 `data.animater.once(texture, data)` 等于把同一个对象既当作动画器自身的数据又当作参数传入,不合理,因此批准拆分:解析数据是纯数据,动画器是独立的 `ITextureAnimater<IAnimationParsedData>` 类型。本计划只改类型,不实现动画器与解析器。
② **精确前后 diff(逐字):**
- **修改前(`types.ts` 第 62–76 行):** `IAnimationAnimaterData` 含 `readonly bitmaps: readonly IAnimationBitmap[]` 与 `readonly frames: readonly IAnimationFrame[]`;`IAnimationData extends IAnimationAnimaterData` 含 `readonly texture: ITexture`、`readonly animater: ITextureAnimater<IAnimationAnimaterData>`、`readonly blend: AnimationBlendMode`。
- **修改后:** 新增 `IAnimationParsedData`,成员顺序固定为 `readonly texture: ITexture`(注释「合成帧使用的纹理」)、`readonly bitmaps: readonly IAnimationBitmap[]`(注释「位图列表」)、`readonly frames: readonly IAnimationFrame[]`(注释「帧列表」)、`readonly blend: AnimationBlendMode`(注释「混合模式」);随后新增 `export type IAnimationAnimater = ITextureAnimater<IAnimationParsedData>;`。两个旧接口及其成员、注释、`extends` 关系全部删除。
- **第 1–61 行逐字不变:** 首行 `import { ITexture, ITextureAnimater } from '@motajs/render';`、`AnimationBlendMode` 枚举、`IAnimationBitmap`、`IAnimationSound`、`IAnimationObject`、`IAnimationFrame` 均不动。
③ **dev.md 合规点(逐条):** UTF-8 无 BOM + CRLF + 4 空格、无制表符;首行必须是普通 `import`,import 之间不夹注释与空行;不使用 `import type`;不使用 `any` / `as` / `?.` / `!`;`IAnimationParsedData` 每个成员前有一行中文 jsDoc,类型别名本身不写 jsDoc(dev.md 不建议给接口 / 类型别名本身写注释);新块只沿用既有注释文本(「合成帧使用的纹理」/「位图列表」/「帧列表」/「混合模式」),不改写任何既有注释;`动画器` 注释随其成员与所属接口一并移除;不写实现体;公开类型集中在 `types.ts`;`index.ts` 保持同目录星号导出。
④ **命名:** 两个新公开名称 `IAnimationParsedData`、`IAnimationAnimater` 与四个成员名 `texture` / `bitmaps` / `frames` / `blend` 全部由用户给定;本计划不新增任何其它公开名称。若需改名,请在批准前说明。
⑤ **影响范围:** 只修改 `packages/render-anim/src/types.ts`,并写入执行后的 `09-53-SUMMARY.md`;不改 `packages/render-anim/src/index.ts`、`packages/render-anim/package.json`、其它包、根配置、锁文件、09-50 / 09-51 / 09-52 的计划与摘要、`STATE.md`、`ROADMAP.md`、`09-CONTEXT.md`;不运行 `pnpm install`;不新增包级 tsconfig。
⑥ **验证形态(只读静态):** prettier `--check`(`types.ts`,并复核 `index.ts` / `package.json` 未被改动)、eslint(`types.ts`)、CRLF / BOM / 制表符门禁、逐接口成员门禁(成员归属、顺序、类型、`readonly`、新增接口的成员注释)、旧类型移除门禁、无实现门禁、无 `import type` / `any` 门禁;类型门禁只过滤 `vue-tsc --noEmit` 输出中的 `packages/render-anim` 诊断,全仓既有类型错误与非 0 退出码属既有基线、不视为本次失败。**不运行测试、构建、dev server 或浏览器;不做运行时主张。**
⑦ **不提交:** 全程 0 次提交,禁止任何 git 写命令;改动留在工作树供审阅。
⑧ **编号与依赖:** 派发编号 09-53 / 波次 2,`depends_on: 09-52`;`09-01`..`09-52` 均已占用,本计划不删除、不覆盖任何既有记录。
⑨ **等待批准:** 请确认以上内容后,回复恰好四个字「可以执行」。任何修改意见请直接说明;未收到「可以执行」之前不开始 Task 1–4 的任何一步。
</action>
<verify>用户针对本修订版恰好回复「可以执行」后才进入 Task 1–4;在此之前不执行 Task 1 / Task 2 / Task 3 / Task 4 的任何一步,也不派发执行子代理。</verify>
<done>已完整汇报 ①–⑨(精确前后 diff、拆分原因、四个成员与类型别名的逐字形态、删除清单、dev.md 合规点、命名、影响范围、只读静态验证口径、不提交、编号),并收到用户针对本修订版的「可以执行」回复。</done>
<how-to-verify>
逐条核对 ①–⑨ 是否与你的意图一致,特别是:② 的修改前 / 修改后逐字块与第 1–61 行不变范围;③ 的注释沿用与 `动画器` 注释移除;④ 的命名;⑤ 的范围(只改 `types.ts`、`index.ts` / `package.json` 不动);⑥ 的「只读静态 + 过滤类型门禁」口径;⑦ 的 0 次提交。
</how-to-verify>
<resume-signal>请在确认本修订版(尤其是 ② 的精确 diff、③ 的注释方案、⑤ 的范围)后,回复恰好四个字「可以执行」。任何修改意见请直接说明。</resume-signal>
</task>
<task type="auto">
<name>Task 1: 读取当前类型与 09-52 记录,采集范围 / 哈希基线(只读)</name>
<files>无(本任务只读;不写仓库内任何文件)</files>
<read_first>
- `AGENTS.md`(执行前获批、命名先反馈、既有注释不得修改、最小修改也走 GSD、GSD 流程豁免范围)
- `dev.md`(CRLF、4 空格、首行 import、禁止 `import type`、无 `any`、成员 jsDoc 在源头、公开接口放 `types.ts`)
- `packages/render-anim/src/types.ts`(当前第 1–61 行与待替换的第 62–76 行)
- `.planning/phases/09-minimum-browser-runnable/09-52-PLAN.md`、`09-52-SUMMARY.md`(原始设计、已落地门禁与未提交状态)
- `packages/render/src/assets/types.ts`(`ITexture` / `ITextureAnimater<T>` 的形状与 jsDoc 风格)
- `packages/render-anim/src/index.ts`、`packages/render-anim/package.json`(只验证对象)
- `tsconfig.json`、`.prettierrc`、`eslint.config.js`(路径映射与格式化 / lint 配置)
- `09-50-PLAN.md`、`09-50-SUMMARY.md`、`09-51-PLAN.md`、`09-51-SUMMARY.md`、`09-53-PLAN.md`(哈希基线对象)
</read_first>
<action>
仅在收到用户恰好回复「可以执行」之后执行。本任务不写仓库内文件,只读取并记录,供 Task 2 落码、Task 3 复核、Task 4 写入 SUMMARY。
1. 读取并记录以下事实(逐条写进执行笔记,Task 4 汇总进 SUMMARY):
- `AGENTS.md`:执行前须获批、命名先反馈、不得修改既有注释、最小修改也走 GSD、GSD 流程豁免范围。
- `dev.md`:CRLF、4 空格、模块无副作用、不转发导出、无类型导入、import 必须在首行且中间不夹注释 / 空行、命名规则、注释规范、类型规范(无必要 `any`、显式成员类型、合理 `readonly`、对象类型单独 `interface`、公开接口放 `types.ts`)。
- `packages/render-anim/src/types.ts`:第 1–61 行逐字不变;第 62–76 行是待替换的组合设计块,必须与计划正文「修改前(逐字)」完全相同。
- `09-52-PLAN.md` / `09-52-SUMMARY.md`:09-52 的原始设计、已落地的 1 个枚举 + 6 个接口、门禁记录、未提交状态。
- `packages/render/src/assets/types.ts`:`ITexture` 为接口;`ITextureAnimater<T>` 的 `once(texture, data)` / `cycled(texture, data)` 均返回 `Generator<ITextureRenderable, void>`;成员 jsDoc 为单行中文、结尾不加句号。
- `packages/render-anim/src/index.ts`:唯一一行 `export * from './types';`。
- `packages/render-anim/package.json`:仅 `name` 与 `dependencies['@motajs/render'] = 'workspace:*'` 两个顶层键。
- 根 `tsconfig.json`:`include` 覆盖 `packages/**/*.ts`,`paths['@motajs/*'] = ['./packages/*/src']`,`strict: true`、`isolatedModules: true`、`noEmit: true`。
- `.prettierrc`(`printWidth: 80` / `tabWidth: 4` / `singleQuote: true` / `arrowParens: avoid` / `trailingComma: none` / `endOfLine: crlf`)与 `eslint.config.js`(typescript-eslint + prettier 插件)。
2. 记录**精确目标形态**:`IAnimationParsedData` 的四个成员(名称 / 类型 / `readonly` / 顺序 / 沿用注释文本)与 `IAnimationAnimater` 类型别名的逐字声明;删除清单(`IAnimationAnimaterData`、`IAnimationData`、`animater` 成员、`动画器` 注释)。方案中不得出现用户未提供的公开名称。
3. 采集**范围基线**:`%TEMP%` **不是文件系统路径**,基线文件的实际位置由 Node 的 `process.env.TEMP` 解析;后续所有门禁都写作 `(process.env.TEMP||'.')`,Windows 下 `TEMP` 始终存在,两处解析到同一目录。用下面这条 `node` 命令把当前 `git status --porcelain -uall` 的完整输出(先统一为 LF)写入 `gsd-09-53-status.txt`(UTF-8 无 BOM),并回显绝对路径:`node -e "const{execSync}=require('child_process'),fs=require('fs');const out=execSync('git status --porcelain -uall',{encoding:'utf8'}).replace(/\r\n/g,'\n');const p=(process.env.TEMP||'.')+'/gsd-09-53-status.txt';fs.writeFileSync(p,out,{encoding:'utf8'});console.log(p);"`。该基线用于 Task 4 做集合差,因为工作树可能已存在大量既有未提交改动。写入后必须立刻断言:基线文件内容与当前 `git status --porcelain -uall` 完全一致(见 `<verify>` 第 1 条),否则重新采集;不得用 `.trim()` 之外的方式放宽比较。
4. 采集**哈希基线**:对以下 10 个路径逐个执行 `git hash-object`,把「无空格路径 + 空格 + 40 位哈希」写入同一 `process.env.TEMP` 目录下的 `gsd-09-53-hashes.txt`(UTF-8 无 BOM):`packages/render-anim/src/types.ts`、`packages/render-anim/src/index.ts`、`packages/render-anim/package.json`、`.planning/phases/09-minimum-browser-runnable/09-50-PLAN.md`、`09-50-SUMMARY.md`、`09-51-PLAN.md`、`09-51-SUMMARY.md`、`09-52-PLAN.md`、`09-52-SUMMARY.md`、`09-53-PLAN.md`。命令:`node -e "const{execSync}=require('child_process'),fs=require('fs');const files=['packages/render-anim/src/types.ts','packages/render-anim/src/index.ts','packages/render-anim/package.json','.planning/phases/09-minimum-browser-runnable/09-50-PLAN.md','.planning/phases/09-minimum-browser-runnable/09-50-SUMMARY.md','.planning/phases/09-minimum-browser-runnable/09-51-PLAN.md','.planning/phases/09-minimum-browser-runnable/09-51-SUMMARY.md','.planning/phases/09-minimum-browser-runnable/09-52-PLAN.md','.planning/phases/09-minimum-browser-runnable/09-52-SUMMARY.md','.planning/phases/09-minimum-browser-runnable/09-53-PLAN.md'];const lines=files.map(f=>f+' '+execSync('git hash-object '+f,{encoding:'utf8'}).trim());const p=(process.env.TEMP||'.')+'/gsd-09-53-hashes.txt';fs.writeFileSync(p,lines.join('\n')+'\n',{encoding:'utf8'});console.log(p);console.log(lines.join('\n'));"`。把 `09-53-PLAN.md` 纳入基线,是为了证明执行期间计划文件本身未被改写(它不在 `files_modified` 内)。
5. 断言 `types.ts` 当前仍是旧组合设计:包含 `export interface IAnimationAnimaterData` 与 `export interface IAnimationData extends IAnimationAnimaterData`,且不包含 `IAnimationParsedData` 与 `IAnimationAnimater =`。若已被改动,停下上报,不得继续 Task 2。
6. 确认 `packages/render-anim` 的 git 状态恰为 3 条未跟踪文件(`?? packages/render-anim/package.json`、`?? packages/render-anim/src/index.ts`、`?? packages/render-anim/src/types.ts`),与本计划假设一致。任一命令非 0 退出或输出与预期不符,必须停下上报,不得继续 Task 2。
7. 采集**不变前缀与注释基线**:用下面这条 `node` 命令计算 `types.ts` 第 1–61 行(按 CRLF 精确拼接、含第 61 行行尾 CRLF)的 sha256,写入 `process.env.TEMP` 的 `gsd-09-53-prefix.txt`:`node -e "const fs=require('fs'),crypto=require('crypto');const s=fs.readFileSync('packages/render-anim/src/types.ts','utf8');if(s.split('\r\n').length<62){console.error('types.ts not CRLF or shorter than 62 lines');process.exit(1);}const prefix=s.split('\r\n').slice(0,61).join('\r\n')+'\r\n';const h=crypto.createHash('sha256').update(prefix,'utf8').digest('hex');const p=(process.env.TEMP||'.')+'/gsd-09-53-prefix.txt';fs.writeFileSync(p,'lines 1-61 sha256 '+h+'\n',{encoding:'utf8'});console.log(p);console.log('sha256 '+h);"`。该哈希是 Task 2 / Task 3 证明「第 1–61 行逐字不变、既有注释未被改写」的唯一依据;Task 2 / Task 3 会重算同一段前缀的 sha256 并与该基线逐字比较,任何注释改写都会使哈希漂移。随后用 `<verify>` 第 5 条命令断言前缀里确实含枚举注释 `/** 普通混合 */` 与 `/** 加算混合 */`,确保基线本身是 09-52 的既有注释文本。
</action>
<verify>
<automated>node -e "const{execSync}=require('child_process'),fs=require('fs');const p=(process.env.TEMP||'.')+'/gsd-09-53-status.txt';const base=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n').trim();const now=execSync('git status --porcelain -uall',{encoding:'utf8'}).replace(/\r\n/g,'\n').trim();if(base!==now){console.error('baseline drift');process.exit(1);}console.log('OK: status baseline matches current tree');"</automated>
<fails_when>基线文件与当前 `git status --porcelain -uall` 不一致(打印 `baseline drift`,退出码 1)。</fails_when>
<automated>node -e "const fs=require('fs');const p=(process.env.TEMP||'.')+'/gsd-09-53-hashes.txt';const s=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n').trim().split('\n').filter(Boolean);const ok=s.length===10&&s.every(l=>{const a=l.split(' ');return a.length===2&&a[1].length===40&&!/[^0-9a-f]/.test(a[1]);});if(!ok){console.error(s.join('\n'));process.exit(1);}console.log(s.join('\n'));"</automated>
<fails_when>哈希基线不是 10 行,或任一行不是「无空格路径 + 40 位小写十六进制」,脚本打印内容并以 1 退出。</fails_when>
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('packages/render-anim/src/types.ts','utf8');const fail=[];if(t.indexOf('export interface IAnimationAnimaterData')===-1){fail.push('old IAnimationAnimaterData missing');}if(t.indexOf('export interface IAnimationData extends IAnimationAnimaterData')===-1){fail.push('old IAnimationData missing');}if(t.indexOf('IAnimationParsedData')!==-1){fail.push('new IAnimationParsedData already present');}if(t.indexOf('IAnimationAnimater =')!==-1){fail.push('new IAnimationAnimater alias already present');}if(fail.length){console.error(fail.join('\n'));process.exit(1);}console.log('OK: pre-edit combined types present, split types absent');"</automated>
<fails_when>`types.ts` 已不是 09-52 的组合设计,或已包含新类型(打印缺失 / 已存在项,退出码 1)。</fails_when>
<automated>node -e "const{execSync}=require('child_process');const out=execSync('git status --porcelain -uall -- packages/render-anim',{encoding:'utf8'}).replace(/\r\n/g,'\n').trim().split('\n').filter(Boolean).sort();const want=['?? packages/render-anim/package.json','?? packages/render-anim/src/index.ts','?? packages/render-anim/src/types.ts'];const ok=out.length===3&&want.every(w=>out.indexOf(w)!==-1);console.log(out.join('\n'));if(!ok){console.error('unexpected render-anim status');process.exit(1);}console.log('OK: render-anim has the 3 expected untracked files');"</automated>
<fails_when>`packages/render-anim` 的未跟踪文件不是恰好 3 个预期路径(打印实际状态与 `unexpected render-anim status`,退出码 1)。</fails_when>
<automated>node -e "const fs=require('fs');const p=(process.env.TEMP||'.')+'/gsd-09-53-prefix.txt';const t=fs.readFileSync(p,'utf8');const m=/sha256 ([0-9a-f]{64})/.exec(t);const s=fs.readFileSync('packages/render-anim/src/types.ts','utf8');const head=s.split('\r\n').slice(0,61).join('\n');const need=['/** 普通混合 */','/** 加算混合 */'];const miss=need.filter(n=>head.indexOf(n)===-1);const fail=[];if(!m){fail.push('prefix baseline missing');}if(s.split('\r\n').length<62){fail.push('types.ts not CRLF or shorter than 62 lines');}if(miss.length){fail.push('missing enum jsDoc: '+miss.join(','));}console.log(fail.length?'FAIL\n'+fail.join('\n'):'OK: prefix baseline '+m[1]+' + enum jsDoc present');process.exit(fail.length?1:0);"</automated>
<fails_when>`gsd-09-53-prefix.txt` 缺失 / 不是 64 位 sha256,`types.ts` 不是 CRLF 或不足 62 行,或第 1–61 行缺少 `/** 普通混合 */` / `/** 加算混合 */` 枚举注释(打印 `FAIL`,退出码 1)。</fails_when>
</verify>
<acceptance_criteria>
- 已读取并在执行笔记中记录 `read_first` 列出的全部事实(规则、当前类型、09-52 设计、`@motajs/render` 类型、index / package、根配置、格式化 / lint 配置)。
- `process.env.TEMP` 目录下的 `gsd-09-53-status.txt`(与门禁 `(process.env.TEMP||'.')` 解析一致,不是 `%TEMP%` 字面量)已生成,且 Task 1 结束时与当前 `git status --porcelain -uall` 完全一致。
- `process.env.TEMP` 目录下的 `gsd-09-53-hashes.txt`(与门禁同一路径解析)恰有 10 行,覆盖 `types.ts`、`index.ts`、`package.json`、09-50 / 09-51 / 09-52 的计划与摘要、`09-53-PLAN.md`,每行「无空格路径 + 空格 + 40 位哈希」。
- `types.ts` 仍为旧组合设计(两个旧接口存在、新接口 / 新别名不存在),且 `packages/render-anim` 的 git 状态恰为 3 条预期未跟踪文件。
- 已记录 `IAnimationParsedData` 的四个成员与 `IAnimationAnimater` 别名的精确方案,且未新增用户未提供的公开名称。
- `process.env.TEMP` 目录下的 `gsd-09-53-prefix.txt` 已生成,含第 1–61 行的 sha256;并已确认第 1–61 行含 `/** 普通混合 */` 与 `/** 加算混合 */` 枚举注释。
</acceptance_criteria>
<done>已读取并记录 dev.md / AGENTS.md 的适用规则、当前 `types.ts`、09-52 的原始设计与摘要、`@motajs/render` 的类型与 jsDoc 风格、`index.ts` / `package.json` 现状;`process.env.TEMP` 目录下的 `gsd-09-53-status.txt` 与 `gsd-09-53-hashes.txt`(与门禁 `(process.env.TEMP||'.')` 解析一致)均已生成且内容完整(哈希基线 10 条,含 `09-53-PLAN.md`);`gsd-09-53-prefix.txt` 已保存第 1–61 行的 sha256 前缀基线并核对枚举注释;`types.ts` 仍为旧组合设计;`packages/render-anim` 恰为 3 条预期未跟踪文件。</done>
</task>
<task type="auto">
<name>Task 2: 用一次精确块替换把 `types.ts` 改为拆分后的类型(只改此文件)</name>
<files>packages/render-anim/src/types.ts</files>
<read_first>
- `.planning/phases/09-minimum-browser-runnable/09-53-PLAN.md` 的「修改前后逐字对照」与「新类型逐成员对照」(唯一内容依据;本任务不新增任何对外名称)
- `packages/render-anim/src/types.ts`(待修改对象,第 1–61 行必须保持逐字不变)
- `packages/render/src/assets/types.ts`(`ITexture` / `ITextureAnimater<T>` 的导入来源与签名)
- `dev.md`(CRLF、4 空格、首行 import、无 `import type` / `any`、成员 jsDoc、不得改写既有注释)
</read_first>
<action>
仅在 Task 1 完成且收到「可以执行」之后执行。本任务只修改 `packages/render-anim/src/types.ts`,不得写其它任何文件。
1. 读取 `types.ts`,确认第 1–61 行与 09-52 落地内容一致,第 62–76 行逐字等于计划正文「修改前(逐字)」块。若不一致,停下上报,不得自行猜测替换范围。
2. 用 Edit 工具做一次精确块替换:`oldString` 为计划正文「修改前(逐字)」整块(从 `export interface IAnimationAnimaterData {` 到 `IAnimationData` 的闭合 `}`),`newString` 为「修改后(逐字)」整块(`IAnimationParsedData` 接口 + `IAnimationAnimater` 类型别名)。`oldString` 必须按 `types.ts` 的实际 CRLF 文本读取(可用 Node 读取第 62–76 行作为匹配源),不得把计划 Markdown 中的 LF 直接当作文件内容;Edit 因换行不匹配而失败时停下上报,不得改用 Write 覆盖整文件。不得改动第 1–61 行;不得改写任何既有注释;不得新增或调整 import。
3. 替换后逐字检查:
- 新接口成员顺序固定为 `texture` → `bitmaps` → `frames` → `blend`;四个成员均为 `readonly`,数组成员仍为 `readonly` 数组。
- 每个成员前保留一行既有中文 jsDoc(「合成帧使用的纹理」/「位图列表」/「帧列表」/「混合模式」),文本逐字不变。
- 第 1–61 行的 sha256 必须与 Task 1 的 `gsd-09-53-prefix.txt` 基线一致;四个成员 jsDoc 必须逐字为 `/** 合成帧使用的纹理 */` / `/** 位图列表 */` / `/** 帧列表 */` / `/** 混合模式 */`。任何注释改写(包括改成英文)都会分别使前缀哈希漂移或精确文本门禁失败。
- 类型别名逐字为 `export type IAnimationAnimater = ITextureAnimater<IAnimationParsedData>;`。
- 两个旧接口及其成员 / 注释 / `extends` 不再存在;解析数据中不再有 `animater` 成员。
- 文件内没有 `class` / `function` / `let` / 箭头函数 / `export default`;`const` / `enum` 仅出现在 `AnimationBlendMode` 声明处。
4. 如需格式修复,仅可运行 `node node_modules/prettier/bin/prettier.cjs --write packages/render-anim/src/types.ts`(本任务唯一被授权的写操作,且仅限此文件);不得对 `index.ts` / `package.json` 或其它文件运行 `--write`。随后运行 `--check` 确认。
5. 不运行测试、构建、dev server、浏览器或包管理命令;不执行 git 写命令。
</action>
<verify>
<automated>node -e "const fs=require('fs');if(!fs.existsSync('packages/render-anim/src/types.ts')){console.error('missing types.ts');process.exit(1);}console.log('OK: types.ts exists');"</automated>
<fails_when>`packages/render-anim/src/types.ts` 不存在(退出码 1)。</fails_when>
<automated>node node_modules/prettier/bin/prettier.cjs --check packages/render-anim/src/types.ts</automated>
<fails_when>输出不是 `All matched files use Prettier code style!`,或退出码非 0(prettier 列出待格式化文件)。</fails_when>
<automated>node -e "const fs=require('fs');const b=fs.readFileSync('packages/render-anim/src/types.ts');const s=b.toString('utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;const tabs=(s.match(/\t/g)||[]).length;const bom=b[0]===0xEF&&b[1]===0xBB&&b[2]===0xBF;const ok=lf===crlf&&tabs===0&&!bom;console.log((ok?'OK':'FAIL')+' lf='+lf+' crlf='+crlf+' tabs='+tabs+' bom='+bom);process.exit(ok?0:1);"</automated>
<fails_when>出现非 CRLF 换行、制表符或 BOM(打印 `FAIL`,退出码 1)。</fails_when>
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('packages/render-anim/src/types.ts','utf8').replace(/\r\n/g,'\n');const fail=[];const block=/export interface IAnimationParsedData \{\n([\s\S]*?)\n\}/.exec(t);if(!block){fail.push('missing IAnimationParsedData');}else{const got=block[1].split('\n').map(l=>l.trim()).filter(l=>l.length&&!l.startsWith('/**')&&!l.startsWith('*')&&!l.startsWith('//')).join(' ');const want='readonly texture: ITexture; readonly bitmaps: readonly IAnimationBitmap[]; readonly frames: readonly IAnimationFrame[]; readonly blend: AnimationBlendMode;';if(got!==want){fail.push('members: '+got);}}if(!/export type IAnimationAnimater = ITextureAnimater<IAnimationParsedData>;/.test(t)){fail.push('alias');}if(/\bIAnimationAnimaterData\b/.test(t)){fail.push('old IAnimationAnimaterData');}if(/\bIAnimationData\b/.test(t)){fail.push('old IAnimationData');}if(/\banimater\b/.test(t)){fail.push('animater member');}console.log(fail.length?'FAIL\n'+fail.join('\n'):'OK: split applied');process.exit(fail.length?1:0);"</automated>
<fails_when>新接口成员顺序 / 类型 / 只读性与计划不一致,类型别名缺失,或旧接口 / `animater` 成员仍存在(打印 `FAIL` 与差异,退出码 1)。</fails_when>
<automated>node -e "const{execSync}=require('child_process'),fs=require('fs');const p=(process.env.TEMP||'.')+'/gsd-09-53-hashes.txt';const base=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n').trim().split('\n').filter(Boolean);const now=base.map(l=>{const f=l.split(' ')[0];return f+' '+execSync('git hash-object '+f,{encoding:'utf8'}).trim();});const changed=now.filter((l,k)=>l!==base[k]);const types=now.filter(l=>l.split(' ')[0]==='packages/render-anim/src/types.ts');const ok=changed.length===1&&types.length===1&&changed[0]===types[0];console.log('changed:\n'+changed.join('\n'));if(!ok){console.error('unexpected changed set');process.exit(1);}console.log('OK: only types.ts changed');"</automated>
<fails_when>除 `types.ts` 外还有基线路经被改动,或 `types.ts` 未变化(打印 `unexpected changed set`,退出码 1)。</fails_when>
<automated>node -e "const fs=require('fs'),crypto=require('crypto');const p=(process.env.TEMP||'.')+'/gsd-09-53-prefix.txt';const exp=/([0-9a-f]{64})/.exec(fs.readFileSync(p,'utf8'));const s=fs.readFileSync('packages/render-anim/src/types.ts','utf8');const prefix=s.split('\r\n').slice(0,61).join('\r\n')+'\r\n';const got=crypto.createHash('sha256').update(prefix,'utf8').digest('hex');const docs=[' /** 合成帧使用的纹理 */',' /** 位图列表 */',' /** 帧列表 */',' /** 混合模式 */'];const miss=docs.filter(d=>s.indexOf(d)===-1);const fail=[];if(!exp){fail.push('prefix baseline missing');}else if(got!==exp[1]){fail.push('prefix drift '+got+' != '+exp[1]);}if(miss.length){fail.push('missing exact jsDoc: '+miss.join(','));}console.log(fail.length?'FAIL\n'+fail.join('\n'):'OK: lines 1-61 unchanged + exact member jsDoc present');process.exit(fail.length?1:0);"</automated>
<fails_when>第 1–61 行前缀 sha256 与 Task 1 基线不一致(说明既有文本 / 注释被改写),或四个成员 jsDoc 不是逐字的 `/** 合成帧使用的纹理 */` / `/** 位图列表 */` / `/** 帧列表 */` / `/** 混合模式 */`(打印 `FAIL` 与差异,退出码 1)。</fails_when>
</verify>
<acceptance_criteria>
- `types.ts` 由 `gsd-executor` 用 Edit 工具做一次精确块替换,未用 Write 覆盖整文件;第 1–61 行逐字不变。
- 新接口 `IAnimationParsedData` 的成员顺序、类型、`readonly`、只读数组与计划逐项一致;四个成员各有且只有一行沿用 09-52 文本的 jsDoc,且逐字为指定中文文本(由前缀哈希与精确文本门禁自动证明)。
- `IAnimationAnimater` 类型别名逐字为 `ITextureAnimater<IAnimationParsedData>`。
- 两个旧接口、`animater` 成员与 `动画器` 注释已全部移除;`index.ts` / `package.json` 的哈希与 Task 1 基线一致。
- 第 1–61 行前缀 sha256 与 Task 1 基线一致,四个成员 jsDoc 逐字存在——既有注释与新成员注释均未被改写。
- `prettier --write` 仅在需要时对 `types.ts` 运行,未触碰任何其它文件;文件为 CRLF、无制表符、无 BOM。
</acceptance_criteria>
<done>`types.ts` 已从组合设计替换为拆分设计:`IAnimationParsedData`(`texture` / `bitmaps` / `frames` / `blend`)+ `IAnimationAnimater = ITextureAnimater<IAnimationParsedData>`;两个旧接口与 `animater` 成员已删除;第 1–61 行逐字不变(前缀 sha256 与 Task 1 基线一致);四个成员 jsDoc 逐字存在;prettier `--check`、CRLF / BOM / 制表符与内容门禁通过;`index.ts` / `package.json` 未改动。</done>
</task>
<task type="auto">
<name>Task 3: dev.md 合规与内容门禁(只读静态检查)</name>
<files>无(本任务只读;不修改任何文件)</files>
<read_first>
- `.planning/phases/09-minimum-browser-runnable/09-53-PLAN.md` 的「修改前后逐字对照」与「新类型逐成员对照」(成员归属、顺序、类型、`readonly`、注释的唯一判定依据)
- `packages/render-anim/src/types.ts`(待检查对象)
- `packages/render-anim/src/index.ts`、`packages/render-anim/package.json`(只验证未改动)
- `dev.md`(CRLF、4 空格、首行 import、无 `import type` / `any`、成员 jsDoc 在源头、无实现体)
- `.prettierrc` 与 `eslint.config.js`(门禁使用的实际配置)
</read_first>
<action>
只运行只读检查;任何一项失败都必须回到 Task 2 修正后重跑全部检查,不得通过放宽检查、删除断言或忽略文件来「变绿」。本任务**严格只读**:不运行 `prettier --write`、不写任何文件。
1. **格式门禁(prettier)**:运行 `node node_modules/prettier/bin/prettier.cjs --check packages/render-anim/src/types.ts packages/render-anim/src/index.ts packages/render-anim/package.json`,必须退出码 0 且输出 `All matched files use Prettier code style!`。失败时**回到 Task 2** 修正(格式归一化由 Task 2 的 `prettier --write` 授权执行),本任务不得运行 `--write`、不得手改文件或格式规则;Task 2 修正后由本任务重跑全部检查。
2. **lint 门禁(eslint)**:运行 `node node_modules/eslint/bin/eslint.js packages/render-anim/src/types.ts packages/render-anim/src/index.ts`,必须退出码 0、无输出。失败时**本任务不修改任何源码**:把 eslint 的原始输出(文件、行号、规则名、说明)逐字记录并上报,然后**回到 Task 2** 修正(只改 `types.ts`,不得添加 `eslint-disable`、不得改 eslint 配置);Task 2 修正后由本任务从第 1 步起重跑全部门禁。
3. **换行 / BOM / 缩进门禁**:运行 `<verify>` 第 3 条 node 命令,必须输出 `OK` 且退出码 0(每个换行都是 CRLF、无制表符、无 BOM)。
4. **内容与结构门禁**:运行 `<verify>` 第 4 条 node 命令。它逐接口解析并断言:
- 恰好 5 个 `export interface`:`IAnimationBitmap`、`IAnimationSound`、`IAnimationObject`、`IAnimationFrame`、`IAnimationParsedData`;无 `extends`。
- 每个接口的成员行序列与计划「修改前后逐字对照」逐字一致;成员被放进错误接口、顺序变化、`readonly` / `?` / 类型变化都会使门禁失败。
- `IAnimationParsedData` 的每个成员前恰有一行单行 jsDoc,且文本逐字为 `/** 合成帧使用的纹理 */` / `/** 位图列表 */` / `/** 帧列表 */` / `/** 混合模式 */`(`docCom` 精确匹配,改写即失败),且无 `animater` 成员。
- `export type IAnimationAnimater = ITextureAnimater<IAnimationParsedData>;` 逐字存在。
- 旧接口 `IAnimationAnimaterData`、`IAnimationData` 不再出现;`RenderAnimater`、`parseRenderAnimation` 不出现。
- 无 `class` / `function` / `let` / 箭头函数 / `export default`;`const` / `enum` 各恰好 1 次且只在 `AnimationBlendMode` 声明处;无 `any`、无 `import type`;首行是普通 `import`;`AnimationBlendMode` 成员序列恰为 `Normal, Add`。
5. **index / package 未改动门禁**:运行 `<verify>` 第 5 条 node 命令,`index.ts` 与 `package.json` 的当前哈希必须与 Task 1 基线一致;任何差异都说明越界修改,必须停下上报并回到 Task 2 修正(只允许改 `types.ts`,不得改这两个文件)。
6. **人工逐条复核(只读,且是绑定判定)**:打开 `types.ts`,逐成员对照计划「新类型逐成员对照」,确认:第 1–61 行逐字未变(前缀 sha256 与 Task 1 基线一致);新接口成员顺序为 `texture` → `bitmaps` → `frames` → `blend`;四个成员均有且只有一行逐字为指定中文文本的 jsDoc(`/** 合成帧使用的纹理 */` / `/** 位图列表 */` / `/** 帧列表 */` / `/** 混合模式 */`);`IAnimationAnimater` 别名逐字正确;两个旧接口的所有成员 / 注释 / `extends` 均已删除;没有实现体、没有 `export default`、没有多余空行或新注释。**本步骤的结论是绑定判定**:必须写入执行笔记与 SUMMARY;任一成员与计划不一致即判本任务失败。
7. **类型门禁(必需,跨平台 node 过滤,不使用 PowerShell 专有的 `Select-String`)**:运行 `<verify>` 第 6 条 node 命令。它先运行 `node node_modules/vue-tsc/bin/vue-tsc.js --version` 哨兵:哨兵非 0 退出即以退出码 2 判失败,证明检查器根本无法启动时不会假通过。哨兵通过后再捕获 `vue-tsc --noEmit` 的 stdout + stderr(非 0 退出码被捕获,不直接判失败);若进程未正常结束(`typeof e.status !== 'number'`,例如 spawn 失败)同样以退出码 2 判失败。只有在「哨兵成功 + 类型检查进程确实完成」的前提下,才用 `packages/render-anim` 过滤结果判定:0 行诊断视为通过,出现任一条即失败。全仓既有 / 预期类型错误与 `vue-tsc` 非 0 退出码属既有基线,不得视为本次失败,也不得因此修改其它文件。若因环境原因哨兵未能启动,SUMMARY 必须写明「类型检查未运行」,不得宣称类型检查通过。
8. **第 1–61 行 / 注释不变门禁**:运行 `<verify>` 第 7 条 node 命令。它重算第 1–61 行前缀的 sha256 并要求与 Task 1 的 `gsd-09-53-prefix.txt` 完全一致,同时要求四个新成员 jsDoc 与枚举注释 `/** 普通混合 */` / `/** 加算混合 */` 精确存在。任一既有注释被改写(包括改成英文)都会使该门禁失败;失败时回到 Task 2 修正,不得放宽或跳过。
本任务不运行测试、构建、dev server、浏览器或包管理命令;**严格只读**:不运行 `prettier --write`、不写任何文件、不修改源码。任何 prettier / eslint / 内容失败都必须回到 Task 2 修正,再由本任务重跑全部门禁;本任务对 eslint 失败只上报原始输出并停止,绝不就地改源码或加 `eslint-disable`。
</action>
<verify>
<automated>node node_modules/prettier/bin/prettier.cjs --check packages/render-anim/src/types.ts packages/render-anim/src/index.ts packages/render-anim/package.json</automated>
<fails_when>输出不是 `All matched files use Prettier code style!`,或退出码非 0(prettier 列出待格式化文件)。</fails_when>
<automated>node node_modules/eslint/bin/eslint.js packages/render-anim/src/types.ts packages/render-anim/src/index.ts</automated>
<fails_when>eslint 打印任一 error / warning,或退出码非 0。</fails_when>
<automated>node -e "const fs=require('fs');const b=fs.readFileSync('packages/render-anim/src/types.ts');const s=b.toString('utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;const tabs=(s.match(/\t/g)||[]).length;const bom=b[0]===0xEF&&b[1]===0xBB&&b[2]===0xBF;const ok=lf===crlf&&tabs===0&&!bom;console.log((ok?'OK':'FAIL')+' lf='+lf+' crlf='+crlf+' tabs='+tabs+' bom='+bom);process.exit(ok?0:1);"</automated>
<fails_when>出现非 CRLF 换行、制表符或 BOM(打印 `FAIL`,退出码 1)。</fails_when>
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('packages/render-anim/src/types.ts','utf8').replace(/\r\n/g,'\n');const exp={IAnimationBitmap:{m:['readonly x: number;','readonly y: number;','readonly width: number;','readonly height: number;']},IAnimationSound:{m:['readonly name: string;','readonly volume?: number;','readonly pitch?: number;']},IAnimationObject:{m:['readonly index: number;','readonly x: number;','readonly y: number;','readonly scale: number;','readonly opacity: number;','readonly mirror: number;','readonly rotate: number;']},IAnimationFrame:{m:['readonly width: number;','readonly height: number;','readonly originX: number;','readonly originY: number;','readonly objects: readonly IAnimationObject[];','readonly sound?: readonly IAnimationSound[];']},IAnimationParsedData:{m:['readonly texture: ITexture;','readonly bitmaps: readonly IAnimationBitmap[];','readonly frames: readonly IAnimationFrame[];','readonly blend: AnimationBlendMode;']}};const docCom={'IAnimationParsedData|readonly texture: ITexture;':'/** 合成帧使用的纹理 */','IAnimationParsedData|readonly bitmaps: readonly IAnimationBitmap[];':'/** 位图列表 */','IAnimationParsedData|readonly frames: readonly IAnimationFrame[];':'/** 帧列表 */','IAnimationParsedData|readonly blend: AnimationBlendMode;':'/** 混合模式 */'};const re=/export interface (\w+)(?: extends (\w+))? \{([\s\S]*?)\n\}/g;const seen={};let mm;while((mm=re.exec(t))){seen[mm[1]]={ext:mm[2]||null,body:mm[3]};}const fail=[];for(const n of Object.keys(exp)){const s=seen[n];if(!s){fail.push('missing interface '+n);continue;}if(s.ext){fail.push(n+' extends '+s.ext);}const lines=s.body.split('\n');const got=lines.map(l=>l.trim()).filter(l=>l.length&&!l.startsWith('/**')&&!l.startsWith('*')&&!l.startsWith('//')).join(' ');const want=exp[n].m.join(' ');if(got!==want){fail.push(n+' members: '+got);}for(const m of exp[n].m){const idx=lines.findIndex(l=>l.trim()===m);if(idx<1){fail.push(n+' missing member '+m);continue;}const prev=lines[idx-1].trim();const w=docCom[n+'|'+m];if(w){if(prev!==w){fail.push(n+' member jsDoc mismatch: '+m+' got '+prev+' want '+w);}}else if(!/^\/\*\* .+ \*\/$/.test(prev)){fail.push(n+' member without single-line jsDoc: '+m);}}}if(Object.keys(seen).length!==5){fail.push('interface count '+Object.keys(seen).length);}if(!/export type IAnimationAnimater = ITextureAnimater<IAnimationParsedData>;/.test(t)){fail.push('alias missing');}if(/\bIAnimationAnimaterData\b/.test(t)){fail.push('old IAnimationAnimaterData remains');}if(/\bIAnimationData\b/.test(t)){fail.push('old IAnimationData remains');}if(/\bRenderAnimater\b/.test(t)){fail.push('RenderAnimater body present');}if(/parseRenderAnimation/.test(t)){fail.push('parser present');}if(/\bclass\b/.test(t)){fail.push('class');}if(/\bfunction\b/.test(t)){fail.push('function');}if(/\blet\b/.test(t)){fail.push('let');}if(t.indexOf('=>')!==-1){fail.push('arrow');}if(/export\s+default/.test(t)){fail.push('default export');}if((t.match(/\bconst\b/g)||[]).length!==1){fail.push('const count');}if((t.match(/\benum\b/g)||[]).length!==1){fail.push('enum count');}if(/\bany\b/.test(t)){fail.push('any');}if(/import\s+type/.test(t)){fail.push('import type');}if(t.indexOf('import { ITexture, ITextureAnimater } from')!==0){fail.push('import not first');}if(t.split('\n')[0].indexOf('@motajs/render')===-1){fail.push('import source');}const em=/export const enum AnimationBlendMode \{\n([\s\S]*?)\n\}/.exec(t);if(!em){fail.push('enum missing');}else{const body=em[1].split('\n').map(l=>l.trim()).filter(l=>l.length&&!l.startsWith('/**')&&!l.startsWith('*')&&!l.startsWith('//')).join(' ');if(body!=='Normal, Add'){fail.push('enum members: '+body);}}if((t.match(/export interface /g)||[]).length!==5){fail.push('export interface count');}if((t.match(/export type /g)||[]).length!==1){fail.push('export type count');}if((t.match(/export const enum /g)||[]).length!==1){fail.push('export enum count');}console.log(fail.length?'FAIL\n'+fail.join('\n'):'OK: split types match');process.exit(fail.length?1:0);"</automated>
<fails_when>脚本打印 `FAIL` 并列出差异(接口数不是 5、成员归属 / 顺序 / 类型 / `readonly` / `?` 不符、成员缺少单行 jsDoc、别名缺失、旧接口或 `animater` 成员残留、实现符号 / `any` / `import type` 出现、枚举成员不是 `Normal, Add`、导出计数不符),退出码 1。</fails_when>
<automated>node -e "const{execSync}=require('child_process'),fs=require('fs');const p=(process.env.TEMP||'.')+'/gsd-09-53-hashes.txt';const base=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n').trim().split('\n').filter(Boolean);const now=base.map(l=>{const f=l.split(' ')[0];return f+' '+execSync('git hash-object '+f,{encoding:'utf8'}).trim();});const changed=now.filter((l,k)=>l!==base[k]);const bad=changed.filter(l=>l.indexOf('packages/render-anim/src/index.ts ')!==0&&l.indexOf('packages/render-anim/package.json ')!==0&&l.indexOf('packages/render-anim/src/types.ts ')!==0);console.log('changed:\n'+changed.join('\n'));if(bad.length||changed.length!==1||changed[0].indexOf('packages/render-anim/src/types.ts ')!==0){console.error('unexpected changed set');process.exit(1);}console.log('OK: only types.ts changed; index/package unchanged');"</automated>
<fails_when>`index.ts` / `package.json` 或其它基线路经发生变化,或 `types.ts` 未变化(打印 `unexpected changed set`,退出码 1)。</fails_when>
<automated>node -e "const{execSync}=require('child_process');const run=cmd=>{try{return{out:execSync(cmd,{encoding:'utf8',maxBuffer:67108864,stdio:['ignore','pipe','pipe']}),status:0};}catch(e){return{out:String(e.stdout||'')+String(e.stderr||''),status:typeof e.status==='number'?e.status:null,code:e.code||null};}};const v=run('node node_modules/vue-tsc/bin/vue-tsc.js --version');if(v.status!==0){console.error('SENTINEL FAIL: vue-tsc could not start: '+v.out+(v.code?' code='+v.code:''));process.exit(2);}const r=run('node node_modules/vue-tsc/bin/vue-tsc.js --noEmit');if(r.status===null){console.error('SENTINEL FAIL: type check did not complete: '+r.out+(r.code?' code='+r.code:''));process.exit(2);}const hits=r.out.split(/\r?\n/).filter(l=>/packages[\\\/]render-anim[\\\/]/.test(l));console.log('sentinel: vue-tsc '+v.out.trim()+' | type check exit '+r.status);console.log(hits.length?hits.join('\n'):'OK: no render-anim diagnostics');process.exit(hits.length?1:0);"</automated>
<fails_when>`vue-tsc --version` 哨兵无法启动(退出码 2),或 `--noEmit` 进程未正常结束、`status` 为 `null`(退出码 2,避免检查器未运行时 0 诊断假通过);或哨兵 / 进程确认完成后过滤结果仍出现任一 `packages/render-anim` 诊断(打印诊断并以 1 退出)。其它文件的诊断与 `vue-tsc` 非 0 退出码不参与判定。</fails_when>
<automated>node -e "const fs=require('fs'),crypto=require('crypto');const p=(process.env.TEMP||'.')+'/gsd-09-53-prefix.txt';const exp=/sha256 ([0-9a-f]{64})/.exec(fs.readFileSync(p,'utf8'));const s=fs.readFileSync('packages/render-anim/src/types.ts','utf8');const prefix=s.split('\r\n').slice(0,61).join('\r\n')+'\r\n';const got=crypto.createHash('sha256').update(prefix,'utf8').digest('hex');const docs=['/** 合成帧使用的纹理 */','/** 位图列表 */','/** 帧列表 */','/** 混合模式 */'];const head=s.split('\r\n').slice(0,61).join('\n');const headNeed=['/** 普通混合 */','/** 加算混合 */'];const missDoc=docs.filter(d=>s.indexOf(d)===-1);const missHead=headNeed.filter(d=>head.indexOf(d)===-1);const fail=[];if(!exp){fail.push('prefix baseline missing');}else if(got!==exp[1]){fail.push('prefix drift '+got+' != '+exp[1]);}if(missDoc.length){fail.push('missing member jsDoc: '+missDoc.join(','));}if(missHead.length){fail.push('missing enum jsDoc: '+missHead.join(','));}console.log(fail.length?'FAIL\n'+fail.join('\n'):'OK: lines 1-61 unchanged, exact jsDoc present');process.exit(fail.length?1:0);"</automated>
<fails_when>第 1–61 行前缀 sha256 与 Task 1 基线 `gsd-09-53-prefix.txt` 不一致(既有注释 / 成员被改写),或四个新成员 jsDoc / 两个枚举成员注释文本缺失(打印 `FAIL` 与差异,退出码 1)。</fails_when>
</verify>
<acceptance_criteria>
- prettier `--check`、eslint、CRLF / BOM / 制表符门禁、逐接口成员门禁(含逐成员 jsDoc 精确文本)、第 1–61 行前缀哈希与枚举注释门禁、类型别名门禁、旧类型移除门禁、无实现 / 无 `any` / 无 `import type` 门禁均以退出码 0 完成。
- 逐接口门禁证明 `IAnimationParsedData` 的四个成员位于正确接口内,且顺序、类型、`readonly`、只读数组与计划完全一致,每个成员的 jsDoc 文本逐字匹配;`IAnimationBitmap` / `IAnimationSound` / `IAnimationObject` / `IAnimationFrame` 的成员序列保持 09-52 的形状。
- 旧接口 `IAnimationAnimaterData` / `IAnimationData`、`animater` 成员、`RenderAnimater` 与 `parseRenderAnimation` 均不存在;`index.ts` / `package.json` 哈希与 Task 1 基线一致。
- 第 1–61 行前缀 sha256 与 Task 1 基线一致,且 `/** 普通混合 */` / `/** 加算混合 */` 等既有注释精确存在——任何注释改写都会被自动门禁拒绝。
- 人工逐成员复核是绑定判定,结论已写入执行笔记与 SUMMARY,未以名称级一致或片段存在代替类型级一致。
- 类型门禁先通过 `vue-tsc --version` 哨兵、并确认 `--noEmit` 进程正常结束后给出 0 行 `packages/render-anim` 诊断(或 SUMMARY 明确写「类型检查未运行」);本任务未写任何文件、未运行 `prettier --write`、未改源码。
</acceptance_criteria>
<done>prettier `--check`、eslint、CRLF / BOM / 制表符门禁、逐接口成员门禁、第 1–61 行前缀哈希与枚举注释门禁、类型别名门禁、旧类型移除门禁、无实现 / 无 `any` / 无 `import type` 门禁与带哨兵的过滤 `vue-tsc` 类型门禁全部通过;`index.ts` / `package.json` 与 09-50 / 09-51 / 09-52 的基线哈希不变;已逐成员人工复核并形成绑定结论;本任务零写入,未运行测试、构建、dev server 或浏览器。</done>
</task>
<task type="auto">
<name>Task 4: 范围 / 哈希闸门自检并写 `09-53-SUMMARY.md`</name>
<files>.planning/phases/09-minimum-browser-runnable/09-53-SUMMARY.md</files>
<read_first>
- `.planning/phases/09-minimum-browser-runnable/09-53-PLAN.md` 的 `<success_criteria>` 与「覆盖审计」(SUMMARY 必须覆盖的条目)
- `process.env.TEMP` 目录下的 `gsd-09-53-status.txt` 与 `gsd-09-53-hashes.txt`(Task 1 采集的范围 / 哈希基线;与门禁 `(process.env.TEMP||'.')` 解析一致)
- Task 1 / Task 2 / Task 3 的执行笔记与各门禁真实输出
- `09-50-PLAN.md`、`09-50-SUMMARY.md`、`09-51-PLAN.md`、`09-51-SUMMARY.md`、`09-52-PLAN.md`、`09-52-SUMMARY.md`(哈希闸门对象)
</read_first>
<action>
只写 `09-53-SUMMARY.md`,不修改任何代码文件。
1. **写 SUMMARY 前的范围闸门(写前,SUMMARY 允许尚不存在)**:运行下面这条 `node` 命令(它只判 `added` / `removed` 为空,不要求 SUMMARY 已出现):`node -e "const{execSync}=require('child_process'),fs=require('fs');const base=fs.readFileSync((process.env.TEMP||'.')+'/gsd-09-53-status.txt','utf8').split(/\r?\n/).filter(l=>l.length>0);const cur=execSync('git status --porcelain -uall',{encoding:'utf8'}).split(/\r?\n/).filter(l=>l.length>0);const added=cur.filter(l=>base.indexOf(l)===-1);const removed=base.filter(l=>cur.indexOf(l)===-1);console.log('added:\n'+added.join('\n'));console.log('removed:\n'+removed.join('\n'));if(added.length||removed.length){console.error('pre-write range drift');process.exit(1);}console.log('OK: pre-write range clean');"`。要求:`added` 为空、`removed` 为空——因为 `types.ts` 虽被修改,但它本来就是未跟踪文件,porcelain 状态不应变化;任何新增 / 消失路径都必须停下上报,不得用 `.trim()` 或子串匹配放宽。这条写前命令必须在写入 SUMMARY 之前成功执行;写后范围仍由 `<verify>` 第 2 条命令复核。
2. **哈希闸门**:运行 `<verify>` 第 3 条 node 命令。要求:10 条基线中恰好只有 `packages/render-anim/src/types.ts` 的哈希变化;`index.ts`、`package.json`、09-50 / 09-51 / 09-52 的计划与摘要、`09-53-PLAN.md` 的哈希逐字不变。若 `types.ts` 未变化或出现第二处变化,停下上报。
3. **写 `09-53-SUMMARY.md`**(UTF-8 无 BOM、CRLF、4 空格缩进、Markdown 文件名连字符命名),内容至少包含:
- **本计划范围**:只改 `packages/render-anim/src/types.ts` 的类型声明;不实现 `RenderAnimater` / `parseRenderAnimation`;不跑测试 / 浏览器 / 构建;0 次提交;09-53 / 波次 2 / `depends_on: 09-52` 的编号说明。**必须逐字写出字段名 `files_modified`**,并说明其恰为 `packages/render-anim/src/types.ts` 与 `.planning/phases/09-minimum-browser-runnable/09-53-SUMMARY.md`(满足关键词门禁)。
- **修改前后 diff**:逐字给出被删除的 `IAnimationAnimaterData` / `IAnimationData` 块与新增的 `IAnimationParsedData` / `IAnimationAnimater` 块;说明第 1–61 行逐字不变。
- **逐成员最终形态**:`IAnimationParsedData` 的四个成员(名称 / 类型 / `readonly` / 顺序 / 沿用注释文本)与 `IAnimationAnimater` 类型别名;删除清单。
- **Task 1 基线**:`types.ts` / `index.ts` / `package.json` 与 09-50 / 09-51 / 09-52 / 09-53-PLAN 的哈希;`packages/render-anim` 的 3 条未跟踪状态;根 tsconfig / `.prettierrc` / eslint 配置事实。
- **门禁真实结果**:prettier / eslint / CRLF / BOM / 逐接口成员 / 类型别名 / 旧类型移除 / 无实现门禁的实际输出(逐字粘贴关键行);类型门禁「已运行(0 行 render-anim 诊断)」或「未运行」二选一,不得含糊;范围闸门与哈希闸门的实际输出。
- **未做 / 跳过项**:不运行测试、构建、dev server、浏览器;不做运行时主张;不运行 `pnpm install`;不执行 git 写命令。
- **设计记录(未实现)**:`RenderAnimater` 将实现 `IAnimationAnimater`(即 `ITextureAnimater<IAnimationParsedData>`),`parseRenderAnimation(buffer: ArrayBuffer): Promise<IAnimationParsedData>`;解析数据不含动画器,`once` / `cycled` 的 `data` 参数只接收纯解析数据。
- **待确认项**:OI-1 命名已批准、OI-2 注释沿用、OI-3 后续实现、OI-4 未校准估算、OI-5 未提交状态。
- **估算**:`confidence: low`,未校准;实际字节 / 行数按最终文件测量。
4. **写 SUMMARY 后的范围闸门**:运行 `<verify>` 第 2 条 node 命令,要求 `added` 恰为 `?? .planning/phases/09-minimum-browser-runnable/09-53-SUMMARY.md`,`removed` 为空,`extra` / `missing` 为空。
5. 不写 `09-53-RESEARCH.md` / `09-53-VALIDATION.md`;不改 `STATE.md` / `ROADMAP.md` / `09-CONTEXT.md` / 09-50 / 09-51 / 09-52 / `index.ts` / `package.json`。
</action>
<verify>
<automated>node -e "const fs=require('fs');const f='.planning/phases/09-minimum-browser-runnable/09-53-SUMMARY.md';const s=fs.readFileSync(f,'utf8');const need=['09-53','IAnimationParsedData','IAnimationAnimater','prettier','eslint','类型门禁','0 次提交','files_modified'];const miss=need.filter(n=>s.indexOf(n)===-1);if(miss.length){console.error('missing '+miss.join(','));process.exit(1);}console.log('OK: summary present');"</automated>
<fails_when>SUMMARY 缺少任一必需关键词(打印 `missing <列表>`,退出码 1),或文件不存在(抛出异常并以非 0 退出)。</fails_when>
<automated>node -e "const{execSync}=require('child_process'),fs=require('fs');const base=fs.readFileSync((process.env.TEMP||'.')+'/gsd-09-53-status.txt','utf8').split(/\r?\n/).filter(l=>l.length>0);const cur=execSync('git status --porcelain -uall',{encoding:'utf8'}).split(/\r?\n/).filter(l=>l.length>0);const summary='?? .planning/phases/09-minimum-browser-runnable/09-53-SUMMARY.md';const added=cur.filter(l=>base.indexOf(l)===-1);const removed=base.filter(l=>cur.indexOf(l)===-1);const extra=added.filter(l=>l!==summary);const missing=added.indexOf(summary)===-1?[summary]:[];console.log('added:\n'+added.join('\n'));console.log('removed:\n'+removed.join('\n'));console.log('extra:\n'+extra.join('\n'));console.log('missing:\n'+missing.join('\n'));process.exit(extra.length+missing.length+removed.length?1:0);"</automated>
<fails_when>`extra` / `missing` / `removed` 任一非空,或 `added` 中出现 SUMMARY 之外的路径(打印对应列表,退出码 1)。</fails_when>
<automated>node -e "const{execSync}=require('child_process'),fs=require('fs');const p=(process.env.TEMP||'.')+'/gsd-09-53-hashes.txt';const base=fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n').trim().split('\n').filter(Boolean);const now=base.map(l=>{const f=l.split(' ')[0];return f+' '+execSync('git hash-object '+f,{encoding:'utf8'}).trim();});const changed=now.filter((l,k)=>l!==base[k]);const types=now.filter(l=>l.split(' ')[0]==='packages/render-anim/src/types.ts');const ok=base.length===10&&changed.length===1&&types.length===1&&changed[0]===types[0];console.log(now.join('\n'));if(!ok){console.error('CHANGED:\n'+changed.join('\n'));process.exit(1);}console.log('OK: only types.ts changed');"</automated>
<fails_when>除 `types.ts` 外还有基线路径变化,或 `types.ts` 未变化,或基线行数不是 10(打印 `CHANGED:` 与差异行,退出码 1)。</fails_when>
<automated>node -e "const fs=require('fs');const b=fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-53-SUMMARY.md');const s=b.toString('utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;const tabs=(s.match(/\t/g)||[]).length;const bom=b[0]===0xEF;const ok=lf===crlf&&tabs===0&&!bom;console.log((ok?'OK':'FAIL')+' lf='+lf+' crlf='+crlf+' tabs='+tabs+' bom='+bom);process.exit(ok?0:1);"</automated>
<fails_when>SUMMARY 出现非 CRLF 换行、制表符或 BOM(打印 `FAIL`,退出码 1)。</fails_when>
</verify>
<acceptance_criteria>
- 写前范围闸门已用 `<action>` 第 1 步给出的显式 `node` 命令执行,`added` / `removed` 为空;写后范围闸门 `added` 恰为 `09-53-SUMMARY.md`,`extra` / `missing` / `removed` 为空。
- 哈希闸门确认 10 条基线中只有 `types.ts` 变化;`index.ts`、`package.json`、09-50 / 09-51 / 09-52、`09-53-PLAN.md` 逐字不变。
- `09-53-SUMMARY.md` 含范围(其中逐字写出 `files_modified` 及其两个路径)、修改前后 diff、逐成员最终形态、Task 1 基线、各门禁真实结果(含类型门禁哨兵与运行状态)、未做项、设计记录、待确认项与未校准估算。
- 全程未执行 git 写命令,0 次提交;除 SUMMARY 外未写任何其它文件。
</acceptance_criteria>
<done>写前范围闸门(显式 `node` 命令)`added` / `removed` 为空;哈希闸门确认只有 `types.ts` 变化;`09-53-SUMMARY.md` 已写入并包含范围(逐字写出 `files_modified`)、前后 diff、逐成员形态、Task 1 基线、各门禁真实结果(含类型门禁哨兵与运行状态)、未做项、设计记录、待确认项与未校准估算;写后范围闸门 `added` 只含 SUMMARY、`extra` / `missing` / `removed` 为空;0 次提交。</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 无 | 本计划只修改 TypeScript 类型声明,不引入运行时代码、网络 / 存储 / 用户输入处理,也不安装任何依赖 |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-53-01 | Tampering | `packages/render-anim/src/types.ts` 的修改范围 | low | mitigate | 用 Edit 精确块替换;逐接口成员门禁、旧类型移除门禁与 10 条哈希闸门共同证明只改了 `types.ts`、且只改了预期的类型块 |
| T-53-02 | Tampering | `packages/render-anim/src/index.ts` / `package.json` | low | mitigate | 两者不在 `files_modified` 内;Task 1 记录哈希,Task 3 / Task 4 复核哈希不变 |
| T-53-03 | Information Disclosure | `packages/render-anim/src/types.ts` | low | accept | 只声明公开类型,无密钥、无运行时数据;文件内容由用户审阅 |
| T-53-SC | Tampering | npm / pnpm 安装 | high | mitigate | 本计划禁止任何包管理安装命令;不新增 registry 依赖,锁文件不变 |
</threat_model>
<verification>
- **格式**:`node node_modules/prettier/bin/prettier.cjs --check packages/render-anim/src/types.ts packages/render-anim/src/index.ts packages/render-anim/package.json` 退出码 0,输出 `All matched files use Prettier code style!`。
- **lint**:`node node_modules/eslint/bin/eslint.js packages/render-anim/src/types.ts packages/render-anim/src/index.ts` 退出码 0、无输出。
- **换行 / BOM / 缩进**:Task 2 / Task 3 node 门禁输出 `OK`(全 CRLF、无制表符、无 BOM)。
- **内容 / 结构**:Task 3 第 4 条 node 门禁输出 `OK: split types match`(5 个 interface + 1 个 type alias + 1 个 enum;`IAnimationParsedData` 四成员顺序 / 类型 / `readonly` 正确;每个成员的单行 jsDoc 文本逐字匹配;旧接口与 `animater` 成员不存在;无实现 / 无 `any` / 无 `import type`;枚举成员 `Normal, Add`)。
- **注释 / 前缀不变**:Task 1 采集 `gsd-09-53-prefix.txt`(第 1–61 行 sha256 + 枚举注释);Task 2 与 Task 3 第 7 条门禁重算同一前缀并要求哈希一致,同时要求四个成员 jsDoc 与 `/** 普通混合 */` / `/** 加算混合 */` 精确存在——任何注释改写(含改成英文)都会失败。
- **类型**:node 先跑 `vue-tsc --version` 哨兵、并确认 `--noEmit` 进程正常结束(`status` 非 `null`),再过滤 `packages/render-anim` 诊断,结果为 0 行;全仓既有错误与退出码不参与判定。
- **范围**:Task 4 写前 `added` / `removed` 为空;写后 `added` 恰为 `09-53-SUMMARY.md`,`extra` / `missing` / `removed` 为空。
- **哈希**:10 条基线中只有 `packages/render-anim/src/types.ts` 变化;`index.ts`、`package.json`、09-50 / 09-51 / 09-52 的计划与摘要、`09-53-PLAN.md` 哈希不变。
- **明确不做**:不运行测试、构建、dev server、浏览器;不运行 `pnpm install`;不执行任何 git 写命令,0 次提交。
</verification>
<success_criteria>
- [ ] Task 0 已完整汇报精确前后 diff、拆分原因、dev.md 合规点、命名、影响范围、验证形态、不提交与编号,并等待用户针对本修订版回复「可以执行」后才继续。
- [ ] `packages/render-anim/src/types.ts` 已删除 `IAnimationAnimaterData` 与 `IAnimationData`,新增 `IAnimationParsedData`(`texture` / `bitmaps` / `frames` / `blend`,顺序与类型逐字一致)与 `export type IAnimationAnimater = ITextureAnimater<IAnimationParsedData>;`。
- [ ] 解析数据中不存在 `animater` 成员;`ITextureAnimater<IAnimationAnimaterData>` 不再出现;`RenderAnimater` / `parseRenderAnimation` 没有任何实现体。
- [ ] 第 1–61 行逐字不变(前缀 sha256 与 Task 1 基线一致);`AnimationBlendMode`(`Normal` / `Add`)、`IAnimationBitmap`、`IAnimationSound`、`IAnimationObject`、`IAnimationFrame` 保持 09-52 的形状与注释。
- [ ] `IAnimationParsedData` 每个成员前有且只有一行中文 jsDoc,且逐字为 `/** 合成帧使用的纹理 */` / `/** 位图列表 */` / `/** 帧列表 */` / `/** 混合模式 */`;枚举注释 `/** 普通混合 */` / `/** 加算混合 */` 精确存在;类型别名本身无 jsDoc。
- [ ] 文件为 UTF-8 无 BOM + CRLF + 4 空格 + 无制表符;首行是普通 `import`;无 `import type`、无 `any`、无 `class` / `function` / `let` / 箭头函数 / `export default`;`const` / `enum` 仅在 `AnimationBlendMode` 声明处。
- [ ] prettier `--check`、eslint、CRLF / BOM / 逐接口成员(含逐成员 jsDoc 精确文本)/ 前缀哈希 / 类型别名 / 旧类型移除 / 无实现门禁全部通过;`vue-tsc` 经 `--version` 哨兵确认可启动且 `--noEmit` 进程正常结束后,过滤 `packages/render-anim` 诊断为 0 行(或 SUMMARY 明确写「类型检查未运行」)。
- [ ] `packages/render-anim/src/index.ts` 与 `package.json` 未被修改,哈希与 Task 1 基线一致;09-50 / 09-51 / 09-52 未被修改;`09-53-PLAN.md` 未被改写。
- [ ] `09-53-SUMMARY.md` 已写入,包含门禁真实结果、类型门禁运行状态、未做项、设计记录、待确认项与未校准估算。
- [ ] 范围闸门与哈希闸门通过;0 次提交;未运行测试 / 构建 / 浏览器 / `pnpm install`。
</success_criteria>
<output>
Create `.planning/phases/09-minimum-browser-runnable/09-53-SUMMARY.md` when done
</output>
@@ -0,0 +1,203 @@
---
phase: 09-minimum-browser-runnable
plan: 53
subsystem: render-anim
tags: [typescript, types-only, render-anim, types-split, animation]
requires:
- "09-52 的 @motajs/render-anim 类型骨架(1 个 export const enum + 6 个接口)"
provides:
- "IAnimationParsedData(texture / bitmaps / frames / blend,不含 animater 成员)"
- "export type IAnimationAnimater = ITextureAnimater<IAnimationParsedData>;"
- "删除 IAnimationAnimaterData 与 IAnimationData 两个旧接口"
affects: [09-minimum-browser-runnable, render-anim]
actuals:
# chars/4 上限值,只统计本计划 types.ts 的替换块(删除 444 字符 + 新增 367 字符 = 811 字符);SUMMARY 自身不计入
tokens: 203
tasks: 5
commits: 0
tech-stack:
added: []
patterns:
- "解析数据与动画器分离:解析数据是纯数据对象,动画器是独立类型别名"
- "ITextureAnimater<T> 的 data 参数只接收纯解析数据,不再把自身当作参数"
key-files:
created: []
modified:
- packages/render-anim/src/types.ts
key-decisions:
- "IAnimationParsedData 取代 IAnimationAnimaterData + IAnimationData 的组合设计,成员顺序固定为 texture / bitmaps / frames / blend"
- "IAnimationAnimater 为类型别名 ITextureAnimater<IAnimationParsedData>,不在解析数据中保留 animater 成员"
- "只落类型声明,RenderAnimater 与 parseRenderAnimation 留给后续实现计划"
patterns-established:
- "类型别名承载动画器契约:动画器类型由 ITextureAnimater<解析数据类型> 组合而成,不重复声明接口成员"
duration: 12min
completed: 2026-11-07
status: complete
---
# Phase 9 Plan 53: `@motajs/render-anim` 类型拆分(纯解析数据 + 独立动画器)
**删除 `IAnimationAnimaterData` / `IAnimationData`,落 `IAnimationParsedData`(`texture` / `bitmaps` / `frames` / `blend`)与 `export type IAnimationAnimater = ITextureAnimater<IAnimationParsedData>;`,不含任何实现。**
## 本计划范围
- 只修改 `packages/render-anim/src/types.ts` 的类型声明;**不实现** `RenderAnimater`、**不实现** `parseRenderAnimation`。
- `files_modified` 恰为两项:`packages/render-anim/src/types.ts` 与 `.planning/phases/09-minimum-browser-runnable/09-53-SUMMARY.md`。
- `packages/render-anim/src/index.ts` 与 `packages/render-anim/package.json` 只验证、不修改(哈希与 Task 1 基线一致)。
- 不运行测试、构建、dev server、浏览器;不做运行时主张;不运行 `pnpm install`,锁文件不变。
- **0 次提交**:全程未执行任何 git 写命令,改动留在工作树供审阅。
- 编号说明:派发编号 09-53 / 波次 2 / `depends_on: 09-52`;`09-01`..`09-52` 均已占用。
## Task 0 执行前汇报关卡(已满足)
| 项 | 内容 |
|----|------|
| 关卡类型 | `checkpoint:human-verify` |
| Gate | `blocking-human` |
| 汇报对象 | 修改前(第 62–76 行)与修改后的逐字 diff、拆分原因、四个成员与类型别名逐字形态、删除清单、dev.md 合规点、命名、影响范围、只读静态验证口径、0 次提交、编号与依赖 |
| 用户回复 | 恰好「可以执行」四个字(针对本修订版) |
| 结论 | 关卡通过,进入 Task 1–4 |
## 修改前后 diff(逐字)
### 修改前(原第 62–76 行,已删除)
```ts
export interface IAnimationAnimaterData {
/** 位图列表 */
readonly bitmaps: readonly IAnimationBitmap[];
/** 帧列表 */
readonly frames: readonly IAnimationFrame[];
}
export interface IAnimationData extends IAnimationAnimaterData {
/** 合成帧使用的纹理 */
readonly texture: ITexture;
/** 动画器 */
readonly animater: ITextureAnimater<IAnimationAnimaterData>;
/** 混合模式 */
readonly blend: AnimationBlendMode;
}
```
### 修改后(现第 62–73 行,新增)
```ts
export interface IAnimationParsedData {
/** 合成帧使用的纹理 */
readonly texture: ITexture;
/** 位图列表 */
readonly bitmaps: readonly IAnimationBitmap[];
/** 帧列表 */
readonly frames: readonly IAnimationFrame[];
/** 混合模式 */
readonly blend: AnimationBlendMode;
}
export type IAnimationAnimater = ITextureAnimater<IAnimationParsedData>;
```
**第 1–61 行逐字不变**:首行普通引入 `import { ITexture, ITextureAnimater } from '@motajs/render';`、`AnimationBlendMode` 枚举、`IAnimationBitmap`、`IAnimationSound`、`IAnimationObject`、`IAnimationFrame` 全部未动;前缀 sha256 门禁逐字证明(见下)。
## 逐成员最终形态
| 导出类型 | 成员(顺序固定) | 成员 jsDoc(沿用既有文本) |
|----------|------------------|----------------------------|
| `IAnimationParsedData`(新增、`export interface`) | `readonly texture: ITexture` | 合成帧使用的纹理 |
| | `readonly bitmaps: readonly IAnimationBitmap[]` | 位图列表 |
| | `readonly frames: readonly IAnimationFrame[]` | 帧列表 |
| | `readonly blend: AnimationBlendMode` | 混合模式 |
| `IAnimationAnimater`(新增、`export type`) | — | 类型别名本身不写 jsDoc |
| `AnimationBlendMode`(不变) | `Normal` / `Add` | 普通混合 / 加算混合 |
| `IAnimationBitmap`(不变) | `x` / `y` / `width` / `height` | 保持 09-52 的既有注释 |
| `IAnimationSound`(不变) | `name` / `volume?` / `pitch?` | 保持 09-52 的既有注释 |
| `IAnimationObject`(不变) | `index` / `x` / `y` / `scale` / `opacity` / `mirror` / `rotate` | 保持 09-52 的既有注释 |
| `IAnimationFrame`(不变) | `width` / `height` / `originX` / `originY` / `objects` / `sound?` | 保持 09-52 的既有注释 |
**删除清单:** `IAnimationAnimaterData`(含 `bitmaps` / `frames` 两个成员与两条注释)、`IAnimationData`(含 `texture` / `animater` / `blend` 三个成员与三条注释、`extends IAnimationAnimaterData` 关系)以及 `动画器` 注释。删除后文件导出恰为:1 个 `export const enum` + 5 个 `export interface` + 1 个 `export type`。
## Task 1 基线(只读采集)
| 路径 | `git hash-object` 基线 |
|------|------------------------|
| `packages/render-anim/src/types.ts` | `53d4b962bb525730d3de85737fbf60764cb30bd4` |
| `packages/render-anim/src/index.ts` | `fcb073fefcd6bebc8afd726f830e66828ebc5e90` |
| `packages/render-anim/package.json` | `4934efaea21143110e313816a317130b296982d0` |
| `09-50-PLAN.md` | `1575237b92a411a9f6819ed4108f22672ef934d4` |
| `09-50-SUMMARY.md` | `9283ff0511cba1a4f11ccd6f13f48777d791e9f7` |
| `09-51-PLAN.md` | `a83f21cbdb8f275cefab756ea041f2a0d93bb857` |
| `09-51-SUMMARY.md` | `257483e79741f5597ca78f3fecf07fb4eba31a8d` |
| `09-52-PLAN.md` | `5fdcef8add173e6432388d5995dfd8ea81394929` |
| `09-52-SUMMARY.md` | `8416ecd0740b3f5fe3a21ec38427d78cdb6dc93c` |
| `09-53-PLAN.md` | `d11b37920f31be4d391195a7e1f875786f44a7dd` |
- `packages/render-anim` 的 git 状态恰为 3 条未跟踪文件:`?? package.json` / `?? src/index.ts` / `?? src/types.ts`。
- 前缀基线:`types.ts` 第 1–61 行 sha256 = `d4d97496b09ffa0cb945ebfb23b43f97e18d825b3ec0a2b1a6e0d26106f7884e`(按 CRLF 精确拼接、含第 61 行行尾 CRLF)。
- 根 `tsconfig.json`:`include` 覆盖 `packages/**/*.ts`,`paths['@motajs/*'] = ['./packages/*/src']`,`strict: true`、`isolatedModules: true`、`noEmit: true`。
- `.prettierrc`:`printWidth: 80` / `tabWidth: 4` / `singleQuote: true` / `arrowParens: avoid` / `trailingComma: none` / `endOfLine: crlf`;`eslint.config.js` 为 `typescript-eslint` + `eslint-plugin-prettier/recommended`。
- `@motajs/render` 侧:`ITextureAnimater<T>` 声明 `once(texture: ITexture, data: T): Generator<ITextureRenderable, void>` 与 `cycled(texture: ITexture, data: T): Generator<ITextureRenderable, void>`;`ITexture` 为接口,两者均可从包根导出取得。
## 门禁真实结果
| 门禁 | 命令要点 | 真实输出 | 结果 |
|------|----------|----------|------|
| 格式 | `node node_modules/prettier/bin/prettier.cjs --check packages/render-anim/src/types.ts packages/render-anim/src/index.ts packages/render-anim/package.json` | `Checking formatting...` / `All matched files use Prettier code style!`,退出码 0 | 通过 |
| lint | `node node_modules/eslint/bin/eslint.js packages/render-anim/src/types.ts packages/render-anim/src/index.ts` | 无任何输出,退出码 0 | 通过 |
| 换行 / BOM / 缩进 | Task 2 / Task 3 node 门禁 | `OK lf=73 crlf=73 tabs=0 bom=false` | 通过 |
| 内容 / 结构(逐接口成员 + 逐成员 jsDoc) | Task 2 块替换门禁 | `OK: split applied` | 通过 |
| 内容 / 结构(5 接口 + 1 type alias + 1 enum) | Task 3 第 4 条 node 门禁 | `OK: split types match`,退出码 0 | 通过 |
| index / package 未改动 | Task 3 第 5 条 node 门禁 | `changed:` 仅 `packages/render-anim/src/types.ts f71b6a97767685cbb3cc654d5c655f8315cf1922` / `OK: only types.ts changed; index/package unchanged` | 通过 |
| 第 1–61 行前缀哈希 + 注释不变 | Task 3 第 7 条 node 门禁 | `OK: prefix baseline d4d97496b09ffa0cb945ebfb23b43f97e18d825b3ec0a2b1a6e0d26106f7884e + enum jsDoc present`;`OK: lines 1-61 unchanged, exact jsDoc present` | 通过 |
| 类型门禁 | node 先跑 `vue-tsc --version` 哨兵,再捕获 `--noEmit` 并过滤 `packages/render-anim` 诊断 | **已运行**:`sentinel: vue-tsc Version 6.0.3 | type check exit 2` / `OK: no render-anim diagnostics`(0 行诊断);全仓既有类型错误与 `vue-tsc` 非 0 退出码属既有基线,不参与判定 | 通过 |
| 范围闸门(SUMMARY 写入前) | Task 4 第 1 步 node 门禁 | `added:` 空、`removed:` 空、`OK: pre-write range clean` | 通过 |
| 哈希闸门 | Task 4 第 2 步 node 门禁(10 条基线) | 仅 `packages/render-anim/src/types.ts` 变化为 `f71b6a97767685cbb3cc654d5c655f8315cf1922`,其余 9 条逐字不变;`OK: only types.ts changed` | 通过 |
| 范围闸门(SUMMARY 写入后) | Task 4 `<verify>` 第 2 条 node 门禁 | 见下「写后范围闸门」 | 见下 |
人工逐成员复核(绑定判定):`types.ts` 现共 73 行 / 1977 字节;新接口成员顺序为 `texture` → `bitmaps` → `frames` → `blend`,四个成员均为 `readonly`、数组成员为只读数组,每个成员有且只有一行逐字为 `/** 合成帧使用的纹理 */` / `/** 位图列表 */` / `/** 帧列表 */` / `/** 混合模式 */` 的 jsDoc;`IAnimationAnimater` 别名逐字为 `ITextureAnimater<IAnimationParsedData>`;两个旧接口的全部成员 / 注释 / `extends` 已删除;无实现体、无 `export default`、无新增其它注释。
## 未做 / 跳过项
- 未运行任何测试、构建、dev server 或浏览器;不做运行时可用性主张。
- 未运行 `pnpm install`;锁文件未变(依赖仅为同仓库工作区包)。
- 未执行任何 git 写命令(`add` / `commit` / `stash` / `checkout` / `restore` / `reset`),**0 次提交**。
- 未创建 `09-53-RESEARCH.md` / `09-53-VALIDATION.md`;未改动 `STATE.md` / `ROADMAP.md` / `09-CONTEXT.md` / 09-50 / 09-51 / 09-52。
- 本 run 无 RESEARCH / 无 VALIDATION:`requirements: []`,Nyquist 不适用;spec-less probe fallback skipped(phase has no requirement IDs to probe)。
## 偏差与问题
1. **[Rule 3 - Blocking] Write 工具把换行归一化为 LF**:写入 SUMMARY 后测得 `crlf=0`,会使计划要求的 CRLF 门禁失败。处理方式:写入后运行一次 Node 脚本,把 `09-53-SUMMARY.md` 按 `/\r?\n/` → `\r\n` 统一为 CRLF,内容其余部分不变;随后 CRLF / BOM / 制表符门禁通过。未新增或删除任何字符。
2. Task 0 的批准记录落在本 SUMMARY,未创建独立 approval 文件——计划正文要求 Task 0 不写文件,本次执行也仅允许写 `files_modified` 的两个路径。
除上述两点外,计划按原文执行。
## 设计记录(未实现)
- `RenderAnimater` 将 `implements IAnimationAnimater`(即 `ITextureAnimater<IAnimationParsedData>`);`parseRenderAnimation(buffer: ArrayBuffer): Promise<IAnimationParsedData>`。
- 解析数据不含动画器:`once(texture, data)` / `cycled(texture, data)` 的 `data` 参数只接收纯解析数据对象,不再出现 `data.animater.once(texture, data)` 这种把同一对象既当自身数据又当参数传入的用法。
- `bitmaps` / `frames` 为只读数组,实现阶段不得改成可变数组;解析器对外返回只读视图。
- `blend` 以 `AnimationBlendMode` 存于文件并由解析数据暴露,动画器不决定混合;`sound` 由宿主播放。
## 待确认项
- **OI-1(已获用户批准)**:公开名称固定为 `IAnimationParsedData` 与 `IAnimationAnimater`;解析数据成员固定为 `texture` / `bitmaps` / `frames` / `blend`。本计划未新增任何其它公开名称。
- **OI-2(只记录)**:四个成员注释文本从 09-52 的既有注释原样沿用;`动画器` 注释已随 `IAnimationData.animater` 成员一并移除。
- **OI-3(只记录,不实现)**:后续 `RenderAnimater` 实现 `IAnimationAnimater`,`parseRenderAnimation(buffer: ArrayBuffer): Promise<IAnimationParsedData>`;本计划未写任何实现体。
- **OI-4(只记录)**:`estimate` 未校准(`confidence: low`);本计划未运行构建或测试,因此不做运行时可用性主张。
- **OI-5(只记录)**:09-52 的产物与本计划的改动仍未提交,留在工作树供审阅。
## 估算
- **估算:** `confidence: low`,未校准(计划 `estimate.tokens: 30000`)。
- **实际(actuals):** `types.ts` 替换块共 811 字符(删除 444 + 新增 367),约 203 tokens(chars/4);文件 1977 字节 / 73 行;任务 5 个;提交 0 次。
## Self-Check: PASSED
- `packages/render-anim/src/types.ts` 存在且为拆分后形态(73 行、1977 字节、全 CRLF、无 BOM、无制表符)。
- `packages/render-anim/src/index.ts` 与 `package.json` 哈希与 Task 1 基线逐字一致,未被修改。
- 09-50 / 09-51 / 09-52 的计划与摘要、`09-53-PLAN.md` 哈希逐字不变。
- `files_modified` 之外零改动(范围闸门 `extra` / `missing` / `removed` 均为空);0 次提交。
---
*Phase: 09-minimum-browser-runnable*
*Completed: 2026-11-07*