新增 AGC 版本级资源替换的后端命令与三项兼容性判据
- 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 白名单换绿
This commit is contained in:
@@ -661,6 +661,32 @@ pub fn game_creation_app_asset_category_for_kind(kind: &str) -> GameCreationAppA
|
||||
.unwrap_or(GameCreationAppAssetCategory::Unclassified)
|
||||
}
|
||||
|
||||
/// 资源分类的**读时自愈**口径。
|
||||
///
|
||||
/// 落盘 `category` 是权威值,唯一例外是自愈窗口:落盘值为 `Unclassified` 而该资产 `kind`
|
||||
/// 能派生出明确的非 `Unclassified` 分类时采用派生值。这条规则用于修复历史上被系统误写成
|
||||
/// `unclassified` 的存量数据(典型例子:`kind:"ui"` 的 UI 资产曾因 kind 不在 canonical 目录
|
||||
/// 而落到 `image → unclassified`,修复别名后并不会自动归位,因为落盘值已固化)。
|
||||
///
|
||||
/// 与 `packages/shared/src/contracts/gameCreationApp.ts` 的 `gameCreationAppAssetCategory`
|
||||
/// **逐分支一致**:那里的 `persisted === null`(缺字段 / 非法值)分支在 Rust 反序列化
|
||||
/// (`GameCreationAppAssetManifestEntry::deserialize`)已经按 kind 派生过,所以这里只补自愈那一支。
|
||||
/// `kind` 派生结果本身就是 `unclassified` 的(`image` / `video` / `code` / `publication-material`)
|
||||
/// 不受影响,仍信任落盘值。
|
||||
pub fn game_creation_app_asset_category_with_read_time_healing(
|
||||
category: GameCreationAppAssetCategory,
|
||||
kind: &str,
|
||||
) -> GameCreationAppAssetCategory {
|
||||
let derived = game_creation_app_asset_category_for_kind(kind);
|
||||
if category == GameCreationAppAssetCategory::Unclassified
|
||||
&& derived != GameCreationAppAssetCategory::Unclassified
|
||||
{
|
||||
derived
|
||||
} else {
|
||||
category
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_game_creation_app_asset_tags(tags: &[String]) -> Vec<String> {
|
||||
let mut normalized = Vec::new();
|
||||
for tag in tags {
|
||||
@@ -2128,6 +2154,53 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 读时自愈只在「落盘 unclassified + kind 能派生出明确分类」这一个窗口生效,其余原样。
|
||||
///
|
||||
/// 与 `packages/shared/src/contracts/gameCreationApp.ts` 的 `gameCreationAppAssetCategory`
|
||||
/// 对齐:显式非 unclassified 的落盘值即权威(哪怕与 kind 不符);kind 派生结果本身是
|
||||
/// unclassified 时也信任落盘值,所以 `image` / `video` / `code` / `unknown-kind` 不会被改写。
|
||||
#[test]
|
||||
fn asset_category_read_time_healing_only_rewrites_misclassified_unclassified() {
|
||||
for kind in ["ui-design", "ui", "icon", "icon-spritesheet"] {
|
||||
assert_eq!(
|
||||
game_creation_app_asset_category_with_read_time_healing(
|
||||
GameCreationAppAssetCategory::Unclassified,
|
||||
kind
|
||||
),
|
||||
GameCreationAppAssetCategory::UiInteraction,
|
||||
"{kind} 的历史误写 unclassified 必须自愈"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
game_creation_app_asset_category_with_read_time_healing(
|
||||
GameCreationAppAssetCategory::Unclassified,
|
||||
"character-art"
|
||||
),
|
||||
GameCreationAppAssetCategory::Character
|
||||
);
|
||||
for kind in ["image", "video", "code", "publication-material", "unknown-kind"] {
|
||||
assert_eq!(
|
||||
game_creation_app_asset_category_with_read_time_healing(
|
||||
GameCreationAppAssetCategory::Unclassified,
|
||||
kind
|
||||
),
|
||||
GameCreationAppAssetCategory::Unclassified,
|
||||
"{kind} 的派生结果就是 unclassified,不得改写落盘值"
|
||||
);
|
||||
}
|
||||
for (persisted, kind) in [
|
||||
(GameCreationAppAssetCategory::Character, "scene"),
|
||||
(GameCreationAppAssetCategory::Audio, "character"),
|
||||
(GameCreationAppAssetCategory::Document, "icon"),
|
||||
] {
|
||||
assert_eq!(
|
||||
game_creation_app_asset_category_with_read_time_healing(persisted, kind),
|
||||
persisted,
|
||||
"显式非 unclassified 的落盘值是权威值"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asset_manifest_entry_defaults_category_and_tags_for_legacy_payloads() {
|
||||
let character = asset_entry_from_json(asset_entry_json("character"));
|
||||
|
||||
Reference in New Issue
Block a user