Skip to main content

qualia_core_db/solvers/grounding/
claim_support.rs

1//! The support arithmetic: how strongly a claim triple is backed by a set of cited
2//! fact quins. Pure over [`NQuin`]s — no I/O, fully testable.
3//!
4//! A claim is the triple `(subject, predicate, object)` of `AgentOutput::semantic_quin`.
5//! Two complementary, real signals are computed over the cited facts:
6//!
7//! * **Role support** — the strongest single fact that matches the claim's components
8//!   *in the same role*: an exact triple match scores `1.0`; a predicate+object or
9//!   subject+predicate match `2/3`; a single role `1/3`. This is "is this exact
10//!   relation attested?".
11//! * **Entity grounding** — the fraction of the claim's two *endpoints* (subject,
12//!   object) that appear *anywhere* in the evidence. This catches a claim that is a
13//!   legitimate multi-hop consequence of cited facts even when no single fact is the
14//!   whole triple: "are the things I'm talking about even in the evidence?".
15//!
16//! The combined score takes the stronger of role support and a capped half-weight of
17//! entity grounding, so an exact attestation always dominates while a claim whose
18//! endpoints are at least cited gets partial (review-band) credit.
19
20use crate::NQuin;
21
22/// Per-claim grounding signals plus the combined score in `[0, 1]`.
23#[derive(Debug, Clone, Copy, PartialEq)]
24pub struct GroundingReport {
25    /// Strongest same-role overlap with a single cited fact (0, 1/3, 2/3, 1).
26    pub role_support: f64,
27    /// Fraction of `{subject, object}` appearing anywhere in the evidence (0, 0.5, 1).
28    pub entity_grounding: f64,
29    /// `true` iff some cited fact equals the claim triple exactly.
30    pub exact: bool,
31    /// Combined grounding score.
32    pub score: f64,
33}
34
35/// Same-role overlap between a claim and one fact: count of matching `(s, p, o)`
36/// positions, as a fraction of three.
37fn role_overlap(claim: &NQuin, fact: &NQuin) -> f64 {
38    let mut m = 0u8;
39    if claim.subject == fact.subject {
40        m += 1;
41    }
42    if claim.predicate == fact.predicate {
43        m += 1;
44    }
45    if claim.object == fact.object {
46        m += 1;
47    }
48    m as f64 / 3.0
49}
50
51/// Strongest same-role support for `claim` across `facts` (max role overlap). `0.0`
52/// for empty evidence.
53pub fn component_support(claim: &NQuin, facts: &[NQuin]) -> f64 {
54    facts
55        .iter()
56        .fold(0.0_f64, |best, f| best.max(role_overlap(claim, f)))
57}
58
59/// Fraction of the claim's endpoints `{subject, object}` that occur in *any* role
60/// (subject, predicate or object) of *any* cited fact.
61pub fn entity_grounding(claim: &NQuin, facts: &[NQuin]) -> f64 {
62    let present = |h: u64| {
63        h != 0
64            && facts
65                .iter()
66                .any(|f| f.subject == h || f.predicate == h || f.object == h)
67    };
68    let mut grounded = 0u8;
69    if present(claim.subject) {
70        grounded += 1;
71    }
72    if present(claim.object) {
73        grounded += 1;
74    }
75    grounded as f64 / 2.0
76}
77
78/// Build the full grounding report for `claim` over `facts`.
79pub fn report(claim: &NQuin, facts: &[NQuin]) -> GroundingReport {
80    let role_support = component_support(claim, facts);
81    let eg = entity_grounding(claim, facts);
82    let exact = facts.iter().any(|f| {
83        f.subject == claim.subject && f.predicate == claim.predicate && f.object == claim.object
84    });
85    // Exact attestation dominates; otherwise the stronger of role support and a capped
86    // half-weight of endpoint grounding.
87    let score = if exact {
88        1.0
89    } else {
90        role_support.max(0.5 * eg)
91    };
92    GroundingReport {
93        role_support,
94        entity_grounding: eg,
95        exact,
96        score,
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    fn quin(s: u64, p: u64, o: u64) -> NQuin {
105        NQuin {
106            subject: s,
107            predicate: p,
108            object: o,
109            context: 0,
110            metadata: 0,
111            parity: 0,
112        }
113    }
114
115    #[test]
116    fn exact_match_scores_full() {
117        let claim = quin(1, 2, 3);
118        let facts = [quin(9, 9, 9), quin(1, 2, 3)];
119        let r = report(&claim, &facts);
120        assert!(r.exact);
121        assert!((r.score - 1.0).abs() < 1e-12);
122    }
123
124    #[test]
125    fn two_role_match_clears_two_thirds() {
126        // predicate + object match, subject differs.
127        let claim = quin(1, 2, 3);
128        let facts = [quin(7, 2, 3)];
129        let r = report(&claim, &facts);
130        assert!(!r.exact);
131        assert!((r.role_support - 2.0 / 3.0).abs() < 1e-12);
132        assert!((r.score - 2.0 / 3.0).abs() < 1e-12);
133    }
134
135    #[test]
136    fn both_endpoints_cited_elsewhere_is_review_band() {
137        // No single fact matches a role of the claim, but both endpoints (1 and 3)
138        // appear across the evidence in *other* roles (1 as a predicate, 3 as a
139        // subject) → entity grounding 1.0, role support 0 → score 0.5.
140        let claim = quin(1, 2, 3);
141        let facts = [quin(50, 1, 60), quin(3, 70, 80)];
142        let r = report(&claim, &facts);
143        assert_eq!(r.role_support, 0.0);
144        assert!((r.entity_grounding - 1.0).abs() < 1e-12);
145        assert!((r.score - 0.5).abs() < 1e-12);
146    }
147
148    #[test]
149    fn unrelated_evidence_scores_zero() {
150        let claim = quin(1, 2, 3);
151        let facts = [quin(4, 5, 6)];
152        let r = report(&claim, &facts);
153        assert!(r.score.abs() < 1e-12);
154    }
155
156    #[test]
157    fn empty_evidence_scores_zero() {
158        let claim = quin(1, 2, 3);
159        let r = report(&claim, &[]);
160        assert!(r.score.abs() < 1e-12);
161        assert!(!r.exact);
162    }
163
164    #[test]
165    fn zero_hash_endpoints_do_not_ground() {
166        // A claim with a zero (unset) subject must not be credited just because some
167        // fact also has structure — entity_grounding ignores zero hashes.
168        let claim = quin(0, 2, 3);
169        let facts = [quin(0, 0, 0)];
170        assert_eq!(entity_grounding(&claim, &facts), 0.0);
171    }
172}