codetether_agent/rlm/oracle/
schema.rs1use serde::{Deserialize, Serialize};
29use std::fmt;
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33#[serde(tag = "kind", rename_all = "lowercase")]
34pub enum FinalPayload {
35 Grep(GrepPayload),
37 Ast(AstPayload),
39 Semantic(SemanticPayload),
41 #[serde(skip)]
43 Malformed {
44 raw: String,
46 error: String,
48 },
49}
50
51impl FinalPayload {
52 pub fn parse(json_str: &str) -> Self {
56 let trimmed = json_str.trim();
57
58 let parsed: Result<serde_json::Value, _> = serde_json::from_str(trimmed);
60
61 match parsed {
62 Ok(value) => {
63 match serde_json::from_value::<FinalPayload>(value.clone()) {
65 Ok(payload) => payload,
66 Err(e) => {
67 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 FinalPayload::Malformed {
112 raw: trimmed.to_string(),
113 error: format!("JSON parse error: {}", e),
114 }
115 }
116 }
117 }
118
119 pub fn is_verifiable(&self) -> bool {
121 matches!(self, FinalPayload::Grep(_) | FinalPayload::Ast(_))
122 }
123
124 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
168pub struct GrepPayload {
169 pub file: String,
171 pub pattern: String,
173 pub matches: Vec<GrepMatch>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
179pub struct GrepMatch {
180 pub line: usize,
182 pub text: String,
184}
185
186impl GrepMatch {
187 pub fn new(line: usize, text: String) -> Self {
189 Self { line, text }
190 }
191
192 pub fn text_matches(&self, actual_line: &str) -> bool {
194 actual_line.contains(&self.text)
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
200pub struct AstPayload {
201 pub file: String,
203 pub query: String,
205 pub results: Vec<AstResult>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
211pub struct AstResult {
212 pub name: String,
214 #[serde(default)]
216 pub args: Vec<String>,
217 #[serde(default)]
219 pub return_type: Option<String>,
220 #[serde(default)]
222 pub span: Option<(usize, usize)>,
223}
224
225impl AstResult {
226 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
239pub struct SemanticPayload {
240 pub file: String,
242 pub answer: String,
244}
245
246impl SemanticPayload {
247 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}