校准读时重写的文档口径:写回一次 + 丢弃可见,并补一条口径变更决策

- decision-log 2026-09-10 那条把「不写迁移脚本、不删除、不重置」改成当前事实:读时会把 legacy 分区(art / code)坐标归并到新分区并写回一次 sidecar(revision +1);不写迁移脚本、不重置坐标位置;但无法归并的坐标会被跳过(跳过条数在画布提示条上可见)。

- 新增 2026-09-11 decision-log 条目:背景(审计查实「读时原地重写」112 条 + 27%(66 + 16)丢弃与旧文档字面冲突)、决策(用户选 A:保留重写、不静默、改文档)、影响范围、验证方式(含两处变异验证)、关联文档。

- 技术方案【GameAgent资源自由画板与快速编辑】:同一句改成读路径写回一次 + 无法归并的坐标被跳过且可在提示条 / data-resource-canvas-layout-* 上读到。

- PRD【项目开发工作台】第 325 行同一句同样校准(避免权威文档里留着「不写迁移脚本、不删除、不重置」这句会误导排障)。

- 测试用例【AGC资源工作台V3端到端验收】S21 台账那格补上「存量项目第一次打开时读路径也会写回一次、并给一次性提示条」,避免把 revision 从 1 开始误判成异常。

- 行为、sidecar schema(v1)与跨端契约均未改动。
This commit is contained in:
2026-09-11 19:19:51 +08:00
parent e15456ea14
commit ee8e12783e
4 changed files with 12 additions and 4 deletions
@@ -322,7 +322,7 @@ type UpdateProjectResourceCanvasLayoutResult =
- 搜索或筛选只隐藏卡片,不删除、压缩或重排其坐标;清空搜索后恢复原位置。
- 窗口尺寸变化只改变当前栏目的可视范围,不回写或裁切持久坐标,也不因资源 extent 或 resize 把已平移的 viewport 拉回内容边界。当前客户端继续以 `1280×800` 横屏合同验收。
- 任一资源出现后,普通用户资源管理固定使用 `UI 交互 -> 角色与对象 -> 场景与环境 -> 音频 -> 文档 -> 待归类 -> 项目版本` 七栏目分页画布。前六栏是 manifest 资产的功能分类 `category``ui-interaction / character / scene / audio / document / unclassified`),项目版本不是资源资产、6 类资产分类轴对它不适用,因此固定单独成栏;已投影资源都进入画布导航、分页、卡片、搜索与详情入口,不再有被分区轴排除的内部栏目。每个栏目按 `projectId + mode + category` 保留独立 viewport,普通 wheel 平移当前视图,`Ctrl/Cmd + wheel` 以指针位置为锚点缩放当前无限画布,空白拖动只平移当前栏目;非空状态不提供分区高度、分区内部滚动或分区内容倍率。搜索和详情开关不得重置 viewport,项目、mode 或栏目切换只恢复各自会话状态,显式复位才重新适配当前栏目内容。
- 布局 sidecar 的 `section` 允许继续出现旧四 / 五栏目取值(`code / document / version / art / audio`),读取时按资源当前分区做一次读时归并:`document / audio / version` 与新分区同名,1:1 保留;旧 `art` 是「扩展名 + mediaType」口径,按资源当前分类落入 `ui-interaction / character / scene / unclassified`;旧 `code` 落入 `unclassified`;无法精确归并时回落到 `unclassified`。归并只改写 `section``x``y``manuallyPlaced` 原样保留,不写迁移脚本、不删除、不重置
- 布局 sidecar 的 `section` 允许继续出现旧四 / 五栏目取值(`code / document / version / art / audio`),读取时按资源当前分区做一次读时归并:`document / audio / version` 与新分区同名,1:1 保留;旧 `art` 是「扩展名 + mediaType」口径,按资源当前分类落入 `ui-interaction / character / scene / unclassified`;旧 `code` 落入 `unclassified`;无法精确归并时回落到 `unclassified`。归并只改写 `section``x``y``manuallyPlaced` 原样保留。读时会把 legacy 分区(`art` / `code`)坐标归并到新分区并写回一次(`revision` +1);不写迁移脚本、不重置坐标位置;但无法归并的坐标会被跳过,跳过条数连同归并条数在画布提示条上可见
- 首次载入项目中的既有资源不显示未读标识。当前会话内,非当前栏目出现稳定 ID 的新资源时,在对应栏目名称右上角显示红点;当前栏目新增资源不显示红点,用户通过点击或程序跳转进入该栏目后立即清除。未读状态只属于当前前端会话,并按 `projectPath + projectId` 隔离,切换项目时清空,不写入 manifest、布局 sidecar 或后端。
- 打开项目、切换 mode 或当前 mode 首次出现新资源时执行“读取 -> 协调 -> 必要时 CAS 写入”;dependency 模式必须先等待与当前 `projectPath + projectId + resource inputs` 匹配的 Rust 图进入 `ready``failed` 终态,等待期间不得创建 fallback、读取 sidecar、协调资源或入队保存。`failed` 只允许以空图降级初始化一次。项目或 mode 已切换后返回的旧异步结果必须丢弃。
- 同一 `projectPath + projectId + mode` 的首次读取与资源集合协调必须分开:资源集合变化不得取消已经发出的读取或保存。当前 scope 内资源自动协调写入使用单写者 FIFO,任一时刻最多一个 CAS 在途,后一笔必须使用前一笔成功返回的 revision。切换项目或 mode 后,旧 scope 的在途请求不能阻塞新 scope 队列;前端放弃旧请求槽位并丢弃其迟到响应,后端继续依靠 `expectedProjectId + expectedRevision + 系统锁` 仲裁已发出的请求。
@@ -15,6 +15,14 @@
- 关联文档:相关 PRD、技术文档、提交或 Issue
```
## 2026-09-11 资源画布 sidecar 的读时归并保持写回,但不再静默,并公开丢弃口径
- 背景:2026-09-10「旧栏目坐标读时归并」那条决策在文档里写成「不写迁移脚本、不删除、不重置」,字面为真、事实上是**读时原地重写**——`useProjectResourceCanvasLayout.ts` 读盘后若 `reconcileLayout(...).changed` 为真就立刻走 `update_local_project_resource_canvas_layout` 做一次 CAS 写盘(`revision` +1)。真机量到的规模(36 份 sidecar / 300 条坐标):**112 条 legacy 分区坐标**`art` 36×2 + `code` 20×2)会在第一次打开项目时被静默改写;另有 **82 条(27%)坐标被丢弃**,其中 **66 条**因为 `resourceId` 在 manifest 里查不到、**16 条**因为持久化分区与资源分类不匹配,丢弃点是 `resourceCanvasLayoutModel.ts` 的 `if (!normalized) return false;`,而这次丢弃同样被上面那次写回**固化**。旧文档口径与读路径真实行为冲突:不改则下一个排障的人会往「是不是有迁移脚本」的方向找。审计给出的两个候选是 A(保留重写、补可见提示 + 改文档)和 B(把丢弃/重写改成只在拖动时发生);用户拍板 **A**,因为 B 会改变布局语义、影响两次 sidecar 的既有行为。
- 决策:**重写行为本身一字不改**(不改「只在拖动时写」、不改 `resourceCanvasLayoutModel.ts` 的丢弃语义、不动跨端契约与 sidecar schema`schemaVersion` 仍是 `v1`),只把两件已发生的事变成可见:①读时归并 → 一次性的画布提示条(`resourceWorkbenchNotice` 所在的那条 `game-resource-book-notices` 提示层、复用既有 `.game-resource-live-notice` 样式与「知道了」关闭按钮),文案点明「旧分区坐标对齐到新分区并写回、坐标位置未变」;②读时丢弃 → 同一条提示里带条数,并把三种情况在 DOM 上分开暴露(`data-resource-canvas-layout-normalized` / `-dropped` / `-dropped-missing-resource` / `-dropped-section-mismatch`,完全没丢时是 0 / 0)。提示是**操作后的一次性提示**,不是常驻说明,也不写功能说明或规则描述(符合「面板里不写功能说明 / 长期规则」的仓库规范)。统计口径直接复用丢弃判据本身(`normalizeResourceCanvasPosition`),只统计、不参与判定。两个排序模式各读一份 sidecar,实现上每个项目只提示一次、只取先到的那份,避免两条提示互相覆盖。
- 影响范围:`apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCanvasLayout.ts`(新增只读统计 `inspectProjectResourceCanvasLayoutRead` 与文案 `describeProjectResourceCanvasLayoutRead`、新增 `readReport` 返回项)、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`(提示条渲染与每项目一次的去重)。**行为、写回时机、丢弃规则、sidecar schema 与跨端契约均不变**,因此首次打开存量项目仍会写回一次 sidecar、`revision` 仍 +1。
- 验证方式:`useProjectResourceCanvasLayout.test.ts` 覆盖三种读盘统计(归并 / 资源不在 manifest / 分区不匹配)与文案四形态、读盘无需归并且不写盘、以及「只补新资源落位的写回不报读时统计」;`appSurface/project-development.suite.ts` 覆盖提示条真的出现(文案 + 四个 `data-*` 计数可读、可关闭)与存量 sidecar 无需归并时**不出现**任何读时提示。变异验证:去掉 `setReadReport` → 读盘统计与提示条用例变红;把 `dropped` 计数改成恒 0 → 丢弃条数与 `data-resource-canvas-layout-dropped` 断言变红。
- 关联文档:`docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md`(读时归并那条)、`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md``docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`、本条上一条 2026-09-10 决策。
## 2026-09-11 资源分类口径拆成「读显示 / 写回」两个口径,读显示口径下沉到 Rust 并与 TS 同构
- 背景:`category` 此前只有 TS 一处实现(`gameCreationAppAssetCategory` 落盘 `category` 权威 + 读时自愈),Rust 侧反序列化只做「缺失 / 非法回退 `kind` 派生」,Agent 资源投影还直接透传落盘 `asset.category`。于是同一条资产有两套口径:真机 122 条资产里 **55 条** `{kind:"ui", category:"unclassified"}` 在 UI 显示「UI 交互」、Agent 读到「待归类」。另有两条写读路径在真机上继续漂移:①「编辑标签」面板用**读显示**口径取值再原样回传,把自愈值写回落盘,把「只改标签」变成静默改分类(真机同一个 `kind:"ui"` 同时存在 55 条 `unclassified` 与 2 条 `ui-interaction`,后者正是被回写的签名);②UI 设计资产的现役写入侧写的是**大写** `"UI"``ui_editor/resource_bridge.rs``workflow.rs``persistence.rs`)、字体上传写 `font`,两者都不在别名表里 → 落 `image → unclassified`,且派生值本身就是 `unclassified`,读时自愈的触发条件「派生值不是 unclassified」永不成立,**8 条**真机 UI 资产永远归不了类;③`register_local_asset_entry` 的更新分支从不重派生 `category`,同路径重登记换了 `kind` 就留下「新 kind + 旧分类」,且陈旧的非 `unclassified` 值会被无条件信任。
@@ -47,7 +55,7 @@
## 2026-09-10 AGC 资源画布分区改为 6 类资产分类加项目版本栏目,旧栏目坐标读时归并
- 背景:资源画布原分区轴是「扩展名 + mediaType」派生的 `document / art / audio / code / version`,普通画布只显示其中四栏并隐藏游戏代码,与 `@` 面板、资源详情筛选已经收敛的单一权威 `category``ui-interaction / character / scene / audio / document / unclassified`)不一致。只登记游戏代码的项目会因资源签名非空进入分页画布,但 code 不在可见栏目里,于是四栏全空、一张卡都不显示。旧布局 sidecar 已存有旧 `section` 值,而 PRD 要求历史手动坐标只读恢复,不删除、不重置、不迁移。
- 决策:分区轴改为 `PROJECT_RESOURCE_CANVAS_SECTIONS` = 6 类资产功能分类 + 末尾独立的「项目版本」栏目;项目版本不是资源资产,6 类资产分类轴对它不适用,固定单独成栏。扩展名分类器降级为只负责准入与卡片显示类型(`.zip` / `.exe` 等无法识别的二进制产物与附件仍不进入画布),不再决定分区。`game-creator-resource-layout.v1` 不升版;Rust `ProjectResourceCanvasSection` 保留旧 `code` / `art` 白名单值继续可反序列化,且不加未知值兜底,损坏 payload 仍失败关闭。读取时按资源当前分区归并:`document / audio / version` 同名 1:1 保留;旧 `art` 按资源当前分类落入 `ui-interaction / character / scene / unclassified`;旧 `code` 落入 `unclassified`;无法精确归并时回落 `unclassified`。归并只改写 `section``x / y / manuallyPlaced` 原样保留,不写迁移脚本;`art` / `code` 平面并入同一新栏目后可能出现同坐标叠卡,按既有「不重置坐标」口径原样保留,不改写自动坐标,新资源仍由既有避让规则落位。
- 决策:分区轴改为 `PROJECT_RESOURCE_CANVAS_SECTIONS` = 6 类资产功能分类 + 末尾独立的「项目版本」栏目;项目版本不是资源资产,6 类资产分类轴对它不适用,固定单独成栏。扩展名分类器降级为只负责准入与卡片显示类型(`.zip` / `.exe` 等无法识别的二进制产物与附件仍不进入画布),不再决定分区。`game-creator-resource-layout.v1` 不升版;Rust `ProjectResourceCanvasSection` 保留旧 `code` / `art` 白名单值继续可反序列化,且不加未知值兜底,损坏 payload 仍失败关闭。读取时按资源当前分区归并:`document / audio / version` 同名 1:1 保留;旧 `art` 按资源当前分类落入 `ui-interaction / character / scene / unclassified`;旧 `code` 落入 `unclassified`;无法精确归并时回落 `unclassified`。归并只改写 `section``x / y / manuallyPlaced` 原样保留**读时会把 legacy 分区(`art` / `code`)坐标归并到新分区并写回一次 sidecar(`revision` +1)**;不写迁移脚本、不重置坐标位置;但无法归并的坐标会被跳过(跳过条数在画布提示条上可见)。`art` / `code` 平面并入同一新栏目后可能出现同坐标叠卡,按既有「不重置坐标」口径原样保留,不改写自动坐标,新资源仍由既有避让规则落位。
- 影响范围:`packages/shared/src/contracts/gameCreationApp.ts``server-rs/crates/shared-contracts/src/game_creation_app.rs``resourceProjectionModel.ts``resourceCanvasSectionMapping.ts``resourceCanvasLayoutModel.ts` 的栏目顺序与协调、`ProjectDevelopmentView` 的栏目标签 / 图标 / 可见栏目表。
- 验证方式:`resourceCanvasSectionMapping.test.ts` 覆盖旧值 × 目标栏目矩阵(坐标与 `manuallyPlaced` 原样保留、`section` 改写、独有旧值改写后必然判定为变化)与回落栏目属于目标集合;`projectResourceProjectionModel.test.ts` 覆盖 6 类分区投影、项目版本独立成栏和只登记游戏代码的项目落在「待归类」;Rust 侧覆盖旧 section JSON 反序列化、未知值拒绝与既有旧 sidecar 文件读取。
- 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md``docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md``docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
@@ -45,7 +45,7 @@
- AGC 侧共享契约维护 canonical 资产类型目录和历史读时映射:`game-background -> scene``character-art -> character``ui-prototype -> ui-design``art-spritesheet -> icon-spritesheet``art-spritesheet-slice -> icon``illustration/game-art -> image`。该目录只约束 AGC 新写入与投影,不要求现役 Web 美术画布同步改造;历史 manifest 保持原值,读取时可归一到 canonical 展示。
- 角色动画派生结果的 manifest `kind` 固定写 `character-animation``source.generationKind` 同步保留 `character-animation`,媒体仍以预览视频加正式序列帧登记。图片编辑等普通派生继续继承源资源语义 kind;后续如需改写为 canonical kind,只能在读取投影或显式迁移中完成,不重写历史 manifest。
- 固定栏目页始终为 `UI 交互 -> 角色与对象 -> 场景与环境 -> 音频 -> 文档 -> 待归类 -> 项目版本`。分区口径是 manifest 资产的功能分类 `category``ui-interaction / character / scene / audio / document / unclassified`);项目版本不是资源资产、6 类资产分类轴对它不适用,固定单独成栏排在末尾;没有 `category` 事实源的任务产物与导入附件归 `待归类`,合法 Agent 文本回执归 `文档`。若某栏目没有对应投影,Dock 仍显示该栏目并允许打开空画布;空画布只表达“当前没有已登记资源”,不得由 UI 补假数据。
- 布局 sidecar 的 `section` 允许继续出现旧四 / 五栏目取值(`code / document / version / art / audio`),读取时按资源当前分区归并:`document / audio / version` 同名 1:1 保留,旧 `art` 按资源当前分类落入 `ui-interaction / character / scene / unclassified`,旧 `code` 落入 `unclassified`,无法精确归并时回落 `unclassified`。归并只改写 `section``x / y / manuallyPlaced` 原样保留,不写迁移脚本、不删除、不重置`game-creator-resource-layout.v1` schema 不升版,否则既有 sidecar 会被直接拒读。
- 布局 sidecar 的 `section` 允许继续出现旧四 / 五栏目取值(`code / document / version / art / audio`),读取时按资源当前分区归并:`document / audio / version` 同名 1:1 保留,旧 `art` 按资源当前分类落入 `ui-interaction / character / scene / unclassified`,旧 `code` 落入 `unclassified`,无法精确归并时回落 `unclassified`。归并只改写 `section``x / y / manuallyPlaced` 原样保留。**归并发生在读路径上、并且会当场把整份布局写回一次 sidecar(`revision` +1)**:第一次打开存量项目时旧分区坐标就被对齐到新分区并落盘,没有单独的迁移脚本,也不重置坐标位置。无法归并的坐标(`resourceId` 已不在 manifest,或持久化分区与资源当前分类不匹配)会被跳过,跳过条数连同归并条数在画布提示条上可见(`data-resource-canvas-layout-*``game-creator-resource-layout.v1` schema 不升版,否则既有 sidecar 会被直接拒读。
- Direct Codex 的游戏生成链路以 `game/index.html``game/style.css``game/game.js` 作为代码资产登记;是否额外产出设计文档由当前任务和 Agent 决定,资源页只消费已登记结果。
## 操作边界
@@ -75,7 +75,7 @@
| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
| --- | --- | --- | --- | --- | --- |
| **S21** 全程台账 | 不刷新、不重开项目,回看资源画布与布局 | 新增资源即时投影;两套布局(dependency / type)各自坐标独立、历史 `manuallyPlaced=true` 保留;搜索与详情开关不重置 viewport | L4751、L262265、L320323、L327329、L500 | 两份 sidecar 都在 `.agent/workbench/resource-layouts/{dependency,type}.json`;拖动超过 5px 阈值才提交一次 CAS,成功后 `revision` 递增 | 双窗口同 revision 的 CAS 冲突(L509)需开两个客户端窗口才可验;`A→B→A` 复用 epoch 的取消语义只能靠定向测试 |
| **S21** 全程台账 | 不刷新、不重开项目,回看资源画布与布局 | 新增资源即时投影;两套布局(dependency / type)各自坐标独立、历史 `manuallyPlaced=true` 保留;搜索与详情开关不重置 viewport | L4751、L262265、L320323、L327329、L500 | 两份 sidecar 都在 `.agent/workbench/resource-layouts/{dependency,type}.json`;拖动超过 5px 阈值才提交一次 CAS,成功后 `revision` 递增;**存量项目「第一次打开」时读路径也会写回一次**(旧 `art` / `code` 分区坐标归并到新分区,`revision` +1),并给一次性提示条,条数含**无法对齐被跳过的坐标**(`data-resource-canvas-layout-*`),所以「`revision` 从 1 开始」不是异常 | 双窗口同 revision 的 CAS 冲突(L509)需开两个客户端窗口才可验;`A→B→A` 复用 epoch 的取消语义只能靠定向测试 |
## 4. 前置条件清单