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::HashSet;
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 distinct_episodes: HashSet<&str> = assertions.iter().map(|a| a.episode.as_str()).collect();
402
403 let trust_weights = if distinct_episodes.is_empty() {
406 Vec::new()
407 } else {
408 vec![(TrustTier::Trusted, distinct_episodes.len() as u32)]
409 };
410
411 Support {
412 affirm_count,
413 deny_count,
414 distinct_episodes: distinct_episodes.len() as u32,
415 trust_weights,
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use crate::confidence::CalibrationTable;
423 use crate::knowledge::{Object, Polarity, Statement};
424 use crate::registry::{Cardinality, Invalidation, ObjectKind, PredicateDef, Temporality};
425 use oxibrain_ports::{TIME_MAX, TIME_MIN, Timestamp};
426
427 fn ts(m: i64) -> Timestamp {
428 Timestamp(m)
429 }
430
431 fn make_assertion(
432 stmt: &str,
433 episode: &str,
434 polarity: Polarity,
435 from: Timestamp,
436 to: Timestamp,
437 ) -> Assertion {
438 Assertion {
439 id: format!("a_{stmt}_{episode}"),
440 statement: stmt.into(),
441 episode: episode.into(),
442 extractor: None,
443 polarity,
444 claimed_from: from,
445 claimed_to: to,
446 confidence: 1.0,
447 recorded_at: ts(1),
448 retracted_at: None,
449 }
450 }
451
452 fn make_stmt(id: &str, subj: &str, pred: &str, obj_id: &str) -> Statement {
453 Statement {
454 id: id.into(),
455 space: "s1".into(),
456 subject: subj.into(),
457 predicate: pred.into(),
458 object: Object::Entity(obj_id.into()),
459 }
460 }
461
462 fn def_employed() -> PredicateDef {
463 PredicateDef {
464 name: "employed_by".into(),
465 object_kind: ObjectKind::Entity(["Organization"].into()),
466 subject_types: vec!["Person".into()],
467 cardinality: Cardinality::Functional,
468 temporality: Temporality::Interval,
469 invalidation: Invalidation::Supersede,
470 symmetric: false,
471 inverse_of: None,
472 description: "".into(),
473 examples: vec![],
474 deprecated_by: None,
475 profile_relevant: false,
476 confidence_prior: 1.0,
477 }
478 }
479
480 fn def_born_in() -> PredicateDef {
481 PredicateDef {
482 name: "born_in".into(),
483 object_kind: ObjectKind::Entity(["Place"].into()),
484 subject_types: vec!["Person".into()],
485 cardinality: Cardinality::Functional,
486 temporality: Temporality::Static,
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_works_on() -> PredicateDef {
499 PredicateDef {
500 name: "works_on".into(),
501 object_kind: ObjectKind::Entity(["Project"].into()),
502 subject_types: vec!["Person".into()],
503 cardinality: Cardinality::MultiValued,
504 temporality: Temporality::Interval,
505 invalidation: Invalidation::Coexist,
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 #[test]
518 fn single_affirm_is_active() {
519 let stmt = make_stmt("st1", "e1", "employed_by", "acme");
520 let group = vec![StatementEntry {
521 statement: stmt,
522 assertions: vec![make_assertion(
523 "st1",
524 "ep1",
525 Polarity::Affirm,
526 ts(100),
527 TIME_MAX,
528 )],
529 }];
530 let beliefs = fold(
531 &def_employed(),
532 &group,
533 TIME_MAX,
534 &CalibrationTable::default(),
535 );
536 assert_eq!(beliefs.len(), 1);
537 assert_eq!(beliefs[0].status, BeliefStatus::Active);
538 assert_eq!(beliefs[0].valid_from, ts(100));
539 }
540
541 #[test]
543 fn hearsay_prior_lowers_confidence() {
544 let stmt = make_stmt("st1", "e1", "allegedly_employed_by", "acme");
545 let ext_assertion = Assertion {
548 id: "a_st1_ep1".into(),
549 statement: "st1".into(),
550 episode: "ep1".into(),
551 extractor: Some("ext1".into()),
552 polarity: Polarity::Affirm,
553 claimed_from: ts(100),
554 claimed_to: TIME_MAX,
555 confidence: 0.9,
556 recorded_at: ts(1),
557 retracted_at: None,
558 };
559 let group = vec![StatementEntry {
560 statement: stmt,
561 assertions: vec![ext_assertion],
562 }];
563 let mut hearsay_def = def_employed();
565 hearsay_def.confidence_prior = 0.3;
566 let hearsay_beliefs = fold(&hearsay_def, &group, TIME_MAX, &CalibrationTable::default());
567
568 let normal_beliefs = fold(
570 &def_employed(),
571 &group,
572 TIME_MAX,
573 &CalibrationTable::default(),
574 );
575
576 assert_eq!(hearsay_beliefs.len(), 1);
577 assert_eq!(normal_beliefs.len(), 1);
578 assert!(
580 hearsay_beliefs[0].confidence < normal_beliefs[0].confidence,
581 "hearsay confidence {} should be < normal confidence {}",
582 hearsay_beliefs[0].confidence,
583 normal_beliefs[0].confidence
584 );
585 }
586
587 #[test]
589 fn supersession_closes_previous() {
590 let stmt_a = make_stmt("st_a", "e1", "employed_by", "acme");
591 let stmt_b = make_stmt("st_b", "e1", "employed_by", "globex");
592 let group = vec![
593 StatementEntry {
594 statement: stmt_a,
595 assertions: vec![make_assertion(
596 "st_a",
597 "ep1",
598 Polarity::Affirm,
599 ts(100),
600 TIME_MAX,
601 )],
602 },
603 StatementEntry {
604 statement: stmt_b,
605 assertions: vec![make_assertion(
606 "st_b",
607 "ep2",
608 Polarity::Affirm,
609 ts(200),
610 TIME_MAX,
611 )],
612 },
613 ];
614 let beliefs = fold(
615 &def_employed(),
616 &group,
617 TIME_MAX,
618 &CalibrationTable::default(),
619 );
620 let acme = beliefs
622 .iter()
623 .find(|b| b.statement == "st_a")
624 .expect("acme belief");
625 let globex = beliefs
626 .iter()
627 .find(|b| b.statement == "st_b")
628 .expect("globex belief");
629 assert_eq!(acme.status, BeliefStatus::Superseded);
630 assert_eq!(acme.valid_to, ts(199));
631 assert_eq!(globex.status, BeliefStatus::Active);
632 assert_eq!(globex.valid_from, ts(200));
633 }
634
635 #[test]
637 fn static_two_values_contradicted() {
638 let stmt_a = make_stmt("st_a", "e1", "born_in", "seoul");
639 let stmt_b = make_stmt("st_b", "e1", "born_in", "busan");
640 let group = vec![
641 StatementEntry {
642 statement: stmt_a,
643 assertions: vec![make_assertion(
644 "st_a",
645 "ep1",
646 Polarity::Affirm,
647 TIME_MIN,
648 TIME_MAX,
649 )],
650 },
651 StatementEntry {
652 statement: stmt_b,
653 assertions: vec![make_assertion(
654 "st_b",
655 "ep2",
656 Polarity::Affirm,
657 TIME_MIN,
658 TIME_MAX,
659 )],
660 },
661 ];
662 let beliefs = fold(
663 &def_born_in(),
664 &group,
665 TIME_MAX,
666 &CalibrationTable::default(),
667 );
668 assert_eq!(beliefs.len(), 2);
669 assert!(
670 beliefs
671 .iter()
672 .all(|b| b.status == BeliefStatus::Contradicted)
673 );
674 }
675
676 #[test]
678 fn multivalued_coexist() {
679 let stmt_a = make_stmt("st_a", "e1", "works_on", "px");
680 let stmt_b = make_stmt("st_b", "e1", "works_on", "py");
681 let group = vec![
682 StatementEntry {
683 statement: stmt_a,
684 assertions: vec![make_assertion(
685 "st_a",
686 "ep1",
687 Polarity::Affirm,
688 ts(100),
689 TIME_MAX,
690 )],
691 },
692 StatementEntry {
693 statement: stmt_b,
694 assertions: vec![make_assertion(
695 "st_b",
696 "ep2",
697 Polarity::Affirm,
698 ts(100),
699 TIME_MAX,
700 )],
701 },
702 ];
703 let beliefs = fold(
704 &def_works_on(),
705 &group,
706 TIME_MAX,
707 &CalibrationTable::default(),
708 );
709 assert_eq!(beliefs.len(), 2);
710 assert!(beliefs.iter().all(|b| b.status == BeliefStatus::Active));
711 }
712
713 #[test]
715 fn denial_clips() {
716 let stmt = make_stmt("st1", "e1", "works_on", "px");
717 let group = vec![StatementEntry {
718 statement: stmt,
719 assertions: vec![
720 make_assertion("st1", "ep1", Polarity::Affirm, ts(100), ts(500)),
721 Assertion {
722 id: "deny1".into(),
723 statement: "st1".into(),
724 episode: "ep2".into(),
725 extractor: None,
726 polarity: Polarity::Deny,
727 claimed_from: ts(200),
728 claimed_to: ts(300),
729 confidence: 1.0,
730 recorded_at: ts(2),
731 retracted_at: None,
732 },
733 ],
734 }];
735 let beliefs = fold(
736 &def_works_on(),
737 &group,
738 TIME_MAX,
739 &CalibrationTable::default(),
740 );
741 assert_eq!(beliefs.len(), 2);
743 assert_eq!(beliefs[0].valid_from, ts(100));
744 assert_eq!(beliefs[0].valid_to, ts(199));
745 assert_eq!(beliefs[1].valid_from, ts(301));
746 assert_eq!(beliefs[1].valid_to, ts(500));
747 }
748
749 #[test]
751 fn retracted_assertion_invisible() {
752 let stmt = make_stmt("st1", "e1", "employed_by", "acme");
753 let group = vec![StatementEntry {
754 statement: stmt,
755 assertions: vec![Assertion {
756 id: "a1".into(),
757 statement: "st1".into(),
758 episode: "ep1".into(),
759 extractor: None,
760 polarity: Polarity::Affirm,
761 claimed_from: ts(100),
762 claimed_to: TIME_MAX,
763 confidence: 1.0,
764 recorded_at: ts(1),
765 retracted_at: Some(ts(5)), }],
767 }];
768 let beliefs = fold(
769 &def_employed(),
770 &group,
771 ts(10),
772 &CalibrationTable::default(),
773 );
774 assert!(
775 beliefs.is_empty(),
776 "retracted assertion should produce no belief"
777 );
778 }
779
780 #[test]
782 fn output_sorted() {
783 let stmt_a = make_stmt("st_a", "e1", "works_on", "px");
784 let stmt_b = make_stmt("st_b", "e1", "works_on", "py");
785 let group = vec![
786 StatementEntry {
787 statement: stmt_b,
788 assertions: vec![make_assertion(
789 "st_b",
790 "ep2",
791 Polarity::Affirm,
792 ts(200),
793 TIME_MAX,
794 )],
795 },
796 StatementEntry {
797 statement: stmt_a,
798 assertions: vec![make_assertion(
799 "st_a",
800 "ep1",
801 Polarity::Affirm,
802 ts(100),
803 TIME_MAX,
804 )],
805 },
806 ];
807 let beliefs = fold(
808 &def_works_on(),
809 &group,
810 TIME_MAX,
811 &CalibrationTable::default(),
812 );
813 assert_eq!(beliefs[0].statement, "st_a");
814 assert_eq!(beliefs[1].statement, "st_b");
815 }
816
817 #[test]
819 fn empty_group() {
820 let beliefs = fold(&def_employed(), &[], TIME_MAX, &CalibrationTable::default());
821 assert!(beliefs.is_empty());
822 }
823}