Skip to main content

agentic_contracts/
grounding.rs

1//! Grounding trait for evidence verification (V2 Pattern).
2//!
3//! v0.2.0 — Rewritten to match how all sisters ACTUALLY implement grounding.
4//!
5//! The real pattern across Memory, Vision, Identity, and Codebase is:
6//!
7//! 1. `ground(claim)` → Search-based claim verification (BM25, word-overlap, etc.)
8//! 2. `evidence(query)` → Get detailed evidence for a query
9//! 3. `suggest(query)` → Fuzzy fallback when exact match fails
10//!
11//! Key differences from v0.1.0:
12//! - No `evidence_id` parameter — all sisters SEARCH for evidence
13//! - `ground()` takes a claim string, not a `GroundingRequest` with evidence_id
14//! - Three-status result: verified / partial / ungrounded
15//! - Optional per sister (Time has no grounding)
16
17use crate::errors::SisterResult;
18use crate::types::{Metadata, SisterType};
19use chrono::{DateTime, Utc};
20use serde::{Deserialize, Serialize};
21
22// ═══════════════════════════════════════════════════════════════════
23// GROUNDING RESULT TYPES
24// ═══════════════════════════════════════════════════════════════════
25
26/// Status of a grounding check
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum GroundingStatus {
30    /// Claim is fully supported by evidence
31    Verified,
32
33    /// Claim is partially supported (some aspects verified, others not)
34    Partial,
35
36    /// No evidence found to support the claim
37    Ungrounded,
38}
39
40impl std::fmt::Display for GroundingStatus {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self {
43            Self::Verified => write!(f, "verified"),
44            Self::Partial => write!(f, "partial"),
45            Self::Ungrounded => write!(f, "ungrounded"),
46        }
47    }
48}
49
50/// Result of a grounding check.
51///
52/// Mirrors the actual response shape all sisters return.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct GroundingResult {
55    /// Grounding status
56    pub status: GroundingStatus,
57
58    /// The claim that was checked
59    pub claim: String,
60
61    /// Confidence level (0.0 = no support, 1.0 = full support)
62    pub confidence: f64,
63
64    /// Evidence that supports (or fails to support) the claim
65    pub evidence: Vec<GroundingEvidence>,
66
67    /// Human-readable explanation
68    pub reason: String,
69
70    /// Suggestions for related content (when ungrounded)
71    #[serde(default)]
72    pub suggestions: Vec<String>,
73
74    /// Timestamp of grounding check
75    pub timestamp: DateTime<Utc>,
76}
77
78impl GroundingResult {
79    /// Create a verified result
80    pub fn verified(claim: impl Into<String>, confidence: f64) -> Self {
81        Self {
82            status: GroundingStatus::Verified,
83            claim: claim.into(),
84            confidence,
85            evidence: vec![],
86            reason: String::new(),
87            suggestions: vec![],
88            timestamp: Utc::now(),
89        }
90    }
91
92    /// Create an ungrounded result
93    pub fn ungrounded(claim: impl Into<String>, reason: impl Into<String>) -> Self {
94        Self {
95            status: GroundingStatus::Ungrounded,
96            claim: claim.into(),
97            confidence: 0.0,
98            evidence: vec![],
99            reason: reason.into(),
100            suggestions: vec![],
101            timestamp: Utc::now(),
102        }
103    }
104
105    /// Create a partial result
106    pub fn partial(claim: impl Into<String>, confidence: f64) -> Self {
107        Self {
108            status: GroundingStatus::Partial,
109            claim: claim.into(),
110            confidence,
111            evidence: vec![],
112            reason: String::new(),
113            suggestions: vec![],
114            timestamp: Utc::now(),
115        }
116    }
117
118    /// Add evidence items
119    pub fn with_evidence(mut self, evidence: Vec<GroundingEvidence>) -> Self {
120        self.evidence = evidence;
121        self
122    }
123
124    /// Add suggestions
125    pub fn with_suggestions(mut self, suggestions: Vec<String>) -> Self {
126        self.suggestions = suggestions;
127        self
128    }
129
130    /// Add reason
131    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
132        self.reason = reason.into();
133        self
134    }
135
136    /// Check if strongly grounded (confidence > 0.8)
137    pub fn is_strongly_grounded(&self) -> bool {
138        self.status == GroundingStatus::Verified && self.confidence > 0.8
139    }
140
141    /// Check if weakly grounded (confidence > 0.5)
142    pub fn is_weakly_grounded(&self) -> bool {
143        self.status != GroundingStatus::Ungrounded && self.confidence > 0.5
144    }
145}
146
147/// A piece of evidence returned by grounding.
148///
149/// Intentionally flexible — each sister populates the fields
150/// relevant to its domain. Memory returns nodes, Vision returns
151/// observations, Identity returns trust grants + receipts, etc.
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct GroundingEvidence {
154    /// Evidence type (sister-specific: "memory_node", "observation",
155    /// "trust_grant", "receipt", "code_symbol", etc.)
156    pub evidence_type: String,
157
158    /// Evidence identifier (node_id, observation_id, grant_id, etc.)
159    pub id: String,
160
161    /// Relevance score (higher = more relevant)
162    pub score: f64,
163
164    /// Human-readable summary of the evidence
165    pub summary: String,
166
167    /// Sister-specific structured data
168    #[serde(default)]
169    pub data: Metadata,
170}
171
172impl GroundingEvidence {
173    /// Create a new evidence item
174    pub fn new(
175        evidence_type: impl Into<String>,
176        id: impl Into<String>,
177        score: f64,
178        summary: impl Into<String>,
179    ) -> Self {
180        Self {
181            evidence_type: evidence_type.into(),
182            id: id.into(),
183            score,
184            summary: summary.into(),
185            data: Metadata::new(),
186        }
187    }
188
189    /// Add structured data
190    pub fn with_data(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
191        if let Ok(v) = serde_json::to_value(value) {
192            self.data.insert(key.into(), v);
193        }
194        self
195    }
196}
197
198// ═══════════════════════════════════════════════════════════════════
199// EVIDENCE DETAIL TYPES (for the evidence() method)
200// ═══════════════════════════════════════════════════════════════════
201
202/// Detailed evidence item returned by the `evidence()` method.
203///
204/// More detailed than `GroundingEvidence` — includes full content,
205/// timestamps, relationships, etc.
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct EvidenceDetail {
208    /// Evidence type
209    pub evidence_type: String,
210
211    /// Unique ID
212    pub id: String,
213
214    /// Relevance score
215    pub score: f64,
216
217    /// When this evidence was created
218    pub created_at: DateTime<Utc>,
219
220    /// Which sister produced this
221    pub source_sister: SisterType,
222
223    /// Full content/description
224    pub content: String,
225
226    /// Sister-specific structured data (edges, dimensions, capabilities, etc.)
227    #[serde(default)]
228    pub data: Metadata,
229}
230
231// ═══════════════════════════════════════════════════════════════════
232// SUGGESTION TYPE (for the suggest() method)
233// ═══════════════════════════════════════════════════════════════════
234
235/// A suggestion returned when a claim doesn't match exactly
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct GroundingSuggestion {
238    /// What type of item this is
239    pub item_type: String,
240
241    /// Item identifier
242    pub id: String,
243
244    /// Relevance score
245    pub relevance_score: f64,
246
247    /// Human-readable description
248    pub description: String,
249
250    /// Sister-specific data
251    #[serde(default)]
252    pub data: Metadata,
253}
254
255// ═══════════════════════════════════════════════════════════════════
256// THE GROUNDING TRAIT
257// ═══════════════════════════════════════════════════════════════════
258
259/// Grounding capability — sisters that verify claims implement this.
260///
261/// Implemented by: Memory, Vision, Identity, Codebase
262/// NOT implemented by: Time (no grounding concept)
263///
264/// The three methods mirror the actual `{sister}_ground`,
265/// `{sister}_evidence`, and `{sister}_suggest` MCP tools.
266pub trait Grounding {
267    /// Verify a claim against stored evidence.
268    ///
269    /// Searches for evidence that supports or refutes the claim.
270    /// Returns verified/partial/ungrounded status with confidence.
271    ///
272    /// # Rule: NEVER throw on missing evidence
273    /// Return `GroundingStatus::Ungrounded` with `confidence: 0.0` instead.
274    fn ground(&self, claim: &str) -> SisterResult<GroundingResult>;
275
276    /// Get detailed evidence for a query.
277    ///
278    /// Returns matching evidence items with full content and metadata.
279    /// `max_results` limits the number of items returned.
280    fn evidence(&self, query: &str, max_results: usize) -> SisterResult<Vec<EvidenceDetail>>;
281
282    /// Find similar items when an exact match fails.
283    ///
284    /// Returns suggestions that are close to the query,
285    /// helping the LLM recover from ungrounded claims.
286    fn suggest(&self, query: &str, limit: usize) -> SisterResult<Vec<GroundingSuggestion>>;
287}
288
289// ═══════════════════════════════════════════════════════════════════
290// LEGACY COMPATIBILITY
291// ═══════════════════════════════════════════════════════════════════
292
293/// Type of evidence (kept for categorization, but no longer used
294/// as the primary lookup mechanism).
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296#[serde(rename_all = "snake_case")]
297pub enum EvidenceType {
298    // Memory evidence
299    MemoryNode,
300    MemoryRelation,
301    MemorySession,
302
303    // Vision evidence
304    Screenshot,
305    DomFingerprint,
306    VisualDiff,
307    VisualComparison,
308
309    // Codebase evidence
310    CodeNode,
311    ImpactAnalysis,
312    Prophecy,
313    DependencyGraph,
314
315    // Identity evidence
316    Receipt,
317    TrustGrant,
318    CompetenceProof,
319    Signature,
320
321    // Time evidence
322    TimelineEvent,
323    DurationProof,
324    DeadlineCheck,
325
326    // Contract evidence
327    Agreement,
328    PolicyCheck,
329    BoundaryVerification,
330
331    // Generic
332    Custom(String),
333}
334
335impl std::fmt::Display for EvidenceType {
336    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337        match self {
338            Self::Custom(s) => write!(f, "{}", s),
339            other => write!(f, "{:?}", other),
340        }
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_grounding_result_verified() {
350        let result = GroundingResult::verified("the sky is blue", 0.95)
351            .with_evidence(vec![GroundingEvidence::new(
352                "memory_node",
353                "node_42",
354                0.95,
355                "Sky color observation from session 1",
356            )])
357            .with_reason("Found strong evidence in memory");
358
359        assert_eq!(result.status, GroundingStatus::Verified);
360        assert!(result.is_strongly_grounded());
361        assert_eq!(result.evidence.len(), 1);
362    }
363
364    #[test]
365    fn test_grounding_result_ungrounded() {
366        let result = GroundingResult::ungrounded("cats can fly", "No evidence found")
367            .with_suggestions(vec!["cats can jump".into(), "birds can fly".into()]);
368
369        assert_eq!(result.status, GroundingStatus::Ungrounded);
370        assert!(!result.is_strongly_grounded());
371        assert!(!result.is_weakly_grounded());
372        assert_eq!(result.suggestions.len(), 2);
373    }
374
375    #[test]
376    fn test_grounding_evidence_builder() {
377        let evidence =
378            GroundingEvidence::new("trust_grant", "atrust_123", 0.8, "Deploy capability")
379                .with_data("capabilities", vec!["deploy:prod"]);
380
381        assert_eq!(evidence.evidence_type, "trust_grant");
382        assert_eq!(evidence.score, 0.8);
383        assert!(evidence.data.contains_key("capabilities"));
384    }
385
386    #[test]
387    fn test_grounding_status_display() {
388        assert_eq!(GroundingStatus::Verified.to_string(), "verified");
389        assert_eq!(GroundingStatus::Partial.to_string(), "partial");
390        assert_eq!(GroundingStatus::Ungrounded.to_string(), "ungrounded");
391    }
392}