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}