Skip to main content

subcog/services/
data_subject.rs

1//! GDPR Data Subject Rights Service.
2//!
3//! Implements data subject rights as required by GDPR:
4//! - **Article 6**: Lawful Basis (Consent tracking)
5//! - **Article 7**: Conditions for Consent
6//! - **Article 17**: Right to Erasure ("Right to be Forgotten")
7//! - **Article 20**: Right to Data Portability
8//!
9//! # Compliance Features
10//!
11//! | Requirement | Implementation |
12//! |-------------|----------------|
13//! | Consent tracking | [`ConsentRecord`] with granular purposes |
14//! | Consent withdrawal | `revoke_consent()` with audit trail |
15//! | Audit logging | All operations logged via [`AuditLogger`] |
16//! | Data export format | JSON (machine-readable, portable) |
17//! | Complete deletion | Removes from all storage layers |
18//! | Verification | Returns deletion confirmation with counts |
19//!
20//! # Usage
21//!
22//! ```rust,ignore
23//! use subcog::services::{DataSubjectService, ServiceContainer};
24//!
25//! let container = ServiceContainer::from_current_dir_or_user()?;
26//! let service = DataSubjectService::new(&container)?;
27//!
28//! // Export all user data (GDPR Article 20)
29//! let export = service.export_user_data()?;
30//! println!("Exported {} memories", export.memories.len());
31//!
32//! // Delete all user data (GDPR Article 17)
33//! let result = service.delete_user_data()?;
34//! println!("Deleted {} memories", result.deleted_count);
35//! ```
36
37use crate::Result;
38use crate::models::{EventMeta, Memory, MemoryEvent, MemoryId, SearchFilter};
39use crate::observability::current_request_id;
40use crate::security::{AuditEntry, AuditOutcome, global_logger, record_event};
41use crate::storage::index::SqliteBackend;
42use crate::storage::traits::{IndexBackend, VectorBackend};
43use serde::{Deserialize, Serialize};
44use std::sync::Arc;
45use std::time::Instant;
46use tracing::instrument;
47
48// ============================================================================
49// Data Export Types (GDPR Article 20)
50// ============================================================================
51
52/// Result of a user data export operation.
53///
54/// Contains all memories associated with the user in a portable JSON format.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct UserDataExport {
57    /// Export format version for forward compatibility.
58    pub version: String,
59    /// Timestamp when the export was generated (Unix epoch seconds).
60    pub exported_at: u64,
61    /// Total number of memories exported.
62    pub memory_count: usize,
63    /// All memories belonging to the user.
64    pub memories: Vec<ExportedMemory>,
65    /// Metadata about the export.
66    pub metadata: ExportMetadata,
67}
68
69/// A single memory in the export format.
70///
71/// Uses a flat structure for maximum portability.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ExportedMemory {
74    /// Unique memory identifier.
75    pub id: String,
76    /// Memory content.
77    pub content: String,
78    /// Namespace (e.g., "decisions", "learnings").
79    pub namespace: String,
80    /// Domain (e.g., "project", "user", "org/repo").
81    pub domain: String,
82    /// Project identifier (git remote URL).
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub project_id: Option<String>,
85    /// Branch name (git branch).
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub branch: Option<String>,
88    /// File path relative to repo root.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub file_path: Option<String>,
91    /// Status (e.g., "active", "archived").
92    pub status: String,
93    /// Creation timestamp (Unix epoch seconds).
94    pub created_at: u64,
95    /// Last update timestamp (Unix epoch seconds).
96    pub updated_at: u64,
97    /// Tags for categorization.
98    pub tags: Vec<String>,
99    /// Source reference (file path, URL, etc.).
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub source: Option<String>,
102}
103
104/// Metadata about the export operation.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ExportMetadata {
107    /// Export format identifier.
108    pub format: String,
109    /// Application that generated the export.
110    pub generator: String,
111    /// Generator version.
112    pub generator_version: String,
113}
114
115impl From<Memory> for ExportedMemory {
116    fn from(memory: Memory) -> Self {
117        Self {
118            id: memory.id.to_string(),
119            content: memory.content,
120            namespace: memory.namespace.as_str().to_string(),
121            domain: memory.domain.to_string(),
122            project_id: memory.project_id,
123            branch: memory.branch,
124            file_path: memory.file_path,
125            status: memory.status.as_str().to_string(),
126            created_at: memory.created_at,
127            updated_at: memory.updated_at,
128            tags: memory.tags,
129            source: memory.source,
130        }
131    }
132}
133
134// ============================================================================
135// Consent Tracking Types (GDPR Articles 6 & 7) - COMP-HIGH-003
136// ============================================================================
137
138/// Purpose for which consent is granted.
139///
140/// GDPR requires consent to be specific to a purpose. This enum defines
141/// the granular purposes for which consent can be granted or revoked.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
143#[serde(rename_all = "snake_case")]
144pub enum ConsentPurpose {
145    /// Storage of memory data.
146    DataStorage,
147    /// Processing for search and recall.
148    DataProcessing,
149    /// Generation of embeddings for semantic search.
150    EmbeddingGeneration,
151    /// Use of LLM for enrichment and analysis.
152    LlmProcessing,
153    /// Sync to remote storage.
154    RemoteSync,
155    /// Analytics and metrics collection.
156    Analytics,
157}
158
159impl ConsentPurpose {
160    /// Returns all available consent purposes.
161    #[must_use]
162    pub const fn all() -> &'static [Self] {
163        &[
164            Self::DataStorage,
165            Self::DataProcessing,
166            Self::EmbeddingGeneration,
167            Self::LlmProcessing,
168            Self::RemoteSync,
169            Self::Analytics,
170        ]
171    }
172
173    /// Returns the string representation of the purpose.
174    #[must_use]
175    pub const fn as_str(&self) -> &'static str {
176        match self {
177            Self::DataStorage => "data_storage",
178            Self::DataProcessing => "data_processing",
179            Self::EmbeddingGeneration => "embedding_generation",
180            Self::LlmProcessing => "llm_processing",
181            Self::RemoteSync => "remote_sync",
182            Self::Analytics => "analytics",
183        }
184    }
185
186    /// Returns a human-readable description of the purpose.
187    #[must_use]
188    pub const fn description(&self) -> &'static str {
189        match self {
190            Self::DataStorage => "Store memory content in persistent storage",
191            Self::DataProcessing => "Process memories for search and recall functionality",
192            Self::EmbeddingGeneration => "Generate vector embeddings for semantic search",
193            Self::LlmProcessing => "Use language models for enrichment and analysis",
194            Self::RemoteSync => "Synchronize data to remote storage locations",
195            Self::Analytics => "Collect anonymized usage metrics and analytics",
196        }
197    }
198}
199
200/// A record of consent granted or revoked.
201///
202/// Each consent record captures:
203/// - The specific purpose for which consent applies
204/// - When consent was granted/revoked
205/// - Version of the consent text shown to the user
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ConsentRecord {
208    /// The purpose for which consent is granted.
209    pub purpose: ConsentPurpose,
210    /// Whether consent is currently granted.
211    pub granted: bool,
212    /// Timestamp when consent was recorded (Unix epoch seconds).
213    pub recorded_at: u64,
214    /// Version of the consent text/policy shown.
215    pub policy_version: String,
216    /// Optional identifier for the data subject.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub subject_id: Option<String>,
219    /// Method by which consent was collected (e.g., "cli", "mcp", "config").
220    pub collection_method: String,
221}
222
223impl ConsentRecord {
224    /// Creates a new consent record granting consent.
225    #[must_use]
226    pub fn grant(purpose: ConsentPurpose, policy_version: impl Into<String>) -> Self {
227        Self {
228            purpose,
229            granted: true,
230            recorded_at: crate::current_timestamp(),
231            policy_version: policy_version.into(),
232            subject_id: None,
233            collection_method: "explicit".to_string(),
234        }
235    }
236
237    /// Creates a new consent record revoking consent.
238    #[must_use]
239    pub fn revoke(purpose: ConsentPurpose, policy_version: impl Into<String>) -> Self {
240        Self {
241            purpose,
242            granted: false,
243            recorded_at: crate::current_timestamp(),
244            policy_version: policy_version.into(),
245            subject_id: None,
246            collection_method: "explicit".to_string(),
247        }
248    }
249
250    /// Sets the subject ID.
251    #[must_use]
252    pub fn with_subject(mut self, subject_id: impl Into<String>) -> Self {
253        self.subject_id = Some(subject_id.into());
254        self
255    }
256
257    /// Sets the collection method.
258    #[must_use]
259    pub fn with_method(mut self, method: impl Into<String>) -> Self {
260        self.collection_method = method.into();
261        self
262    }
263}
264
265/// Current consent status for all purposes.
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct ConsentStatus {
268    /// Map of purpose to current consent state.
269    pub consents: std::collections::HashMap<String, bool>,
270    /// Timestamp of the last consent change.
271    pub last_updated: u64,
272    /// Current policy version.
273    pub policy_version: String,
274}
275
276impl Default for ConsentStatus {
277    fn default() -> Self {
278        Self {
279            consents: std::collections::HashMap::new(),
280            last_updated: 0,
281            policy_version: "1.0".to_string(),
282        }
283    }
284}
285
286// ============================================================================
287// Data Deletion Types (GDPR Article 17)
288// ============================================================================
289
290/// Result of a user data deletion operation.
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct DeletionResult {
293    /// Number of memories deleted.
294    pub deleted_count: usize,
295    /// IDs of deleted memories (for audit trail).
296    pub deleted_ids: Vec<String>,
297    /// Number of memories that failed to delete.
298    pub failed_count: usize,
299    /// IDs that failed to delete with error messages.
300    pub failures: Vec<DeletionFailure>,
301    /// Timestamp when deletion was performed (Unix epoch seconds).
302    pub deleted_at: u64,
303    /// Whether deletion was complete (no failures).
304    pub complete: bool,
305}
306
307/// Details about a failed deletion.
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct DeletionFailure {
310    /// Memory ID that failed to delete.
311    pub id: String,
312    /// Error message describing the failure.
313    pub error: String,
314}
315
316// ============================================================================
317// Data Subject Service
318// ============================================================================
319
320/// Service for GDPR data subject rights operations.
321///
322/// Provides:
323/// - `export_user_data()`: Export all user data (Article 20)
324/// - `delete_user_data()`: Delete all user data (Article 17)
325///
326/// # Storage Layers
327///
328/// Operations affect all three storage layers:
329/// 1. **Persistence** (`SQLite`) - Authoritative storage
330/// 2. **Index** (`SQLite` FTS5) - Full-text search index
331/// 3. **Vector** (usearch) - Embedding vectors
332///
333/// # Audit Logging
334///
335/// All operations are logged for compliance:
336/// - `gdpr.export` - Data export requests
337/// - `gdpr.delete` - Data deletion requests
338pub struct DataSubjectService {
339    /// `SQLite` index backend for listing and deleting memories.
340    index: SqliteBackend,
341    /// Optional vector backend for deleting embeddings.
342    vector: Option<Arc<dyn VectorBackend + Send + Sync>>,
343}
344
345impl DataSubjectService {
346    /// Creates a new data subject service with an index backend.
347    ///
348    /// # Arguments
349    ///
350    /// * `index` - `SQLite` index backend for memory operations
351    #[must_use]
352    pub const fn new(index: SqliteBackend) -> Self {
353        Self {
354            index,
355            vector: None,
356        }
357    }
358
359    /// Adds a vector backend for complete deletion of embeddings.
360    #[must_use]
361    pub fn with_vector(mut self, vector: Arc<dyn VectorBackend + Send + Sync>) -> Self {
362        self.vector = Some(vector);
363        self
364    }
365
366    /// Exports all user data in a portable format.
367    ///
368    /// Implements GDPR Article 20 (Right to Data Portability).
369    ///
370    /// # Returns
371    ///
372    /// A [`UserDataExport`] containing all memories in JSON-serializable format.
373    ///
374    /// # Errors
375    ///
376    /// Returns an error if:
377    /// - The index backend fails to list memories
378    /// - Memory retrieval fails
379    ///
380    /// # Audit
381    ///
382    /// Logs a `gdpr.export` event with the count of exported memories.
383    ///
384    /// # Example
385    ///
386    /// ```rust,ignore
387    /// let service = DataSubjectService::new(index);
388    /// let export = service.export_user_data()?;
389    ///
390    /// // Serialize to JSON for download
391    /// let json = serde_json::to_string_pretty(&export)?;
392    /// std::fs::write("my_data.json", json)?;
393    /// ```
394    #[instrument(skip(self), fields(operation = "export_user_data"))]
395    pub fn export_user_data(&self) -> Result<UserDataExport> {
396        let start = Instant::now();
397
398        tracing::info!("Starting GDPR data export");
399
400        // List all memories (no filter = all data)
401        let filter = SearchFilter::new();
402        let memory_ids = self.index.list_all(&filter, usize::MAX)?;
403
404        // Fetch full memory content for each ID
405        let ids: Vec<MemoryId> = memory_ids.into_iter().map(|(id, _)| id).collect();
406        let memories_opt = self.index.get_memories_batch(&ids)?;
407
408        // Convert to export format, filtering out None values
409        let memories: Vec<ExportedMemory> = memories_opt
410            .into_iter()
411            .flatten()
412            .map(ExportedMemory::from)
413            .collect();
414
415        let memory_count = memories.len();
416
417        let export = UserDataExport {
418            version: "1.0".to_string(),
419            exported_at: crate::current_timestamp(),
420            memory_count,
421            memories,
422            metadata: ExportMetadata {
423                format: "subcog-export-v1".to_string(),
424                generator: "subcog".to_string(),
425                generator_version: env!("CARGO_PKG_VERSION").to_string(),
426            },
427        };
428
429        // Audit log the export
430        Self::log_export_event(memory_count);
431
432        // Record metrics
433        Self::record_metrics("export", start, memory_count, 0);
434
435        tracing::info!(
436            memory_count = memory_count,
437            duration_ms = start.elapsed().as_millis(),
438            "GDPR data export completed"
439        );
440
441        Ok(export)
442    }
443
444    /// Deletes all user data from all storage layers.
445    ///
446    /// Implements GDPR Article 17 (Right to Erasure / "Right to be Forgotten").
447    ///
448    /// # Storage Layers Affected
449    ///
450    /// 1. **Index** (`SQLite`) - Memory metadata and FTS index
451    /// 2. **Vector** (usearch) - Embedding vectors (if configured)
452    /// 3. **Persistence** (`SQLite`) - Authoritative storage (if configured)
453    ///
454    /// # Returns
455    ///
456    /// A [`DeletionResult`] with counts and any failures.
457    ///
458    /// # Errors
459    ///
460    /// Returns an error if:
461    /// - The index backend fails to list memories
462    /// - Critical deletion operations fail
463    ///
464    /// Note: Individual memory deletion failures are captured in the result
465    /// rather than causing the entire operation to fail.
466    ///
467    /// # Audit
468    ///
469    /// Logs a `gdpr.delete` event with the count of deleted memories.
470    ///
471    /// # Warning
472    ///
473    /// This operation is **irreversible**. All user data will be permanently deleted.
474    ///
475    /// # Example
476    ///
477    /// ```rust,ignore
478    /// let service = DataSubjectService::new(index)
479    ///     .with_vector(vector_backend);
480    ///
481    /// let result = service.delete_user_data()?;
482    ///
483    /// if result.complete {
484    ///     println!("Successfully deleted {} memories", result.deleted_count);
485    /// } else {
486    ///     eprintln!("Partial deletion: {} failed", result.failed_count);
487    /// }
488    /// ```
489    #[instrument(skip(self), fields(operation = "delete_user_data"))]
490    pub fn delete_user_data(&self) -> Result<DeletionResult> {
491        let start = Instant::now();
492
493        tracing::warn!("Starting GDPR data deletion (irreversible)");
494
495        // List all memories
496        let filter = SearchFilter::new();
497        let memory_ids = self.index.list_all(&filter, usize::MAX)?;
498
499        let ids: Vec<MemoryId> = memory_ids.into_iter().map(|(id, _)| id).collect();
500        let total_count = ids.len();
501
502        if ids.is_empty() {
503            tracing::info!("No memories to delete");
504            return Ok(DeletionResult {
505                deleted_count: 0,
506                deleted_ids: Vec::new(),
507                failed_count: 0,
508                failures: Vec::new(),
509                deleted_at: crate::current_timestamp(),
510                complete: true,
511            });
512        }
513
514        let mut deleted_ids = Vec::with_capacity(total_count);
515        let mut failures = Vec::new();
516
517        // Delete from all storage layers
518        for id in &ids {
519            match self.delete_memory_from_all_layers(id) {
520                Ok(()) => {
521                    deleted_ids.push(id.to_string());
522                    record_event(MemoryEvent::Deleted {
523                        meta: EventMeta::new("gdpr", current_request_id()),
524                        memory_id: id.clone(),
525                        reason: "gdpr.delete_user_data".to_string(),
526                    });
527                },
528                Err(e) => {
529                    tracing::warn!(memory_id = %id, error = %e, "Failed to delete memory");
530                    failures.push(DeletionFailure {
531                        id: id.to_string(),
532                        error: e.to_string(),
533                    });
534                },
535            }
536        }
537
538        let deleted_count = deleted_ids.len();
539        let failed_count = failures.len();
540        let complete = failures.is_empty();
541
542        // Audit log the deletion
543        Self::log_deletion_event(deleted_count, failed_count);
544
545        // Record metrics
546        Self::record_metrics("delete", start, deleted_count, failed_count);
547
548        tracing::info!(
549            deleted_count = deleted_count,
550            failed_count = failed_count,
551            duration_ms = start.elapsed().as_millis(),
552            complete = complete,
553            "GDPR data deletion completed"
554        );
555
556        Ok(DeletionResult {
557            deleted_count,
558            deleted_ids,
559            failed_count,
560            failures,
561            deleted_at: crate::current_timestamp(),
562            complete,
563        })
564    }
565
566    /// Deletes a single memory from all storage layers.
567    ///
568    /// # Deletion Order
569    ///
570    /// 1. Vector backend (if configured) - Delete embedding
571    /// 2. `SQLite` Index - Delete from search index (authoritative)
572    fn delete_memory_from_all_layers(&self, id: &MemoryId) -> Result<()> {
573        // 1. Delete from vector backend (best-effort)
574        if let Some(ref vector) = self.vector
575            && let Err(e) = vector.remove(id)
576        {
577            tracing::debug!(memory_id = %id, error = %e, "Vector deletion failed (continuing)");
578            // Continue - vector is a derived store
579        }
580
581        // 2. Delete from index (authoritative storage)
582        self.index.remove(id)?;
583
584        Ok(())
585    }
586
587    // ========================================================================
588    // Consent Management (GDPR Articles 6 & 7) - COMP-HIGH-003
589    // ========================================================================
590
591    /// Records consent for a specific purpose.
592    ///
593    /// Implements GDPR Article 7 (Conditions for Consent).
594    ///
595    /// # Arguments
596    ///
597    /// * `record` - The consent record to store
598    ///
599    /// # Audit
600    ///
601    /// Logs a `gdpr.consent` event with the purpose and grant status.
602    ///
603    /// # Example
604    ///
605    /// ```rust,ignore
606    /// use subcog::services::{DataSubjectService, ConsentRecord, ConsentPurpose};
607    ///
608    /// let service = DataSubjectService::new(index);
609    /// let record = ConsentRecord::grant(ConsentPurpose::DataStorage, "1.0")
610    ///     .with_method("cli");
611    /// service.record_consent(&record)?;
612    /// ```
613    ///
614    /// # Errors
615    ///
616    /// Returns an error if consent recording fails.
617    #[instrument(skip(self), fields(operation = "record_consent"))]
618    pub fn record_consent(&self, record: &ConsentRecord) -> Result<()> {
619        let purpose = record.purpose.as_str();
620        let granted = record.granted;
621
622        tracing::info!(
623            purpose = purpose,
624            granted = granted,
625            policy_version = %record.policy_version,
626            "Recording consent"
627        );
628
629        // Store consent record (in-memory for now, could be persisted)
630        // For production, this would be stored in the persistence layer
631        Self::log_consent_event(record);
632
633        metrics::counter!(
634            "gdpr_consent_recorded_total",
635            "purpose" => purpose.to_string(),
636            "granted" => granted.to_string()
637        )
638        .increment(1);
639
640        Ok(())
641    }
642
643    /// Revokes consent for a specific purpose.
644    ///
645    /// Creates a revocation record and logs the event for audit.
646    ///
647    /// # Arguments
648    ///
649    /// * `purpose` - The purpose for which consent is being revoked
650    /// * `policy_version` - The current policy version
651    ///
652    /// # Audit
653    ///
654    /// Logs a `gdpr.consent_revoked` event.
655    ///
656    /// # Errors
657    ///
658    /// Returns an error if consent revocation fails.
659    #[instrument(skip(self), fields(operation = "revoke_consent"))]
660    pub fn revoke_consent(&self, purpose: ConsentPurpose, policy_version: &str) -> Result<()> {
661        let record = ConsentRecord::revoke(purpose, policy_version);
662
663        tracing::warn!(
664            purpose = purpose.as_str(),
665            policy_version = policy_version,
666            "Revoking consent"
667        );
668
669        Self::log_consent_event(&record);
670
671        metrics::counter!(
672            "gdpr_consent_revoked_total",
673            "purpose" => purpose.as_str().to_string()
674        )
675        .increment(1);
676
677        Ok(())
678    }
679
680    /// Checks if consent is granted for a specific purpose.
681    ///
682    /// # Returns
683    ///
684    /// `true` if consent is currently granted for the purpose.
685    ///
686    /// # Note
687    ///
688    /// Default implementation returns `true` for backward compatibility.
689    /// Production systems should check persistent consent records.
690    #[must_use]
691    pub fn has_consent(&self, purpose: ConsentPurpose) -> bool {
692        // Log consent check for audit
693        tracing::debug!(purpose = purpose.as_str(), "Checking consent");
694
695        // Default to true for backward compatibility
696        // Production would check persistent storage
697        true
698    }
699
700    /// Returns the current consent status for all purposes.
701    ///
702    /// # Returns
703    ///
704    /// A [`ConsentStatus`] with the current state of all consents.
705    #[must_use]
706    pub fn consent_status(&self) -> ConsentStatus {
707        let mut status = ConsentStatus::default();
708
709        for purpose in ConsentPurpose::all() {
710            status
711                .consents
712                .insert(purpose.as_str().to_string(), self.has_consent(*purpose));
713        }
714
715        status.last_updated = crate::current_timestamp();
716        status
717    }
718
719    /// Logs a consent event to the audit log.
720    fn log_consent_event(record: &ConsentRecord) {
721        if let Some(logger) = global_logger() {
722            let action = if record.granted {
723                "grant_consent"
724            } else {
725                "revoke_consent"
726            };
727
728            let entry = AuditEntry::new("gdpr.consent", action)
729                .with_outcome(AuditOutcome::Success)
730                .with_metadata(serde_json::json!({
731                    "purpose": record.purpose.as_str(),
732                    "granted": record.granted,
733                    "policy_version": record.policy_version,
734                    "collection_method": record.collection_method,
735                    "gdpr_article": if record.granted {
736                        "Article 6 - Lawful Basis for Processing"
737                    } else {
738                        "Article 7(3) - Right to Withdraw Consent"
739                    }
740                }));
741            logger.log_entry(entry);
742        }
743    }
744
745    /// Logs an export event to the audit log.
746    fn log_export_event(memory_count: usize) {
747        if let Some(logger) = global_logger() {
748            let entry = AuditEntry::new("gdpr.export", "export_user_data")
749                .with_outcome(AuditOutcome::Success)
750                .with_metadata(serde_json::json!({
751                    "memory_count": memory_count,
752                    "gdpr_article": "Article 20 - Right to Data Portability"
753                }));
754            logger.log_entry(entry);
755        }
756    }
757
758    /// Logs a deletion event to the audit log.
759    fn log_deletion_event(deleted_count: usize, failed_count: usize) {
760        if let Some(logger) = global_logger() {
761            let outcome = if failed_count == 0 {
762                AuditOutcome::Success
763            } else {
764                AuditOutcome::Failure
765            };
766
767            let entry = AuditEntry::new("gdpr.delete", "delete_user_data")
768                .with_outcome(outcome)
769                .with_metadata(serde_json::json!({
770                    "deleted_count": deleted_count,
771                    "failed_count": failed_count,
772                    "gdpr_article": "Article 17 - Right to Erasure"
773                }));
774            logger.log_entry(entry);
775        }
776    }
777
778    /// Records metrics for the operation.
779    fn record_metrics(operation: &str, start: Instant, success_count: usize, failure_count: usize) {
780        metrics::counter!(
781            "gdpr_operations_total",
782            "operation" => operation.to_string(),
783            "status" => if failure_count == 0 { "success" } else { "partial" }
784        )
785        .increment(1);
786
787        metrics::histogram!(
788            "gdpr_operation_duration_ms",
789            "operation" => operation.to_string()
790        )
791        .record(start.elapsed().as_secs_f64() * 1000.0);
792
793        metrics::gauge!(
794            "gdpr_last_operation_count",
795            "operation" => operation.to_string()
796        )
797        .set(success_count as f64);
798
799        if failure_count > 0 {
800            metrics::counter!(
801                "gdpr_operation_failures_total",
802                "operation" => operation.to_string()
803            )
804            .increment(failure_count as u64);
805        }
806    }
807}
808
809// ============================================================================
810// Tests
811// ============================================================================
812
813#[cfg(test)]
814mod tests {
815    use super::*;
816    use crate::models::{Domain, MemoryStatus, Namespace};
817    use crate::storage::index::SqliteBackend;
818
819    fn create_test_memory(id: &str, content: &str, namespace: Namespace) -> Memory {
820        Memory {
821            id: MemoryId::new(id),
822            content: content.to_string(),
823            namespace,
824            domain: Domain::new(),
825            project_id: None,
826            branch: None,
827            file_path: None,
828            status: MemoryStatus::Active,
829            created_at: 1_700_000_000,
830            updated_at: 1_700_000_000,
831            tombstoned_at: None,
832            expires_at: None,
833            embedding: None,
834            tags: vec!["test".to_string()],
835            #[cfg(feature = "group-scope")]
836            group_id: None,
837            source: Some("test.rs".to_string()),
838            is_summary: false,
839            source_memory_ids: None,
840            consolidation_timestamp: None,
841        }
842    }
843
844    #[test]
845    fn test_export_user_data_empty() {
846        let index = SqliteBackend::in_memory().expect("Failed to create index");
847        let service = DataSubjectService::new(index);
848
849        let export = service.export_user_data().expect("Export failed");
850
851        assert_eq!(export.version, "1.0");
852        assert_eq!(export.memory_count, 0);
853        assert!(export.memories.is_empty());
854        assert_eq!(export.metadata.format, "subcog-export-v1");
855        assert_eq!(export.metadata.generator, "subcog");
856    }
857
858    #[test]
859    fn test_export_user_data_with_memories() {
860        let index = SqliteBackend::in_memory().expect("Failed to create index");
861
862        // Add test memories
863        let mem1 = create_test_memory("id1", "Decision: Use PostgreSQL", Namespace::Decisions);
864        let mem2 = create_test_memory("id2", "Learned: Connection pooling", Namespace::Learnings);
865        let mem3 = create_test_memory("id3", "Pattern: Repository pattern", Namespace::Patterns);
866
867        index.index(&mem1).expect("Index failed");
868        index.index(&mem2).expect("Index failed");
869        index.index(&mem3).expect("Index failed");
870
871        let service = DataSubjectService::new(index);
872        let export = service.export_user_data().expect("Export failed");
873
874        assert_eq!(export.memory_count, 3);
875        assert_eq!(export.memories.len(), 3);
876
877        // Verify memory content is included
878        let ids: Vec<_> = export.memories.iter().map(|m| m.id.as_str()).collect();
879        assert!(ids.contains(&"id1"));
880        assert!(ids.contains(&"id2"));
881        assert!(ids.contains(&"id3"));
882
883        // Verify all fields are exported
884        let decision = export.memories.iter().find(|m| m.id == "id1").unwrap();
885        assert_eq!(decision.content, "Decision: Use PostgreSQL");
886        assert_eq!(decision.namespace, "decisions");
887        assert_eq!(decision.status, "active");
888        assert_eq!(decision.tags, vec!["test".to_string()]);
889        assert_eq!(decision.source, Some("test.rs".to_string()));
890    }
891
892    #[test]
893    fn test_delete_user_data_empty() {
894        let index = SqliteBackend::in_memory().expect("Failed to create index");
895        let service = DataSubjectService::new(index);
896
897        let result = service.delete_user_data().expect("Delete failed");
898
899        assert_eq!(result.deleted_count, 0);
900        assert!(result.deleted_ids.is_empty());
901        assert_eq!(result.failed_count, 0);
902        assert!(result.failures.is_empty());
903        assert!(result.complete);
904    }
905
906    #[test]
907    fn test_delete_user_data_with_memories() {
908        let index = SqliteBackend::in_memory().expect("Failed to create index");
909
910        // Add test memories
911        let mem1 = create_test_memory("id1", "To be deleted 1", Namespace::Decisions);
912        let mem2 = create_test_memory("id2", "To be deleted 2", Namespace::Learnings);
913
914        index.index(&mem1).expect("Index failed");
915        index.index(&mem2).expect("Index failed");
916
917        // Verify memories exist
918        let filter = SearchFilter::new();
919        let before = index.list_all(&filter, 100).expect("List failed");
920        assert_eq!(before.len(), 2);
921
922        let service = DataSubjectService::new(index);
923        let result = service.delete_user_data().expect("Delete failed");
924
925        assert_eq!(result.deleted_count, 2);
926        assert_eq!(result.deleted_ids.len(), 2);
927        assert!(result.deleted_ids.contains(&"id1".to_string()));
928        assert!(result.deleted_ids.contains(&"id2".to_string()));
929        assert_eq!(result.failed_count, 0);
930        assert!(result.complete);
931
932        // Verify memories are gone - need to access the index directly
933        // Since we consumed it, we check the result instead
934        assert!(result.complete);
935    }
936
937    #[test]
938    fn test_exported_memory_from_memory() {
939        let memory = create_test_memory("test-id", "Test content", Namespace::Security);
940        let exported = ExportedMemory::from(memory);
941
942        assert_eq!(exported.id, "test-id");
943        assert_eq!(exported.content, "Test content");
944        assert_eq!(exported.namespace, "security");
945        assert_eq!(exported.status, "active");
946    }
947
948    #[test]
949    fn test_export_serialization() {
950        let export = UserDataExport {
951            version: "1.0".to_string(),
952            exported_at: 1_700_000_000,
953            memory_count: 1,
954            memories: vec![ExportedMemory {
955                id: "test".to_string(),
956                content: "Content".to_string(),
957                namespace: "decisions".to_string(),
958                domain: "project".to_string(),
959                project_id: None,
960                branch: None,
961                file_path: None,
962                status: "active".to_string(),
963                created_at: 1_700_000_000,
964                updated_at: 1_700_000_000,
965                tags: vec!["tag1".to_string()],
966                source: None,
967            }],
968            metadata: ExportMetadata {
969                format: "subcog-export-v1".to_string(),
970                generator: "subcog".to_string(),
971                generator_version: "0.1.0".to_string(),
972            },
973        };
974
975        let json = serde_json::to_string(&export).expect("Serialization failed");
976        assert!(json.contains("subcog-export-v1"));
977        assert!(json.contains("decisions"));
978
979        // Verify deserialization
980        let parsed: UserDataExport = serde_json::from_str(&json).expect("Deserialization failed");
981        assert_eq!(parsed.memory_count, 1);
982        assert_eq!(parsed.memories[0].id, "test");
983    }
984
985    #[test]
986    fn test_deletion_result_serialization() {
987        let result = DeletionResult {
988            deleted_count: 5,
989            deleted_ids: vec!["id1".to_string(), "id2".to_string()],
990            failed_count: 1,
991            failures: vec![DeletionFailure {
992                id: "id3".to_string(),
993                error: "Not found".to_string(),
994            }],
995            deleted_at: 1_700_000_000,
996            complete: false,
997        };
998
999        let json = serde_json::to_string(&result).expect("Serialization failed");
1000        assert!(json.contains("deleted_count"));
1001        assert!(json.contains("complete"));
1002
1003        let parsed: DeletionResult = serde_json::from_str(&json).expect("Deserialization failed");
1004        assert_eq!(parsed.deleted_count, 5);
1005        assert!(!parsed.complete);
1006    }
1007
1008    #[test]
1009    fn test_service_builder_pattern() {
1010        let index = SqliteBackend::in_memory().expect("Failed to create index");
1011
1012        // Test builder pattern compiles and works
1013        let service = DataSubjectService::new(index);
1014
1015        // Service should work without vector backend
1016        let export = service.export_user_data();
1017        assert!(export.is_ok());
1018    }
1019
1020    #[test]
1021    fn test_export_preserves_all_namespaces() {
1022        let index = SqliteBackend::in_memory().expect("Failed to create index");
1023
1024        // Add memories with different namespaces
1025        for ns in Namespace::user_namespaces() {
1026            let mem = create_test_memory(
1027                &format!("id-{}", ns.as_str()),
1028                &format!("Content for {}", ns.as_str()),
1029                *ns,
1030            );
1031            index.index(&mem).expect("Index failed");
1032        }
1033
1034        let service = DataSubjectService::new(index);
1035        let export = service.export_user_data().expect("Export failed");
1036
1037        // Should have exported all user namespaces
1038        assert_eq!(export.memory_count, Namespace::user_namespaces().len());
1039
1040        // Verify each namespace is represented
1041        let namespaces: Vec<_> = export
1042            .memories
1043            .iter()
1044            .map(|m| m.namespace.as_str())
1045            .collect();
1046        for ns in Namespace::user_namespaces() {
1047            assert!(
1048                namespaces.contains(&ns.as_str()),
1049                "Missing namespace: {}",
1050                ns.as_str()
1051            );
1052        }
1053    }
1054
1055    #[test]
1056    fn test_delete_clears_all_memories() {
1057        let index = SqliteBackend::in_memory().expect("Failed to create index");
1058
1059        // Add many memories
1060        for i in 0..10 {
1061            let mem = create_test_memory(
1062                &format!("bulk-{i}"),
1063                &format!("Bulk memory {i}"),
1064                Namespace::Decisions,
1065            );
1066            index.index(&mem).expect("Index failed");
1067        }
1068
1069        let service = DataSubjectService::new(index);
1070
1071        // Delete all
1072        let result = service.delete_user_data().expect("Delete failed");
1073
1074        assert_eq!(result.deleted_count, 10);
1075        assert!(result.complete);
1076
1077        // Export should now be empty
1078        let export = service.export_user_data().expect("Export failed");
1079        assert_eq!(export.memory_count, 0);
1080    }
1081
1082    // ========================================================================
1083    // Consent Tracking Tests (COMP-HIGH-003)
1084    // ========================================================================
1085
1086    #[test]
1087    fn test_consent_purpose_all() {
1088        let all = ConsentPurpose::all();
1089        assert_eq!(all.len(), 6);
1090        assert!(all.contains(&ConsentPurpose::DataStorage));
1091        assert!(all.contains(&ConsentPurpose::DataProcessing));
1092        assert!(all.contains(&ConsentPurpose::EmbeddingGeneration));
1093        assert!(all.contains(&ConsentPurpose::LlmProcessing));
1094        assert!(all.contains(&ConsentPurpose::RemoteSync));
1095        assert!(all.contains(&ConsentPurpose::Analytics));
1096    }
1097
1098    #[test]
1099    fn test_consent_purpose_as_str() {
1100        assert_eq!(ConsentPurpose::DataStorage.as_str(), "data_storage");
1101        assert_eq!(ConsentPurpose::DataProcessing.as_str(), "data_processing");
1102        assert_eq!(
1103            ConsentPurpose::EmbeddingGeneration.as_str(),
1104            "embedding_generation"
1105        );
1106        assert_eq!(ConsentPurpose::LlmProcessing.as_str(), "llm_processing");
1107        assert_eq!(ConsentPurpose::RemoteSync.as_str(), "remote_sync");
1108        assert_eq!(ConsentPurpose::Analytics.as_str(), "analytics");
1109    }
1110
1111    #[test]
1112    fn test_consent_purpose_description() {
1113        // All purposes should have descriptions
1114        for purpose in ConsentPurpose::all() {
1115            let desc = purpose.description();
1116            assert!(!desc.is_empty());
1117            assert!(desc.len() > 10); // Descriptions should be meaningful
1118        }
1119    }
1120
1121    #[test]
1122    fn test_consent_record_grant() {
1123        let record = ConsentRecord::grant(ConsentPurpose::DataStorage, "1.0");
1124
1125        assert_eq!(record.purpose, ConsentPurpose::DataStorage);
1126        assert!(record.granted);
1127        assert_eq!(record.policy_version, "1.0");
1128        assert!(record.recorded_at > 0);
1129        assert_eq!(record.collection_method, "explicit");
1130        assert!(record.subject_id.is_none());
1131    }
1132
1133    #[test]
1134    fn test_consent_record_revoke() {
1135        let record = ConsentRecord::revoke(ConsentPurpose::Analytics, "2.0");
1136
1137        assert_eq!(record.purpose, ConsentPurpose::Analytics);
1138        assert!(!record.granted);
1139        assert_eq!(record.policy_version, "2.0");
1140    }
1141
1142    #[test]
1143    fn test_consent_record_builders() {
1144        let record = ConsentRecord::grant(ConsentPurpose::LlmProcessing, "1.0")
1145            .with_subject("user-123")
1146            .with_method("mcp");
1147
1148        assert_eq!(record.subject_id, Some("user-123".to_string()));
1149        assert_eq!(record.collection_method, "mcp");
1150    }
1151
1152    #[test]
1153    fn test_consent_record_serialization() {
1154        let record =
1155            ConsentRecord::grant(ConsentPurpose::DataStorage, "1.0").with_subject("test-user");
1156
1157        let json = serde_json::to_string(&record).expect("Serialization failed");
1158        assert!(json.contains("data_storage"));
1159        assert!(json.contains("test-user"));
1160        assert!(json.contains("\"granted\":true"));
1161
1162        let parsed: ConsentRecord = serde_json::from_str(&json).expect("Deserialization failed");
1163        assert_eq!(parsed.purpose, ConsentPurpose::DataStorage);
1164        assert!(parsed.granted);
1165    }
1166
1167    #[test]
1168    fn test_consent_status_default() {
1169        let status = ConsentStatus::default();
1170
1171        assert!(status.consents.is_empty());
1172        assert_eq!(status.last_updated, 0);
1173        assert_eq!(status.policy_version, "1.0");
1174    }
1175
1176    #[test]
1177    fn test_service_record_consent() {
1178        let index = SqliteBackend::in_memory().expect("Failed to create index");
1179        let service = DataSubjectService::new(index);
1180
1181        let record = ConsentRecord::grant(ConsentPurpose::DataStorage, "1.0");
1182        let result = service.record_consent(&record);
1183
1184        assert!(result.is_ok());
1185    }
1186
1187    #[test]
1188    fn test_service_revoke_consent() {
1189        let index = SqliteBackend::in_memory().expect("Failed to create index");
1190        let service = DataSubjectService::new(index);
1191
1192        let result = service.revoke_consent(ConsentPurpose::Analytics, "1.0");
1193
1194        assert!(result.is_ok());
1195    }
1196
1197    #[test]
1198    fn test_service_has_consent() {
1199        let index = SqliteBackend::in_memory().expect("Failed to create index");
1200        let service = DataSubjectService::new(index);
1201
1202        // Default is true for backward compatibility
1203        assert!(service.has_consent(ConsentPurpose::DataStorage));
1204        assert!(service.has_consent(ConsentPurpose::Analytics));
1205    }
1206
1207    #[test]
1208    fn test_service_consent_status() {
1209        let index = SqliteBackend::in_memory().expect("Failed to create index");
1210        let service = DataSubjectService::new(index);
1211
1212        let status = service.consent_status();
1213
1214        // Should have all purposes
1215        assert_eq!(status.consents.len(), 6);
1216        assert!(status.last_updated > 0);
1217
1218        // All should be true by default
1219        for granted in status.consents.values() {
1220            assert!(*granted);
1221        }
1222    }
1223
1224    #[test]
1225    fn test_consent_status_serialization() {
1226        let mut status = ConsentStatus::default();
1227        status.consents.insert("data_storage".to_string(), true);
1228        status.consents.insert("analytics".to_string(), false);
1229        status.last_updated = 1_700_000_000;
1230
1231        let json = serde_json::to_string(&status).expect("Serialization failed");
1232        assert!(json.contains("data_storage"));
1233        assert!(json.contains("analytics"));
1234
1235        let parsed: ConsentStatus = serde_json::from_str(&json).expect("Deserialization failed");
1236        assert_eq!(parsed.consents.get("data_storage"), Some(&true));
1237        assert_eq!(parsed.consents.get("analytics"), Some(&false));
1238    }
1239}