Skip to main content

termivar_scanner/knowledge/
snapshot.rs

1//! Immutable subject snapshots and revision/authority guards.
2
3use termivar_core::{EntityId, Evidence, Fact, Hypothesis, HypothesisState, Ontology};
4
5use crate::knowledge::{
6    collect_indexed, subject_revision, validate_revisions, KnowledgeAuthority, KnowledgeBase,
7    KnowledgeBaseError,
8};
9
10/// Consistent, immutable knowledge for one subject at one point in time.
11///
12/// Rule evaluation uses this snapshot so every expression in one decision
13/// cycle observes the same ontology, evidence, facts, and hypotheses.
14#[derive(Debug, Clone)]
15pub struct KnowledgeSnapshot {
16    authority: KnowledgeAuthority,
17    subject: EntityId,
18    subject_revision: u64,
19    ontology_revision: u64,
20    ontology: Ontology,
21    evidence: Vec<Evidence>,
22    facts: Vec<Fact>,
23    hypotheses: Vec<Hypothesis>,
24}
25
26impl KnowledgeSnapshot {
27    /// Returns the subject captured by this snapshot.
28    pub fn subject(&self) -> &EntityId {
29        &self.subject
30    }
31
32    /// Returns the subject-local knowledge revision captured by this snapshot.
33    pub fn subject_revision(&self) -> u64 {
34        self.subject_revision
35    }
36
37    /// Returns the global ontology revision captured by this snapshot.
38    pub fn ontology_revision(&self) -> u64 {
39        self.ontology_revision
40    }
41
42    /// Returns evidence ordered by stable evidence ID.
43    pub fn evidence(&self) -> &[Evidence] {
44        &self.evidence
45    }
46
47    /// Returns facts ordered by stable fact ID.
48    pub fn facts(&self) -> &[Fact] {
49        &self.facts
50    }
51
52    /// Returns hypotheses ordered by stable hypothesis ID.
53    pub fn hypotheses(&self) -> &[Hypothesis] {
54        &self.hypotheses
55    }
56
57    /// Returns the ontology captured in the same read transaction.
58    pub fn ontology(&self) -> &Ontology {
59        &self.ontology
60    }
61
62    pub(crate) fn authority(&self) -> &KnowledgeAuthority {
63        &self.authority
64    }
65
66    pub(crate) fn with_evidence_correlation(&self, correlation_id: &str) -> Self {
67        Self {
68            authority: self.authority.clone(),
69            subject: self.subject.clone(),
70            subject_revision: self.subject_revision,
71            ontology_revision: self.ontology_revision,
72            ontology: self.ontology.clone(),
73            evidence: self
74                .evidence
75                .iter()
76                .filter(|evidence| evidence.source().correlation_id() == Some(correlation_id))
77                .cloned()
78                .collect(),
79            facts: self.facts.clone(),
80            hypotheses: self.hypotheses.clone(),
81        }
82    }
83
84    pub(crate) fn with_projected_hypothesis_state(
85        &self,
86        hypothesis_id: &str,
87        state: HypothesisState,
88    ) -> Option<Self> {
89        let mut projected = self.clone();
90        projected
91            .hypotheses
92            .iter_mut()
93            .find(|hypothesis| hypothesis.id() == hypothesis_id)?
94            .set_state(state);
95        Some(projected)
96    }
97}
98
99impl KnowledgeBase {
100    /// Captures all rule-visible knowledge for a subject under one read lock.
101    pub fn snapshot_for_subject(&self, subject: &EntityId) -> KnowledgeSnapshot {
102        let state = self.read_state();
103        KnowledgeSnapshot {
104            authority: self.authority.clone(),
105            subject: subject.clone(),
106            subject_revision: subject_revision(&state, subject),
107            ontology_revision: state.ontology_revision,
108            ontology: state.ontology.clone(),
109            evidence: collect_indexed(state.evidence_by_subject.get(subject), &state.evidence),
110            facts: collect_indexed(state.facts_by_subject.get(subject), &state.facts),
111            hypotheses: collect_indexed(
112                state.hypotheses_by_subject.get(subject),
113                &state.hypotheses,
114            ),
115        }
116    }
117
118    /// Validates snapshot revisions without cloning rule-visible records.
119    pub(crate) fn validate_snapshot_revisions(
120        &self,
121        subject: &EntityId,
122        expected_subject_revision: u64,
123        expected_ontology_revision: u64,
124    ) -> Result<(), KnowledgeBaseError> {
125        let state = self.read_state();
126        validate_revisions(
127            &state,
128            subject,
129            expected_subject_revision,
130            expected_ontology_revision,
131        )
132    }
133
134    /// Rejects a snapshot token minted by a different in-memory knowledge base.
135    pub(crate) fn validate_snapshot_authority(
136        &self,
137        authority: &KnowledgeAuthority,
138        subject: &EntityId,
139    ) -> Result<(), KnowledgeBaseError> {
140        if self.authority.is_same_as(authority) {
141            Ok(())
142        } else {
143            Err(KnowledgeBaseError::SnapshotAuthorityMismatch {
144                subject: subject.clone(),
145            })
146        }
147    }
148
149    /// Runs a short external commit only while a snapshot remains current.
150    ///
151    /// The read lock stays held for the callback, preventing knowledge writers
152    /// from invalidating the snapshot between the revision check and the
153    /// external state transition. The callback must not call back into this
154    /// knowledge base.
155    #[cfg(feature = "scanning")]
156    pub(crate) fn commit_if_snapshot_current<T>(
157        &self,
158        snapshot: &KnowledgeSnapshot,
159        commit: impl FnOnce() -> T,
160    ) -> Result<T, KnowledgeBaseError> {
161        self.validate_snapshot_authority(snapshot.authority(), snapshot.subject())?;
162        let state = self.read_state();
163        validate_revisions(
164            &state,
165            snapshot.subject(),
166            snapshot.subject_revision(),
167            snapshot.ontology_revision(),
168        )?;
169        Ok(commit())
170    }
171}