资源替换改成直接替换:只改该版本绑定,不建新版本

- 口径变更:资源替换从「版本级替换(改绑定 + 追加下一迭代版本)」改成「直接替换(只改 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
This commit is contained in:
2026-09-11 19:55:45 +08:00
parent 17e8e477a4
commit b6a2eae38d
2 changed files with 373 additions and 354 deletions
@@ -6,27 +6,35 @@ use shared_contracts::game_creation_app::{
GameCreationAppAssetManifestEntry,
};
/// 版本级资源替换(PRD §3.2 / §5.3)。
/// 版本级资源替换(**直接替换**口径)。
///
/// 落盘语义:**改 manifest 绑定,但落在追加的新版本上** —— 不原地修改既有版本、不动资源文件、
/// 不建文件副本。一次 CAS 写入里完成三件事:
/// 1. 追加一条 `createdReason = resource-replacement` 的新版本,`parentVersionId` 指向被替换的源版本;
/// 2. 新版本的 `resourceBindings` = 源版本绑定集合**去掉源素材**,并保证**替换素材在集合里**
/// (替换素材是源版本创建之后才登记时,按源素材原来的位置插回,顺序稳定);
/// 3. 替换前后的资源身份由两份不可变记录 + `parentVersionId` + `createdReason` 确定:
/// 源素材的 `asset:{sourceResourceId}` 绑定在父版本里存在、在子版本里消失;替换素材的绑定在
/// 子版本里存在。读侧差异:`父 − 子` 恰好是 `{sourceResourceId}``子 父` 是
/// `{replacementResourceId}`(当替换素材在父版本创建时就已登记、因此已在父绑定里时,
/// `子 − 父` 为空集)。
/// **已知限制**:后一种情况下版本记录无法单独反推"是哪次替换摘掉了源素材",要无歧义地
/// 持久化配对就得给 `GameIterationVersion` 增字段(跨端契约变更);本切片按 PRD §5.4
/// 的版本字段表口径不新增字段,因此不声称这一点是完整的前后身份记录。
/// 落盘语义:**改 manifest 里该版本的绑定,指向另一个已登记资源** ——
/// 不追加新版本、不删除版本、不动资源文件、不建文件副本。
///
/// 只追加语义不变:本模块走 `mutate_manifest_at`(默认空放行集合),从不调用
/// `mutate_manifest_at_allowing_version_removals`;写入边界继续由 `validate_game_iteration_versions`
/// 与 `validate_version_records_are_append_only` 拦截修改 / 删除 / 重排
/// 写入通道是 `project/manifest.rs` 的 `mutate_manifest_allowing_version_binding_rewrites`
/// 它只放行"显式列出的版本改写自己的 `resourceBindings`",不增删、不重排、不改其它字段。
/// 因此本命令既表达了"这个版本现在用另一个素材",又没有把版本记录的只追加保证打穿
///
/// 恒等绑定口径(`slotId` 恒为 `asset:{resourceId}`)下的绑定改写 = **源素材从这个版本的绑定
/// 集合里消失 + 替换素材出现在这个集合里**:
/// - 替换素材是源版本创建之后才登记时,按源素材原来的位置插回,保持顺序稳定;
/// - 替换素材早就登记(因此早就在集合里)时只做摘除 —— 不能"把源素材那条槽位改写成替换素材",
/// 那会撞「资源槽位重复」。
///
/// 准入与提示:
/// - **硬门禁**只有 `categoryEqual` 与 `subtypeEqual`(拦"把角色图换成背景音乐"这类明显错误);
/// - `sizeSpecEqual` 只作为**提示**、不拒绝:它的完整判据今天不存在(manifest 资产表没有
/// `width / height / durationMs`,实际只等于"媒体格式相等"),硬拦会误拒 `png ↔ webp` 这种
/// 直接替换里最常见的正常需求。
///
/// 已知代价:直接替换形态下**没有可回溯的替换历史**。替换前的绑定身份只存在于
/// ① `asset.version_binding.replace` 审计记录 ② manifest 的 `.previous` 恢复副本。
/// 需要"某个版本历史上被换过几次、换成过什么"时要另立切片(例如 PRD §3.2 的版本级替换)。
const VERSION_REPLACEMENT_MAX_ID_CHARS: usize = 512;
/// 直接替换的审计记录类型,与 `asset.register` / `asset.update` 同族。
const VERSION_BINDING_REPLACE_RECORD_TYPE: &str = "asset.version_binding.replace";
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(crate) struct ReadLocalProjectVersionReplacementCandidatesInput {
@@ -35,7 +43,7 @@ pub(crate) struct ReadLocalProjectVersionReplacementCandidatesInput {
pub(crate) source_resource_id: String,
}
/// PRD §5.3 的三项兼容性。三项必须同时为 `true` 才允许创建下一版本
/// 三项兼容性。只有前两项是硬门禁;`size_spec_equal` 仅用于提示
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ProjectVersionResourceCompatibility {
@@ -45,18 +53,21 @@ pub(crate) struct ProjectVersionResourceCompatibility {
}
impl ProjectVersionResourceCompatibility {
fn all_compatible(self) -> bool {
self.category_equal && self.subtype_equal && self.size_spec_equal
}
/// 不兼容原因:按 PRD §5.3 的字段顺序报告第一项不等的维度。
fn blocked_reason(self) -> Option<&'static str> {
/// 硬门禁:分类与类型。任一不等即拒绝替换。
fn blocking_reason(self) -> Option<&'static str> {
if !self.category_equal {
Some("分类不同")
} else if !self.subtype_equal {
Some("类型不同")
} else if !self.size_spec_equal {
Some("尺寸规格不同")
} else {
None
}
}
/// 提示:通过硬门禁、但尺寸规格判据不等时给出(不拒绝)。
fn warning(self) -> Option<&'static str> {
if self.blocking_reason().is_none() && !self.size_spec_equal {
Some("格式与源素材不同")
} else {
None
}
@@ -67,10 +78,13 @@ impl ProjectVersionResourceCompatibility {
#[serde(rename_all = "camelCase")]
pub(crate) struct LocalProjectVersionReplacementCandidate {
pub(crate) resource_id: String,
/// 能否选中:等价于"没有硬门禁原因"。
pub(crate) compatible: bool,
pub(crate) compatibility: ProjectVersionResourceCompatibility,
/// 不兼容原因;兼容时为 `null`。候选**渲染但禁用**,不用隐藏伪装成"素材不存在"。
/// 硬门禁原因;通过时为 `null`。候选**渲染但禁用**,不用隐藏伪装成"素材不存在"。
pub(crate) blocked_reason: Option<&'static str>,
/// 非阻断提示(当前只有"格式与源素材不同")。
pub(crate) warning: Option<&'static str>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
@@ -92,26 +106,28 @@ pub(crate) struct ReplaceLocalProjectVersionResourceInput {
pub(crate) replacement_resource_id: String,
}
/// PRD §5.3 的 `ProjectVersionResourceReplacement`
/// 这一次替换的记录:哪个版本的哪个绑定指向改成了哪个素材
///
/// 直接替换不产生新版本,所以没有 `parentVersionId`、也没有可推的前后版本对。
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ProjectVersionResourceReplacement {
pub(crate) source_version_id: String,
pub(crate) version_id: String,
pub(crate) source_resource_id: String,
pub(crate) replacement_resource_id: String,
pub(crate) compatibility: ProjectVersionResourceCompatibility,
pub(crate) warning: Option<&'static str>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ReplaceLocalProjectVersionResourceResult {
pub(crate) version_id: String,
pub(crate) parent_version_id: String,
pub(crate) committed_project_revision: u64,
pub(crate) replacement: ProjectVersionResourceReplacement,
}
/// 槽位 ID 恒等于 `asset:{resourceId}`v3 恒等绑定口径),替换时必须同步换掉
/// 槽位 ID 恒等于 `asset:{resourceId}`v3 恒等绑定口径),绑定改写时同步维护
fn version_binding_slot_id(resource_id: &str) -> String {
format!("asset:{resource_id}")
}
@@ -120,8 +136,7 @@ fn version_binding_slot_id(resource_id: &str) -> String {
///
/// 只做「声明值归一 + 缺声明时按扩展名回退」:去参数段(`; charset=...`)、去首尾空白、
/// 转小写,并把 `image/jpg` 归到 canonical 的 `image/jpeg`(与 `assets.rs` 的
/// `CanvasImageFormat::from_media_type` 同口径)。这里**不**做全量 MIME 规范化表 —— 尺寸规格
/// 判据比较的是"格式身份",不是完整 MIME 语义。
/// `CanvasImageFormat::from_media_type` 同口径)。**不**做全量 MIME 规范化表
fn canonical_asset_media_format(asset: &GameCreationAppAssetManifestEntry) -> String {
let declared = asset
.media_type
@@ -159,24 +174,21 @@ fn asset_known_duration_ms(asset: &GameCreationAppAssetManifestEntry) -> Option<
asset.image_sequence_duration_ms
}
/// PRD §5.3 的 `sizeSpecEqual`。
/// `sizeSpecEqual` 判据(**只提示、不拒绝**
///
/// 判据 = 规范化媒体格式相等 **且**「任一方有事实的维度必须相等」(双方都无事实的维度不阻断)。
/// 判据 = 规范化媒体格式相等 **且**「任一方有事实的维度必须相等」(双方都无事实的维度不参与)。
///
/// **诚实标注(刻意接受的降级)**:当前 manifest 资产表**没有** `width / height / durationMs`
/// 字段(`GameCreationAppAssetManifestEntry` 只有 `imageSequenceFrames[].width|height` 与
/// `imageSequenceDurationMs`),而现役写入侧(上传、派生、画板回传、生成回流)几乎全部写
/// `imageSequenceFrames: None`,所以这条判据在实际数据上**退化为"媒体格式相等"**
/// `png ↔ webp` 会被判为尺寸规格不同而拒绝
///
/// 这不是完整实现。若产品要求「同分类同类型、跨图片格式也能替换」,必须改走"给 manifest asset
/// 增 width / height / durationMs 并在写入侧回填"的方案(跨端契约变更),届时本判据改为消费落盘尺寸
/// 事实并把"缺失即未知"的口径写进契约;在尺寸事实落盘之前,不得声称本项是完整尺寸规格比较。
/// **诚实标注(刻意接受的降级)**:manifest 资产表没有 `width / height / durationMs` 字段,
/// 且现役写入侧(上传、派生、画板回传、生成回流)几乎全部写 `imageSequenceFrames: None`
/// 所以这条判据在真实数据上**退化为"媒体格式相等"**。正因为它不完整,本轮**不用它拒绝**替换,
/// 只在弹窗里提示"格式与源素材不同";要变成硬判据,必须先给 manifest asset 增尺寸字段并在写入侧
/// 回填(跨端契约变更)
fn version_resource_size_spec_equal(
source: &GameCreationAppAssetManifestEntry,
replacement: &GameCreationAppAssetManifestEntry,
) -> bool {
let format_equal = canonical_asset_media_format(source) == canonical_asset_media_format(replacement);
let format_equal =
canonical_asset_media_format(source) == canonical_asset_media_format(replacement);
let size_equal = match (
asset_known_frame_size(source),
asset_known_frame_size(replacement),
@@ -197,8 +209,8 @@ fn version_resource_size_spec_equal(
/// 三项兼容性判据(后端是权威判据,前端只做呈现)。
///
/// - `categoryEqual`:功能分类相等,用**读时自愈**口径(PRD §5.3「分类取值优先级」收口);
/// - `subtypeEqual`canonical `kind` 相等(别名表在 `shared-contracts`);
/// - `sizeSpecEqual`:见 [`version_resource_size_spec_equal`] 的降级标注
/// - `subtypeEqual`canonical `kind` 相等(别名表在 `shared-contracts`——以上两项是硬门禁
/// - `sizeSpecEqual`:见 [`version_resource_size_spec_equal`],只作提示
fn version_resource_compatibility(
source: &GameCreationAppAssetManifestEntry,
replacement: &GameCreationAppAssetManifestEntry,
@@ -280,7 +292,7 @@ fn manifest_asset<'a>(
.ok_or_else(|| format!("项目资源不存在:{resource_id}"))
}
/// 读取源素材在当前 manifest 里可用的替换候选,并给出后端权威兼容性结论。
/// 读取源素材在当前 manifest 里可用的替换候选,并给出后端权威兼容性结论与提示
///
/// 只读:不改 manifest、不推进 revision。候选按 `manifest.assets` 顺序返回,源素材自身排除。
pub(crate) fn read_local_project_version_replacement_candidates_at(
@@ -303,9 +315,10 @@ pub(crate) fn read_local_project_version_replacement_candidates_at(
let compatibility = version_resource_compatibility(source_asset, asset);
LocalProjectVersionReplacementCandidate {
resource_id: asset.id.clone(),
compatible: compatibility.all_compatible(),
compatible: compatibility.blocking_reason().is_none(),
compatibility,
blocked_reason: compatibility.blocked_reason(),
blocked_reason: compatibility.blocking_reason(),
warning: compatibility.warning(),
}
})
.collect();
@@ -317,16 +330,17 @@ pub(crate) fn read_local_project_version_replacement_candidates_at(
})
}
/// 用 `replacement_resource_id` 替换源版本引用的 `source_resource_id`,追加下一迭代版本
/// 把源版本引用的 `source_resource_id` **直接替换**成 `replacement_resource_id`。
///
/// 语义与拒绝路径:
/// - 可运行版本不可变:既有版本记录一个字节都不改,新记录只能追加在末尾
/// - 三项兼容性必须同时为 `true`,否则拒绝并给出不等维度**不做假成功**
/// - **不追加新版本**:只改该版本的绑定集合,其余版本整条不动、版本数量不变
/// - 硬门禁:分类不同 / 类型不同 → 拒绝并说明哪一项不等**不做假成功**
/// - 源版本不存在 / 源版本未绑定该素材 / 替换素材未登记 / 替换素材与源素材相同 → 拒绝;
/// - CAS`expectedProjectId` 与 `expectedProjectRevision` 必须与锁内读到的事实一致,
/// 任何拒绝都保证 manifest 与 revision 不变;
/// - 成功后推进一次项目 revision,且新版本的 `projectRevision` 必须等于推进后的值
/// `validate_game_iteration_versions` 要求子版本修订严格大于父版本)。
/// - 成功后推进一次项目 revision(改绑定属于 `versions` 变化,跨面快照门禁要求 revision 前进),
/// 并追加一条 `asset.version_binding.replace` 审计。审计写失败会把错误报出,但 manifest 已落盘
/// (与 `asset.register` 同口径,不做回滚)。
pub(crate) fn replace_local_project_version_resource_at(
root: &Path,
expected_project_id: &str,
@@ -345,7 +359,8 @@ pub(crate) fn replace_local_project_version_resource_at(
return Err("替换素材 expectedProjectId 不能为空".to_string());
}
let source_version_id = normalized_version_id(source_version_id)?.to_string();
let source_resource_id = normalized_resource_id(source_resource_id, "sourceResourceId")?.to_string();
let source_resource_id =
normalized_resource_id(source_resource_id, "sourceResourceId")?.to_string();
let replacement_resource_id =
normalized_resource_id(replacement_resource_id, "replacementResourceId")?.to_string();
if source_resource_id == replacement_resource_id {
@@ -368,115 +383,103 @@ pub(crate) fn replace_local_project_version_resource_at(
.checked_add(1)
.ok_or_else(|| "项目 revision 已达到上限".to_string())?;
let mut appended: Option<(String, ProjectVersionResourceCompatibility)> = None;
mutate_manifest_at(root, |manifest| {
// 锁内复核 CAS:项目写锁已持有,此处再读一次 durable revision,把"读 revision → 写 manifest"
// 之间的窗口收干,任何漂移都在写入前失败关闭。
if read_game_creator_agent_runtime_project_revision(root)?.revision != expected_project_revision
{
return Err("project-revision-conflict".to_string());
}
if manifest.project_id != expected_project_id {
return Err("project-identity-conflict".to_string());
}
let source_version = replacement_source_version(manifest, &source_version_id)?;
let source_version_id_for_child = source_version.version_id.clone();
let source_version_revision = source_version.project_revision;
if target_revision <= source_version_revision {
return Err(format!(
"替换版本 revision {target_revision} 必须大于源版本 revision {source_version_revision}"
));
}
require_source_binding(source_version, &source_resource_id)?;
let source_asset = manifest_asset(manifest, &source_resource_id)?.clone();
let replacement_asset = manifest_asset(manifest, &replacement_resource_id)?.clone();
let compatibility = version_resource_compatibility(&source_asset, &replacement_asset);
if !compatibility.all_compatible() {
return Err(format!(
"resource-replacement-incompatible{}",
compatibility
.blocked_reason()
.unwrap_or("替换兼容性未通过")
));
}
let mut applied: Option<ProjectVersionResourceCompatibility> = None;
mutate_manifest_allowing_version_binding_rewrites(
root,
&|_manifest| vec![source_version_id.clone()],
|manifest| {
// 锁内复核 CAS:项目写锁已持有,此处再读一次 durable revision,把"读 revision →
// 写 manifest"之间的窗口收干,任何漂移都在写入前失败关闭。
if read_game_creator_agent_runtime_project_revision(root)?.revision
!= expected_project_revision
{
return Err("project-revision-conflict".to_string());
}
if manifest.project_id != expected_project_id {
return Err("project-identity-conflict".to_string());
}
let version_index = manifest
.versions
.iter()
.position(|version| version.version_id == source_version_id)
.ok_or_else(|| format!("源项目版本不存在:{source_version_id}"))?;
require_source_binding(&manifest.versions[version_index], &source_resource_id)?;
let source_asset = manifest_asset(manifest, &source_resource_id)?.clone();
let replacement_asset = manifest_asset(manifest, &replacement_resource_id)?.clone();
let compatibility = version_resource_compatibility(&source_asset, &replacement_asset);
if let Some(reason) = compatibility.blocking_reason() {
return Err(format!("resource-replacement-incompatible{reason}"));
}
let source_slot_id = version_binding_slot_id(&source_resource_id);
let Some(source_index) = source_version
.resource_bindings
.iter()
.position(|binding| {
binding.slot_id == source_slot_id && binding.resource_id == source_resource_id
})
else {
return Err(format!(
"源版本未绑定该素材:{source_version_id_for_child} · {source_resource_id}"
));
};
// 子版本绑定 = 父版本绑定去掉源素材,并保证替换素材在集合里。
//
// 恒等绑定口径下,一个版本的 `resourceBindings` 是该版本**使用的素材集合**
// (创建时随清单冻结),不是"哪个位置用了它"的槽位表。所以"替换"落盘为:
// 源素材从这个集合里消失 + 替换素材出现在这个集合里。不能把源素材那条槽位改写成
// 替换素材 —— 替换素材若在该版本创建时就已登记,它本来就已在集合中,改槽位会撞
// 「资源槽位重复」。
//
// 替换素材是版本创建之后才登记时(最常见的真实路径:先生成/上传了更好的素材再替换),
// 按源素材原来的位置插回,保持槽位顺序稳定,子父差异恰好一增一减。
let mut resource_bindings: Vec<GameIterationVersionResourceBinding> = source_version
.resource_bindings
.iter()
.filter(|binding| {
!(binding.slot_id == source_slot_id && binding.resource_id == source_resource_id)
})
.cloned()
.collect();
if !resource_bindings
.iter()
.any(|binding| binding.resource_id == replacement_resource_id)
{
resource_bindings.insert(
source_index.min(resource_bindings.len()),
GameIterationVersionResourceBinding {
slot_id: version_binding_slot_id(&replacement_resource_id),
resource_id: replacement_resource_id.clone(),
},
);
}
let version_id = format!("replace-{target_revision}");
manifest.versions.push(GameIterationVersion {
version_id: version_id.clone(),
parent_version_id: Some(source_version_id_for_child),
project_revision: target_revision,
resource_bindings,
created_reason: GameIterationVersionCreatedReason::ResourceReplacement,
created_at: unix_timestamp(),
edit_prompt: None,
});
appended = Some((version_id, compatibility));
Ok(())
})?;
// 绑定改写 = 源素材从该版本的绑定集合里消失 + 保证替换素材在集合里。
let slot_id = version_binding_slot_id(&source_resource_id);
let version = &mut manifest.versions[version_index];
let source_index = version
.resource_bindings
.iter()
.position(|binding| {
binding.slot_id == slot_id && binding.resource_id == source_resource_id
})
.ok_or_else(|| {
format!("源版本未绑定该素材:{source_version_id} · {source_resource_id}")
})?;
let mut resource_bindings: Vec<GameIterationVersionResourceBinding> = version
.resource_bindings
.iter()
.filter(|binding| {
!(binding.slot_id == slot_id && binding.resource_id == source_resource_id)
})
.cloned()
.collect();
if !resource_bindings
.iter()
.any(|binding| binding.resource_id == replacement_resource_id)
{
resource_bindings.insert(
source_index.min(resource_bindings.len()),
GameIterationVersionResourceBinding {
slot_id: version_binding_slot_id(&replacement_resource_id),
resource_id: replacement_resource_id.clone(),
},
);
}
version.resource_bindings = resource_bindings;
applied = Some(compatibility);
Ok(())
},
)?;
let committed_project_revision = advance_agent_runtime_project_revision_locked(root)
.map_err(|error| format!("替换版本已追加,但项目 revision 未能推进:{error}"))?;
let Some((version_id, compatibility)) = appended else {
return Err("替换版本写入未产生结果".to_string());
.map_err(|error| format!("绑定已改写,但项目 revision 未能推进:{error}"))?;
let Some(compatibility) = applied else {
return Err("替换写入未产生结果".to_string());
};
if committed_project_revision != target_revision {
return Err(format!(
"替换版本已追加,但项目 revision 推进结果与预期不一致:期望 {target_revision},实际 {committed_project_revision}"
"绑定已改写,但项目 revision 推进结果与预期不一致:期望 {target_revision},实际 {committed_project_revision}"
));
}
append_agent_db_record(
root,
serde_json::json!({
"recordType": VERSION_BINDING_REPLACE_RECORD_TYPE,
"versionId": source_version_id,
"sourceResourceId": source_resource_id,
"replacementResourceId": replacement_resource_id,
"projectRevision": committed_project_revision,
}),
)?;
Ok(ReplaceLocalProjectVersionResourceResult {
version_id,
parent_version_id: source_version_id.clone(),
version_id: source_version_id.clone(),
committed_project_revision,
replacement: ProjectVersionResourceReplacement {
source_version_id,
version_id: source_version_id,
source_resource_id,
replacement_resource_id,
compatibility,
warning: compatibility.warning(),
},
})
}
File diff suppressed because it is too large Load Diff