1use crate::confidence::{CalibrationTable, ConfidenceComponents, calibrate};
8use crate::interval::{Interval, clip, merge_overlapping, overlaps};
9use crate::knowledge::{
10 Assertion, Belief, BeliefStatus, Polarity, Statement, StatementId, Support,
11};
12use crate::registry::{Cardinality, Invalidation, PredicateDef, Temporality};
13use crate::types::TrustTier;
14use oxibrain_ports::Timestamp;
15
16#[derive(Debug, Clone)]
18pub struct StatementEntry {
19 pub statement: Statement,
20 pub assertions: Vec<Assertion>,
21}
22
23struct VisibleStmt {
25 stmt: Statement,
26 affirm: Vec<Interval>,
27 assertions: Vec<Assertion>, }
29
30pub fn fold(
37 def: &PredicateDef,
38 group: &[StatementEntry],
39 at: Timestamp,
40 calibration: &CalibrationTable,
41) -> Vec<Belief> {
42 let mut visible: Vec<VisibleStmt> = Vec::new();
45 for entry in group {
46 let vis: Vec<&Assertion> = entry
47 .assertions
48 .iter()
49 .filter(|a| {
50 a.recorded_at <= at && (a.retracted_at.is_none() || a.retracted_at.unwrap() > at)
51 })
52 .collect();
53 if vis.is_empty() {
54 continue;
55 }
56
57 let mut affirm: Vec<Interval> = vis
58 .iter()
59 .filter(|a| a.polarity == Polarity::Affirm)
60 .map(|a| Interval::new(a.claimed_from, a.claimed_to))
61 .collect();
62 let deny: Vec<Interval> = vis
63 .iter()
64 .filter(|a| a.polarity == Polarity::Deny)
65 .map(|a| Interval::new(a.claimed_from, a.claimed_to))
66 .collect();
67
68 merge_overlapping(&mut affirm);
70
71 for d in &deny {
73 affirm = clip(&affirm, d);
74 }
75
76 visible.push(VisibleStmt {
77 stmt: entry.statement.clone(),
78 affirm,
79 assertions: vis.into_iter().cloned().collect(),
80 });
81 }
82
83 if visible.is_empty() {
84 return Vec::new();
85 }
86
87 let beliefs = match (def.cardinality, def.invalidation, def.temporality) {
89 (Cardinality::MultiValued, _, _) => {
91 fold_independent(&visible, calibration, def.confidence_prior)
92 }
93
94 (Cardinality::Functional, _, Temporality::Static) => {
96 fold_contradiction(&visible, calibration, def.confidence_prior)
97 }
98
99 (Cardinality::Functional, Invalidation::Supersede, _) => {
101 fold_supersede(&visible, calibration, def.confidence_prior)
102 }
103
104 (Cardinality::Functional, Invalidation::ExplicitOnly, _) => {
106 fold_independent(&visible, calibration, def.confidence_prior)
107 }
108
109 (Cardinality::Functional, Invalidation::Coexist, _) => {
111 fold_independent(&visible, calibration, def.confidence_prior)
112 }
113 };
114
115 let mut beliefs = beliefs;
117 beliefs.sort_by(|a, b| (&a.statement, a.valid_from).cmp(&(&b.statement, b.valid_from)));
118 beliefs
119}
120
121fn belief_confidence(
124 assertions: &[Assertion],
125 support: &Support,
126 calibration: &CalibrationTable,
127 prior: f32,
128) -> f32 {
129 let is_declaration = assertions.iter().all(|a| a.extractor.is_none());
131 if is_declaration {
132 return 1.0;
133 }
134
135 let raw = assertions
140 .iter()
141 .map(|a| a.confidence)
142 .fold(0.0_f32, f32::max)
143 * prior;
144
145 let extractor_id = assertions
147 .iter()
148 .filter_map(|a| a.extractor.as_deref())
149 .next()
150 .unwrap_or("unknown");
151 let calibrated = calibrate(extractor_id, calibration);
152
153 let n = support.distinct_episodes.max(1) as f32;
155 let corroboration = (1.0 - (-0.3 * n).exp()).clamp(0.5, 1.0);
156
157 let trust = if support.trust_weights.is_empty() {
159 1.0
160 } else {
161 let total: u32 = support.trust_weights.iter().map(|(_, c)| *c).sum();
162 if total == 0 {
163 1.0
164 } else {
165 let weighted: f32 = support
166 .trust_weights
167 .iter()
168 .map(|(tier, count)| {
169 let w = match tier {
170 TrustTier::Trusted => 1.0,
171 TrustTier::SemiTrusted => 0.7,
172 TrustTier::Untrusted => 0.3,
173 };
174 w * *count as f32
175 })
176 .sum();
177 (weighted / total as f32).clamp(0.3, 1.0)
178 }
179 };
180
181 let recency = 1.0;
183
184 ConfidenceComponents {
185 raw,
186 calibrated,
187 corroboration,
188 trust,
189 recency,
190 }
191 .combine()
192}
193
194fn fold_independent(
196 visible: &[VisibleStmt],
197 calibration: &CalibrationTable,
198 prior: f32,
199) -> Vec<Belief> {
200 let mut beliefs = Vec::new();
201 for vs in visible {
202 let support = compute_support(&vs.assertions);
203 let conf = belief_confidence(&vs.assertions, &support, calibration, prior);
204 for iv in &vs.affirm {
205 beliefs.push(Belief {
206 statement: vs.stmt.id.clone(),
207 valid_from: iv.start,
208 valid_to: iv.end,
209 support: support.clone(),
210 confidence: conf,
211 status: BeliefStatus::Active,
212 });
213 }
214 }
215 beliefs
216}
217
218fn fold_contradiction(
220 visible: &[VisibleStmt],
221 calibration: &CalibrationTable,
222 prior: f32,
223) -> Vec<Belief> {
224 let affirming: Vec<&VisibleStmt> = visible.iter().filter(|vs| !vs.affirm.is_empty()).collect();
226 if affirming.len() <= 1 {
227 return fold_independent(visible, calibration, prior);
228 }
229
230 let mut contradicted: Vec<&str> = Vec::new(); for i in 0..affirming.len() {
234 for j in (i + 1)..affirming.len() {
235 let a = &affirming[i];
236 let b = &affirming[j];
237 let overlap = a
238 .affirm
239 .iter()
240 .any(|ai| b.affirm.iter().any(|bi| overlaps(ai, bi)));
241 if overlap {
242 if !contradicted.contains(&a.stmt.id.as_str()) {
243 contradicted.push(&a.stmt.id);
244 }
245 if !contradicted.contains(&b.stmt.id.as_str()) {
246 contradicted.push(&b.stmt.id);
247 }
248 }
249 }
250 }
251
252 let mut beliefs = Vec::new();
253 for vs in visible {
254 let support = compute_support(&vs.assertions);
255 let conf = belief_confidence(&vs.assertions, &support, calibration, prior);
256 let is_contradicted = contradicted.contains(&vs.stmt.id.as_str());
257 for iv in &vs.affirm {
258 beliefs.push(Belief {
259 statement: vs.stmt.id.clone(),
260 valid_from: iv.start,
261 valid_to: iv.end,
262 support: support.clone(),
263 confidence: conf,
264 status: if is_contradicted {
265 BeliefStatus::Contradicted
266 } else {
267 BeliefStatus::Active
268 },
269 });
270 }
271 }
272 beliefs
273}
274
275fn fold_supersede(
277 visible: &[VisibleStmt],
278 calibration: &CalibrationTable,
279 prior: f32,
280) -> Vec<Belief> {
281 let mut all: Vec<(StatementId, Interval)> = Vec::new();
283 for vs in visible {
284 for iv in &vs.affirm {
285 all.push((vs.stmt.id.clone(), *iv));
286 }
287 }
288
289 all.sort_by(|a, b| (&a.1.start, &a.0).cmp(&(&b.1.start, &b.0)));
291
292 let mut beliefs: Vec<Belief> = Vec::new();
293 struct Active {
294 stmt: StatementId,
295 start: Timestamp,
296 end: Timestamp,
297 }
298
299 let mut current: Option<Active> = None;
300
301 for (stmt_id, iv) in &all {
302 let vs = visible
303 .iter()
304 .find(|vs| &vs.stmt.id == stmt_id)
305 .expect("statement exists in group");
306 let support = compute_support(&vs.assertions);
307 let conf = belief_confidence(&vs.assertions, &support, calibration, prior);
308
309 match ¤t {
310 None => {
311 beliefs.push(Belief {
312 statement: stmt_id.clone(),
313 valid_from: iv.start,
314 valid_to: iv.end,
315 support,
316 confidence: conf,
317 status: BeliefStatus::Active,
318 });
319 current = Some(Active {
320 stmt: stmt_id.clone(),
321 start: iv.start,
322 end: iv.end,
323 });
324 }
325 Some(cur) if cur.stmt == *stmt_id => {
326 beliefs.push(Belief {
327 statement: stmt_id.clone(),
328 valid_from: iv.start,
329 valid_to: iv.end,
330 support,
331 confidence: conf,
332 status: BeliefStatus::Active,
333 });
334 if iv.end > cur.end {
335 current = Some(Active {
336 stmt: stmt_id.clone(),
337 start: cur.start,
338 end: iv.end,
339 });
340 }
341 }
342 Some(cur) => {
343 if iv.start == cur.start {
344 if let Some(last) = beliefs.last_mut() {
345 if last.statement == cur.stmt && last.status == BeliefStatus::Active {
346 last.status = BeliefStatus::Contradicted;
347 }
348 }
349 beliefs.push(Belief {
350 statement: stmt_id.clone(),
351 valid_from: iv.start,
352 valid_to: iv.end,
353 support,
354 confidence: conf,
355 status: BeliefStatus::Contradicted,
356 });
357 } else {
358 if let Some(last) = beliefs.last_mut() {
359 if last.statement == cur.stmt
360 && last.status == BeliefStatus::Active
361 && last.valid_to >= iv.start
362 {
363 last.valid_to = Timestamp(iv.start.millis() - 1);
364 last.status = BeliefStatus::Superseded;
365 }
366 }
367 beliefs.push(Belief {
368 statement: stmt_id.clone(),
369 valid_from: iv.start,
370 valid_to: iv.end,
371 support,
372 confidence: conf,
373 status: BeliefStatus::Active,
374 });
375 }
376 current = Some(Active {
377 stmt: stmt_id.clone(),
378 start: iv.start,
379 end: iv.end,
380 });
381 }
382 }
383 }
384
385 beliefs
386}
387
388fn compute_support(assertions: &[Assertion]) -> Support {
390 use std::collections::BTreeMap;
391
392 let affirm_count = assertions
393 .iter()
394 .filter(|a| a.polarity == Polarity::Affirm)
395 .count() as u32;
396 let deny_count = assertions
397 .iter()
398 .filter(|a| a.polarity == Polarity::Deny)
399 .count() as u32;
400
401 let mut episode_trust: BTreeMap<&str, TrustTier> = BTreeMap::new();
404 for a in assertions {
405 episode_trust.entry(a.episode.as_str()).or_insert(a.trust);
406 }
407
408 let mut trusted = 0u32;
409 let mut semi = 0u32;
410 let mut untrusted = 0u32;
411 for tier in episode_trust.values() {
412 match tier {
413 TrustTier::Trusted => trusted += 1,
414 TrustTier::SemiTrusted => semi += 1,
415 TrustTier::Untrusted => untrusted += 1,
416 }
417 }
418
419 let mut trust_weights = Vec::new();
420 if trusted > 0 {
421 trust_weights.push((TrustTier::Trusted, trusted));
422 }
423 if semi > 0 {
424 trust_weights.push((TrustTier::SemiTrusted, semi));
425 }
426 if untrusted > 0 {
427 trust_weights.push((TrustTier::Untrusted, untrusted));
428 }
429
430 Support {
431 affirm_count,
432 deny_count,
433 distinct_episodes: episode_trust.len() as u32,
434 trust_weights,
435 }
436}
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use crate::knowledge::Object;
441 use crate::registry::{Cardinality, Invalidation, ObjectKind, PredicateDef, Temporality};
442 use oxibrain_ports::{TIME_MAX, TIME_MIN, Timestamp};
443
444 fn ts(m: i64) -> Timestamp {
445 Timestamp(m)
446 }
447
448 fn make_assertion(
449 stmt: &str,
450 episode: &str,
451 polarity: Polarity,
452 from: Timestamp,
453 to: Timestamp,
454 ) -> Assertion {
455 Assertion {
456 id: format!("a_{stmt}_{episode}"),
457 statement: stmt.into(),
458 episode: episode.into(),
459 extractor: None,
460 polarity,
461 claimed_from: from,
462 claimed_to: to,
463 confidence: 1.0,
464 recorded_at: ts(1),
465 retracted_at: None,
466 trust: TrustTier::Trusted,
467 }
468 }
469
470 fn make_stmt(id: &str, subj: &str, pred: &str, obj_id: &str) -> Statement {
471 Statement {
472 id: id.into(),
473 space: "s1".into(),
474 subject: subj.into(),
475 predicate: pred.into(),
476 object: Object::Entity(obj_id.into()),
477 }
478 }
479
480 fn def_employed() -> PredicateDef {
481 PredicateDef {
482 name: "employed_by".into(),
483 object_kind: ObjectKind::Entity(["Organization"].into()),
484 subject_types: vec!["Person".into()],
485 cardinality: Cardinality::Functional,
486 temporality: Temporality::Interval,
487 invalidation: Invalidation::Supersede,
488 symmetric: false,
489 inverse_of: None,
490 description: "".into(),
491 examples: vec![],
492 deprecated_by: None,
493 profile_relevant: false,
494 confidence_prior: 1.0,
495 }
496 }
497
498 fn def_born_in() -> PredicateDef {
499 PredicateDef {
500 name: "born_in".into(),
501 object_kind: ObjectKind::Entity(["Place"].into()),
502 subject_types: vec!["Person".into()],
503 cardinality: Cardinality::Functional,
504 temporality: Temporality::Static,
505 invalidation: Invalidation::Supersede,
506 symmetric: false,
507 inverse_of: None,
508 description: "".into(),
509 examples: vec![],
510 deprecated_by: None,
511 profile_relevant: false,
512 confidence_prior: 1.0,
513 }
514 }
515
516 fn def_works_on() -> PredicateDef {
517 PredicateDef {
518 name: "works_on".into(),
519 object_kind: ObjectKind::Entity(["Project"].into()),
520 subject_types: vec!["Person".into()],
521 cardinality: Cardinality::MultiValued,
522 temporality: Temporality::Interval,
523 invalidation: Invalidation::Coexist,
524 symmetric: false,
525 inverse_of: None,
526 description: "".into(),
527 examples: vec![],
528 deprecated_by: None,
529 profile_relevant: false,
530 confidence_prior: 1.0,
531 }
532 }
533
534 #[test]
536 fn single_affirm_is_active() {
537 let stmt = make_stmt("st1", "e1", "employed_by", "acme");
538 let group = vec![StatementEntry {
539 statement: stmt,
540 assertions: vec![make_assertion(
541 "st1",
542 "ep1",
543 Polarity::Affirm,
544 ts(100),
545 TIME_MAX,
546 )],
547 }];
548 let beliefs = fold(
549 &def_employed(),
550 &group,
551 TIME_MAX,
552 &CalibrationTable::default(),
553 );
554 assert_eq!(beliefs.len(), 1);
555 assert_eq!(beliefs[0].status, BeliefStatus::Active);
556 assert_eq!(beliefs[0].valid_from, ts(100));
557 }
558
559 #[test]
561 fn hearsay_prior_lowers_confidence() {
562 let stmt = make_stmt("st1", "e1", "allegedly_employed_by", "acme");
563 let ext_assertion = Assertion {
566 id: "a_st1_ep1".into(),
567 statement: "st1".into(),
568 episode: "ep1".into(),
569 extractor: Some("ext1".into()),
570 polarity: Polarity::Affirm,
571 claimed_from: ts(100),
572 claimed_to: TIME_MAX,
573 confidence: 0.9,
574 recorded_at: ts(1),
575 retracted_at: None,
576 trust: TrustTier::Trusted,
577 };
578 let group = vec![StatementEntry {
579 statement: stmt,
580 assertions: vec![ext_assertion],
581 }];
582 let mut hearsay_def = def_employed();
584 hearsay_def.confidence_prior = 0.3;
585 let hearsay_beliefs = fold(&hearsay_def, &group, TIME_MAX, &CalibrationTable::default());
586
587 let normal_beliefs = fold(
589 &def_employed(),
590 &group,
591 TIME_MAX,
592 &CalibrationTable::default(),
593 );
594
595 assert_eq!(hearsay_beliefs.len(), 1);
596 assert_eq!(normal_beliefs.len(), 1);
597 assert!(
599 hearsay_beliefs[0].confidence < normal_beliefs[0].confidence,
600 "hearsay confidence {} should be < normal confidence {}",
601 hearsay_beliefs[0].confidence,
602 normal_beliefs[0].confidence
603 );
604 }
605
606 #[test]
608 fn supersession_closes_previous() {
609 let stmt_a = make_stmt("st_a", "e1", "employed_by", "acme");
610 let stmt_b = make_stmt("st_b", "e1", "employed_by", "globex");
611 let group = vec![
612 StatementEntry {
613 statement: stmt_a,
614 assertions: vec![make_assertion(
615 "st_a",
616 "ep1",
617 Polarity::Affirm,
618 ts(100),
619 TIME_MAX,
620 )],
621 },
622 StatementEntry {
623 statement: stmt_b,
624 assertions: vec![make_assertion(
625 "st_b",
626 "ep2",
627 Polarity::Affirm,
628 ts(200),
629 TIME_MAX,
630 )],
631 },
632 ];
633 let beliefs = fold(
634 &def_employed(),
635 &group,
636 TIME_MAX,
637 &CalibrationTable::default(),
638 );
639 let acme = beliefs
641 .iter()
642 .find(|b| b.statement == "st_a")
643 .expect("acme belief");
644 let globex = beliefs
645 .iter()
646 .find(|b| b.statement == "st_b")
647 .expect("globex belief");
648 assert_eq!(acme.status, BeliefStatus::Superseded);
649 assert_eq!(acme.valid_to, ts(199));
650 assert_eq!(globex.status, BeliefStatus::Active);
651 assert_eq!(globex.valid_from, ts(200));
652 }
653
654 #[test]
656 fn static_two_values_contradicted() {
657 let stmt_a = make_stmt("st_a", "e1", "born_in", "seoul");
658 let stmt_b = make_stmt("st_b", "e1", "born_in", "busan");
659 let group = vec![
660 StatementEntry {
661 statement: stmt_a,
662 assertions: vec![make_assertion(
663 "st_a",
664 "ep1",
665 Polarity::Affirm,
666 TIME_MIN,
667 TIME_MAX,
668 )],
669 },
670 StatementEntry {
671 statement: stmt_b,
672 assertions: vec![make_assertion(
673 "st_b",
674 "ep2",
675 Polarity::Affirm,
676 TIME_MIN,
677 TIME_MAX,
678 )],
679 },
680 ];
681 let beliefs = fold(
682 &def_born_in(),
683 &group,
684 TIME_MAX,
685 &CalibrationTable::default(),
686 );
687 assert_eq!(beliefs.len(), 2);
688 assert!(
689 beliefs
690 .iter()
691 .all(|b| b.status == BeliefStatus::Contradicted)
692 );
693 }
694
695 #[test]
697 fn multivalued_coexist() {
698 let stmt_a = make_stmt("st_a", "e1", "works_on", "px");
699 let stmt_b = make_stmt("st_b", "e1", "works_on", "py");
700 let group = vec![
701 StatementEntry {
702 statement: stmt_a,
703 assertions: vec![make_assertion(
704 "st_a",
705 "ep1",
706 Polarity::Affirm,
707 ts(100),
708 TIME_MAX,
709 )],
710 },
711 StatementEntry {
712 statement: stmt_b,
713 assertions: vec![make_assertion(
714 "st_b",
715 "ep2",
716 Polarity::Affirm,
717 ts(100),
718 TIME_MAX,
719 )],
720 },
721 ];
722 let beliefs = fold(
723 &def_works_on(),
724 &group,
725 TIME_MAX,
726 &CalibrationTable::default(),
727 );
728 assert_eq!(beliefs.len(), 2);
729 assert!(beliefs.iter().all(|b| b.status == BeliefStatus::Active));
730 }
731
732 #[test]
734 fn denial_clips() {
735 let stmt = make_stmt("st1", "e1", "works_on", "px");
736 let group = vec![StatementEntry {
737 statement: stmt,
738 assertions: vec![
739 make_assertion("st1", "ep1", Polarity::Affirm, ts(100), ts(500)),
740 Assertion {
741 id: "deny1".into(),
742 statement: "st1".into(),
743 episode: "ep2".into(),
744 extractor: None,
745 polarity: Polarity::Deny,
746 claimed_from: ts(200),
747 claimed_to: ts(300),
748 confidence: 1.0,
749 recorded_at: ts(2),
750 retracted_at: None,
751 trust: TrustTier::Trusted,
752 },
753 ],
754 }];
755 let beliefs = fold(
756 &def_works_on(),
757 &group,
758 TIME_MAX,
759 &CalibrationTable::default(),
760 );
761 assert_eq!(beliefs.len(), 2);
763 assert_eq!(beliefs[0].valid_from, ts(100));
764 assert_eq!(beliefs[0].valid_to, ts(199));
765 assert_eq!(beliefs[1].valid_from, ts(301));
766 assert_eq!(beliefs[1].valid_to, ts(500));
767 }
768
769 #[test]
771 fn retracted_assertion_invisible() {
772 let stmt = make_stmt("st1", "e1", "employed_by", "acme");
773 let group = vec![StatementEntry {
774 statement: stmt,
775 assertions: vec![Assertion {
776 id: "a1".into(),
777 statement: "st1".into(),
778 episode: "ep1".into(),
779 extractor: None,
780 polarity: Polarity::Affirm,
781 claimed_from: ts(100),
782 claimed_to: TIME_MAX,
783 confidence: 1.0,
784 recorded_at: ts(1),
785 retracted_at: Some(ts(5)), trust: TrustTier::Trusted,
787 }],
788 }];
789 let beliefs = fold(
790 &def_employed(),
791 &group,
792 ts(10),
793 &CalibrationTable::default(),
794 );
795 assert!(
796 beliefs.is_empty(),
797 "retracted assertion should produce no belief"
798 );
799 }
800
801 #[test]
803 fn output_sorted() {
804 let stmt_a = make_stmt("st_a", "e1", "works_on", "px");
805 let stmt_b = make_stmt("st_b", "e1", "works_on", "py");
806 let group = vec![
807 StatementEntry {
808 statement: stmt_b,
809 assertions: vec![make_assertion(
810 "st_b",
811 "ep2",
812 Polarity::Affirm,
813 ts(200),
814 TIME_MAX,
815 )],
816 },
817 StatementEntry {
818 statement: stmt_a,
819 assertions: vec![make_assertion(
820 "st_a",
821 "ep1",
822 Polarity::Affirm,
823 ts(100),
824 TIME_MAX,
825 )],
826 },
827 ];
828 let beliefs = fold(
829 &def_works_on(),
830 &group,
831 TIME_MAX,
832 &CalibrationTable::default(),
833 );
834 assert_eq!(beliefs[0].statement, "st_a");
835 assert_eq!(beliefs[1].statement, "st_b");
836 }
837
838 #[test]
840 fn empty_group() {
841 let beliefs = fold(&def_employed(), &[], TIME_MAX, &CalibrationTable::default());
842 assert!(beliefs.is_empty());
843 }
844}