1use std::{collections::HashMap, fmt};
4
5use serde::{Deserialize, Serialize};
6use termivar_core::{
7 EntityId, Evidence, EvidenceId, Fact, Hypothesis, HypothesisState, OntologyStats,
8};
9
10use crate::knowledge::{index, KnowledgeBase, KnowledgeSnapshot, KnowledgeState};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15#[non_exhaustive]
16pub enum KnowledgeWrite {
17 Inserted,
19 Updated,
21 Unchanged,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub(crate) enum HypothesisStateTransition {
27 Missing,
28 SubjectMismatch {
29 actual: EntityId,
30 },
31 StaleSnapshot(KnowledgeBaseError),
32 TerminalConflict {
33 current: HypothesisState,
34 attempted: HypothesisState,
35 },
36 Written(KnowledgeWrite),
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum KnowledgeRecordKind {
43 Evidence,
45 Fact,
47 Hypothesis,
49 Entity,
51 Relation,
53}
54
55impl fmt::Display for KnowledgeRecordKind {
56 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57 let name = match self {
58 Self::Evidence => "evidence",
59 Self::Fact => "fact",
60 Self::Hypothesis => "hypothesis",
61 Self::Entity => "entity",
62 Self::Relation => "relation",
63 };
64 formatter.write_str(name)
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum KnowledgeBaseError {
72 SnapshotAuthorityMismatch {
74 subject: EntityId,
76 },
77
78 IdentityConflict {
80 kind: KnowledgeRecordKind,
82 id: String,
84 },
85
86 RelationEvidenceMismatch {
88 relation_id: String,
90 evidence_id: String,
92 },
93
94 RelationSubjectMismatch {
96 relation_id: String,
98 evidence_subject: String,
100 relation_from: String,
102 },
103
104 RelationLimitExceeded {
106 field: &'static str,
109 actual: usize,
111 maximum: usize,
113 },
114
115 StaleSnapshot {
117 subject: EntityId,
119 expected_subject_revision: u64,
121 actual_subject_revision: u64,
123 expected_ontology_revision: u64,
125 actual_ontology_revision: u64,
127 },
128
129 ReasoningSubjectMismatch {
131 hypothesis_id: String,
133 expected: EntityId,
135 actual: EntityId,
137 },
138
139 MissingDerivationParent {
142 child: String,
144 parent: String,
146 },
147
148 SelfDerivation {
150 evidence_id: String,
152 },
153
154 DerivationSubjectMismatch {
157 child: String,
159 parent: String,
161 },
162
163 DerivationCycle {
165 evidence_id: String,
167 },
168}
169
170impl fmt::Display for KnowledgeBaseError {
171 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
172 match self {
173 Self::SnapshotAuthorityMismatch { subject } => write!(
174 formatter,
175 "knowledge snapshot for {subject} belongs to a different knowledge base"
176 ),
177 Self::IdentityConflict { kind, id } => {
178 write!(
179 formatter,
180 "{kind} identity {id} already has different meaning"
181 )
182 },
183 Self::RelationEvidenceMismatch {
184 relation_id,
185 evidence_id,
186 } => write!(
187 formatter,
188 "relation {relation_id} must be backed only by evidence {evidence_id}"
189 ),
190 Self::RelationSubjectMismatch {
191 relation_id,
192 evidence_subject,
193 relation_from,
194 } => write!(
195 formatter,
196 "relation {relation_id} starts at {relation_from}, not evidence subject {evidence_subject}"
197 ),
198 Self::RelationLimitExceeded {
199 field,
200 actual,
201 maximum,
202 } => write!(
203 formatter,
204 "knowledge relation {field} size {actual} exceeds hard ceiling {maximum}"
205 ),
206 Self::StaleSnapshot {
207 subject,
208 expected_subject_revision,
209 actual_subject_revision,
210 expected_ontology_revision,
211 actual_ontology_revision,
212 } => write!(
213 formatter,
214 "knowledge snapshot for {subject} is stale (subject revision {expected_subject_revision}->{actual_subject_revision}, ontology revision {expected_ontology_revision}->{actual_ontology_revision})"
215 ),
216 Self::ReasoningSubjectMismatch {
217 hypothesis_id,
218 expected,
219 actual,
220 } => write!(
221 formatter,
222 "reasoning hypothesis {hypothesis_id} belongs to {actual}, expected snapshot subject {expected}"
223 ),
224 Self::MissingDerivationParent { child, parent } => write!(
225 formatter,
226 "derived evidence {child} references parent {parent} that is neither stored nor in the same batch"
227 ),
228 Self::SelfDerivation { evidence_id } => write!(
229 formatter,
230 "derived evidence {evidence_id} references itself as a parent"
231 ),
232 Self::DerivationSubjectMismatch { child, parent } => write!(
233 formatter,
234 "derived evidence {child} references parent {parent} recorded for a different subject"
235 ),
236 Self::DerivationCycle { evidence_id } => write!(
237 formatter,
238 "derivation lineage forms a cycle through evidence {evidence_id}"
239 ),
240 }
241 }
242}
243
244impl std::error::Error for KnowledgeBaseError {}
245
246#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
248#[non_exhaustive]
249pub struct KnowledgeBaseStats {
250 pub evidence: usize,
252 pub facts: usize,
254 pub hypotheses: usize,
256 pub entities: usize,
258 pub relations: usize,
260 pub ontology: OntologyStats,
262}
263
264impl KnowledgeBase {
265 pub fn insert_evidence(
270 &self,
271 evidence: Evidence,
272 ) -> Result<KnowledgeWrite, KnowledgeBaseError> {
273 let id = evidence.id().clone();
274 let subject = evidence.subject().clone();
275 let predicate = evidence.predicate().clone();
276 let mut state = self.write_state();
277
278 if let Some(existing) = state.evidence.get(&id) {
279 return if existing == &evidence {
280 Ok(KnowledgeWrite::Unchanged)
281 } else {
282 Err(identity_conflict(KnowledgeRecordKind::Evidence, &id))
283 };
284 }
285
286 if evidence.origin().derivation().is_some() {
287 let mut pending = HashMap::with_capacity(1);
288 pending.insert(id.clone(), evidence.clone());
289 validate_batch_derivations(&state, &pending)?;
290 }
291
292 index_derivation(&mut state, &evidence);
293 state.evidence.insert(id.clone(), evidence);
294 bump_subject_revision(&mut state, &subject);
295 index(&mut state.evidence_by_subject, subject, id.clone());
296 index(&mut state.evidence_by_predicate, predicate, id);
297 Ok(KnowledgeWrite::Inserted)
298 }
299
300 pub fn insert_evidence_batch(
308 &self,
309 evidence: Vec<Evidence>,
310 ) -> Result<Vec<KnowledgeWrite>, KnowledgeBaseError> {
311 let mut state = self.write_state();
312 let mut pending = HashMap::<EvidenceId, Evidence>::new();
313
314 for observation in &evidence {
315 let id = observation.id();
316 if state
317 .evidence
318 .get(id)
319 .is_some_and(|existing| existing != observation)
320 || pending
321 .get(id)
322 .is_some_and(|existing| existing != observation)
323 {
324 return Err(identity_conflict(KnowledgeRecordKind::Evidence, id));
325 }
326 pending
327 .entry(id.clone())
328 .or_insert_with(|| observation.clone());
329 }
330
331 validate_batch_derivations(&state, &pending)?;
335
336 let mut writes = Vec::with_capacity(evidence.len());
337 for observation in evidence {
338 let id = observation.id().clone();
339 if state.evidence.contains_key(&id) {
340 writes.push(KnowledgeWrite::Unchanged);
341 continue;
342 }
343
344 let subject = observation.subject().clone();
345 let predicate = observation.predicate().clone();
346 index_derivation(&mut state, &observation);
347 state.evidence.insert(id.clone(), observation);
348 bump_subject_revision(&mut state, &subject);
349 index(&mut state.evidence_by_subject, subject, id.clone());
350 index(&mut state.evidence_by_predicate, predicate, id);
351 writes.push(KnowledgeWrite::Inserted);
352 }
353 Ok(writes)
354 }
355
356 pub fn upsert_fact(&self, fact: Fact) -> Result<KnowledgeWrite, KnowledgeBaseError> {
361 let id = fact.id().to_owned();
362 let subject = fact.subject().clone();
363 let predicate = fact.predicate().clone();
364 let mut state = self.write_state();
365
366 if let Some(existing) = state.facts.get(&id) {
367 if existing == &fact {
368 return Ok(KnowledgeWrite::Unchanged);
369 }
370 if existing.subject() != fact.subject()
371 || existing.predicate() != fact.predicate()
372 || existing.value() != fact.value()
373 {
374 return Err(identity_conflict(KnowledgeRecordKind::Fact, &id));
375 }
376 state.facts.insert(id, fact);
377 bump_subject_revision(&mut state, &subject);
378 return Ok(KnowledgeWrite::Updated);
379 }
380
381 state.facts.insert(id.clone(), fact);
382 bump_subject_revision(&mut state, &subject);
383 index(&mut state.facts_by_subject, subject, id.clone());
384 index(&mut state.facts_by_predicate, predicate, id);
385 Ok(KnowledgeWrite::Inserted)
386 }
387
388 pub fn upsert_hypothesis(
393 &self,
394 hypothesis: Hypothesis,
395 ) -> Result<KnowledgeWrite, KnowledgeBaseError> {
396 self.upsert_hypothesis_batch(vec![hypothesis])
397 .map(|writes| writes[0])
398 }
399
400 pub fn upsert_hypothesis_batch(
408 &self,
409 hypotheses: Vec<Hypothesis>,
410 ) -> Result<Vec<KnowledgeWrite>, KnowledgeBaseError> {
411 self.upsert_hypothesis_batch_with_policy(hypotheses, false, None)
412 }
413
414 pub(crate) fn upsert_reasoning_hypothesis_batch(
420 &self,
421 snapshot: &KnowledgeSnapshot,
422 hypotheses: Vec<Hypothesis>,
423 ) -> Result<Vec<KnowledgeWrite>, KnowledgeBaseError> {
424 if let Some(hypothesis) = hypotheses
425 .iter()
426 .find(|hypothesis| hypothesis.subject() != snapshot.subject())
427 {
428 return Err(KnowledgeBaseError::ReasoningSubjectMismatch {
429 hypothesis_id: hypothesis.id().to_owned(),
430 expected: snapshot.subject().clone(),
431 actual: hypothesis.subject().clone(),
432 });
433 }
434 self.upsert_hypothesis_batch_with_policy(hypotheses, true, Some(snapshot))
435 }
436
437 fn upsert_hypothesis_batch_with_policy(
438 &self,
439 hypotheses: Vec<Hypothesis>,
440 preserve_terminal_state: bool,
441 expected_snapshot: Option<&KnowledgeSnapshot>,
442 ) -> Result<Vec<KnowledgeWrite>, KnowledgeBaseError> {
443 if let Some(snapshot) = expected_snapshot {
444 self.validate_snapshot_authority(snapshot.authority(), snapshot.subject())?;
445 }
446 let mut state = self.write_state();
447 if let Some(snapshot) = expected_snapshot {
448 let actual_subject_revision = subject_revision(&state, snapshot.subject());
449 if actual_subject_revision != snapshot.subject_revision()
450 || state.ontology_revision != snapshot.ontology_revision()
451 {
452 return Err(KnowledgeBaseError::StaleSnapshot {
453 subject: snapshot.subject().clone(),
454 expected_subject_revision: snapshot.subject_revision(),
455 actual_subject_revision,
456 expected_ontology_revision: snapshot.ontology_revision(),
457 actual_ontology_revision: state.ontology_revision,
458 });
459 }
460 }
461 let mut pending = HashMap::<String, Hypothesis>::new();
462
463 for hypothesis in &hypotheses {
464 let id = hypothesis.id();
465 if state
466 .hypotheses
467 .get(id)
468 .is_some_and(|existing| !same_hypothesis_claim(existing, hypothesis))
469 || pending
470 .get(id)
471 .is_some_and(|existing| !existing.same_evaluation_as(hypothesis))
472 {
473 return Err(identity_conflict(KnowledgeRecordKind::Hypothesis, &id));
474 }
475 pending
476 .entry(id.to_owned())
477 .or_insert_with(|| hypothesis.clone());
478 }
479
480 let mut writes = Vec::with_capacity(hypotheses.len());
481 for mut hypothesis in hypotheses {
482 let id = hypothesis.id().to_owned();
483 if preserve_terminal_state {
484 let terminal_state = state.hypotheses.get(&id).and_then(|existing| {
485 matches!(
486 existing.state(),
487 HypothesisState::Confirmed | HypothesisState::Rejected
488 )
489 .then_some(existing.state())
490 });
491 if let Some(terminal_state) = terminal_state {
492 hypothesis.set_state(terminal_state);
493 }
494 }
495
496 if let Some(existing) = state.hypotheses.get(&id) {
497 if existing.same_evaluation_as(&hypothesis) {
498 writes.push(KnowledgeWrite::Unchanged);
499 } else {
500 let subject = hypothesis.subject().clone();
501 state.hypotheses.insert(id, hypothesis);
502 bump_subject_revision(&mut state, &subject);
503 writes.push(KnowledgeWrite::Updated);
504 }
505 continue;
506 }
507
508 let subject = hypothesis.subject().clone();
509 let predicate = hypothesis.predicate().clone();
510 state.hypotheses.insert(id.clone(), hypothesis);
511 bump_subject_revision(&mut state, &subject);
512 index(&mut state.hypotheses_by_subject, subject, id.clone());
513 index(&mut state.hypotheses_by_predicate, predicate, id);
514 writes.push(KnowledgeWrite::Inserted);
515 }
516 Ok(writes)
517 }
518
519 pub(crate) fn transition_hypothesis_state(
525 &self,
526 hypothesis_id: &str,
527 expected_subject: &EntityId,
528 new_state: HypothesisState,
529 expected_revisions: Option<(u64, u64)>,
530 ) -> HypothesisStateTransition {
531 let mut state = self.write_state();
532 let Some(hypothesis) = state.hypotheses.get(hypothesis_id) else {
533 return HypothesisStateTransition::Missing;
534 };
535 if hypothesis.subject() != expected_subject {
536 return HypothesisStateTransition::SubjectMismatch {
537 actual: hypothesis.subject().clone(),
538 };
539 }
540 if hypothesis.state() == new_state {
541 return HypothesisStateTransition::Written(KnowledgeWrite::Unchanged);
542 }
543 if is_terminal_hypothesis_state(hypothesis.state())
544 && is_terminal_hypothesis_state(new_state)
545 {
546 return HypothesisStateTransition::TerminalConflict {
547 current: hypothesis.state(),
548 attempted: new_state,
549 };
550 }
551 if let Some((expected_subject_revision, expected_ontology_revision)) = expected_revisions {
552 let actual_subject_revision = subject_revision(&state, expected_subject);
553 if actual_subject_revision != expected_subject_revision
554 || state.ontology_revision != expected_ontology_revision
555 {
556 return HypothesisStateTransition::StaleSnapshot(
557 KnowledgeBaseError::StaleSnapshot {
558 subject: expected_subject.clone(),
559 expected_subject_revision,
560 actual_subject_revision,
561 expected_ontology_revision,
562 actual_ontology_revision: state.ontology_revision,
563 },
564 );
565 }
566 }
567
568 let hypothesis = state
569 .hypotheses
570 .get_mut(hypothesis_id)
571 .expect("validated hypothesis remains present under the write lock");
572 hypothesis.set_state(new_state);
573 bump_subject_revision(&mut state, expected_subject);
574 HypothesisStateTransition::Written(KnowledgeWrite::Updated)
575 }
576}
577
578pub(super) fn identity_conflict(
579 kind: KnowledgeRecordKind,
580 id: &impl fmt::Display,
581) -> KnowledgeBaseError {
582 KnowledgeBaseError::IdentityConflict {
583 kind,
584 id: id.to_string(),
585 }
586}
587
588fn validate_batch_derivations(
597 state: &KnowledgeState,
598 pending: &HashMap<EvidenceId, Evidence>,
599) -> Result<(), KnowledgeBaseError> {
600 for (child_id, child) in pending {
601 let Some(derivation) = child.origin().derivation() else {
602 continue;
603 };
604 for parent in derivation.parents() {
605 if parent == child_id {
606 return Err(KnowledgeBaseError::SelfDerivation {
607 evidence_id: child_id.to_string(),
608 });
609 }
610 let parent_subject = pending
611 .get(parent)
612 .map(Evidence::subject)
613 .or_else(|| state.evidence.get(parent).map(Evidence::subject));
614 let Some(parent_subject) = parent_subject else {
615 return Err(KnowledgeBaseError::MissingDerivationParent {
616 child: child_id.to_string(),
617 parent: parent.to_string(),
618 });
619 };
620 if parent_subject != child.subject() {
621 return Err(KnowledgeBaseError::DerivationSubjectMismatch {
622 child: child_id.to_string(),
623 parent: parent.to_string(),
624 });
625 }
626 }
627 }
628 detect_batch_derivation_cycles(pending)
629}
630
631fn detect_batch_derivation_cycles(
637 pending: &HashMap<EvidenceId, Evidence>,
638) -> Result<(), KnowledgeBaseError> {
639 enum Color {
640 White,
641 Gray,
642 Black,
643 }
644 let adjacency: HashMap<EvidenceId, Vec<EvidenceId>> = pending
645 .iter()
646 .map(|(id, evidence)| {
647 let parents = evidence
648 .origin()
649 .derivation()
650 .map(|derivation| {
651 derivation
652 .parents()
653 .iter()
654 .filter(|parent| pending.contains_key(*parent))
655 .cloned()
656 .collect::<Vec<_>>()
657 })
658 .unwrap_or_default();
659 (id.clone(), parents)
660 })
661 .collect();
662 let mut color: HashMap<EvidenceId, Color> = pending
663 .keys()
664 .map(|id| (id.clone(), Color::White))
665 .collect();
666 for start in pending.keys() {
667 if !matches!(color.get(start), Some(Color::White)) {
668 continue;
669 }
670 color.insert(start.clone(), Color::Gray);
671 let mut stack: Vec<(EvidenceId, usize)> = vec![(start.clone(), 0)];
672 while let Some((node, index)) = stack.last().cloned() {
673 let neighbors = &adjacency[&node];
674 if index < neighbors.len() {
675 stack.last_mut().unwrap().1 = index + 1;
676 let next = neighbors[index].clone();
677 match color.get(&next) {
678 Some(Color::Gray) => {
679 return Err(KnowledgeBaseError::DerivationCycle {
680 evidence_id: next.to_string(),
681 });
682 },
683 Some(Color::White) => {
684 color.insert(next.clone(), Color::Gray);
685 stack.push((next, 0));
686 },
687 _ => {},
688 }
689 } else {
690 color.insert(node, Color::Black);
691 stack.pop();
692 }
693 }
694 }
695 Ok(())
696}
697
698fn index_derivation(state: &mut KnowledgeState, evidence: &Evidence) {
700 if let Some(derivation) = evidence.origin().derivation() {
701 let child = evidence.id().clone();
702 for parent in derivation.parents() {
703 state
704 .derivation_children
705 .entry(parent.clone())
706 .or_default()
707 .insert(child.clone());
708 }
709 }
710}
711
712pub(super) fn subject_revision(state: &KnowledgeState, subject: &EntityId) -> u64 {
713 state.subject_revisions.get(subject).copied().unwrap_or(0)
714}
715
716pub(super) fn validate_revisions(
717 state: &KnowledgeState,
718 subject: &EntityId,
719 expected_subject_revision: u64,
720 expected_ontology_revision: u64,
721) -> Result<(), KnowledgeBaseError> {
722 let actual_subject_revision = subject_revision(state, subject);
723 if actual_subject_revision != expected_subject_revision
724 || state.ontology_revision != expected_ontology_revision
725 {
726 return Err(KnowledgeBaseError::StaleSnapshot {
727 subject: subject.clone(),
728 expected_subject_revision,
729 actual_subject_revision,
730 expected_ontology_revision,
731 actual_ontology_revision: state.ontology_revision,
732 });
733 }
734 Ok(())
735}
736
737pub(super) fn bump_subject_revision(state: &mut KnowledgeState, subject: &EntityId) {
738 let revision = state.subject_revisions.entry(subject.clone()).or_default();
739 *revision = revision
740 .checked_add(1)
741 .expect("subject knowledge revision must not overflow");
742}
743
744pub(super) fn bump_ontology_revision(state: &mut KnowledgeState) {
745 state.ontology_revision = state
746 .ontology_revision
747 .checked_add(1)
748 .expect("ontology knowledge revision must not overflow");
749}
750
751fn same_hypothesis_claim(left: &Hypothesis, right: &Hypothesis) -> bool {
752 left.subject() == right.subject()
753 && left.predicate() == right.predicate()
754 && left.value() == right.value()
755}
756
757fn is_terminal_hypothesis_state(state: HypothesisState) -> bool {
758 matches!(
759 state,
760 HypothesisState::Confirmed | HypothesisState::Rejected
761 )
762}