Skip to main content

graphforge_knowledge/research/
suppressions.rs

1//! Append-only scope events preserve immutable assertions and shared graph objects.
2use super::{MAX_RESEARCH_ROWS, ResearchSuppressionRecord, encoding};
3use crate::{KnowledgeError, require_uuid, require_v7};
4use arrow::record_batch::RecordBatch;
5use std::collections::{HashMap, HashSet};
6use uuid::Uuid;
7
8/// Frozen suppression history, independent of supported/canonical status.
9#[derive(Debug, Clone, Default, PartialEq, Eq)]
10pub struct ResearchSuppressionLedger {
11    events: Vec<ResearchSuppressionRecord>,
12}
13impl ResearchSuppressionLedger {
14    /// Validate exact immutable event identities and deterministic ordering.
15    pub fn new(mut events: Vec<ResearchSuppressionRecord>) -> Result<Self, KnowledgeError> {
16        if events.len() > MAX_RESEARCH_ROWS {
17            return Err(KnowledgeError::Limit {
18                participant: "claim_suppressions",
19                observed: events.len(),
20                limit: MAX_RESEARCH_ROWS,
21            });
22        }
23        let mut ids = HashSet::new();
24        for row in &events {
25            require_v7(row.suppression_uuid, "suppression_uuid")?;
26            require_v7(row.assertion_uuid, "assertion_uuid")?;
27            for (id, field) in [
28                (row.context_uuid, "context_uuid"),
29                (row.creator_uuid, "creator_uuid"),
30                (row.provenance_uuid, "provenance_uuid"),
31            ] {
32                require_uuid(id, field)?;
33            }
34            if !ids.insert(row.suppression_uuid) {
35                return Err(KnowledgeError::Duplicate("suppression_uuid"));
36            }
37        }
38        events.sort_by_key(|row| row.suppression_uuid);
39        Ok(Self { events })
40    }
41    /// Merge selected histories without rewriting an existing event.
42    pub fn merge(&self, staged: &Self) -> Result<Self, KnowledgeError> {
43        let old: HashMap<_, _> = self
44            .events
45            .iter()
46            .map(|row| (row.suppression_uuid, row))
47            .collect();
48        let mut rows = self.events.clone();
49        for row in &staged.events {
50            if let Some(existing) = old.get(&row.suppression_uuid) {
51                if *existing != row {
52                    return Err(KnowledgeError::Conflict("suppression_uuid"));
53                }
54            } else {
55                if rows.len() == MAX_RESEARCH_ROWS {
56                    return Err(KnowledgeError::Limit {
57                        participant: "claim_suppressions",
58                        observed: rows.len() + 1,
59                        limit: MAX_RESEARCH_ROWS,
60                    });
61                }
62                rows.push(row.clone());
63            }
64        }
65        Self::new(rows)
66    }
67    /// Inspect retained history including inherited events.
68    #[must_use]
69    pub fn events(&self) -> &[ResearchSuppressionRecord] {
70        &self.events
71    }
72    /// Suppression in the exact context or its frozen inherited context chain.
73    #[must_use]
74    pub fn is_suppressed(&self, assertion: Uuid, contexts: &HashSet<Uuid>) -> bool {
75        self.events
76            .iter()
77            .any(|row| row.assertion_uuid == assertion && contexts.contains(&row.context_uuid))
78    }
79    /// Encode typed scope events as Arrow.
80    pub fn batch(&self) -> Result<RecordBatch, KnowledgeError> {
81        encoding::suppression_batch(&self.events)
82    }
83    /// Decode bounded immutable scope events.
84    pub fn from_batches(batches: &[RecordBatch]) -> Result<Self, KnowledgeError> {
85        Self::new(encoding::suppression_rows(batches)?)
86    }
87}