Skip to main content

qualia_core_db/solvers/grounding/
mod.rs

1//! **KG↔LLM grounding evaluation** — does a model's asserted claim actually trace to
2//! the graph facts it cited?
3//!
4//! The output gate (`inference/orchestrator.rs::orchestrate_inference`) already refuses
5//! any LLM output that carries *no* provenance citation. That is a presence check. This
6//! library deepens it into a **support** check: given the structured claim the model
7//! emitted (`AgentOutput::semantic_quin`) and the **resolved** cited facts, it measures
8//! the degree to which the claim is supported by those facts and returns a graded
9//! verdict.
10//!
11//! ## Why this is knowledge-level, not wisdom-level
12//!
13//! Grounding only asks "is this asserted *knowledge* traceable to attested facts?" — a
14//! Data→Knowledge check the machine is allowed to make. It never authors the final
15//! "ought"; weakly-grounded claims are routed to **human review**, not silently
16//! accepted or rewritten. This mirrors the engine-wide identity discipline: a partial
17//! match is a *proposal requiring ratification* (`closeMatch`), never an asserted fact.
18//!
19//! ## Fail-closed
20//!
21//! No citations, or a claim that traces to nothing in the evidence, yields
22//! [`GroundingVerdict::Ungrounded`] — the gate blocks. A score is only ever produced
23//! from real component arithmetic over the cited quins; nothing is fabricated.
24//!
25//! Kernel-class `Reduction` over the evidence set; CPU path always present (§13).
26
27pub mod claim_support;
28pub mod evaluate;
29
30pub use claim_support::{component_support, entity_grounding, report, GroundingReport};
31pub use evaluate::{
32    evaluate_grounding, evaluate_output_grounding, grounding_verdict, resolve_citations,
33};
34
35use crate::NQuin;
36
37/// Thresholds partitioning the grounding score into the three verdicts. `deny < permit`.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct GroundingThresholds {
40    /// Below this the claim is treated as ungrounded → block.
41    pub deny: f64,
42    /// At/above this the claim is well-grounded → allow.
43    pub permit: f64,
44}
45
46impl Default for GroundingThresholds {
47    fn default() -> Self {
48        // A 2-of-3 role match (≈0.67) clears `permit`; a single role or both endpoints
49        // merely *appearing* in evidence (≈0.33–0.5) lands in the human-review band; a
50        // lone endpoint (≤0.25) is ungrounded.
51        Self {
52            deny: 0.3,
53            permit: 0.6,
54        }
55    }
56}
57
58/// The graded outcome of grounding a claim in cited evidence.
59#[derive(Debug, Clone, Copy, PartialEq)]
60pub enum GroundingVerdict {
61    /// Claim is well-supported by the cited facts — safe to commit.
62    Grounded { score: f64 },
63    /// Partial support — must be routed to human review (do not auto-commit).
64    Weak { score: f64 },
65    /// Claim does not trace to the cited facts — block.
66    Ungrounded { score: f64 },
67}
68
69impl GroundingVerdict {
70    pub fn score(&self) -> f64 {
71        match *self {
72            GroundingVerdict::Grounded { score }
73            | GroundingVerdict::Weak { score }
74            | GroundingVerdict::Ungrounded { score } => score,
75        }
76    }
77
78    /// True only for [`GroundingVerdict::Grounded`].
79    pub fn is_grounded(&self) -> bool {
80        matches!(self, GroundingVerdict::Grounded { .. })
81    }
82}
83
84/// Resolves a provenance citation hash to its full fact quin. Implemented by whatever
85/// holds the graph (the daemon's quin store, a temporal-graph snapshot, a test stub).
86/// Grounding needs the full triple of each cited fact — a bare hash is not enough — so
87/// the gate activates only where facts are resolvable, and never false-denies a
88/// grounded claim for lack of a resolver.
89pub trait GroundingResolver {
90    /// Return the fact quin for a provenance citation hash, or `None` if unknown.
91    fn resolve(&self, citation_hash: u64) -> Option<NQuin>;
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn verdict_accessors() {
100        assert!(GroundingVerdict::Grounded { score: 0.9 }.is_grounded());
101        assert!(!GroundingVerdict::Weak { score: 0.4 }.is_grounded());
102        assert!((GroundingVerdict::Ungrounded { score: 0.1 }.score() - 0.1).abs() < 1e-12);
103    }
104
105    #[test]
106    fn default_thresholds_ordered() {
107        let t = GroundingThresholds::default();
108        assert!(t.deny < t.permit);
109    }
110}