diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index 4fdf5a5..7cfa8bb 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs @@ -551,30 +551,30 @@ impl Nip34WritePolicy { None }; - // Query for all dependent events - match crate::nostr::policy::query_dependent_events( + // Determine which events should be deleted using graph-based algorithm + match crate::nostr::policy::determine_events_to_delete( &self.ctx.database, &event_ids, &addresses, ) .await { - Ok(all_events) => { + Ok(events_to_delete) => { tracing::info!( event_id = %event_id_str, author = %event.pubkey, event_count = event_ids.len(), address_count = addresses.len(), - total_events = all_events.len(), - "Processing deletion request - moving {} events to holding database", - all_events.len() + total_events = events_to_delete.len(), + "Processing deletion request - moving {} events to holding database (graph-based)", + events_to_delete.len() ); // Move events to holding database match crate::nostr::policy::move_to_holding_database( &self.ctx.database, holding_db, - &all_events, + &events_to_delete, &event.id, self.ctx.config.archive_retention_secs, archive_path, @@ -585,7 +585,7 @@ impl Nip34WritePolicy { tracing::info!( event_id = %event_id_str, moved_count = moved_count, - "Successfully moved {} events to holding database", + "Successfully moved {} events to holding database (graph-based)", moved_count ); WritePolicyResult::Accept diff --git a/src/nostr/policy/deletion_ops.rs b/src/nostr/policy/deletion_ops.rs index 2e7a2a7..101d9cc 100644 --- a/src/nostr/policy/deletion_ops.rs +++ b/src/nostr/policy/deletion_ops.rs @@ -11,6 +11,130 @@ use super::SharedDatabase; use crate::database::{DeletionMetadata, HoldingDatabase}; use crate::git::archive::{archive_repository, create_archive_metadata}; +/// Determine which events should be deleted using graph-based algorithm +/// +/// This is the main integration point for the multi-maintainer deletion algorithm. +/// It replaces the simple cascade deletion with a sophisticated graph-based approach: +/// +/// 1. Query all potentially affected events (same as before) +/// 2. Build EventGraph to understand dependencies +/// 3. Re-evaluate events to determine which have alternative retention reasons +/// 4. Traverse graph to mark events for deletion +/// 5. Return only events that should actually be deleted +/// +/// # Arguments +/// * `database` - Main database to query +/// * `event_ids` - Event IDs being deleted +/// * `addresses` - Addresses being deleted (format: `::`) +/// +/// # Returns +/// Set of event IDs that should be DELETED (not all dependents, only those without retention reasons) +pub async fn determine_events_to_delete( + database: &SharedDatabase, + event_ids: &[EventId], + addresses: &[String], +) -> Result, String> { + // Step 1: Query all potentially affected events (same as simple cascade) + let potentially_affected = query_dependent_events(database, event_ids, addresses).await?; + + tracing::debug!( + event_ids_count = event_ids.len(), + addresses_count = addresses.len(), + potentially_affected_count = potentially_affected.len(), + "Queried potentially affected events for graph-based deletion" + ); + + // Step 2: Query all events to build the graph + // We need ALL events in the database to properly build the dependency graph + let all_events = query_all_events(database).await?; + + tracing::debug!( + all_events_count = all_events.len(), + "Queried all events for graph building" + ); + + // Step 3: Build event dependency graph + let graph = super::build_event_graph(&all_events); + + tracing::info!( + nodes = graph.node_count(), + edges = graph.edge_count(), + max_depth = graph.max_depth(), + "Built event dependency graph" + ); + + // Step 4: Re-evaluate events to determine retention reasons + // For each deleted address, run re-evaluation + let mut all_kept_events = std::collections::HashMap::new(); + let mut all_deleted_events = HashSet::new(); + + for address in addresses { + let reeval_result = + super::reevaluate_events_without_announcement(database, address, &potentially_affected) + .await?; + + tracing::info!( + address = %address, + kept = reeval_result.keep.len(), + deleted = reeval_result.delete.len(), + "Re-evaluation complete for address" + ); + + // Merge results + all_kept_events.extend(reeval_result.keep); + all_deleted_events.extend(reeval_result.delete); + } + + // Step 5: Traverse graph to mark events for deletion + // Use the first deleted event ID for logging (or generate a dummy one) + let deleted_event_id = event_ids.first().copied().unwrap_or_else(|| { + EventId::from_hex("0000000000000000000000000000000000000000000000000000000000000000") + .unwrap() + }); + + let traversal_result = + super::traverse_and_mark_deletions(&graph, &all_kept_events, deleted_event_id); + + // Log retention reasons + for (event_id, reason) in &all_kept_events { + tracing::debug!( + event_id = %event_id, + event_type = %reason.event_type, + valid_announcements = ?reason.valid_announcements, + "Event KEPT: has alternative retention reason" + ); + } + + // Log circular dependencies + for circular_dep in &traversal_result.circular_dependencies { + if circular_dep.anchored { + tracing::info!( + events = ?circular_dep.events, + "Detected anchored circular dependency (events will be kept)" + ); + } else { + tracing::warn!( + events = ?circular_dep.events, + "Detected unanchored circular dependency (events will be deleted)" + ); + } + } + + // Log summary + tracing::info!( + total_evaluated = potentially_affected.len(), + kept = traversal_result.keep.len(), + deleted = traversal_result.delete.len(), + circular_deps = traversal_result.circular_dependencies.len(), + "Graph-based deletion complete: {} events kept, {} events deleted", + traversal_result.keep.len(), + traversal_result.delete.len() + ); + + // Return only events that should be deleted + Ok(traversal_result.delete) +} + /// Query all events that depend on the given event IDs /// /// This performs a recursive traversal to find: @@ -18,8 +142,7 @@ use crate::git::archive::{archive_repository, create_archive_metadata}; /// 2. Events with `e` tags referencing the events found in step 1 /// 3. Recursively find events referencing those events /// -/// For Phase 1, this is a simple cascade (no graph complexity). -/// Phase 3 will add multi-maintainer graph algorithm. +/// This is used by the graph-based deletion algorithm to find potentially affected events. /// /// # Arguments /// * `database` - Main database to query @@ -69,6 +192,48 @@ pub async fn query_dependent_events( Ok(all_dependents) } +/// Query all events from the database +/// +/// This is used by the graph-based deletion algorithm to build the complete +/// dependency graph. +/// +/// # Arguments +/// * `database` - Database to query +/// +/// # Returns +/// Vector of all events in the database +async fn query_all_events(database: &SharedDatabase) -> Result, String> { + // Query all NIP-34 event types + let kinds = vec![ + Kind::from(30617), // Repository announcements + Kind::from(1617), // Patches + Kind::from(1621), // Issues + Kind::from(1619), // PR Updates + Kind::from(1630), // Issue status + Kind::from(1631), // PR status + Kind::from(1632), // Patch status + Kind::from(1633), // Repository status + Kind::from(30618), // Repository state + Kind::from(1630), // Pull requests + ]; + + let mut all_events = Vec::new(); + + for kind in kinds { + let filter = Filter::new().kind(kind); + let events: Vec = database + .query(filter) + .await + .map_err(|e| format!("Database query failed: {}", e))? + .into_iter() + .collect(); + + all_events.extend(events); + } + + Ok(all_events) +} + /// Query events that have an `a` tag referencing the given address /// /// # Arguments diff --git a/src/nostr/policy/mod.rs b/src/nostr/policy/mod.rs index 8bad506..aeca768 100644 --- a/src/nostr/policy/mod.rs +++ b/src/nostr/policy/mod.rs @@ -23,7 +23,8 @@ mod traversal; pub use announcement::{AnnouncementPolicy, AnnouncementResult}; pub use deletion::{DeletionPolicy, DeletionResult}; pub use deletion_ops::{ - archive_repositories_for_addresses, move_to_holding_database, query_dependent_events, + archive_repositories_for_addresses, determine_events_to_delete, move_to_holding_database, + query_dependent_events, }; pub use graph::{build_event_graph, EventGraph, EventNode}; pub use pr_event::PrEventPolicy;