Skip to main content

codetether_agent/rlm/oracle/
schema.rs

1//! FINAL(JSON) schema for oracle-eligible RLM outputs.
2//!
3//! This module defines the structured JSON schema that RLM FINAL() outputs must conform to
4//! for deterministic oracle verification. The `kind` field drives validator routing.
5//!
6//! # Schema Types
7//!
8//! - **Grep**: Pattern-match results (line numbers, text content)
9//! - **Ast**: Structural AST query results (function signatures, struct fields)
10//! - **Semantic**: Free-form text answers (unverifiable - stored but not golden)
11//!
12//! # Usage
13//!
14//! ```ignore
15//! use codetether_agent::rlm::oracle::schema::{FinalPayload, GrepPayload, AstPayload};
16//!
17//! // Parse a FINAL() JSON output
18//! let payload = FinalPayload::parse(r#"{"kind": "grep", "file": "src/main.rs", ...}"#)?;
19//!
20//! match payload {
21//!     FinalPayload::Grep(grep) => { /* verify with GrepOracle */ }
22//!     FinalPayload::Ast(ast) => { /* verify with TreeSitterOracle */ }
23//!     FinalPayload::Semantic(_) => { /* cannot verify - skip */ }
24//!     FinalPayload::Malformed { .. } => { /* log and skip */ }
25//! }
26//! ```
27
28use serde::{Deserialize, Serialize};
29use std::fmt;
30
31/// The top-level FINAL() payload envelope.
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33#[serde(tag = "kind", rename_all = "lowercase")]
34pub enum FinalPayload {
35    /// Grep/pattern-match query results
36    Grep(GrepPayload),
37    /// AST/structural query results
38    Ast(AstPayload),
39    /// Semantic/free-form text (unverifiable)
40    Semantic(SemanticPayload),
41    /// Malformed JSON that couldn't be parsed
42    #[serde(skip)]
43    Malformed {
44        /// The raw string that failed to parse
45        raw: String,
46        /// Error message from parsing attempt
47        error: String,
48    },
49}
50
51impl FinalPayload {
52    /// Parse a JSON string into a FinalPayload.
53    ///
54    /// Returns `FinalPayload::Malformed` if parsing fails.
55    pub fn parse(json_str: &str) -> Self {
56        let trimmed = json_str.trim();
57        
58        // Try to parse as JSON
59        let parsed: Result<serde_json::Value, _> = serde_json::from_str(trimmed);
60        
61        match parsed {
62            Ok(value) => {
63                // Try to deserialize into our enum
64                match serde_json::from_value::<FinalPayload>(value.clone()) {
65                    Ok(payload) => payload,
66                    Err(e) => {
67                        // JSON is valid but doesn't match our schema
68                        // Check if it has a "kind" field we can use
69                        if let Some(kind) = value.get("kind").and_then(|k| k.as_str()) {
70                            match kind {
71                                "grep" => {
72                                    serde_json::from_value(value).unwrap_or_else(|e2| {
73                                        FinalPayload::Malformed {
74                                            raw: trimmed.to_string(),
75                                            error: format!("GrepPayload parse error: {}", e2),
76                                        }
77                                    })
78                                }
79                                "ast" => {
80                                    serde_json::from_value(value).unwrap_or_else(|e2| {
81                                        FinalPayload::Malformed {
82                                            raw: trimmed.to_string(),
83                                            error: format!("AstPayload parse error: {}", e2),
84                                        }
85                                    })
86                                }
87                                "semantic" => {
88                                    serde_json::from_value(value).unwrap_or_else(|e2| {
89                                        FinalPayload::Malformed {
90                                            raw: trimmed.to_string(),
91                                            error: format!("SemanticPayload parse error: {}", e2),
92                                        }
93                                    })
94                                }
95                                _ => FinalPayload::Malformed {
96                                    raw: trimmed.to_string(),
97                                    error: format!("Unknown kind: {}", kind),
98                                }
99                            }
100                        } else {
101                            FinalPayload::Malformed {
102                                raw: trimmed.to_string(),
103                                error: format!("Missing 'kind' field: {}", e),
104                            }
105                        }
106                    }
107                }
108            }
109            Err(e) => {
110                // Not valid JSON at all
111                FinalPayload::Malformed {
112                    raw: trimmed.to_string(),
113                    error: format!("JSON parse error: {}", e),
114                }
115            }
116        }
117    }
118
119    /// Check if this payload is verifiable by an oracle.
120    pub fn is_verifiable(&self) -> bool {
121        matches!(self, FinalPayload::Grep(_) | FinalPayload::Ast(_))
122    }
123
124    /// Get the file path this payload references (if any).
125    pub fn file(&self) -> Option<&str> {
126        match self {
127            FinalPayload::Grep(p) => Some(&p.file),
128            FinalPayload::Ast(p) => Some(&p.file),
129            FinalPayload::Semantic(p) => Some(&p.file),
130            FinalPayload::Malformed { .. } => None,
131        }
132    }
133
134    /// Convert to a debuggable string representation.
135    pub fn summary(&self) -> String {
136        match self {
137            FinalPayload::Grep(p) => {
138                format!("Grep(file={}, pattern={}, {} matches)", 
139                    p.file, p.pattern, p.matches.len())
140            }
141            FinalPayload::Ast(p) => {
142                format!("Ast(file={}, query={}, {} results)", 
143                    p.file, p.query, p.results.len())
144            }
145            FinalPayload::Semantic(p) => {
146                let preview = if p.answer.len() > 50 {
147                    format!("{}...", &p.answer[..50])
148                } else {
149                    p.answer.clone()
150                };
151                format!("Semantic(file={}, answer={})", p.file, preview)
152            }
153            FinalPayload::Malformed { error, .. } => {
154                format!("Malformed({})", error)
155            }
156        }
157    }
158}
159
160impl fmt::Display for FinalPayload {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        write!(f, "{}", self.summary())
163    }
164}
165
166/// Grep/pattern-match payload.
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
168pub struct GrepPayload {
169    /// File that was searched
170    pub file: String,
171    /// Regex pattern used
172    pub pattern: String,
173    /// Matched lines
174    pub matches: Vec<GrepMatch>,
175}
176
177/// A single grep match with line number and text.
178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
179pub struct GrepMatch {
180    /// Line number (1-indexed, matching `grep -n`)
181    pub line: usize,
182    /// Full text of the matched line (or substring)
183    pub text: String,
184}
185
186impl GrepMatch {
187    /// Create a new match.
188    pub fn new(line: usize, text: String) -> Self {
189        Self { line, text }
190    }
191
192    /// Check if this match's text is a substring of the actual line.
193    pub fn text_matches(&self, actual_line: &str) -> bool {
194        actual_line.contains(&self.text)
195    }
196}
197
198/// AST/structural query payload.
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
200pub struct AstPayload {
201    /// File that was queried
202    pub file: String,
203    /// Tree-sitter query or query type
204    pub query: String,
205    /// Query results
206    pub results: Vec<AstResult>,
207}
208
209/// A single AST query result.
210#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
211pub struct AstResult {
212    /// Name of the matched item (function name, struct name, etc.)
213    pub name: String,
214    /// Function arguments/parameters (as string)
215    #[serde(default)]
216    pub args: Vec<String>,
217    /// Return type (as string)
218    #[serde(default)]
219    pub return_type: Option<String>,
220    /// Span: (start_line, end_line)
221    #[serde(default)]
222    pub span: Option<(usize, usize)>,
223}
224
225impl AstResult {
226    /// Create a new AST result for a function.
227    pub fn function(name: String, args: Vec<String>, return_type: Option<String>, span: Option<(usize, usize)>) -> Self {
228        Self {
229            name,
230            args,
231            return_type,
232            span,
233        }
234    }
235}
236
237/// Semantic/free-form text payload (unverifiable).
238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
239pub struct SemanticPayload {
240    /// File that was analyzed
241    pub file: String,
242    /// Free-form text answer
243    pub answer: String,
244}
245
246impl SemanticPayload {
247    /// Create a new semantic payload.
248    pub fn new(file: String, answer: String) -> Self {
249        Self { file, answer }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn parse_grep_payload() {
259        let json = r#"{
260            "kind": "grep",
261            "file": "src/main.rs",
262            "pattern": "async fn",
263            "matches": [
264                {"line": 42, "text": "async fn process() {"},
265                {"line": 100, "text": "async fn handle() {"}
266            ]
267        }"#;
268
269        let payload = FinalPayload::parse(json);
270        match payload {
271            FinalPayload::Grep(p) => {
272                assert_eq!(p.file, "src/main.rs");
273                assert_eq!(p.pattern, "async fn");
274                assert_eq!(p.matches.len(), 2);
275                assert_eq!(p.matches[0].line, 42);
276            }
277            _ => panic!("Expected Grep payload"),
278        }
279    }
280
281    #[test]
282    fn parse_ast_payload() {
283        let json = r#"{
284            "kind": "ast",
285            "file": "src/main.rs",
286            "query": "functions",
287            "results": [
288                {"name": "process", "args": ["input: &str"], "return_type": "Result<String>"}
289            ]
290        }"#;
291
292        let payload = FinalPayload::parse(json);
293        match payload {
294            FinalPayload::Ast(p) => {
295                assert_eq!(p.file, "src/main.rs");
296                assert_eq!(p.query, "functions");
297                assert_eq!(p.results.len(), 1);
298                assert_eq!(p.results[0].name, "process");
299            }
300            _ => panic!("Expected Ast payload"),
301        }
302    }
303
304    #[test]
305    fn parse_semantic_payload() {
306        let json = r#"{
307            "kind": "semantic",
308            "file": "src/main.rs",
309            "answer": "This module provides async processing."
310        }"#;
311
312        let payload = FinalPayload::parse(json);
313        match payload {
314            FinalPayload::Semantic(p) => {
315                assert_eq!(p.file, "src/main.rs");
316                assert!(p.answer.contains("async processing"));
317            }
318            _ => panic!("Expected Semantic payload"),
319        }
320    }
321
322    #[test]
323    fn parse_malformed_json() {
324        let json = "not valid json at all";
325        let payload = FinalPayload::parse(json);
326        match payload {
327            FinalPayload::Malformed { raw, error } => {
328                assert_eq!(raw, "not valid json at all");
329                assert!(error.contains("JSON parse error"));
330            }
331            _ => panic!("Expected Malformed payload"),
332        }
333    }
334
335    #[test]
336    fn parse_missing_kind_field() {
337        let json = r#"{"file": "src/main.rs", "data": "value"}"#;
338        let payload = FinalPayload::parse(json);
339        match payload {
340            FinalPayload::Malformed { error, .. } => {
341                assert!(error.contains("kind"));
342            }
343            _ => panic!("Expected Malformed payload"),
344        }
345    }
346
347    #[test]
348    fn grep_match_text_matching() {
349        let m = GrepMatch::new(42, "async fn".to_string());
350        assert!(m.text_matches("pub async fn process() -> Result<()> {"));
351        assert!(!m.text_matches("fn process() -> Result<()> {"));
352    }
353
354    #[test]
355    fn is_verifiable() {
356        let grep_json = r#"{"kind": "grep", "file": "x.rs", "pattern": "fn", "matches": []}"#;
357        let semantic_json = r#"{"kind": "semantic", "file": "x.rs", "answer": "text"}"#;
358
359        assert!(FinalPayload::parse(grep_json).is_verifiable());
360        assert!(!FinalPayload::parse(semantic_json).is_verifiable());
361    }
362
363    #[test]
364    fn file_extraction() {
365        let grep_json = r#"{"kind": "grep", "file": "src/main.rs", "pattern": "fn", "matches": []}"#;
366        assert_eq!(FinalPayload::parse(grep_json).file(), Some("src/main.rs"));
367    }
368}