--- title: 0.3.x 优化清单:全库代码体检(2026-09-15) --- # 0.3.x 优化清单:全库代码体检 > 体检范围:`packages/{spec,store,render-mc,tools,client}` 全部源码 + `scripts/` + `build.mjs`(约 6700 行),逐文件通读。 > 基线:`tsc --noEmit` 五包全绿;`smoke.ts` 28 项全绿;`build.mjs` 产物 + `smoke-host.mjs` 宿主挂载冒烟通过(v0.3.0,分支 `feat/0.3.0-elements` @ f9c77ab)。 > 维度:工作流稳定性与产出正确性 → 无效/冗余代码 → 执行效率 → 校验与规划对账 → 组件抽离与 UI。 > 编号规则:`O1…O21`,优先级 P0(正确性/稳定性,会产出错误结果或卡住工作流)> P1(低成本、明确收益)> P2(锦上添花/观察项)。 --- ## 0. 结论速览 | 编号 | 维度 | 条目 | 优先级 | 处置结果(2026-09-15 修复轮) | | --- | --- | --- | --- | --- | | O1 | 正确性 | `anim_render` 的 `scenes` 参数是死参数——宣传可抽查单幕,实际恒渲整片 | **P0** | ✅ 已实现(`pickScenes` 切片 + 接线冒烟) | | O2 | 稳定性 | 渲染链路无并发互斥:共享 workDir + 固定端口,并行渲染/预览互相踩 | **P0** | ✅ 已修复(渲染串行闸 + 随机端口) | | O3 | 稳定性 | 事件 sink 按「单槽位当前 agent」归因会话,多会话并发时事件可能落错 sidecar | P1 | ✅ 已修复(emit 闭包绑定当次 agent) | | O4 | 稳定性 | 杂项:端口占用报错不可读、`statSync` 不验 isFile、`sessionsDirReady` 一次性 | P2 | ✅ 已修复(详见 §8,含对原表述的一处更正) | | O5 | 死代码 | `AnimRendererRegistry.list()` 无任何调用方 | P1 | ✅ 已删除 | | O6 | 死代码 | codegen `emitNode` 的 `component === null` 分支不可达(「暂未实现」注释过时) | P1 | ✅ 已删除分支并收窄 `COMPONENT` 类型 | | O7 | 过度导出 | `starPath` / `trackEndMs` / `sceneContentEndMs` 导出无外部消费者 | P2 | ✅ 已收敛为模块内函数 | | O8 | 冗余代码 | 资产 id 净化三份拷贝、会话 id 净化两份拷贝、`probe(ctx,key)` 两份拷贝 | P1 | ✅ 已收敛(spec 包 `safeName` + tools 包 `ctx-probe.ts`) | | O9 | 文档勘误 | client/index.ts「9 个工具」实为 10 个等注释漂移 | P1 | ✅ 已修正 | | O10 | 效率 | 每次渲染固定 4s 编辑器等待 + 重启 vite/浏览器(0.3.0 规划 §5 已挂账,补充测量口径) | P1 | ⏸ 维持现状(沙箱无浏览器无法测量,按「先测再动」留真机) | | O11 | 效率 | 后台渲染卡片完成后仍每 2s 轮询 `/api/state`,不停止 | P1 | ✅ 已修复(落定停表 + 不可达退避 10s) | | O12 | 效率 | codegen 微效率:`indexOf` O(n²)、`tweensOf` 重复求值(量级小,可不改) | P2 | ⏸ 维持现状(量级不支撑) | | O13 | 效率 | `waitForFrames` 800ms 全目录扫描轮询(量级小,可不改) | P2 | ⏸ 维持现状 | | O14 | 校验挂账 | 「类型枚举一致性」冒烟断言未落地(0.3.0 规划 §2.3 承诺、M0 验收标准) | **P0** | ✅ 已落地(`LAYER_TYPES` 唯一权威源 + 三表断言 + 工具描述第四面断言) | | O15 | 校验挂账 | group `children` 的 validate 校验未落地(0.3.0 规划 §2.4 承诺),仅 codegen 警告降级 | P1 | ✅ 已落地(引用体检 + 冒烟五形态) | | O16 | 组件抽离 | cards.tsx 拆分时机与建议(当前 476 行可接受,加交互前拆) | P2 | ⏸ 维持现状(触发点未到) | | O17 | UI | ReadCard 看不到读取内容——`/api/spec` 在、卡片无消费 UI | P2 | ✅ 已补全(展开时懒拉 API + 客户端 JSON Pointer) | | O18 | UI | AssetCard 长路径无折行处理;PreviewCard 无放大/原帧链接 | P2 | ✅ 已修复(含 pacing 中性行与警告拆分) | | O19 | 效率/工作流 | `anim_preview` 同步挂住模型回合(观察项) | P2 | ⏸ 维持现状(0.4 随面板交互评估) | | O20 | 稳定性 | 真机首写 `anim_draft_scene` 三类机械错误(ease 写字符串、type 大小写、漏 name/tracks)全靠报错回修,一幕烧两个来回 | P1 | ✅ 已修复(`coerceScene` 边界自动纠正 + 校验文案可执行化 + 工具描述给完整可抄示例,见 §9) | | O21 | 正确性 | 真机日志回归:模型按 SVG 习惯写 `strokeWidth`/`textAlign`,渲染端全部静默忽略——线条描边宽度、文本对齐意图没进成片;另有 vite 配置 CJS 弃用告警刷屏 | P1 | ✅ 已修复(`strokeWidth → lineWidth` 别名 + `textAlign` 直通 + `vite.config.mts`,见 §10) | --- ## 1. 工作流稳定性与产出正确性 ### O1 `anim_render` 的 `scenes` 参数是死参数(P0) **证据链**:工具 schema 宣称「只渲染这些场景(0 基索引),省略则整片」(`packages/tools/src/register.ts:609`);`RenderRequest.scenes` 在接缝定义里成对出现(`packages/tools/src/render.ts:48`、`packages/render-mc/src/contract.ts:41`);`anim/render-start` 事件也把它落盘(`packages/tools/src/ops.ts:392`)。但唯一实现方 `MotionCanvasRenderer.render()` **从未读取 `request.scenes`**(`packages/render-mc/src/adapter.ts:106-125` 直接渲整份 `request.spec`)。 **后果**:模型按工具描述传 `scenes: [1]` 抽查单幕,实际渲染整片——耗时以分钟计却只为一幕;回执里的 `durationMs`/`frameCount` 仍按整片报告,模型无从发现。这是「工具描述承诺了但实现没有」的最恶劣形态:模型的一切微调决策都建立在假回执上。 **处置建议**(二选一,推荐 A): - A. 实现它:adapter 里按索引挑场景(类似 `truncateSpecAtMs` 的切片思路,重编号后交给 codegen),`durationMs`/`frameCount` 按切片后的时间线报告;`scenes` 越界索引报可读错误。冒烟补一条「scenes=[1] 只产出该幕帧数」断言。 - B. 摘除宣传:从工具 schema、README 特性表/工具一览、`RenderRequest`/`render-start` 载荷里删掉 `scenes`。0.4 再做。 ### O2 渲染链路无并发互斥:并行渲染互相踩(P0) **证据**:`MotionCanvasRenderer` 全进程只有一份且共享同一个 `#workDir`(`outputDir/work`,`packages/tools/src/index.ts:223`);vite 端口固定 5179 且 `strictPort: true`(`packages/render-mc/src/runtime.ts:225,286`);每次 `renderProject` 先 `resetDir` 清空输出目录(`runtime.ts:249`)。`opRender` / 渲染器层没有任何队列或锁(`packages/tools/src/ops.ts:407-435`)。 **后果**:后台渲染(`anim_render` 转 ctx.jobs 后立即返回)进行期间,模型完全可能再调一次 `anim_render` 或 `anim_preview`——第二次调用会:① 覆写 workDir 里正在渲染的 project/scenes 文件;② `resetDir` 清掉正在产帧的 output 目录;③ 若①②都错开,`strictPort` 下 `server.listen()` 直接 EADDRINUSE 抛裸错误。结果是两条渲染互相出残片或莫名失败,且报错不说人话。多会话共用一个宿主时更容易触发。 **处置建议**:按性价比排序—— 1. **互斥队列(最小改动)**:在 `AnimRendererRegistry` 或 `MotionCanvasRenderer` 内加一条 promise 链,`preview`/`render` 串行执行;排队期间 `anim_render` 可正常转后台任务(job 已发布、只是 run 里等锁)。 2. 端口改 `port: 0`(随机可用端口,`createServer` 返回后取 resolved port 供 `page.goto`),彻底消除端口冲突; 3. workDir 按 specId 分子目录(`work/`),顺带让 vite 预打包缓存按项目复用——改动面大,可与 1/2 分开评估。 ### O3 事件 sink 的会话归因是单槽位(P1) **证据**:`resolveEventSink` 用单个 `currentAgent` 槽位定位当前会话(`packages/tools/src/register.ts:109-148`),`register()` 包装器在每次工具执行前 `setAgent`(`register.ts:295-303`)。注释假设「工具调用是串行的(同一 agent 回合内)」——这只在单会话内成立。 **后果**:宿主是「一个 profile 多个会话」形态,插件在根上下文、工具全会话共享。A 会话的后台渲染事件在 job 里异步产生,期间 B 会话的工具调用把槽位覆盖成 B——A 的 `anim/render-progress` 从此落进 B 的 sidecar。恢复时 A 的时间线缺进度事件、B 的 sidecar 混入异物(fold 会跳过未知 specId,暂无实害,但事件归因已不可信)。单用户串行使用不触发。 **处置建议**:把 agent 绑进 emit 而不是 sink 槽位——`wrapped` 里捕获当次 `exec.agent`,构造 per-call 的 `emit`(形如 `sink.appendAs(agent, event)`)传给 ops;`EventSink` 的槽位与 `setAgent` 一并移除。改动集中在 `register.ts`,ops 层签名不变(emit 已是参数传入)。 ### O4 稳定性杂项(P2) - **端口占用报错不可读**:`strictPort: true` 下 EADDRINUSE 的原始报错直接抛给模型,没有「已有渲染在进行,请等待或用 job_output 查看进度」的可操作提示(`runtime.ts:286-289`)。做了 O2 后此条自动消解,保留为验收点。 - **`statSync` 不验 isFile**:`opAssetImport` 只 `statSync` 存在性(`packages/tools/src/ops.ts:611-614`),传目录会在 `copyFileSync` 处抛原始系统错误。加 `stats.isFile()` 校验并把报错写成人话。 - **`sessionsDirReady` 标记位**:*(2026-09-15 更正)原体检表述「mkdir 失败一次后永远跳过」不准确——mkdir 失败时标记位并不会置位,重试是正常的;真实的残留弱点是目录建成后一旦 append 阶段失败(如目录被外部删除),标记位已置真、后续不再重建目录。修复时直接移除标记位、每次 append 幂等 mkdir(recursive 对已存在目录是廉价操作),两种边界都消除。* --- ## 2. 无效代码与冗余代码 ### O5 死代码:`AnimRendererRegistry.list()`(P1) `packages/tools/src/render.ts:97-99`——全库(含冒烟)无任何调用方。注销回落逻辑靠 `#defaultName` 自身即可,`list()` 删掉或留作 seam 公共 API 需要一个理由;当前状态下删。 ### O6 不可达分支与过时注释:codegen 的 `component === null`(P1) `packages/render-mc/src/codegen.ts:431-435`——`COMPONENT` 表 13 个类型已全部映射到 MC 组件(0.3.0 M0/M1/M3 完成),`null` 分支(「类型暂未实现,已跳过」)不可达。要么删分支并把 `COMPONENT` 类型收窄为 `Record`,要么保留分支但把注释改成「防御:新增 LayerType 忘了映射表时显式跳过」。当前注释会误导读者以为还有未实现类型。 ### O7 过度导出(P2) - `starPath`(`codegen.ts:172`)仅本文件使用,`export` 多余; - `trackEndMs` / `sceneContentEndMs`(`packages/spec/src/timeline.ts:25,32`)仅包内消费。spec 是公共包,留作 API 可以,但当前没有外部消费者,按 YAGNI 收敛为模块内函数亦可。 ### O8 同一逻辑的多份拷贝(P1) 五处「净化正则」与两处 `probe` 是手工一致性——任何一处改动都要记得同步其余: | 逻辑 | 位置 | 说明 | | --- | --- | --- | | 资产 id → 文件名安全串 | `codegen.ts:164-166`(sanitizeAssetId)、`adapter.ts:221-223`(safeAssetName)、`ops.ts:618`(内联) | adapter 注释自认「与 codegen 的 URL 生成保持一致」——这是靠人肉维持的契约:codegen 生成 `/assets/<净化id>`,adapter 复制到 `<净化id>`,两边净化不一致时资产 404 | | 会话 id → sidecar 文件名 | `tools/index.ts:154`、`register.ts:130` | 必须一致,否则恢复读不到写入的文件 | | cordis 代理安全读属性 | `tools/index.ts:66-72`、`register.ts:84-90` | 同包两份一模一样的 `probe(ctx, key)` | **处置建议**:前两类下沉到 `@dsh-anim/spec`(或 store)导出一个 `safeName(id)`——spec 是 host/client/render 共享的纯数据层,放工具函数不破坏分层;`probe` 在 tools 包内提取成 `ctx-probe.ts` 小模块。另外 `ops.ts:597` 已用 `/^[A-Za-z0-9._-]+$/` 硬校验 assetId,`ops.ts:618` 的 replace 是永真的二次防御,注释说明或删除。 ### O9 注释与事实漂移(P1) - `packages/client/src/index.ts:4`:「把 9 个 anim_* 工具的会话卡片」——`VIEWS` 实际 10 项(同文件 `index.ts:46-57`); - 冒烟脚本、README 的工具数表述已一致(10),仅此一处遗留。 --- ## 3. 执行效率 ### O10 渲染固定开销:4s 编辑器等待 + 每渲重启 vite/浏览器(P1,延续 0.3.0 规划 §5) `runtime.ts:344` 的 `await sleep(4000)` 与每次渲染的 vite 冷启动/浏览器拉起是单片 4~10 秒的固定开销;vite 预打包缓存已钉在 `workDir/.vite`(`runtime.ts:284-286`)所以依赖预打包只付一次,剩余是编辑器加载 + `waitForSelector('canvas')` 后的兜底等待。 **建议**:先测量再动——在 `page.waitForSelector('canvas')` 通过后记录实际耗时,若多环境稳定 ≪4s,把固定 sleep 改成「canvas 出现 + 动态等待 Render 按钮可点(或轮询编辑器全局状态)」,按环境自适应,比预热/复用浏览器实例的改动小一个量级、且不碰 headless 稳定性。浏览器实例复用(规划 §5 原案)仍作为改动大的备选。 ### O11 后台渲染卡片完成后轮询不停止(P1) `BackgroundTicket` 的 `setInterval(tick, 2000)` 只在组件卸载时清理(`packages/client/src/cards.tsx:358-377`);`status.status === 'completed'` 后组件改渲染 VideoPanel 但**实例未卸载**,interval 继续每 2s 打一次 `/api/state`。长会话里多张历史渲染卡片 = 持续无效请求。修法:`status` 落定(completed/failed/killed)时 `clearInterval`;顺带把「unreachable 连续 N 次后退避到 10s」加上,宿主重启场景不再徒劳打点。 ### O12 codegen 微效率(P2,可不改) - `varName` 每次 `scene.layers.indexOf(layer)`(`codegen.ts:424`),逐层 O(n),整幕 O(n²); - `lastEnd` 对每条轨道再次调用 `tweensOf`(`codegen.ts:543-548`),而 `emitTracks` 刚算过一遍(`tweensOf` 内部还排序)。 教学 spec 的层数量级是十,实际影响不可测。仅当未来出现「百层单幕」再改(预建 `Map`、复用 tween 结果)。 ### O13 `waitForFrames` 轮询成本(P2,可不改) 每 800ms `collectFrames` 全目录 `readdirSync`(`runtime.ts:399-425`)。千帧级目录下单次扫描毫秒级,渲染本身以分钟计,无感。保留现状。 --- ## 4. 校验与 0.3.0 规划对账 ### O14 「类型枚举一致性」冒烟断言未落地(P0) 0.3.0 规划 §2.3 明确承诺「冒烟测试加一条类型枚举一致性断言」,且是 §6 里程碑 M0 的验收标准之一;实施进度表 M0 标 ✅,但 `scripts/smoke.ts` 中没有任何涉及 `LAYER_TYPES` / `STATIC_PROPS` / `COMPONENT` 集合比对的断言(全文 grep 无命中)。三处枚举漂移的防线**没有建起来**——这正是该断言要防的事故,属于「规划承诺与实现不符」。 **处置**:补一条冒烟——`types.ts` 的 `LayerType` 联合、`validate.ts` 的 `LAYER_TYPES`、`codegen.ts` 的 `STATIC_PROPS`/`COMPONENT`/`ANIMATABLE_BY_TYPE` 四个集合键一致。实现层面需要先小改:`LAYER_TYPES` 从 validate.ts 导出(或把权威枚举提到 types.ts 导出 `LAYER_TYPES` 数组,两处消费)。 ### O15 group `children` 的 validate 校验未落地(P1) 0.3.0 规划 §2.4 承诺「validate 校验 children 引用存在、无环、不跨场景(场景内作用域)」,实施进度表 M0 亦声称 validate 同步放开。实际 `packages/spec/src/validate.ts` **没有任何 children 校验**,只有 codegen 生成期的警告降级(`codegen.ts:397-421`:引用不存在/嵌套 group/重复归属逐条警告)。 **后果**:错写 `children` 要到渲染时才发现(警告混在渲染回执里),与「写幕时就把问题暴露给模型」的校验分层目标不符;且「无环」目前完全没人管——codegen 的单层分组逻辑碰上自引用只会走「不能包含自己」警告,环(A→B→A)在单层 MVP 下等价于重复归属警告,但未来放开多层分组时这里会变成真 bug。 **处置**:validateLayer 增加 children 软/硬检查——引用存在(软警告或硬错,建议硬错:错 id 几乎必然是笔误)、同场景内(硬错)、不指向 group(MVP 硬错)、无重复归属(软警告)。冒烟补四形态断言。 --- ## 5. 组件抽离与 UI ### O16 cards.tsx 拆分时机(P2) 476 行单文件目前可读性尚可(样式常量 → 基础件 Card/Fallback/Warnings → 10 张卡片,分区清晰)。**触发点**:0.3.0 规划 §4 的面板按钮交互一旦启动,每张卡片都要加状态与事件逻辑,届时先拆为 `styles.ts`(样式常量)+ `primitives.tsx`(Card/Fallback/Warnings/VideoPanel/ProgressBar)+ 按卡分文件,避免单文件破千行。现在不动。 ### O17 ReadCard 看不到读取内容(P2) `anim_get` 的 `presentationMeta` 刻意不带 value(`register.ts:474-480` 注释「内容客户端按需拉 /api/spec」),`/dsh-anim/api/spec` 端点也在(`web.ts:256-260`)——但 `ReadCard`(`cards.tsx:263-275`)只显示 path + 时长,没有消费 `/api/spec` 的 UI。结果:卡片替换了原始回执后,**用户在面板上反而看不到模型读到了什么**(回执文本只在无 meta 时经 Fallback 展示)。 **建议**:ReadCard 加一个「展开内容」`
`,懒拉 `/dsh-anim/api/spec?id=` 并按 path 取片段展示(复用 `preBox` 样式,沿用 4000 字截断)。低成本补全这条半截的数据通道。 ### O18 卡片视觉细节(P2) 整体评价:风格统一、全部走 `--dsw-alias-*` 变量(亮暗主题自适应)、层次清楚,没有需要返工的问题。可打磨的三处: - **AssetCard** 的 `src` 是绝对路径,长路径无 `wordBreak: 'break-all'`(`cards.tsx:465-476`),极端字符长度下靠卡片级 `overflow: hidden` 截断,观感突兀;BackgroundTicket 里同样的路径就处理了(`cards.tsx:391`); - **PreviewCard** 缩略图无点击放大、无「查看原帧」链接(`mediaUrl` 已具备服务 png 的能力,加个 `` 包住 figure 即可); - **WarnBox 复用语义**:`anim_plan` 的 pacing(含「全片 24.0s,共 4 幕」这类中性信息)走 `Warnings` 样式全量黄色(`cards.tsx:200`),中性信息可拆出 muted 样式,让真正的告警保留视觉权重。 ### O19 `anim_preview` 同步挂住模型回合(P2,观察) preview 走同步路径(`opPreview` 直接 await),一次带截短 + 降采样的预览典型十几秒到一分钟,期间模型干等。jobs 化 preview(立即返回 + 卡片轮询缩略图)能改善交互节奏,但要动 job 契约与卡片,收益中等。**建议**:挂到 0.4 与「面板 P2 交互」一起评估,当前只把「预览耗时以十秒计」写进工具描述的预期管理(现描述已有「慢且没必要整片预览」的引导,够用)。 --- ## 6. 建议处置与落点 **0.3.1(小版本:正确性 + 清账)**——全部为小改动,互不阻塞: | 项 | 动作 | 验收 | | --- | --- | --- | | O1 | 实现 `scenes` 切片渲染(方案 A) | 冒烟:`scenes:[1]` 的 frameCount/durationMs 与该幕一致 | | O14 | 枚举一致性冒烟断言(含最小导出重构) | 冒烟新增 1 项,故意漂移时红 | | O2-1 | 渲染互斥队列 + 端口改 0 | 冒烟:并发两次 render 全部成功且产物完整 | | O5/O6/O9 | 死代码与注释清理 | typecheck + 冒烟全绿 | | O8 | 净化函数/probe 收敛 | typecheck + 冒烟全绿 | **0.3.x 后续 / 随 M2 面板交互**:O3(emit 绑定 agent)、O11(轮询停止)、O15(children 校验)、O17(ReadCard 展开)、O18(视觉细节)。 **观察项(不改,记录在案)**:O4 杂项、O7 过度导出、O10 的测量先行、O12/O13 微效率、O16 拆分时机、O19 preview 后台化。 **明确不做**:为 O12/O13 做预防性优化(量级不支撑);为 O19 在 0.3.x 内做 jobs 化。 --- ## 7. 值得保持的亮点(避免「优化」误伤) 通读中确认以下设计是刻意的、有注释锚点的正确决策,后续改动不要「顺手优化」掉: - **patch 引擎的深拷贝 + 整批校验回滚**(`spec/patch.ts`、`store/index.ts:91-109`)——半改成功的 spec 比没改更糟,性能换正确性是对的; - **事件 gate 的 thunk 缓冲**(`ops.ts:346-356`)——保证 render-start → progress → finished 的因果序与 jobId 一致性; - **waitForFrames 的尾部静止补偿**(`runtime.ts:386-457`)——四种停滞结局区分处理,是从真机事故里长出来的逻辑; - **事件深清理 `stripUndefined`**(`register.ts:71-81`)+ 冒烟金丝雀(`smoke-host.mjs:45-96`)——无损 JSON 约束在边界上双重设防; - **渲染 seam 的结构复述**(`contract.ts` 刻意不反向依赖 host)——注释已写明字段必须成对改,属可接受的显式成本。 --- ## 8. 修复记录(2026-09-15 同日修复轮) §0 表「处置结果」列的依据。14 项修复 + 5 项维持现状,全部改动已过 typecheck(五包全绿)、`smoke.ts` 34 项(原 28 项 + 新增 6 项)、`build.mjs`、`smoke-host.mjs`(含新增的参数描述对齐断言)。 **P0/P1 修复的落点**: | 项 | 改动 | | --- | --- | | O1 | `adapter.ts` 新增 `pickScenes()`(0 基索引、保序去重、越界可读报错),`render()` 按切片后的 spec 渲染与报告时长/帧数;冒烟加纯函数断言 + 「SENTINEL 哨兵」接线测试(materialize 捕获生成物,证明切片真正流进 codegen) | | O2 | `MotionCanvasRenderer` 内加 promise 串行闸 `#serialized()`,preview/render 共用;`runtime.ts` 默认端口 5179 → 0(系统分派随机可用端口),监听后按 `httpServer.address()` 解析实际端口供页面访问 | | O3 | `EventSink.append(event, agent)`——会话归因改为参数传入;`registerAnimTools` 的 `emitFor(exec)` 为每次工具调用构造绑定当次 agent 的 emit 闭包,后台渲染事件永不落错 sidecar;单槽位 `currentAgent`/`setAgent` 移除 | | O14 | `types.ts` 新增 `LAYER_TYPES` 常量、`LayerType` 从它派生(唯一权威源);validate 的放行集合改为派生;codegen 三张表导出;冒烟新增三表一致性断言 + `anim_draft_scene` 参数描述(第四张面)与枚举对齐断言 | | O15 | `validateLayer` 查 children 形态(字符串数组);`validateScene` 查引用关系——引用存在/不自引用/不嵌套 group 硬错,重复归属软警告;冒烟五形态断言 | | O8 | `spec/naming.ts` 的 `safeName()` 成为净化唯一实现(codegen URL、adapter 落盘、ops 资产复制、index/register sidecar 文件名共用);tools 包新建 `ctx-probe.ts` 收敛两份 `probe` | | O11 | `BackgroundTicket` 渲染落定即 `clearInterval`;连续 3 次不可达退避到 10s 轮询 | | O17 | `ReadCard` 新增「查看读取内容」:展开时懒拉 `/dsh-anim/api/spec`,客户端最小 JSON Pointer(RFC 6901,含 `~0`/`~1` 转义)取片段,4000 字截断 | | O18 | `opPlan` 的 pacing 不再追加剧性行「全片 Xs 共 N 幕」(totalMs/sceneCount 字段不变,卡片摘要行已展示)——行为变更,回执消费方无影响;AssetCard src 加 `wordBreak: break-all`;PreviewCard 缩略图包 `` 可看原帧 | | O4/O5/O6/O7/O9 | isFile 校验;sidecar mkdir 标记位移除(含对原表述的更正,见 O4);`AnimRendererRegistry.list()` 删除;`COMPONENT` 收窄为 `Record` 并删不可达分支;`starPath`/`trackEndMs`/`sceneContentEndMs` 转模块内;client 头注释 9→10 | **维持现状(防「顺手优化」)**:O10 固定 4s 等待(沙箱无浏览器无法测量,改动前必须真机拿数据,见 0.3.0 规划 §5)、O12/O13 微效率、O16 拆分时机、O19 preview 后台化。 **冒烟新增断言清单**(28 → 34 项):枚举一致性 ×2(三张表、工具描述)、group children 五形态、`pickScenes` 纯函数、`scenes` 切片接线、渲染串行闸并发不重叠;`smoke-host` 新增 1 项(scene 参数描述 × LAYER_TYPES,从源码解析权威枚举)。 --- ## 9. 修复记录(2026-09-16 增补:真机首写稳定性 O20) **真机证据**:首次调用 `anim_draft_scene` 高频报「场景草稿非法」,三类错误几乎每次同现: 1. `ease: "easeOut"` 写成字符串(校验器只说「应为 { kind: ... } 对象」,模型不易自救); 2. 图层 `type` 缺失或大小写写飘("Text"),且「缺 type」与「type 值非法」混为同一句「未知图层类型」; 3. 图层漏 `name`(纯展示字段)与漏 `tracks`(静态图层本就合法)。 每类都是无歧义的机械错误,靠「报错 → 模型重写」回修要白烧一整个来回。 **处置(三层防线)**: | 层 | 改动 | 落点 | | --- | --- | --- | | 边界自动纠正 | 新增 `coerceScene()`:ease 字符串包装成 `{ kind }`(未知缓动名也包装,让校验器报「未知缓动类型 + 可选列表」);type 按不区分大小写匹配 `LAYER_TYPES` 纠正;图层/场景漏 `name` 用 id 补、漏 `tracks` 补空数组。只修无歧义、不丢内容的错(漏 props/type 不代劳——代劳只会把内容丢失藏进渲染结果);深拷贝入参,绝不改调用方对象。修复清单随回执 `repairs` 字段回报,模型读到下次自己写对 | `packages/tools/src/ops.ts` | | 校验文案可执行化 | 「缺字段」与「值非法」分开说(`缺少 type 字段(图层类型),可选:…` vs `未知图层类型 "textbox",可选:…`);缺 `props` 给字段形态示例;ease 报错附可照抄示例 `{"kind":"easeInOut"}` 并回显收到的值;`缺少必填字段` 一律带字段名 | `packages/spec/src/validate.ts` | | 工具描述引导 | `anim_draft_scene` 的 scene 参数描述补**完整最小示例**(模型照抄形状再改内容,比字段清单可靠)+ 三条高频错误自查(ease 对象形态、图层五必填、type 全小写) | `packages/tools/src/register.ts` | **配套**:`opDraftScene` 校验失败的报错尾部追加最小行动指引(五必填字段 + ease 对象形态);回执新增 `repairs`,SceneCard 用中性色「已自动纠正」区块展示(与黄色 warnings 区分——修复是信息不是警告)。 **验收**:冒烟新增 3 项(报错文案五形态、`coerceScene` 纠正+不可变、`opDraftScene` 端到端含硬报错指引),34 → 37 项;typecheck 五包全绿。 --- ## 10. 修复记录(2026-09-16 增补:真机日志回归 O21) **真机证据**:一次 6 幕教学动画的真机渲染日志刷出 100+ 行「图层 X 的属性 strokeWidth 不被 line/arrow/rect/circle 支持,已忽略」与数条「textAlign 不被 text 支持,已忽略」。 **定性**: 1. `strokeWidth`(SVG/CSS 习惯名)在整个代码库里零匹配——IR 的描边宽度叫 `lineWidth`。模型写的描边宽度被**静默丢弃**,所有线条按 MC 默认线宽出片,属「渲染不报错、看片才发现」的产出偏差(与 `color → fill` 别名要解决的是同一类问题,`codegen.ts` 对 color 已有先例); 2. `textAlign` 同为零匹配,但 MC 的 `Layout` 基类原生携带 `textAlign` signal(`Txt` 继承它),本可直接支持,只是没进 `STATIC_PROPS.text` 白名单; 3. 「The CJS build of Vite's Node API is deprecated」来自 workDir 的 `vite.config.ts`:目录无 `"type": "module"`,Vite 5 把 TS 配置打包成 CJS `require('vite')` 触发弃用提示; 4. 同一批告警按幕 × 渲染次数重复打印属预期行为(1/2 修复后这批自然消失),告警聚合留作观察项,不单独动。 **处置**: | 层 | 改动 | 落点 | | --- | --- | --- | | 渲染别名 | `emitNode` 静态属性别名 `strokeWidth → lineWidth`(`lineWidth` 已显式给出时规范名优先,冗余的 `strokeWidth` 照常告警忽略);`emitTracks` 对 `props.strokeWidth` 轨道目标做同一改写后再查 `ANIMATABLE_BY_TYPE`——存量 spec 里已落库的 strokeWidth 因此一并复活 | `packages/render-mc/src/codegen.ts` | | 渲染直通 | `STATIC_PROPS.text` 增加 `textAlign`(MC Layout 原生 signal,直通即生效) | 同上 | | 边界归一化 | `coerceScene` 第 4 条:`props.strokeWidth` 改写为 `props.lineWidth`(规范名已在则不动、不丢内容),repair 说明教规范名;新草稿自此以规范名落库 | `packages/tools/src/ops.ts` | | 描述引导 | `anim_draft_scene` 描述写明「lineWidth 描边宽度(SVG 习惯名 strokeWidth 会被自动换算)」与「text 支持 textAlign(left/center/right)」 | `packages/tools/src/register.ts` | | 日志净化 | 渲染配置改写为 `vite.config.mts` 强制 Vite 走 ESM 链路加载,从源头掐掉 CJS 弃用告警 | `packages/render-mc/src/runtime.ts` | **验收**:冒烟新增 5 项(别名静态+轨道+textAlign 直通、规范名优先、`coerceScene` 归一化、`.mts` 源文件名断言、描述内完整示例自洽断言——示例可解析/过校验/codegen 零警告,防止模板被改坏后模型照抄毒图稿),37 → 42 项;typecheck 五包全绿。