Skip to main content

termivar_scanner/knowledge/
store.rs

1//! Knowledge-base ownership, ontology lifecycle, statistics, and lock boundaries.
2
3use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
4
5use termivar_core::{
6    ConceptId, Ontology, OntologyAxiom, OntologyConcept, OntologyError, OntologyRelationType,
7    OntologyWrite, RelationTypeId,
8};
9
10use crate::knowledge::{
11    bump_ontology_revision, KnowledgeAuthority, KnowledgeBaseStats, KnowledgeState,
12};
13
14/// Thread-safe, indexed knowledge shared by evidence and decision engines.
15///
16/// Writes are atomic across primary records and secondary indexes. Read methods
17/// return owned snapshots, so callers never keep an internal lock while doing
18/// asynchronous work.
19///
20/// Ontology definitions provide domain meaning, while entities and relations
21/// form the instance graph. Evidence and entities are immutable once their IDs
22/// are observed. Facts, hypotheses, and relations may be updated only while
23/// their claim identity or graph identity remains unchanged.
24///
25/// # Examples
26///
27/// ```rust
28/// use termivar_core::{
29///     ConfidenceScore, EntityId, Evidence, EvidenceKind, EvidenceSource,
30///     EvidenceValue, KnowledgePredicate,
31/// };
32/// use termivar_scanner::{KnowledgeBase, KnowledgeWrite};
33///
34/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
35/// let knowledge = KnowledgeBase::new();
36/// let subject = EntityId::new("endpoint:https://example.test")?;
37/// let predicate = KnowledgePredicate::new("http.header", "server")?;
38/// let evidence = Evidence::new(
39///     subject.clone(),
40///     EvidenceKind::Http,
41///     predicate.clone(),
42///     EvidenceValue::Text("nginx".into()),
43///     EvidenceSource::new("discovery.headers", "server-header")?,
44///     ConfidenceScore::from_percent(85)?,
45/// );
46///
47/// assert_eq!(knowledge.insert_evidence(evidence)?, KnowledgeWrite::Inserted);
48/// assert_eq!(knowledge.evidence_for_subject(&subject).len(), 1);
49/// assert_eq!(knowledge.evidence_for_predicate(&predicate).len(), 1);
50/// # Ok(())
51/// # }
52/// ```
53#[derive(Debug, Clone, Default)]
54pub struct KnowledgeBase {
55    pub(super) authority: KnowledgeAuthority,
56    state: Arc<RwLock<KnowledgeState>>,
57}
58
59impl KnowledgeBase {
60    /// Creates an empty knowledge base with standard ontology relation types.
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Registers a domain concept in the ontology.
66    pub fn register_concept(
67        &self,
68        concept: OntologyConcept,
69    ) -> Result<OntologyWrite, OntologyError> {
70        let mut state = self.write_state();
71        let write = state.ontology.add_concept(concept)?;
72        if write == OntologyWrite::Inserted {
73            bump_ontology_revision(&mut state);
74        }
75        Ok(write)
76    }
77
78    /// Registers a custom semantic relation type in the ontology.
79    pub fn register_relation_type(
80        &self,
81        relation_type: OntologyRelationType,
82    ) -> Result<OntologyWrite, OntologyError> {
83        let mut state = self.write_state();
84        let write = state.ontology.add_relation_type(relation_type)?;
85        if write == OntologyWrite::Inserted {
86            bump_ontology_revision(&mut state);
87        }
88        Ok(write)
89    }
90
91    /// Registers a validated semantic axiom in the ontology.
92    pub fn register_axiom(&self, axiom: OntologyAxiom) -> Result<OntologyWrite, OntologyError> {
93        let mut state = self.write_state();
94        let write = state.ontology.add_axiom(axiom)?;
95        if write == OntologyWrite::Inserted {
96            bump_ontology_revision(&mut state);
97        }
98        Ok(write)
99    }
100
101    pub(crate) fn install_ontology_definitions(
102        &self,
103        concepts: &[OntologyConcept],
104        axioms: &[OntologyAxiom],
105    ) -> Result<(usize, usize), OntologyError> {
106        let mut state = self.write_state();
107        let mut prospective = state.ontology.clone();
108        let mut concepts_inserted = 0;
109        let mut axioms_inserted = 0;
110
111        for concept in concepts {
112            concepts_inserted += usize::from(matches!(
113                prospective.add_concept(concept.clone())?,
114                OntologyWrite::Inserted
115            ));
116        }
117        for axiom in axioms {
118            axioms_inserted += usize::from(matches!(
119                prospective.add_axiom(axiom.clone())?,
120                OntologyWrite::Inserted
121            ));
122        }
123
124        state.ontology = prospective;
125        if concepts_inserted != 0 || axioms_inserted != 0 {
126            bump_ontology_revision(&mut state);
127        }
128        Ok((concepts_inserted, axioms_inserted))
129    }
130
131    /// Returns an owned, internally consistent ontology snapshot.
132    pub fn ontology_snapshot(&self) -> Ontology {
133        self.read_state().ontology.clone()
134    }
135
136    /// Evaluates a semantic relationship using the registered ontology.
137    pub fn ontology_is_related(
138        &self,
139        subject: &ConceptId,
140        relation: &RelationTypeId,
141        object: &ConceptId,
142    ) -> Result<bool, OntologyError> {
143        self.read_state()
144            .ontology
145            .is_related(subject, relation, object)
146    }
147
148    /// Evaluates the canonical transitive ontology hierarchy.
149    pub fn ontology_is_a(
150        &self,
151        child: &ConceptId,
152        ancestor: &ConceptId,
153    ) -> Result<bool, OntologyError> {
154        self.read_state().ontology.is_a(child, ancestor)
155    }
156
157    /// Returns a consistent count snapshot under one read lock.
158    pub fn stats(&self) -> KnowledgeBaseStats {
159        let state = self.read_state();
160        KnowledgeBaseStats {
161            evidence: state.evidence.len(),
162            facts: state.facts.len(),
163            hypotheses: state.hypotheses.len(),
164            entities: state.entities.len(),
165            relations: state.relations.len(),
166            ontology: state.ontology.stats(),
167        }
168    }
169
170    pub(super) fn read_state(&self) -> RwLockReadGuard<'_, KnowledgeState> {
171        self.state
172            .read()
173            .unwrap_or_else(std::sync::PoisonError::into_inner)
174    }
175
176    pub(super) fn write_state(&self) -> RwLockWriteGuard<'_, KnowledgeState> {
177        self.state
178            .write()
179            .unwrap_or_else(std::sync::PoisonError::into_inner)
180    }
181}