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}