@@ -2228,6 +2228,239 @@ impl GraphRegistry {
22282228 Ok ( relations)
22292229 }
22302230
2231+ /// Adds a relation to an element with full validation and target resolution
2232+ /// This is the comprehensive method used by CRUD operations
2233+ ///
2234+ /// # Arguments
2235+ /// * `source_id` - Source element identifier
2236+ /// * `target` - Target (element name, URL, or file path)
2237+ /// * `relation_type` - Relation type name
2238+ /// * `git_root` - Git root path for file resolution
2239+ ///
2240+ /// # Returns
2241+ /// Modified file path
2242+ pub fn add_element_relation_full (
2243+ & mut self ,
2244+ source_id : & str ,
2245+ target : & str ,
2246+ relation_type : & str ,
2247+ git_root : & std:: path:: Path ,
2248+ ) -> Result < String , ReqvireError > {
2249+ use crate :: relation:: { RELATION_TYPES , Relation , RelationTarget , LinkType } ;
2250+ use std:: path:: PathBuf ;
2251+
2252+ // Validate source element exists
2253+ if !self . nodes . contains_key ( source_id) {
2254+ return Err ( ReqvireError :: ElementNotFound (
2255+ format ! ( "Source element '{}' not found" , source_id)
2256+ ) ) ;
2257+ }
2258+
2259+ // Validate relation type
2260+ if !RELATION_TYPES . contains_key ( relation_type) {
2261+ return Err ( ReqvireError :: UnsupportedRelationType (
2262+ format ! ( "Invalid relation type '{}'. Valid types: {}" ,
2263+ relation_type, crate :: relation:: supported_relation_types_list( ) )
2264+ ) ) ;
2265+ }
2266+
2267+ // Get source element info
2268+ let source_node = self . nodes . get ( source_id) . unwrap ( ) ;
2269+ let source_name = source_node. element . name . clone ( ) ;
2270+ let source_file_path = source_node. element . file_path . clone ( ) ;
2271+ let source_type = source_node. element . element_type . clone ( ) ;
2272+
2273+ // Determine target type: element name, external URL, or internal path
2274+ let is_external_url = crate :: utils:: is_external_url ( target) ;
2275+ let is_internal_path = !is_external_url && (
2276+ target. ends_with ( ".md" ) ||
2277+ target. contains ( '/' ) ||
2278+ git_root. join ( target) . exists ( )
2279+ ) ;
2280+
2281+ // Resolve target and create relation components
2282+ let ( target_display_name, relation_target_link, target_id_for_check, element_id_opt) =
2283+ if is_external_url {
2284+ // External URL - use as-is
2285+ ( target. to_string ( ) , LinkType :: ExternalUrl ( target. to_string ( ) ) , target. to_string ( ) , None )
2286+ } else if is_internal_path {
2287+ // Internal file path
2288+ let source_folder = crate :: utils:: get_parent_dir ( & source_file_path) ;
2289+
2290+ // Calculate relative path from source file to target
2291+ let target_path = PathBuf :: from ( target) ;
2292+ let relative_path = pathdiff:: diff_paths ( & target_path, & source_folder)
2293+ . unwrap_or_else ( || target_path. clone ( ) ) ;
2294+
2295+ // Extract filename for display name
2296+ let display = target_path. file_name ( )
2297+ . map ( |n| n. to_string_lossy ( ) . to_string ( ) )
2298+ . unwrap_or_else ( || target. to_string ( ) ) ;
2299+
2300+ ( display, LinkType :: InternalPath ( relative_path) , target. to_string ( ) , None )
2301+ } else {
2302+ // Element name - resolve to get identifier
2303+ let target_element = self . get_element_by_name ( target)
2304+ . ok_or_else ( || ReqvireError :: ElementNotFound (
2305+ format ! ( "Target element '{}' not found" , target)
2306+ ) ) ?;
2307+
2308+ let target_id = target_element. identifier . clone ( ) ;
2309+ let target_display_name = target_element. name . clone ( ) ;
2310+ let target_file_path = target_element. file_path . clone ( ) ;
2311+ let target_type = target_element. element_type . clone ( ) ;
2312+
2313+ // Validate type compatibility for element-to-element relations
2314+ // TODO: Add relation-type-specific validation here
2315+ // For now, just check that we're not linking incompatible types
2316+ if !source_type. is_merge_compatible ( & target_type) {
2317+ // Note: This is a simplified check - we may want more nuanced validation
2318+ // based on the specific relation type
2319+ log:: warn!(
2320+ "Adding relation '{}' between potentially incompatible types: {} ({}) -> {} ({})" ,
2321+ relation_type, source_name, source_type. as_str( ) ,
2322+ target_display_name, target_type. as_str( )
2323+ ) ;
2324+ }
2325+
2326+ // Calculate relative identifier from source element's file to target element
2327+ let source_folder = crate :: utils:: get_parent_dir ( & source_file_path) ;
2328+
2329+ let relation_target = if source_file_path == target_file_path {
2330+ // Same file - use just the fragment
2331+ let ( _path, fragment_opt) = crate :: utils:: extract_path_and_fragment ( & target_id) ;
2332+ let fragment = fragment_opt. unwrap_or ( & target_id) ;
2333+ LinkType :: Identifier ( format ! ( "#{}" , fragment) )
2334+ } else {
2335+ // Different files - calculate relative path
2336+ let relative_id = crate :: utils:: to_relative_identifier (
2337+ & target_id,
2338+ & source_folder,
2339+ true
2340+ ) . unwrap_or_else ( |_| target_id. clone ( ) ) ;
2341+ LinkType :: Identifier ( relative_id)
2342+ } ;
2343+
2344+ // Extract element ID (fragment) for change tracking
2345+ let ( _path, fragment_opt) = crate :: utils:: extract_path_and_fragment ( & target_id) ;
2346+ let element_id = fragment_opt. map ( |s| s. to_string ( ) ) ;
2347+
2348+ ( target_display_name, relation_target, target_id, element_id)
2349+ } ;
2350+
2351+ // Get source node again (mutable this time)
2352+ let source_node = self . nodes . get ( source_id) . unwrap ( ) ;
2353+
2354+ // Validate: Check if relation already exists (idempotent)
2355+ let relation_exists = source_node. element . relations . iter ( ) . any ( |r| {
2356+ r. user_created &&
2357+ r. relation_type . name == relation_type &&
2358+ r. target . link . as_str ( ) == target_id_for_check
2359+ } ) ;
2360+
2361+ if relation_exists {
2362+ return Err ( ReqvireError :: RelationError (
2363+ format ! ( "Relation '{}' from '{}' to '{}' already exists" ,
2364+ relation_type, source_name, target)
2365+ ) ) ;
2366+ }
2367+
2368+ // Validate: Check for cross-section duplicate (target in Attachments)
2369+ let in_attachments = source_node. element . attachments . iter ( ) . any ( |a| {
2370+ a. target . as_str ( ) == target_id_for_check
2371+ } ) ;
2372+
2373+ if in_attachments {
2374+ return Err ( ReqvireError :: CrossSectionDuplicate (
2375+ format ! ( "Target '{}' already exists in Attachments of '{}'. Cannot add to Relations." ,
2376+ target, source_name)
2377+ ) ) ;
2378+ }
2379+
2380+ // Create the relation
2381+ let relation_type_info = RELATION_TYPES . get ( relation_type) . unwrap ( ) ;
2382+ let relation = Relation {
2383+ relation_type : relation_type_info,
2384+ target : RelationTarget {
2385+ text : target_display_name,
2386+ link : relation_target_link,
2387+ element_id : element_id_opt,
2388+ } ,
2389+ user_created : true ,
2390+ } ;
2391+
2392+ // Add relation to source element
2393+ let source_node = self . nodes . get_mut ( source_id) . unwrap ( ) ;
2394+ source_node. element . relations . push ( relation) ;
2395+
2396+ // Mark file as modified
2397+ let file_path = source_node. element . file_path . clone ( ) ;
2398+ self . modified_files . insert ( file_path. clone ( ) ) ;
2399+
2400+ Ok ( file_path)
2401+ }
2402+
2403+ /// Removes a relation from an element with full target resolution
2404+ /// This is the comprehensive method used by CRUD operations
2405+ ///
2406+ /// # Arguments
2407+ /// * `source_id` - Source element identifier
2408+ /// * `target` - Target (element name, URL, or file path)
2409+ ///
2410+ /// # Returns
2411+ /// Tuple of (modified file path, relation type, target display name) or None if no relation found
2412+ pub fn remove_element_relation_full (
2413+ & mut self ,
2414+ source_id : & str ,
2415+ target : & str ,
2416+ ) -> Result < Option < ( String , String , String ) > , ReqvireError > {
2417+ // Validate source element exists
2418+ if !self . nodes . contains_key ( source_id) {
2419+ return Err ( ReqvireError :: ElementNotFound (
2420+ format ! ( "Source element '{}' not found" , source_id)
2421+ ) ) ;
2422+ }
2423+
2424+ let source_node = self . nodes . get ( source_id) . unwrap ( ) ;
2425+ let source_file_path = source_node. element . file_path . clone ( ) ;
2426+
2427+ // Try to resolve target as element name first
2428+ let target_id_to_find = if let Some ( target_element) = self . get_element_by_name ( target) {
2429+ target_element. identifier . clone ( )
2430+ } else {
2431+ // Not an element name - could be URL or path
2432+ target. to_string ( )
2433+ } ;
2434+
2435+ // Find matching relation
2436+ let relation_match = source_node. element . relations . iter ( )
2437+ . find ( |r| {
2438+ r. user_created && r. target . link . as_str ( ) == target_id_to_find
2439+ } )
2440+ . cloned ( ) ; // Clone to avoid borrow issues
2441+
2442+ if let Some ( relation) = relation_match {
2443+ let relation_type = relation. relation_type . name . to_string ( ) ;
2444+ let target_display_name = relation. target . text . clone ( ) ;
2445+
2446+ // Remove the relation
2447+ let source_node = self . nodes . get_mut ( source_id) . unwrap ( ) ;
2448+ source_node. element . relations . retain ( |r| {
2449+ !( r. user_created &&
2450+ r. relation_type . name == relation_type &&
2451+ r. target . link . as_str ( ) == target_id_to_find)
2452+ } ) ;
2453+
2454+ // Mark file as modified
2455+ self . modified_files . insert ( source_file_path. clone ( ) ) ;
2456+
2457+ Ok ( Some ( ( source_file_path, relation_type, target_display_name) ) )
2458+ } else {
2459+ // No relation found - could be an attachment (handled by crud layer)
2460+ Ok ( None )
2461+ }
2462+ }
2463+
22312464 /// Gets statistics about the graph
22322465 pub fn get_graph_stats ( & self ) -> ( usize , usize ) {
22332466 let element_count = self . nodes . len ( ) ;
@@ -2321,6 +2554,41 @@ impl GraphRegistry {
23212554 Ok ( new_element)
23222555 }
23232556
2557+ /// Check if removing an element would orphan any children
2558+ ///
2559+ /// Returns a sorted list of child element names that would be orphaned
2560+ fn check_for_orphaned_children ( & self , element_id : & str ) -> Result < Vec < String > , ReqvireError > {
2561+ let mut orphaned_children: Vec < String > = Vec :: new ( ) ;
2562+ let hierarchical_types = crate :: relation:: get_hierarchical_relation_types ( ) ;
2563+
2564+ for ( _child_id, child_node) in & self . nodes {
2565+ // Count how many hierarchical parent relations this child has to the element being deleted
2566+ let mut parents_to_target = 0 ;
2567+ let mut total_parents = 0 ;
2568+
2569+ for rel in & child_node. element . relations {
2570+ let target_id = match & rel. target . link {
2571+ crate :: relation:: LinkType :: Identifier ( id) => id. as_str ( ) ,
2572+ _ => continue , // Skip external links
2573+ } ;
2574+ if hierarchical_types. contains ( & rel. relation_type . name ) {
2575+ total_parents += 1 ;
2576+ if target_id == element_id {
2577+ parents_to_target += 1 ;
2578+ }
2579+ }
2580+ }
2581+
2582+ // If child only has hierarchical parent relations to the target element, it will be orphaned
2583+ if parents_to_target > 0 && total_parents == parents_to_target {
2584+ orphaned_children. push ( child_node. element . name . clone ( ) ) ;
2585+ }
2586+ }
2587+
2588+ orphaned_children. sort ( ) ;
2589+ Ok ( orphaned_children)
2590+ }
2591+
23242592 /// Enhanced remove element that tracks modifications and performs cleanup
23252593 pub fn remove_element_with_cleanup ( & mut self , element_id : & str ) -> Result < Vec < String > , ReqvireError > {
23262594 if !self . nodes . contains_key ( element_id) {
@@ -2331,8 +2599,25 @@ impl GraphRegistry {
23312599
23322600 // Get element info before removal
23332601 let element = & self . nodes . get ( element_id) . unwrap ( ) . element ;
2602+ let element_name = element. name . clone ( ) ;
23342603 let file_path = element. file_path . clone ( ) ;
23352604
2605+ // Validate: Check for orphaned children before removal
2606+ let orphaned_children = self . check_for_orphaned_children ( element_id) ?;
2607+ if !orphaned_children. is_empty ( ) {
2608+ return Err ( ReqvireError :: InvalidOperation (
2609+ format ! (
2610+ "Cannot delete '{}' because it has {} child element(s) with parent hierarchical relations that would become orphaned: {}.\n \n \
2611+ To proceed, either:\n \
2612+ 1. Delete the child elements first, or\n \
2613+ 2. Update the child elements to link to a different parent element",
2614+ element_name,
2615+ orphaned_children. len( ) ,
2616+ orphaned_children. join( ", " )
2617+ )
2618+ ) ) ;
2619+ }
2620+
23362621 // Track all files that will be modified
23372622 let mut modified_files = vec ! [ file_path. clone( ) ] ;
23382623
@@ -2485,6 +2770,11 @@ impl GraphRegistry {
24852770 ) ) ;
24862771 }
24872772
2773+ // Get target element data first (needed for validation)
2774+ let target_node = self . nodes . get ( target_id) . unwrap ( ) ;
2775+ let target_name = target_node. element . name . clone ( ) ;
2776+ let target_type = target_node. element . element_type . clone ( ) ;
2777+
24882778 // Validate all sources exist and collect their data
24892779 let mut source_data: Vec < ( String , String , String , Vec < crate :: relation:: Relation > , Vec < crate :: element:: Attachment > ) > = Vec :: new ( ) ;
24902780 for source_id in source_ids {
@@ -2494,6 +2784,24 @@ impl GraphRegistry {
24942784 ) ) ?;
24952785
24962786 let source_element = & source_node. element ;
2787+
2788+ // Validate: Check if source would merge into itself
2789+ if source_id == target_id {
2790+ return Err ( ReqvireError :: InvalidOperation (
2791+ "Cannot merge element into itself" . to_string ( )
2792+ ) ) ;
2793+ }
2794+
2795+ // Validate: Check type compatibility
2796+ if !target_type. is_merge_compatible ( & source_element. element_type ) {
2797+ return Err ( ReqvireError :: MergeTypeMismatch ( format ! (
2798+ "Cannot merge '{}' ({}) into '{}' ({}): type mismatch. \
2799+ Elements must be in the same category (requirement/verification/refinement/other).",
2800+ source_element. name, source_element. element_type. as_str( ) ,
2801+ target_name, target_type. as_str( )
2802+ ) ) ) ;
2803+ }
2804+
24972805 source_data. push ( (
24982806 source_id. clone ( ) ,
24992807 source_element. name . clone ( ) ,
@@ -2506,7 +2814,7 @@ impl GraphRegistry {
25062814 ) ) ;
25072815 }
25082816
2509- // Get target element data
2817+ // Re-get target element data (needed after validation)
25102818 let target_node = self . nodes . get ( target_id) . unwrap ( ) ;
25112819 let target_file_path = target_node. element . file_path . clone ( ) ;
25122820 let mut merged_content = String :: new ( ) ;
0 commit comments