合并 feat/agc-resource-replace:把「替换素材」接进资源工作台
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Successful in 3m14s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Native shell tests (pull_request) Successful in 20m52s

- 解冲突 project/manifest.rs:保留版本写入公共体 write_manifest_locked_with_version_guard(版本只追加 + 绑定改写放行),并把 schema 校验并入该公共体,两侧语义都不丢
- 解冲突 shared-contracts:取当前分支的读显示口径 game_creation_app_asset_effective_category(含与 TS 交叉钉住的 EFFECTIVE_CATEGORY_CONTRACT 矩阵),同步改写 version_resource_replacement 对旧函数名的调用
- 解冲突 index.tsx:两处均为纯 import 追加,两侧都保留
- 解冲突 decision-log 与技术方案文档:两侧条目都是追加,全部保留
- 随之进入本分支:版本绑定改写窄放行通道、直接替换命令与其兼容性判据、资源卡工具条「替换素材」入口
This commit is contained in:
2026-09-12 14:19:03 +08:00
20 changed files with 3308 additions and 44 deletions
@@ -8494,3 +8494,28 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 原因:预览起停不是源码变更,不能靠"给它也推一次 revision"来消除冲突——那会让运行时的验证凭证(`expected_revision` / `verified_revision` / `failed_playtest_revision`)凭空漂移,把自主构建流程拖进无谓的重新验证。正确的修法是把判据面收回到契约所说的对象,而不是让簿记写入承担源码语义。
- 验证:`projectResourceLiveUpdateModel.test.ts` 新增「同 revision 只有 `preview` 变化必须被接受」与「同 revision 资产变化必须仍被拒收」两条;变异回整份 JSON 指纹后前者以 `expected 'revision-conflict' to be 'accepted'` 失败。定向:模型 17 passed、workspaceLauncherManifestMerge 5 passed、appSurface 413 passed、typecheck exit 0、check:encoding 4399 files passed。
- 影响范围:`apps/ai-game-creator-shell/src/view/project-development/projectResourceLiveUpdateModel.ts`、`apps/ai-game-creator-shell/tests/projectResourceLiveUpdateModel.test.ts`、`pitfalls.md` 本条。
## 2026-09-11 AGC 资源替换按 PRD 恢复为「版本级替换」:改绑定落在追加的新版本上
- 背景:PRD §3.2(`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md:36-41`)要求「替换版本引用资源时创建下一迭代版本,不原地修改既有版本」,并要求新版本记录 `parentVersionId`、替换前后资源身份与创建原因;§5.3(:356-387)给出 `ProjectVersionResourceReplacement` 与「三项兼容性必须同时为 true 才能创建下一版本」。本文件 2026-09-10 那条(`decision-log.md:8263`)与 Issue #309 的 C1 决策 / 贯穿性决策 6 / 验收总纲当时写的是相反口径:「改 manifest 绑定……**不创建新版本**」「替换功能(候选素材 / 替换关系 / 替换队列 / Agent 审核 / 单点替换)整条取消,不实现」。用户裁决:**按 PRD 来**,实际机制仍是「改 manifest 绑定」。
- 决策:资源替换 = **改绑定 + 追加下一迭代版本**。新命令 `replace_local_project_version_resource`(写,`asset.register`)在持项目写锁与 `expectedProjectId + expectedProjectRevision` CAS 下,一次 `mutate_manifest_at` 写入里追加 `createdReason='resource-replacement'` 的子版本(`parentVersionId` 指向被替换的源版本),子版本绑定 = 源版本绑定**去掉源素材**并保证**替换素材在集合里**;只读命令 `read_local_project_version_replacement_candidates`(`asset.list`)返回候选与后端权威的三项兼容性。既有版本记录一个字节不改,**继续走默认空放行集合**,不新增/放宽 `mutate_manifest_at_allowing_version_removals`。
- 恒等绑定口径的硬约束(实测得出):每个版本的 `resourceBindings` 是「该版本使用的素材集合」而不是槽位表,替换素材若在源版本创建时就已登记,它本来就在集合里,因此**不能**把源素材那条槽位改写成替换素材(会撞「资源槽位重复」);正确落盘是「源素材从集合里消失 + 替换素材在集合里」,替换素材是后来才登记时按源素材原位置插回。
- 三项兼容性判据:`categoryEqual` 用 PRD §5.3 的**读时自愈**口径(为此在 `server-rs/crates/shared-contracts` 新增 `game_creation_app_asset_category_with_read_time_healing`,与 `packages/shared` 的 `gameCreationAppAssetCategory` 逐分支一致并配用例锁定——此前 Rust 反序列化只有「缺失/非法 → 按 kind 派生」,没有自愈,两侧口径并不一致);`subtypeEqual` 比较 canonical `kind`;`sizeSpecEqual` 比较规范化媒体格式 + 已知帧宽高 + 已知时长,任一方有事实的维度必须相等。
- 已知降级(刻意接受,不得当成完整实现):manifest 资产表没有 `width / height / durationMs`,现役写入侧几乎全写 `imageSequenceFrames: None`,所以 `sizeSpecEqual` 在真实数据上退化为**媒体格式相等**(`png ↔ webp` 会被拒)。要支持跨图片格式替换,必须先给 manifest asset 增尺寸字段并在写入侧回填(跨端契约变更)。
- 替换前后资源身份:按 PRD §5.4 的版本字段表口径**用推导记录**,不新增 manifest / 跨端契约字段(`父 − 子 = {源素材}`、`子 − 父 = {替换素材}`,配对由 `parentVersionId` + `createdReason` 确定)。已知限制:替换素材在源版本创建时就已登记时 `子 − 父` 为空集,版本记录无法单独反推配对;要无歧义持久化配对须先给 `GameIterationVersion` 增字段。
- 边界:C6 的候选素材 / 替换关系图 / 替换队列 / Agent 审核 / 批量提交**仍然不做**(#309 该部分不变);§3.2 末条「当前运行中的版本不消费尚未生成的新版本变更」保持——成功后只切记录层当前版本,不自动重载/重启预览,也不做运行时资源重映射。入口只在资源卡选中工具条、且只对「manifest 资产 + 被当前版本绑定」的素材渲染;候选弹窗复用美术画布的 `ImageCanvasProjectAssetPickerDialog`(AGC 首次使用),以可选 prop 扩展且默认值保持网页端行为逐字不变。
- 影响范围:`server-rs/crates/shared-contracts/src/game_creation_app.rs`、`src-tauri/src/project/version_resource_replacement.rs`(新增)、`commands.rs`、`main.rs`、`tests/version_resource_replacement.rs`(新增)、`src/features/resource-canvas/resourceVersionReplacement{Model,Transport}.ts`(新增)、`view/project-development/index.tsx`、`src/components/image-editor/ImageCanvasProjectAssetPickerDialog.tsx`、`apps/ai-game-creator-shell/src/styles.css`。
- 验证方式:Rust 定向 8 条 + `shared-contracts` 20 条;变异验证三条(去掉媒体格式维度 → 2 条转红;绕开读时自愈 → 自愈用例转红;改成原地改既有版本 → 4 条转红并被「项目版本记录写入后不可修改、删除或重排」拦下,证明只追加守卫真的在挡)。前端新增 12 条,变异验证两条(放宽入口判据 → 「不给假按钮」转红;失败路径静默关弹窗 → 「保留弹窗显示原因」转红)。AGC 全量 1231 passed / 4 skipped / 0 failed;共享美术画布组件 1385 passed;typecheck / `cargo check --locked --all-targets` / `check:encoding` / `git diff --check` 全绿。跨端契约、SpacetimeDB schema、manifest 结构与布局 sidecar schema 均未改动。
- 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`(§3.2 保持,§5.3 补实现口径与降级声明,§7.8 新增替换验收)、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`(新增同章节)、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(§7.3 第 1 条已按本次口径改写)、Issue #309。
## 2026-09-11 AGC 资源替换改成「直接替换」:只改该版本的绑定,不建新版本
- 背景:同日上午按 PRD §3.2 / §5.3 落地的「版本级替换(改绑定 + 追加下一迭代版本)」,当天下午被用户改口径:「**替换这块先做成直接替换**」(同日 DDL:替换先做,运行画面点选、切图集像素规整先不做)。直接替换 = 改 manifest 里该版本的绑定指向另一个已登记资源,**不追加新版本**,也就是 #309 原先那条「更换资源 = 改 manifest 绑定」的轻形态。
- 决策(实现):删除版本级路径(按「四不写」:不建 `replace-{revision}` 版本、不写 `parentVersionId`、不写 `createdReason=resource-replacement`、不留兼容分支)。直接替换走**新增的第二条、更窄的版本放行通道** `mutate_manifest_allowing_version_binding_rewrites`:不增不删(版本数量必须相等)、不重排(版本 ID 序列逐项相同)、只有显式放行列出的版本允许 `resourceBindings` 不同、其余字段(`versionId / parentVersionId / projectRevision / createdReason / createdAt / editPrompt`)逐字段相等、未放行版本整条相等、放行集合取自**写入前** manifest、与删除放行集合**互斥**。既有 `validate_version_records_are_append_only` 与既有删除放行通道的语义一行未改;唯一结构性改动是把写盘公共体抽成带校验器参数的内部函数(`write_manifest_locked` 变成一行委托),装盘 / 回读 / 原子替换仍只有一份实现。
- 恒等绑定口径的硬约束(实测得出,两个形态共用):每个版本的 `resourceBindings` 是「该版本使用的素材集合」而不是槽位表,所以绑定改写 = **源素材从集合里消失 + 保证替换素材在集合里**;不能把源素材那条槽位改写成替换素材(替换素材若早已登记就会撞「资源槽位重复」)。
- 准入与提示:硬门禁只有 `categoryEqual` 与 `subtypeEqual`(拒绝并说明哪一项不等);`sizeSpecEqual` **降级为提示**(「格式与源素材不同」)不再拒绝。理由:它的完整判据今天不存在(manifest 资产表没有 `width / height / durationMs`,现役写入侧几乎全写 `imageSequenceFrames: None`,实际只等于"媒体格式相等"),硬拦会误拒 `png ↔ webp` 这类直接替换里最常见的需求。要变成硬判据须先给 manifest asset 增尺寸字段并在写入侧回填(跨端契约变更)。
- 存储与留痕:改绑定属于 `versions` 变化,因此写入后**推进一次 project revision**(跨面快照门禁要求 revision 前进),并追加一条 `asset.version_binding.replace` 审计(复用既有 `append_agent_db_record`,字段 `versionId / sourceResourceId / replacementResourceId / projectRevision`)。审计写失败会报错但不回滚,与 `asset.register` 同口径。
- **代价(用户已确认接受,记账备查):直接替换没有可回溯的替换历史。** 上一轮版本级形态里"父版本绑定"就是那份历史;现在替换前的身份只存在于 ① 那条审计记录 ② manifest 的 `.previous` 恢复副本(只保留上一次写入)。需要"某个版本历史上被换过几次、换成过什么"时必须另立切片(PRD §3.2 / §5.3 保留为未来合同正是为此)。
- 文档口径:PRD §3.2 的两条与 §5.3 的 `ProjectVersionResourceReplacement` / "三项兼容性必须同时为 true 才能创建下一版本"**保留为未来合同**并就地标注「当前实现为直接替换(2026-09-11),版本级替换暂缓」;PRD 的**存储边界**("只允许追加…有且只有一个例外")改写成当前状态——现在是**两个例外**(删除素材时连带删版本 / 显式放行版本的绑定改写),因为它是"现在写入校验行为"的描述,留未来式会误导。
- 影响范围:`src-tauri/src/project/manifest.rs`、`src-tauri/src/project/manifest/version_binding_rewrite_tests.rs`(新增)、`src-tauri/src/project/version_resource_replacement.rs`、`src-tauri/src/tests/version_resource_replacement.rs`、`src/features/resource-canvas/resourceVersionReplacement{Model,Transport}.ts`、`view/project-development/index.tsx`、`src/components/image-editor/ImageCanvasProjectAssetPickerDialog.tsx`、`apps/ai-game-creator-shell/src/styles.css`。
- 验证方式:放行通道定向 7 条 + 替换定向 8 条 + `shared-contracts` 20 条;**变异验证四条**(去掉"未放行版本整条相等"→两周转红;放行集合改成整个版本数组→"未放行版本"转红;去掉长度检查→"不增不删"转红;准入删掉 category→硬门禁与候选两条转红,均已实测并还原)。前端 13 条(模型 9 + 真链路 4)。门禁:AGC 全量 1231 passed / 4 skipped / 0 failed、共享美术画布组件 1385 passed、`ai-game-creator-shell:typecheck`(含 check-config)、`cargo check --locked --all-targets`、`check:encoding`、`git diff --check` 全绿。
- 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`(§3.2 / §5.3 / §5.4 存储边界 / §7.8 验收)、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`(同章节)、`docs/technical/【技术方案】AGC资源派生与非破坏性编辑合同-2026-09-09.md`、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`、Issue #309。