Skip to main content

codetether_agent/cli/
oracle.rs

1//! Oracle command handlers.
2
3use super::{OracleArgs, OracleCommand, OracleSyncArgs, OracleValidateArgs};
4use crate::rlm::oracle::{OracleResult, OracleTraceStorage, TraceValidator};
5use crate::rlm::{RlmAnalysisResult, RlmChunker, RlmStats, SubQuery};
6use anyhow::{Context, Result};
7use serde_json::json;
8use std::io::Read;
9
10pub async fn execute(args: OracleArgs) -> Result<()> {
11    match args.command {
12        OracleCommand::Validate(v) => execute_validate(v).await,
13        OracleCommand::Sync(s) => execute_sync(s).await,
14    }
15}
16
17async fn execute_validate(args: OracleValidateArgs) -> Result<()> {
18    let source = load_source(&args)?;
19    let payload = load_payload(&args)?;
20    let source_path = args.file.as_ref().map(|p| p.to_string_lossy().to_string());
21
22    let synthetic = RlmAnalysisResult {
23        answer: payload.clone(),
24        iterations: 1,
25        sub_queries: vec![SubQuery {
26            query: args.query.clone(),
27            context_slice: source_path.clone(),
28            response: payload.clone(),
29            tokens_used: 0,
30        }],
31        stats: RlmStats {
32            input_tokens: RlmChunker::estimate_tokens(&source),
33            output_tokens: RlmChunker::estimate_tokens(&payload),
34            iterations: 1,
35            subcalls: 0,
36            elapsed_ms: 0,
37            compression_ratio: 1.0,
38        },
39    };
40
41    let validator = TraceValidator::new();
42    let oracle_result = validator.validate(
43        &synthetic,
44        &source,
45        source_path.as_deref(),
46        None,
47        Some(Vec::new()),
48    );
49
50    let persist = if args.persist {
51        let storage = OracleTraceStorage::from_env_or_vault().await;
52        Some(storage.persist_result(&oracle_result).await?)
53    } else {
54        None
55    };
56
57    if args.json {
58        let out = json!({
59            "oracle": oracle_json(&oracle_result),
60            "record": oracle_result.to_record(),
61            "persist": persist,
62        });
63        println!("{}", serde_json::to_string_pretty(&out)?);
64    } else {
65        println!("{}", oracle_status_line(&oracle_result));
66        if let Some(ref p) = persist {
67            if p.uploaded {
68                println!(
69                    "[persist: uploaded ✓] {}",
70                    p.remote_url.clone().unwrap_or_default()
71                );
72            } else {
73                println!("[persist: spooled —] {}", p.spooled_path);
74            }
75            if let Some(ref w) = p.warning {
76                println!("warning: {}", w);
77            }
78        }
79    }
80
81    Ok(())
82}
83
84async fn execute_sync(args: OracleSyncArgs) -> Result<()> {
85    let storage = OracleTraceStorage::from_env_or_vault().await;
86    let stats = storage.sync_pending().await?;
87
88    if args.json {
89        println!("{}", serde_json::to_string_pretty(&stats)?);
90    } else {
91        println!(
92            "synced={} retained={} failed={} pending_after={}",
93            stats.uploaded, stats.retained, stats.failed, stats.pending_after
94        );
95    }
96
97    Ok(())
98}
99
100fn load_source(args: &OracleValidateArgs) -> Result<String> {
101    match (&args.file, &args.content) {
102        (Some(path), None) => std::fs::read_to_string(path)
103            .with_context(|| format!("Failed to read source file {}", path.display())),
104        (None, Some(content)) if content == "-" => {
105            let mut stdin_content = String::new();
106            std::io::stdin().read_to_string(&mut stdin_content)?;
107            Ok(stdin_content)
108        }
109        (None, Some(content)) => Ok(content.clone()),
110        (Some(_), Some(_)) => {
111            anyhow::bail!("Provide either --file or --content, not both");
112        }
113        (None, None) => {
114            anyhow::bail!("Provide --file <path> or --content <text|->");
115        }
116    }
117}
118
119fn load_payload(args: &OracleValidateArgs) -> Result<String> {
120    match (&args.payload, &args.payload_file) {
121        (Some(payload), None) => Ok(payload.clone()),
122        (None, Some(path)) => std::fs::read_to_string(path)
123            .with_context(|| format!("Failed to read payload file {}", path.display())),
124        (Some(_), Some(_)) => {
125            anyhow::bail!("Provide either --payload or --payload-file, not both");
126        }
127        (None, None) => {
128            anyhow::bail!("Provide --payload <json> or --payload-file <path>");
129        }
130    }
131}
132
133fn oracle_json(result: &OracleResult) -> serde_json::Value {
134    match result {
135        OracleResult::Golden(trace) => json!({
136            "status": "golden",
137            "verdict": trace.verdict,
138            "verification_method": format!("{:?}", trace.verification_method),
139            "trace_id": trace.trace_id,
140        }),
141        OracleResult::Consensus {
142            trace,
143            agreement_ratio,
144        } => json!({
145            "status": "consensus",
146            "agreement_ratio": agreement_ratio,
147            "verdict": trace.verdict,
148            "verification_method": format!("{:?}", trace.verification_method),
149            "trace_id": trace.trace_id,
150        }),
151        OracleResult::Unverified { reason, trace } => json!({
152            "status": "unverified",
153            "reason": reason,
154            "verdict": trace.verdict,
155            "verification_method": format!("{:?}", trace.verification_method),
156            "trace_id": trace.trace_id,
157        }),
158        OracleResult::Failed {
159            reason,
160            diff,
161            trace,
162        } => json!({
163            "status": "failed",
164            "reason": reason,
165            "diff": diff,
166            "verdict": trace.verdict,
167            "verification_method": format!("{:?}", trace.verification_method),
168            "trace_id": trace.trace_id,
169        }),
170    }
171}
172
173fn oracle_status_line(result: &OracleResult) -> String {
174    match result {
175        OracleResult::Golden(_) => {
176            "[oracle: golden ✓] deterministic verification passed".to_string()
177        }
178        OracleResult::Consensus {
179            agreement_ratio, ..
180        } => format!(
181            "[oracle: consensus ✓] semantic agreement {:.1}%",
182            agreement_ratio * 100.0
183        ),
184        OracleResult::Unverified { reason, .. } => {
185            format!("[oracle: unverified —] {}", reason)
186        }
187        OracleResult::Failed { reason, .. } => format!("[oracle: failed ✗] {}", reason),
188    }
189}