AGC 资源画布与替换改造 V3.0 #316
Reference in New Issue
Block a user
Delete Branch "feat/agc-canvas-resource-workbench-v3"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
AGC 资源工作台 V3.0
一、交付范围与完成情况
本次改动要点
versions[].resourceBindings即"该版本用了哪些资源"的权威记录,slotId恒为asset:{assetId};运行期资源观察方案(代码注入 / Phaser wrapper / Vite 虚拟模块 / 脚手架哈希校验 /.agent使用侧车)整体撤回,project/asset_usage.rs与其 16 条测试按四不写删除。delete_local_project_asset新增deleteReferencedVersions,三分支语义(无引用直接删 / 有引用且不勾选只删素材并保留悬空绑定 / 勾选则连带删除引用它的版本)在同一次 manifest 写入内原子完成;新增只读命令read_local_project_asset_read_references(列出被哪些版本使用)驱动二次确认弹窗。版本数组"只允许追加"的唯一例外由mutate_manifest_at_allowing_version_removals承担,放行集合按写入前 manifest 求值、被放行版本不得出现在追加段。ImageCanvasSelectedLayerToolbarView,支持 Shift/Ctrl/Meta 多选);快速编辑走浮层面板,先normalize_local_project_raster_resource正规化再derive_local_project_resource(editKind='image-reference')产出新素材;删除可编辑「资源详情」面板与「编辑资源」全屏路由——口径澄清:只读「信息」浮层已回归(工具条「信息」动作,与运行页签共用一份只读字段),取消的仍只是可编辑详情面板与全屏编辑路由;分类与标签、重命名入口搬到工具条;资源卡的@引用入口也挪进工具条(卡片右上角圆钮退役);「当前使用」的资源卡改为真发光;新增撤销重做(只作用于卡片布局,不回滚素材)、框选、独立资源面板(预览 / 上传 / 下载 / 多选)。isResourceUsedByCurrentVersion;未动共享 union 与supportedActions);后端新增read_local_project_version_resource_replacement_candidates与replace_local_project_version_resource两项命令。落盘 = 追加下一迭代版本(parentVersionId+createdReason: 'resource-replacement'),既有版本记录一个字节不改;三项兼容性(categoryEqual/subtypeEqual/sizeSpecEqual)由后端权威计算,前端只呈现不重算,候选弹窗列全部候选、不兼容项渲染但禁用并给出不等维度。不新增字段:替换前后身份按parentVersionId+createdReason推导(父−子 = {源素材},子−父 = {替换素材})。如实声明降级:sizeSpecEqual在真实数据上退化为「媒体格式相等」(manifest 资产表没有width / height / durationMs,写入侧几乎全部写imageSequenceFrames: None),因此png ↔ webp会被拒——刻意接受的最小实现。成功后切到新版本并重读 manifest,不重载预览(守 PRD §3.2)。落在分支feat/agc-resource-replace(4 提交 / 18 文件 / +2771 −37,HEADb1377572d),待并入本 PR。@面板两个页签(当前版本素材 / 全部画布素材)各自独立筛选与搜索;改名后引用芯片与候选列表刷新;跨会话草稿恢复后光标落到文本末尾。WorkspaceLauncherShell,运行模块右上角GameRunVersionPicker(无版本不渲染);切换版本时资源卡"当前使用"高亮与@面板同步,并重载当前预览。polish_local_project_prompt(入参有界 4000/1000 字符、输出 2048 token),润色回填 / 恢复原文 / 失败保留原文 / 可重复润色;发送前提醒为独立 portal 弹窗(AI 润色 / 使用原文提交 / 关闭 / 不再提醒),偏好存本机。计费不自建:泥点扣费仍走 server-rs 的/api/llm/*路由,本次 server-rs 零改动。packages/shared共享层并与画布共用同一组件、各自独立状态;提交时把参考图素材 ID 作为快照落进既有generationInputs.references,零后端改动。agc_list_registered_assets投影暴露category/tags(不在投影层重新派生,避免覆盖用户显式分类);删除normalize_local_project_raster_resource的过渡 allowlist 条目(解除条件已由 C3 快速编辑满足)。本批新增可交付项(均为已提交事实)
feat/agc-resource-replace)。@引用挪进工具条:删除资源卡右上角的 @ 引用圆钮(连同 44×44 热区伪元素与全部样式一起退役),选中资源工具条新增「@引用」动作,出站负载与来源逐字未变(source: 'resource-card');键盘通路(聚焦后回车)与点击通路负载逐字相同、每次激活只派发一次。aria-pressed反映开合),与运行页签「信息展示」共用ResourceInfoFieldsView、字段清单只在resolveResourceInfoFieldRows单点定义;字段为名称 / 路径 / 类型 + 只读分类与标签,刻意不含来源任务、资产 ID 等 manifest 内部标识。.game-resource-card.is-current-version从描边改为真发光(外向扩散层3px/32%+20px/34%)并新增呼吸关键帧(prefers-reduced-motion下关动画但保留可见档位);顺带修掉两处既有缺陷——「点击版本卡后高亮绑定资源」这条规则因卡片本体border: 0从未生效,以及当前版本卡被选中 / hover 同权重规则盖住看不出选中。ResourcePromptPolishSlot,生成素材弹窗与快速编辑浮层共用(取数走 AGC 侧usePromptPolish,与聊天框同款失败态:失败保留原文、可重试);请求身份绑定铸造时的那句提示词(resolveResourceEditRequestIdentity,request_fingerprint含 prompt,提示词变了就重铸operationId/ 幂等键);超上限先按类型截断(背景音乐 140 / 音效 1900 / 视频与角色动画 4000 / 其余 32000)再回填并给出「已按长度上限截断」,不静默丢内容。IntersectionObserver之外新增sweepVisiblePreviews()可见性兜底扫描,按与 observer 对齐的视口几何判据(含 160px 余量)独立复核登记表,只补idle(loading/queued/loaded/failed一律不碰),仍走同一条队列与同一套去重 / 优先级 / 全局 3 槽 /48项64 MiB预算,不退化成全量预读;收口了2a157ea6f引入的回归(observer 可能永不建出,导致整栏 51 项只剩占位图标)。schemaVersion=game-creator-conversation.v1、无type、role 落在写入器自己的角色集合内、content 为非空字符串的行;tool旧行已识别但不进 Codex 上下文),其余形状继续失败关闭;写侧不再往共用的project.jsonl投影旧格式行,两条对话链互读对方的行、混合历史不再双向失败关闭;历史格式类失败给出专门提示并按不可重试处理(打开 / 读取类 IO 失败仍可重试)。docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md(新增 257 行)。二、门禁实测
npm run ai-game-creator-shell:typecheck(含check-config、skill-pack)npm run test -- apps/ai-game-creator-shell/testsnpm run test -- src/components/image-editorcargo check --all-targetscargo test --test-threads=1asset_export5、asset_rename9、asset_delete7、bridge_registered_resource2 —— 全 0 failednpm run check:encodinggit diff --check既有环境失败(非本次引入,已用纯净基线证明):
cargo test并行跑project::约 10 条环境依赖用例失败;单线程纯净基线同样有 4 条(npm 构建 2 + 本地 preview 2)。仓库正式门禁口径是--test-threads=1。三、明确未做(如实列出,未留任何半成品入口)
RESOURCE_CANVAS_GENERATION_OPTIONS与 Rustresolve_resource_edit_source的 create 分支一一对应)。图片无源生成仍被后端硬拦(「当前资源类型不支持无源生成」),所以入口不提供图片选项——宁可少一个入口,也不给一个提交必然失败、或拿图片参数糊弄用户的按钮。工具条上未接通的 7 个动作(重绘 / 裁剪扩图 / 去背景 / 像素完美 / 切图集 / 提取 UI 素材 / 角色动画 / 下载已改为显式保存)按 opt-in 机制不渲染按钮。四、已知边界与后续项
ImageCanvasSelectedLayerToolbarView→ImageCanvasGenerationModel→(值导入ApiClientError)→src/services/apiClient→hostBridge的跨端耦合会把主仓 service 层编进 AGC bundle。已核实该链在 Tauri 中无导入即执行副作用,并在 AGCvite-env.d.ts补了跨端全局声明;彻底解耦的正解是把canOpenRedrawPanel/isQuickEditSupportedLayer两个谓词下沉到packages/image-canvas-core。assets/<name>的引用(未做引用扫描与自动替换),后果由用户自负。cargo fmt --check对未改动文件也存在既有格式漂移,未在本次顺手全仓格式化。suzmii referenced this pull request2026-09-09 22:12:29 +08:00
- 新增 game_iteration_resource_bindings:优先使用使用清单里的 slotId 到 resourceId 绑定,再为未被槽位覆盖的资源补 asset:{id} 恒等绑定 - 恒等绑定保留是有意的:历史版本与既有校验语义不受影响,同一资源被多个槽位使用时只取首个槽位,不产生冲突语义 - ensure_initial_game_iteration_version_at 与 append_agent_game_iteration_version_at 共用该构造函数 - 补测试:无使用清单时保持恒等绑定;登记运行槽位后槽位绑定优先且未覆盖资源仍有兜底- appSurface/harness 补齐三处 jsdom 缺口:ClipboardEvent、DragEvent、Range.prototype.getBoundingClientRect;否则 Lexical 的粘贴通路直接抛 ReferenceError / TypeError - appSurface/harness 的 submitChat 改为 async:全选并让编辑器吸收选区、清空、走产品真实 paste 通路写入,再让出一帧让 React 追平 draft,最后点发送 - 新增 composerText / composerValue / composerDisabled / setComposerText 助手,按原生控件与 Lexical contenteditable 两种 DOM 口径读写输入区 - 15 个测试文件中 116 处 toHaveProperty('value', …) 断言等义改写为 await composerText() / await composerValue();1 处 placeholder 断言改查输入区占位文案;2 处 disabled 断言改用 composerDisabled(同时覆盖 data-disabled 与 contenteditable=false);9 处 fireEvent.change 写入改用 setComposerText - 328 处 submitChat 调用补 await,9 个非 async 用例补 async - Godot 输入区键盘语义用例按 Lexical 实际行为改写:Shift+Enter 与组合态 Enter 由编辑器消化并插入换行、不发送、草稿保留 - 未改动 src 下任何产品代码,未放宽或删除断言- 整体删除运行期资源使用侧车模块 project/asset_usage.rs 与 tests/asset_usage.rs(含 16 条测试),并清理 project.rs、tests/mod.rs 里的 mod / use - game_iteration_resource_bindings 收敛为纯恒等映射 slotId=asset:{assetId}、resourceId=assetId,不再读取侧车、不再做槽位优先 - assets.rs 删除 GAME_ITERATION_IDENTITY_BINDING_PREFIX、asset_is_bound_to_runtime_slot 与 remove_asset_usage_references,删除不再判定运行槽位、不再清理侧车引用 - manifest 写入层把“版本数组只允许追加”校验改为显式接收放行删除的版本 ID:新增 mutate_manifest_at_allowing_version_removals,其余写入路径继续传空放行集合 - delete_manifest_asset_at 改为三分支语义:无任何版本引用直接删登记、被引用且未确认时只删登记并原样保留悬空绑定、被引用且确认时素材与相关版本在同一次 manifest 写入里一起删除 - DeleteLocalProjectAssetInput 增加 deleteReferencedVersions 入参,commands.rs 与 main.rs 同步 - 新增只读命令 read_local_project_asset_references,列出引用了该素材的版本标识、revision 与创建时间 - 新增 tests/asset_delete.rs,覆盖恒等绑定、无引用删除、未确认保留悬空绑定、确认连带删除、引用投影、只允许追加校验仍拒绝其它路径与 CAS- agent-swarm-test-chat.mjs:secureWindowsPrivateRuntimePath 增加可选第三参 secureWindowsPath,传入时直接调用它,不传时完整保留原有的 await import + secureWindowsGameCreatorPathForCurrentUser 调用。 - agent-swarm-test-chat.mjs:copyPrivateRuntimeConfigEntry 增加可选第五参 secureWindowsPath,并透传给上面的加固函数。 - agent-swarm-test-chat.mjs:prepareSwarmTestRuntimeConfig 增加可选第三参 { secureWindowsPath = null } = {},透传给运行配置目录加固与两次配置文件复制。 - 生产路径行为逐字不变:默认值为 null 时 if 分支不进入,执行的原语句与改动前完全一致;唯一生产调用点仍按单参调用(prepareSwarmTestRuntimeConfig(sourceConfigDir))。 - 加这个接缝的原因:Windows 上每次私有路径加固都要拉起一个真实 powershell.exe(本机实测约 0.6 秒 / 次),一次 prepareSwarmTestRuntimeConfig 在双配置文件场景最多加固 3 次,只校验隔离与清理语义的用例不该为此耗掉 vitest 的 5000ms 默认预算。- agentSwarmTestEntry.test.ts:新增模块级 skippedWindowsAclOptions,仅 win32 下为 { secureWindowsPath: async () => {} },其它平台为空对象。 - agentSwarmTestEntry.test.ts:atomically replaces a private AppData config file 的两次 writeGameCreatorConfigAtomically 传入该桩,3663ms → 24ms,不再只剩 27% 超时余量。 - agentSwarmTestEntry.test.ts:moves the default LLM to primary config and lets later GUI saves win 原本的局部 persistenceOptions 收敛为同一份模块级常量,去掉重复声明。 - agentSwarmTestEntry.test.ts:isolated Swarm runtime config 的三处 prepareSwarmTestRuntimeConfig 传入该桩,privately copies 1948ms → 84ms,refuses to delete 1331ms → 79ms。 - 断言零改动:这几条用例在 win32 下本就不校验 ACL(mode / dev / ino 断言全部在 process.platform !== 'win32' 分支内),非 win32 下该常量为空对象、行为逐字不变。 - 实测:该文件 13.59s → 1.35s,文件内最慢用例从 3663ms 降到 84ms。- 资源卡预览不再计算 SHA-256:`AgentRuntimeInspectionImage.sha256` 改为 `Option<String>`,读取函数新增 `include_sha256` 开关,门禁、分块读取、漂移与重开身份复核、签名与尺寸校验仍只有一份实现,只在末尾按调用方决定是否算摘要。预览路径传 `false`(它的 `LocalProjectImagePreview` 从来不消费摘要),Agent `image.inspect` 路径继续传 `true`。 - 取摘要改为 `sha256_digest() -> Result`,缺摘要失败关闭;`project_gates.rs` 提前算成 `image_sha256` 再进判定闭包,避免退化成「空摘要 == 记录里的空摘要」而静默通过视觉检查复核;`runtime_tools/media.rs` 改用仓库既有的 `match { Err => return failed observation }` 风格,不引入新的 panic 面。 - 新增 `read_manifest_cached_for_preview`,只接到 `read_local_project_image_preview_at`、`read_local_project_text_preview_at`、`read_local_project_media_preview_at` 三个预览命令,消掉一次画布装载中「每张卡片各读并解析一遍同一份 manifest」的重复开销。`read_manifest` 本体、`read_manifest_for_project` 和写入路径的安装后回读一字未动。 - 缓存命中判据每次重新获取当前文件身份:路径复核 + 非符号链接或 reparse point + 普通文件 + 长度 + mtime + 文件身份(Windows `(volume serial, file index)`,Unix `(dev, ino)`);失败一律不入缓存,错误语义与 `read_manifest` 完全一致。为此放开 `metadata_is_windows_reparse_point` 为 `pub(crate)` 并新增跨平台 `open_file_identity_key`。 - 写入侧硬做失效:`write_manifest_with_lock_hook` 成功写入后调用 `forget_preview_manifest`。只靠长度 + mtime + 身份堵不住同长度原地改写,显式失效后正确性不再依赖时间戳粒度,读取侧身份复核保留为兜底。 - 收益如实记录:真机项目实测 52 次 manifest 读加解析约 10 ms 量级,并省下约 3.15 MiB 重复磁盘读取。这是消除重复工作的正确改动,但不是性能救命项;用户感知的加载耗时大头仍在资源卡图片解码。 - 决策记录补「AGC 资源卡预览维持 data URL 加纯内存 LRU,不引入 Tauri asset 协议」:正式否决该候选路径,写明原始合同原文(不向 WebView 暴露任意本机文件协议或绝对路径)、打通所需配置(`assetProtocol` 加 `protocol-asset` feature 加 scope 只能 `**/*`)、会绕过的全部门禁(`file.read` 权限、项目边界、登记复核、敏感路径、父目录链接、硬链接、读取漂移与重开身份、按魔术字类型校验、尺寸上限、取消语义与全局 3 permit),以及收益与代价不成比例;同时否决磁盘缩略图缓存。 - `pitfalls.md` 补同题排障口径,含判据陷阱:把「每次启动重读」当成本瓶颈是错的,真机 52 张 PNG 分层实测 Rust 加 JS 合计仅约 0.3 s。- 删除资源总览标题栏里那颗副标题的**两个分支**:`${canvasResources.length} 项正式与候选资源` 与空项目时的 `从资源栏目开始整理资源` - 位置:`apps/ai-game-creator-shell/src/view/project-development/index.tsx` 的 `<header className="game-resource-book-main-heading">`(基线364492de3的 5867-5871 行那颗 `<span>`,标题栏自 5863 行起) - 同步删除只服务于它的 `.game-resource-book-main-heading > span` 样式,模块内不再留墓碑规则 - 该文案没有承载 `aria-label` 等语义(区域语义一直在容器的 `aria-label="资源总览"` 上),因此不需要把语义迁移到容器,也没有改成"更短的说明" - 全量计数没有丢:同一次改动后由总览第 0 张「所有资源」卡的计数徽标承载,仍是同一份画布投影 - 卡片内部的说明/装饰文案(「打开 UI 交互」「打开角色与对象」「1 个直接子版本」等)保持不动 - 断言:资源总览标题栏整段 `textContent` 恰为「资源总览」(精确相等,不放宽成 `not.toContain`),并复核总览第 0 张卡与「文档」栏目卡的内部文案仍在 - 既有用例没有任何一条断言过这段副标题文案(全仓检索只命中源文件),因此没有"按文案已删更新"或"按四不写删除"的对象;本用例是补上的正向合同断言- `ensure_editor_image_edit_source_kind_allowed` 的 `is_none_or(...)` 折叠为仓库 rustfmt 口径的换行 - 白名单用例里 `ensure_editor_image_edit_source_kind_allowed(Some("art-spritesheet"), Some("video"))` 断行 - `is_game_creator_resource_editor_generation(Some(&json!({})))` 断言断行 - `let queued_payload: Value = serde_json::from_str(...)` 折行 - 仅 `server-rs/crates/api-server/src/editor_project.rs` 一个文件,无其他文件被 rustfmt 改动- 修正 resolves asset category and tags with legacy and forward compatibility 中 { kind: 'character', category: 'unclassified' } 的期望:由 'unclassified' 改为 'character' - 该断言是 09-09 引入的旧口径,与 2026-09-10 落地的分类自愈规则冲突;按拍板保留自愈规则(它修复真机 57 条 kind:ui 资产落待归类),因此精确更新断言而非放宽为 not.toBe - 在断言处补注释说明这是本批有意行为,并写明已知盲区:用户手动把可明确分类的资产设为待归类时会被覆盖 - 新增用例「自愈规则覆盖落盘 unclassified:这是显式接受的盲区」,覆盖 character / ui / art-spritesheet 三条具体映射 - 新增用例「派生结果本身就是 unclassified 的 kind 不受自愈规则影响,保持落盘值」,覆盖 image / video / code / publication-material 与未知 kind - 新增用例「落盘为明确分类时信任落盘值,不被 kind 派生结果覆盖」,覆盖 image+scene / character+audio / ui+document - 变异验证:临时移除自愈规则后上述 2 条断言失败(expected 'unclassified' to be 'character'),恢复后全部通过- `editor_api_rejection_reason_from_body` 用例里 3 处 `serde_json::json!(...).to_string().as_str()` 实参按 rustfmt 口径断行 - `editor_api_rejection_reason_from_body("<html>502</html>")` 断言折行 - 连带收掉 `remote-terminal-failed` 的 match 分支:该 arm 因上一提交改宽而超行宽,按 rustfmt 折成块 - `resource_editor.rs` 现在通过 `cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check`- styles.css:`.game-resource-card-reference` 视觉尺寸 30×30 → 24×24(0.8×),新加 `.game-resource-card-reference svg` 把内部 `AtSign` 图标同步从 16px 收到 13px,圆角/描边/底色/阴影 token 全部沿用原值。 - styles.css:`bottom` 由 9px 调到 14px,与右侧 34px 播放钮圆心对齐(9 + 34 / 2 = 14 + 24 / 2),避免缩小后两颗圆钮错位。 - styles.css:新增 `.game-resource-card-reference::before` 伪元素撑出 44×44 热区(本仓 window chrome / 素材选择列表同口径的最小可点尺寸),只扩命中范围、不参与布局也不改变绘制;整体向左让 4px,让热区右边缘与播放钮的盒子恰好相邻,不覆盖它。 - 语义未动:仍是「插入素材引用」入口,className、`aria-label={\`引用资源 ${resource.label}\`}`、key 与 `dispatchResourceReferenceInsert` 调用一行未改,本次只改 CSS 声明。 - 测试:新增 CSS 声明级用例,断言按钮与图标确实变小(24 < 30、13 < 16)、热区不小于 44×44、热区右边缘不越过播放钮盒子,并顺带钉住 `index.tsx` 里 @ 入口的 className / aria-label / 插入动作仍在。 - 变异验证:把按钮改回 30px、热区改回 24px、图标改回 16px → 用例红灯;只把热区缩回 24px → 用例同样红灯(均已实测,随后原样还原)。背景:全局 3 槽跨视图共享,而 hook 的 scopeKey 只含 `projectPath + projectId`(不含视图)。用户在栏目页滚一遍会按可见性入队最多 51 个预览 job;点「回到资源总览」时它们仍排在同一个队列里,总览自己那几张「该出图」的卡只能排在后面 —— 这就是「进总览要等图」的真实来源。 - `nextPreviewJob` 改为两级裁决:**理由优先级固定 `play > detail > visible` 不变**,只在**同一理由内部**让当前预取作用域的请求优先。 - ⚠️ 早期写法用「作用域匹配则权值 +3」,会让当前视图的 `visible` 压过上一视图的 `play` —— 直接破坏 PRD §3.3.2 的固定调度优先级。这个错误是被本提交新增的契约用例抓到的,已改为同级裁决,并把该约束写进函数注释。 - 新增 `prefetchScopeKey`:优先取调用方显式传入的值;未传时退化为**被喂进 hook 的资源集合签名**,因此调用方(`index.tsx`)无需改动即可生效。 - 队列任务记录入队时的 `prefetchScopeKey`,供同级裁决使用。 - 新增只读 `previewQueueSnapshot()`:暴露「谁在排队、属于哪个预取作用域、当前活动读取数」。队列病理此前只能靠猜,有了它「当前视图的卡是否真的进了队列」可直接断言。不参与渲染、无副作用。 断言(`tests/useProjectResourceCardPreviews.test.ts`):新增「当前预取作用域越过上一作用域的排队预取」契约用例,三条: 1. 当前作用域的 `visible` 越过上一作用域排队的 `visible`; 2. **但不得越过 PRD 优先级** —— 当前作用域的 `visible` 仍排在 `play` 之后; 3. 同一作用域内仍是 `detail > visible`。 变异验证:把作用域偏好关掉(`&& false`)→ 第 1 条断言立即失败(`expected 'previous' to be 'current'`);恢复后 22/22 通过。断言不是恒真假守卫。 验证:typecheck exit 0;该测试文件 22 passed;check:encoding 4388 文件;`git diff --check` 干净。(AGC 全量与本条无关的并发改动混跑,故此处只报本文件的定向结果;全量对照见后续提交的回报。)- direct_tools_mcp.rs:删除 conversation.record_codex_response 末尾往 project.jsonl 追加 legacy 行的投影,它写的是 {schemaVersion:game-creator-conversation.v1,...} 旧行,会毒化共用该文件的 DirectProject 历史 - 顺带删掉只为那条投影存在的 key/message_id 计算与描述它的注释,不留墓碑 - 该工具的事实来源仍是自己的只读 journal .agent/conversations/codex-responses.jsonl,返回内容与 conversation.list/read 行为不变 - 补断言:record_codex_response 只写 codex-responses.jsonl,project.jsonl 保持为空- 技术方案:把「不迁移旧 {role,content} 行 / 不会对旧格式做迁移或兼容」改成当前状态——读侧白名单兼容 legacy 行(含 tool 行只识别不注入)、写侧 DirectProject 自己的旧行写点已收口 - 技术方案:修正「Rust 是该历史文件的唯一写入方」,写清 project.jsonl 同时被通用对话链(agent_id=None 的项目主对话与 Runtime 公开状态消息)读写,并记录两条链互读兼容的规则 - 技术方案:说明为什么故意不给通用对话写入器加「Direct-owned 就拒写」的硬报错(会把旧行噪声换成任务起不来),以及这类失败为何按不可重试处理 - 技术方案:更新时间改 2026-09-11 - decision-log:新增 2026-09-11 条目,记录格式切换当年「唯一写入方 + 测试手动清理」的前提在存量用户项目上不成立、现改为「读侧白名单兼容 + 写侧统一」,以及为什么不做数据迁移- 新增 `gameCreationAppAssetPersistedCategory`:写回口径,只做「缺失或非法 → 按 kind 派生」兜底,不套用读时自愈;它必然等于 Rust 反序列化后的落盘 `category` - `ResourceClassificationPanel` 的分类初值从 `gameCreationAppAssetCategory`(读显示口径、会把落盘 `unclassified` 自愈成派生值)改为 `gameCreationAppAssetPersistedCategory`,保存时回传落盘原值 - 修掉真机上「同一 `kind:"ui"` 资产同时存在 55 条 `unclassified` 与 2 条 `ui-interaction`」的漂移签名——后者正是自愈值被回写持久化的结果 - 面板注释更正:原注释写「保证『只改标签』不会顺带改动分类」,但那行读的是自愈后的值,这个保证是假的 - `gameCreationAppAssetCategory` 的文档补上「这是读显示口径,回写必须用 `gameCreationAppAssetPersistedCategory`」,避免下次再混用 - 新增面板用例「保存标签不改动落盘 category:自愈值不得被回写」:夹具 `{kind:'ui', category:'unclassified'}`,断言写入命令收到的 `category` 仍是 `unclassified` - 既有面板夹具 `{kind:'character', category:'character'}` 是自愈 no-op,永远测不出漂移,因此保留它作为对照、另加自愈夹具 - 新增契约用例「写回口径保留落盘 unclassified,读显示口径才自愈」,把两个口径的差集钉死- `canonical_game_creation_app_asset_kind`(Rust)与 `canonicalGameCreationAppAssetKind`(TS)改为对 trim 后的小写值查别名表与 canonical 目录,修正「UI 设计资产的现役写入侧写的是大写 `"UI"`」这条真机事实 - 别名表补 `font → document`:字体上传(`ttf / otf / woff / woff2`)登记的 manifest `kind` 就是 `font` - 为什么必须在别名表收口、读时自愈救不回来:`"UI"` 与 `"font"` 的派生结果本身就是 `unclassified`,自愈规则「派生值不是 unclassified 才触发」永不成立;真机 8 条 `kind:"UI"` + `mediaType:"application/json"` + `localPath:"ui/UI 设计 N.json"` 与写入路径逐字段吻合 - 改写 Rust 断言 `game_creation_app_asset_category_for_kind("UI") == Unclassified` → `UiInteraction`,并补 `canonical_...("UI") == "ui-design"` 与 `font == Document`:旧断言把这条 bug 钉成了「期望行为」,新形态才是唯一真源——写入侧真实写出的 kind 必须能落进明确栏目,否则真机资产永远归不了类 - 改写 TS 断言 `gameCreationAppAssetCategoryForKind('UI') == 'unclassified'` → `'ui-interaction'`,理由同上 - `game_creation_app.rs` 里「现役写入侧仍会写出这些非 canonical 值,必须在这里收口」的注释此前只收口了小写 `ui`,与大写写入侧矛盾,现按注释本意收口 - 新增写侧→分类的端到端契约用例:`resource_bridge.rs` 的 `bridge_is_idempotent_and_installs_source_image` 走真实生产函数 → 真实 `register_local_asset_at(..., "UI", ...)` → 断言落盘 `category` 为 `ui-interaction` - 新增 `assetKindCanonicalMapping.test.ts` 的「写侧 kind 字面量 → 分类」用例组:直接解析 UI 编辑器写侧源码第 3 个实参,写点换个新字面量就会红,防下次再漏- publishPreview 记录本轮驱逐条数,驱逐发生后在 previewsRef 落盘之后补一次 sweepVisiblePreviews; - 被驱逐的 identity 状态正好回到 undefined,而 sweepVisiblePreviews 只对 undefined 的卡重新入队,此前只差不这一次调用; - 调用点必须在 previewsRef.current = next 之后,否则扫描看到被驱逐卡仍是 loaded 而不做任何事; - 注释写明重入有界:重新入队走 publishPreview({status:'loading'}),不进 loaded/failed 驱逐分支,递归深度恒为 1; - 只在真的发生驱逐时补扫,普通 loading/loaded 发布不增加扫描开销; - 新增用例:灌满 48 张缓存把仍可见的目标卡挤出(LRU 不看可见性),断言该卡被重新请求读成 loaded。- 两处 .catch(() => {…}) 没有 message 参数,失败原因被整个吞掉:用户只看到「布局保存失败」「布局读取失败」,排障端也拿不到为什么(revision 越界 / IPC 拒绝 / 权限); - 写回失败(update_local_project_resource_canvas_layout 的 .catch)与读盘失败(read_local_project_resource_canvas_layout 的 .catch)都补上 error 参数,并落一行 console.warn; - 新增 describeResourceCanvasLayoutFailure:把 error.message 压成一行、截断 400 字,并复用 errorReporting 的 normalizeDiagnosticText 脱敏(路径 / token / URL / 长 id 打码); - errorReporting 的 normalizeDiagnosticText 改为导出:脱敏口径只留一份,业务文件不再各写一套正则; - 用户可见文案一字不改(LayoutNotice 联合类型与既有四条用例的断言原样保留),原因只进日志;日志由客户端已装的 webview→application.log 桥带进诊断日志; - 新增两条用例:写回失败必须留下原因(并断言可见文案仍是「布局保存失败,已恢复上次布局」)、IPC 拒绝且原因里带本机绝对路径与 token 时必须脱敏成 <path> 且压成一行; - 变异验证:把写回那处改回吞掉原因 → 写回用例红灯;把读盘那处改回吞掉原因 → 脱敏用例红灯(各自只红对应一条,既有 24 条全绿)。- 新增 validate_manifest_schema_version:只接受 GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION,未知版本报「manifest schemaVersion 不受支持:{实际值}(当前支持 {当前值})」 - read_manifest 在解析后立即校验 schemaVersion,与既有的 godotProjectRoot / versions 校验同级;读到未知版本直接拒绝打开,不再被当成已知版本继续使用 - write_manifest_locked 落盘前同样校验,保证本客户端永远不会把未知 schemaVersion 写进项目(该函数是 write_manifest 与 mutate_manifest_at 共用的唯一落盘入口) - 前向兼容取舍:刻意不做「接受未来版本 + 读时就地升级」——当前并不存在 v2 定义,凭空写一个升级只能把未知数据改写成当前版本的形状,正是本次要修掉的「静默接受」;代价是未来发 v2 时旧客户端明确报错要求升级,而不是把项目按旧结构写回 - project/manifest/import_tests.rs 补 3 条断言:当前版本必须被接受且全字段回读相等(正向断言,挡住「无条件拒绝」这种改法)、未知版本读失败且磁盘文件逐字节未变、未知版本写失败且不落盘- update_manifest_asset_classification_at 在 manifest 写成功后追加 recordType=asset.classification.update 的审计,复用既有 append_agent_db_record 惯例(照 assets.rs 的 asset.register / asset.update:manifest 写成功后追加) - 字段:assetId(沿用 assets.rs 的 assetId)、expectedProjectRevision(本次写入实际校验的 CAS 基准 revision,沿用分类输入契约里的同名参数)、previousCategory / previousTags(变更前值)、category / tags(变更后值) - 不记 localPath / kind / mediaType / source:本记录针对的是已有 assetId 的分类变更,这些字段并未改变,且能从 manifest 或既有 asset.register 记录追到,多记一份会在改名后产生互相矛盾的审计 - 不用 projectRevision 这个名字:既有 agent.runtime.action_receipt 里 projectRevision 的语义是「动作完成后的 revision」,本条审计追加在 revision 推进之前,沿用同名会指代不一致;审计是持久化数据,故在新记录类型里另起不冲突的名字,不动既有记录格式约定 - 位置放在推进 revision 之前是刻意的:分类已经真实落盘,审计不能因为紧随其后的 revision 推进失败而缺失,否则「改过但查不到」正是这条缺陷;推进失败仍然照旧报错 - 审计追加失败按既有惯例映射为可见错误「资源分类已写入,但审计记录失败:{error}」,不静默吞掉 - project/manifest/classification_tests.rs 补 3 条断言:成功写入恰好一条且前后值正确(含第二次写入的 previous 必须取第一次的落盘值)、四类被拒写入不产生任何审计、审计写失败时错误可见且不留假审计(同时确认分类本身已落盘、不回滚)- 新增 scripts/lint-staged-rustfmt.mjs:对 server-rs 与 apps/ai-game-creator-shell/src-tauri 两个 workspace 跑 cargo fmt --all --manifest-path <m> -- --check,只查不改 - 为什么需要包装脚本:lint-staged 会把命中的暂存文件路径追加到命令末尾,而 cargo fmt 只按 workspace 粒度格式化、不接受文件参数,直接写成 npm run check:rustfmt 会被多余参数打断;脚本因此忽略 argv,并按 workspace 逐个检查 - package.json 的 lint-staged 配置新增 "*.rs": ["node scripts/lint-staged-rustfmt.mjs"] - 成因:原配置只覆盖 *.{js,mjs,cjs,ts,tsx},Rust 格式在本地完全没有守卫,唯一防线是 CI 的 check:rustfmt,本 PR 已因此红过一次(见 15660a98b) - 已实测:把包装脚本直接跑一遍 exit 0;lint-staged 分派层面确认 "*.rs" 任务会被真的触发(对 6 个 .rs 文件跑通);脚本本身过 eslint 与 prettier --check- index.tsx 选中工具条的引用按钮:title 由「@引用」改为「引用」,可见文案 <span>@引用</span> 改为 <span>引用</span>。图标 AtSign 本身就是 `@`,文案再写一次就渲染成「@ @引用」。 - label={`引用资源 ${selectedResource.label}`} 保持不变:无障碍名仍要能读出「引用资源 <素材名>」,本次只去掉可见文案与 title 里的 `@`。 - 断言(appSurface/project-development.suite.ts):静态形状用例改判 title="引用" / <span>引用</span>,并新增 not.toContain('@引用') 反向守卫;键盘通路用例改判 title === '引用'、可见标签 .genarrative-image-canvas__chrome-button-label 文本为「引用」,并新增 aria-label === '引用资源 hero.png',钉住无障碍名没有被一起去掉。 - 变异验证(提交前已跑):把 index.tsx 的 title 与可见文案一起改回「@引用」→ appSurface 两条用例变红(资源卡的 @ 引用入口只留在选中工具条里 / 工具条里的「引用」用键盘也能插进聊天输入框),还原后复跑 412 passed。 - 门禁:`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts` 412 passed(改动前后同一时刻对照,均为 412 passed)。- 原缺陷:预览 Hook 的预取作用域 key 缺省退化为"被喂进来的资源集合签名",而宿主按设计要求恒传**全量投影**(传全量才不会让总览里其它栏目只剩栏目标题栏),换栏目/换视图一字不改 ⇒ key 不变 ⇒ 取消上一视图排队预取的 effect 永不执行,"同级裁决"一并退化为 no-op。已有用例用"改 resources"模拟切视图,测的是生产不存在的调用方形状。 - resourceCanvasController.ts 新增 createResourceCanvasPrefetchScopeKey({ view, target, mode }):由视图身份(main / child:<栏目>)加排序模式算出标识。 - index.tsx 在资源画布处显式算出该标识并传给预览 Hook(栏目取 resourceBookState.view / resourceBookState.category,排序取 sortMode)。 - useProjectResourceCardPreviews.ts 把 prefetchScopeKey 由可选改为**必填**并删掉那段兜底:它能"看起来还在工作",正是它掩盖了漏接线;现在漏传是类型错误而不是静默少取消一次。 - 断言:resourceCanvasController.test.ts 新增标识派生两组用例(换栏目/换视图/换排序模式必须变,同一视图必须稳定);useProjectResourceCardPreviews.test.ts 改写两条既有切视图用例为生产形状(resources 恒为全量、只改视图标识与 eagerResources),并在用例内前置断言"换栏目必须换标识";其余既有用例经用例侧包装补齐标识,形状与生产一致,断言一条未放宽。 - 变异验证(提交前已跑):把 createResourceCanvasPrefetchScopeKey 的栏目分量去掉(只留 view + mode)→ 3 条用例变红(标识派生 / 取消上一视图排队预取 / 取消后重新可见再入队),还原后复跑 34 passed。 - 门禁:`npx vitest run` 上述两文件 34 passed;typecheck 见随后整轮。- 删掉 publishPreview 里"驱逐后立刻补一次可见性扫描"(原 useProjectResourceCardPreviews.ts 的 `if (evictedCount > 0) sweepVisiblePreviews()`)。它原先的注释宣称"递归深度恒为 1、不自激",该不变量是假的,判据有三条:(1) 触发条件是"本轮驱逐了任意条目",而第一轮只淘汰视口外条目,那种情况下根本没有可见卡丢状态,补扫却照样把空闲且可见的卡重新入队;(2) 真正驱逐到可见卡时(全表都在视口内且仍超预算,第二轮全表 LRU 回退)补扫把刚被驱逐的卡重新入队 → 读回来又超预算 → 再驱逐 → 再补扫,每个周期跨一次异步读取,是不收敛的回路;(3) 那种回读换不来稳定结果,读回的卡立刻被下一轮 LRU 淘汰(第二轮回退是全表 LRU,刚读回的卡恰好最新),只在可见集合里轮转。因此正确反应是接受这次有界淘汰(由「全可见且超预算时仍必须淘汰」钉住),而不是反复重读;代码注释按这三条重写,不留误导后人的陈述。 - 顺带删掉只为那段补扫存在的 `evictedCount`(否则是死变量)。 - 断言:新增 useProjectResourceCardPreviews.test.ts「settles instead of re-reading evicted visible cards forever」——全部登记且都在视口内(第一轮无从下手),断言读取次数在观察窗口内不再增长、且缓存稳定在上限。 - 变异验证(提交前已跑):把补扫加回去 → 该用例**不是断言变红而是 worker 被撑爆**(`ERR_WORKER_OUT_OF_MEMORY`,约 74 秒后终止),即回路真的不收敛;还原后复跑 32 passed,该用例 2.1s 通过。 - 超时预算改为"标注并给出实测依据",不再用放宽时间门吸收负载代价:`releases Blob URLs while continuously browsing beyond the cache limit` 的 `}, 30000)` 保留,并在用例前写明理由 —— 它逐个 waitFor 等 79 张卡串行读完,轮数由条目上限 48→72 机械地从 55 涨到 79(+44%),实测 6120 ms,已超 vitest 默认 5s,属结构性必需。同一提交里的 `waitFor({ timeout: 30000 })`(由 20000 顺手放大)按实测**回到 20000** 并写明依据(该用例实测 124 ms,约 160 倍余量)。 - 门禁:useProjectResourceCardPreviews 32 passed。cfd89ba57顺手加宽的一处测试超时,并写明另一处保留的理由 c3ccae75af- 新增 resourceBookShowsAllCategories:把"子画布里的 all"这一态判定收在 resourceBookModel 一处 - resourceBookCategoryCardPresentation:展开态下真实栏目返回 child(卡片仍由各栏目组承载,每张资源只有一个宿主)、all 自己返回 null(只出标题栏与全量计数) - 新增纯函数 buildResourceBookAllLayout:按栏目顺序纵向排"分带",用各栏目可见资源的 extent 定带宽高;卡片世界坐标 = 栏目内局部坐标 + 带原点 - 新增 resourceBookAllTitlebarRects:分带 → 栏目标题栏矩形,让展开态复用总览态"按矩形钉标题栏"的同一条渲染路径(ResourceBookScene 不需要知道"展开态"这个概念) - 新增 resourceBookAllBandLocalPoint:展开态世界坐标 → 栏目内局部坐标,这是落盘前唯一的换算(减带原点) - buildResourceBookScenePlan:展开态下 all 只出标题栏(钉在视口 + 全量计数),真实栏目按带偏移铺满 child 卡;真实栏目这一支仍按 presentation === child 取卡,"能出卡的判定"只有一处 - buildResourceBookScenePlan:返回总览的淡出改为按"刚离开的那段子画布"判定(exitingCategory 传 all 时覆盖所有栏目),并用带偏移位置淡出,否则返回动画会跳位置 - buildResourceBookScenePlan:「所有资源」组不再接 child / exiting 卡,避免同一张卡在两组里各挂一份(DOM 查询与 FLIP 键撞车) - index.tsx:删掉独立网格页组件、其 host 块与 resourceBookAllResourcesSections memo,展开态整体交给画本场景 - index.tsx:新增内存态 resourceBookAllViewport(不落盘、不进按真实栏目键的 resourceCanvasViewports),滚轮 / 缩放 / 适应 / 平移 / 框选都作用到它 - index.tsx:分带几何按"项目 + 排序 + 可见资源集"冻结(签名 + ref 短路;签名不含 state.token——返回总览时 token 也会变,含它会在淡出期间重算),拖动不重算带高,避免"拖上面一带让后面所有带整体位移" - index.tsx:拖动放开——展开态下每张卡都能拖,section 取 resource.category(栏目页仍只拖当前分页栏目) - index.tsx:提交前用 resourceBookAllBandLocalPoint 把世界坐标减回带原点,写回的仍是同一 mode 同一 sidecar 的同一 section(.agent/workbench/resource-layouts/{dependency,type}.json),没有第二份空间,不改契约与 schema - index.tsx:renderResourceBookCard 去掉 options.draggable:展开态已可拖,这个"只读"开关没有调用方了 - 测试:resourceBookModel.test 钉住展开态 presentation 与"栏目页仍是原判据";resourceBookLayout.test 退役旧分组函数用例,新增分带几何 / 标题栏矩形 / 减带原点换算 / 展开态计划(含"每张资源只有一个宿主"与按带位置淡出)断言 - 测试:appSurface 两处「所有资源」判据从旧网格宿主改为画本场景(.game-resource-book-scene-card[data-resource-book-category] 与 .is-active[data-resource-book-category="all"]),并断言旧网格容器 / 宿主一个都不再存在- 现象:dd7cf401a 给 lint-staged 加 *.rs 键,但本地从未跑过 check:git-hooks(它是 npm run lint 链里的一环),而 scripts/git-hooks.test.mjs 用 assert.deepEqual 钉住 lint-staged 的整份配置形状 → CI 以 deepStrictEqual 失败;本批更早还因 check:rustfmt 红过一次(15660a98b 修 8 处格式偏差),成因是 pre-commit 的 lint-staged 当时只覆盖 *.{js,mjs,cjs,ts,tsx},Rust 格式本地无守卫 - 原因:npm run lint 是一条 && 长链(encoding / npm-workspaces / git-hooks / rustfmt / spacetime-schema / production-ops / preview-deployer / maintenance-page / eslint / typecheck),中间任一步失败则其后步骤根本不执行,「跑到第 N 步就以为本地绿了」;更隐蔽的是本地若因环境原因在中间断掉(如 Windows 上 check:git-hooks 第 2 个用例的 EBUSY),后面那些本来能通过的步骤也从未被验证 - 处理:push 前按完整链路跑;&& 链在某一步失败时,必须把失败步之后的每一步单独再跑一遍(10 步 + git diff --check)并逐条记 exit code(取 exit code 不要接管道);改动碰到 package.json / .husky/ / scripts/ 下的门禁资产时,check:git-hooks 是必跑项 - 验证:本次 check:git-hooks 因本机 Windows EBUSY 中断后,补跑其后各步全部 exit 0(spacetime-schema / production-ops / preview-deployer / maintenance-page / lint:eslint / typecheck / encoding),只有 check:rustfmt 红且红在别人在途的 .rs(assets.rs:519 落在其未提交 hunk +470,62、commands.rs:3079 落在其未提交 hunk +3068,58);变异验证:把 *.rs 从 package.json 摘掉 → 第 1 个用例以同样的 deepStrictEqual operator 变红,还原(SHA256 一致)后回 ok - 纯文档追加:文件末尾 +8 行、无删改;本条只提交 docs/project-memory/shared-memory/pitfalls.md 一个路径- docs/project-memory/shared-memory/pitfalls.md:在「只跑门禁子集 → 同一天两次 CI 红」条目的「原因」里补第三类成因 - 口径差异:CI 跑 git diff --check "${base_ref}"..."${head_ref}"(提交范围),本地裸跑 git diff --check(工作树 vs 索引)在干净树上恒为空,所以本地绿不能证明 CI 这一跳会过 - 实例:pitfalls.md 末尾多出的空行(f16aa440b 引入),因前两次推送分别停在 check:git-hooks 与 check:rustfmt 而一直未被这一跳检查面对review吧(
- shared-contracts 新增 game_creation_app_asset_category_with_read_time_healing:把 PRD §5.3「分类取值优先级」的读时自愈口径落到 Rust(落盘 unclassified 且 kind 能明确分类时采用派生值),与 packages/shared 的 gameCreationAppAssetCategory 逐分支一致,并补定向用例锁定该窗口 - 新增 project/version_resource_replacement.rs:三项兼容性判据(categoryEqual 用读时自愈口径、subtypeEqual 用 canonical kind、sizeSpecEqual 用规范化媒体格式 + 已知帧尺寸与时长事实) - sizeSpecEqual 在代码注释里明确标注降级:manifest 资产表今天没有 width/height/durationMs 字段,且现役写入侧几乎全部写 imageSequenceFrames=None,所以该项实际退化为「媒体格式相等」;要支持跨图片格式替换必须先给 manifest asset 加尺寸字段(跨端契约变更) - 新增 replace_local_project_version_resource_at:持项目写锁并按 expectedProjectId + expectedProjectRevision 做 CAS,一次写入里追加 createdReason=resource-replacement 的子版本(parentVersionId 指向源版本),子版本绑定 = 源版本绑定去掉源素材并保证替换素材在集合里;全程不调用 mutate_manifest_at_allowing_version_removals,既有版本记录一个字节不改 - 替换前后资源身份按 PRD §5.4 版本字段表口径用推导记录(父−子 = {源素材}、子−父 = {替换素材}),并注明「替换素材在源版本创建时就已登记」时子−父为空集的已知限制 - 新增 read_local_project_version_replacement_candidates_at:只读返回候选与后端权威兼容性结论,候选渲染但禁用并给出原因,不在前端重算判据 - commands.rs 新增两个命令包装(读用 asset.list、写用 asset.register),main.rs 注册进 generate_handler - 新增 8 条定向用例:只追加与父子/修订关系、两条绑定路径(1:1 交换与替换素材已绑定)、三项兼容性逐项拒绝且零副作用、CAS、四条拒绝路径、候选读取顺序与原因、读时自愈口径锁定 - 中间状态声明:本提交落地时前端调用方尚未提交,npm run ai-game-creator-shell:typecheck 会因 check-config.mjs 要求「每个 Tauri 命令都有 App invoke 调用方」而失败;这是刻意保留的中间状态,不得把这两个命令加进 native-only 白名单换绿- 口径变更:资源替换从「版本级替换(改绑定 + 追加下一迭代版本)」改成「直接替换(只改 manifest 里该版本的绑定,不建新版本)」,原因是用户 2026-09-11 的裁决「替换这块先做成直接替换」(同日 DDL) - 按「四不写」删除版本级路径:不再创建 replace-{revision} 版本、不再写 parentVersionId / createdReason=resource-replacement、不再有「替换前后身份可推」的父子版本对;相关注释与常量一并删除,不留兼容分支 - 改绑定改走 manifest.rs 的窄放行通道 mutate_manifest_allowing_version_binding_rewrites(上一提交新增),放行集合固定为 [sourceVersionId]:不增删版本、不重排、只改这一个版本的 resourceBindings - 绑定改写语义不变(恒等绑定口径):源素材从该版本的绑定集合里消失 + 保证替换素材在集合里;替换素材是版本创建后才登记时按源素材原位置插回,早已登记时只摘除(避免「资源槽位重复」) - 准入校验按用户裁决调整:categoryEqual 与 subtypeEqual 仍是硬门禁(拒绝并说明哪一项不等),sizeSpecEqual 降级为提示不再拒绝 —— 它的完整判据今天不存在(manifest 无 width/height/durationMs,实际只等于媒体格式相等),硬拦会误拒 png↔webp 这类直接替换里最常见的需求;提示文案为「格式与源素材不同」,同时出现在候选与写入结果里 - 写入成功后推进一次项目 revision(改绑定属于 versions 变化,跨面快照门禁要求 revision 前进),并追加一条 asset.version_binding.replace 审计(复用既有 append_agent_db_record,字段 versionId / sourceResourceId / replacementResourceId / projectRevision);审计写失败报错但不回滚,与 asset.register 同口径 - 返回结构去掉 parentVersionId:改为 { versionId, committedProjectRevision, replacement: { versionId, sourceResourceId, replacementResourceId, compatibility, warning } } - 定向用例按新形态重写并补齐 8 条:只改绑定不产生新版本且除绑定外逐字段不变、替换素材早已绑定时只摘除、分类/类型硬门禁拒绝且零副作用、跨格式只提示仍放行、已知帧尺寸与时长事实只提示、读时自愈口径、四条拒绝路径、CAS、候选读取顺序与原因、审计记录留痕 - 已知代价(写进模块文档):直接替换没有可回溯的替换历史,替换前身份只剩这条审计与 manifest 的 .previous 副本 - 验证:定向 8 passed / 0 failed;cargo check --locked --all-targets exit 0- projectResourceLiveUpdateModel 新增 manifestCasFingerprint,按 { assets, versions } 判定同版本号冲突 - 同 revision 且判据面一致时,仅非 CAS 账目状态(preview 运行地址/端口、任务进度、项目名)变化改为 accepted 并让内容落地,revision 不推不退 - 判据面变化仍判 revision-conflict,保留「改了被保护内容却没推 revision」的可观测信号,不吞真实缺陷 - 新增回归用例:同 revision 仅预览状态变化必须被接受;同 revision 资产变化必须仍被拒收 - 变异验证:把判据改回整份 JSON 指纹后,新用例以 expected 'revision-conflict' to be 'accepted' 失败 - 定向验证:projectResourceLiveUpdateModel 17 passed、workspaceLauncherManifestMerge 5 passed、appSurface 413 passed、npm run typecheck exit 0、npm run check:encoding 4399 files passed、git diff --check 干净 - 同步 decision-log 与 pitfalls 记录 CAS 判据面契约与本次取证终于差不多整完了
- packages/shared/src/components/styles.css:528 [bug medium] min-width: max-content 收窄到新的 .platform-segmented-tabs--scroll 修饰类:grid 分支没有溢出容器、轨道是 minmax(0,1fr),同样的下限只会让内容画到相邻格上;grid 侧靠轨道等分与宿主 truncateLabels 收口,选中 PlatformSegmentedTabs.tsx 的 scroll 分支补修修饰类。 - packages/shared/src/components/PlatformResourceFilterBar.test.tsx 同步改成钉「下限只挂在 --scroll」+「grid 分支不带 --scroll/overflow-x-auto」(声明级断言,jsdom 不计算外部样式表,属弱验证,真机判据见提交说明)。 - packages/shared/src/components/PlatformResourceFilterBar.tsx:24 [maintainability low] Props 改为以 tagItems 为判别项的联合类型:给 tagItems 就必须给 onToggleTag,非法状态编译期不可表达;组件内部再兜 disabled={!onToggleTag} 供给不带类型检查的调用方,并补对应运行时用例。 - packages/shared/src/components/PlatformResourceFilterBar.tsx:107 [style low] 标签行补 scrollbar-hide:它是唯一还画滚动条的横向滚动容器,与 styles.css 里 .scrollbar-hide 的注释口径对齐;补类名与声明级用例。 - packages/shared/src/contracts/gameCreationAppAssetTagLibrary.ts:51 [maintainability low] assetIds 排序改为确定性 code-unit sort(),不再依赖运行环境默认 locale(标签那一层仍显式钉 zh-CN),补跨输入顺序可复现的用例。 - packages/shared/src/contracts/gameCreationAppAssetTagLibrary.ts:68 [bug low] assetTagsMatchSelection 两侧对称归一化:selectedTags 也过 normalizeGameCreationAppAssetTags,' 像素风 ' 这类未归一化的选中项不再静默匹配不上。 - packages/shared/src/contracts/gameCreationApp.test.ts [test low] 新增解析 Rust PROJECT_RESOURCE_CANVAS_SECTIONS 与 TS 常量对齐的交叉校验(仿 assetKindCanonicalMapping.test.ts 解析 Rust 常量先例),钉住两侧分区顺序不漂移。 - packages/shared/src/contracts/gameCreationApp.ts 同步「读显示 / 写回」口径注释:未知 category 已在 Rust 读侧失败关闭,「非法值 → 派生」只剩内存对象语义。 - apps/ai-game-creator-shell/src/styles.css:5790 [bug medium] :has() 悬停高亮分类值换成现行 7 个分区(ui-interaction/character/scene/audio/document/unclassified/version),删掉永不渲染的 code/art 死选择器,补齐缺失栏目的高亮。 - apps/ai-game-creator-shell/src/styles.css:6873 [bug medium] hover/focus-within/is-selected 由只写 border-color 改为完整 border: 1px solid #d57b51(基规则 border: 0 下 border-color 无效果,选中/焦点态此前完全不可见),box-sizing: border-box 下不改卡片尺寸。 - apps/ai-game-creator-shell/src/styles.css:10151 [bug medium] 去掉高优先级 :focus-within 规则里的 box-shadow: none:它盖掉了 --platform-input-focus-ring 焦点光环,direct-codex 输入区此前没有任何可见焦点(无障碍回归),现在光环生效。 - apps/ai-game-creator-shell/src/styles.css:10198 [bug low] 润色状态文字与操作排改为同一行分列(grid-row: 2 + grid-column: 1 / 2),不再整行独占或与按钮重叠,状态出现/消失不改变输入框高度。 - apps/ai-game-creator-shell/src/styles.css:10206 [maintainability low] 删除永不命中的 .resource-reference-input-at direct-codex 规则(showTriggerButton={!directCodex} 不渲染该按钮),保留说明指向真正的 .project-supervisor-reference-trigger。 - 附带 prettier 对 .design-agent-reasoning summary 单行规则的换行归一(该文件本来未过 prettier --check)。本轮修复与 PR review 处置汇总(附 Native shell 现状说明)
一、交互与功能修复
undefined is not an object (evaluating 'listeners[eventId].handlerId'):tauri 2.11.3 注入的 unlisten 脚本缺条目判空 +listenpromise 与 cleanup 竞态(StrictMode 下每次挂载都走)。新增统一订阅入口:注销先摘回调再 unlisten,并去重。createPortal到 body 后仍被自家「点外部」判定吃掉mousedown(菜单先卸载、click 落空)。改为把 portal 菜单纳入浮层边界,复用共享 dismiss 口径。scale抵消活在缩放层里),转场控制器按节点是否在 world 内选坐标系。onWheel(且 wheel 委托是 passive,preventDefault无效)。统一浮层判据 + 画布侧一处守卫。二、Code review 处置(三批共 73 条)
deny_unknown_fields(失败关闭)、未知category改 fail-closed、Rust↔TS 分类顺序交叉校验、静态图别名补asset、AGC 三条 CSS 真 bug(:has()用了已退役分类值 / 卡片border:0下状态边框无效 / 焦点光环被盖)、lint-staged按暂存文件筛 workspace。image-sequence被静默丢弃等。三、工程与门禁
Resolve comparison base门禁(要求 PR 头包含当前 master tip)。四、CI 现状
五、这 5 条不是本 PR 引入的(证据)
b12a81e9c自身就是红的:前三项 success,Native shell failure(13m9s);其日志test result: FAILED. 2367 passed; 5 failed,5 条用例名与本 PR 完全一致。f505d2792同为 failure,其父2e87a1403为 success ⇒ 该红是这两个提交新引入。src-tauri差异,只有合入 master 的b12a81e9c那 5 个文件。action_execution::canvas_only_execution_tests::art_director_canvas_only_execution_rejects_file_write_and_mixed_batch_without_side_effectsruntime_tools::media::platform_art_generation_observation_tests::stale_ready_child_is_rejected_before_external_canvas_requesttests::collaboration::delegation::autonomous_delegate_descendant_inherits_and_enforces_the_current_root_guardtests::collaboration::delegation::autonomous_direct_child_collaboration_mutations_require_the_current_roottests::collaboration::delegation::canvas_replacement_rejects_parent_run_that_terminates_during_external_request六、根因与建议修法
b12a81e9c在project_gates.rs的ensure_current_autonomous_ready_child_mutation_at_locked中按autonomous_relaxed_run_at提前返回Ok,使该函数后续约 120 行「当前根 Run 身份 + 根存活」校验成为不可达死代码;同时在action_execution.rs为 Canvas-only 门禁加了 relaxed 旁路——正对应上面 5 条的判据(file.write实得ok期望rejected;agent.message实得ok期望failed;过期 ready child 未在外部请求前被拒)。建议(请引入方确认口径):
b12a81e9c(kdletters)、f505d2792(lhk229)。View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.