diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs b/apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs index 82ec4561c..4fbfeecb0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs @@ -400,6 +400,12 @@ pub(crate) fn build_project_resource_graph( .push(resource_ids[0].clone()); } + let resources_by_manifest_asset_id = resource_ids_by_manifest_asset + .iter() + .filter(|(_, resource_ids)| resource_ids.len() == 1) + .map(|(asset_id, resource_ids)| (asset_id.clone(), resource_ids[0].clone())) + .collect::>(); + let mut unresolved_reference_resource_ids = BTreeSet::new(); let mut reference_edge_by_id = BTreeMap::::new(); for (asset_id, target_resource_ids) in &resource_ids_by_manifest_asset { @@ -418,16 +424,32 @@ pub(crate) fn build_project_resource_graph( .filter(|value| !value.is_empty()) .collect::>() { - let source_candidates = resources_by_external_id + let mut source_candidates = resources_by_external_id .get(external_reference_id) .map(Vec::as_slice) - .unwrap_or(&[]); + .unwrap_or(&[]) + .iter() + .collect::>(); + if let Some(referenced_asset_id) = external_reference_id + .strip_prefix("local-asset:") + .map(str::trim) + .filter(|value| !value.is_empty()) + { + if let Some(source_resource_id) = + resources_by_manifest_asset_id.get(referenced_asset_id) + { + source_candidates.insert(source_resource_id); + } + } if source_candidates.len() != 1 { unresolved_reference_resource_ids.insert(external_reference_id.to_string()); continue; } - let source_resource_id = &source_candidates[0]; - if !resource_by_id.contains_key(source_resource_id) + let source_resource_id = source_candidates + .iter() + .next() + .expect("a non-empty candidate set must have one resource"); + if !resource_by_id.contains_key(source_resource_id.as_str()) || !resource_by_id.contains_key(target_resource_id) { continue; @@ -438,7 +460,7 @@ pub(crate) fn build_project_resource_graph( ProjectResourceReferenceEdge { id, kind: "asset-reference".to_string(), - source_resource_id: source_resource_id.clone(), + source_resource_id: source_resource_id.to_string(), target_resource_id: target_resource_id.clone(), cyclic: false, }, @@ -835,6 +857,75 @@ mod tests { ); } + #[test] + fn graph_resolves_local_asset_reference_identities() { + let manifest = manifest( + Vec::new(), + vec![ + asset("source-1", None, &[], None), + asset( + "derivative-1", + Some("local-asset:derivative-1"), + &["local-asset:source-1"], + None, + ), + ], + ); + let graph = build_project_resource_graph( + &manifest, + vec![ + resource("asset:source-1", Some("source-1"), None), + resource("asset:derivative-1", Some("derivative-1"), None), + ], + &[], + false, + ); + + assert_eq!(graph.reference_edges.len(), 1); + assert_eq!( + graph.reference_edges[0].source_resource_id, + "asset:source-1" + ); + assert_eq!( + graph.reference_edges[0].target_resource_id, + "asset:derivative-1" + ); + assert!(graph.unresolved_reference_resource_ids.is_empty()); + assert!(graph + .connection_index + .iter() + .any(|index| index.resource_id == "asset:derivative-1" + && index.upstream_reference_resource_ids == vec!["asset:source-1"])); + } + + #[test] + fn graph_resolves_local_asset_identity_without_ambiguous_remote_duplicate() { + let manifest = manifest( + Vec::new(), + vec![ + asset("source-1", Some("external-source"), &[], None), + asset( + "derivative-1", + Some("local-asset:derivative-1"), + &["local-asset:source-1"], + None, + ), + ], + ); + let graph = build_project_resource_graph( + &manifest, + vec![ + resource("asset:source-1", Some("source-1"), None), + resource("asset:derivative-1", Some("derivative-1"), None), + ], + &[], + false, + ); + + assert_eq!(graph.reference_edges.len(), 1); + assert!(graph.unresolved_reference_resource_ids.is_empty()); + } + #[test] fn graph_aggregates_flows_filters_missing_resources_and_detects_cycles_iteratively() { let manifest = manifest(