Skip to main content

codetether_agent/rlm/oracle/
batch_write.rs

1//! JSONL split-write implementation for batch stats.
2//!
3//! Writes each validation bucket (golden, consensus, failed,
4//! unverified) to a separate `.jsonl` file under a given
5//! output directory.
6//!
7//! # Examples
8//!
9//! ```ignore
10//! let stats = batch.write_jsonl_split("output", "prefix")?;
11//! ```
12
13use std::fs::File;
14use std::io::{BufWriter, Write};
15use std::path::Path;
16
17use anyhow::Result;
18
19use super::batch::{BatchValidationStats, SplitWriteStats};
20use super::record::OracleTraceRecord;
21
22impl BatchValidationStats {
23    /// Write each bucket to a separate JSONL file.
24    ///
25    /// Creates `{prefix}.golden.jsonl`, `{prefix}.consensus.jsonl`,
26    /// `{prefix}.failed.jsonl`, and `{prefix}.unverified.jsonl`
27    /// under `out_dir`.
28    ///
29    /// # Examples
30    ///
31    /// ```ignore
32    /// let stats = batch.write_jsonl_split("out", "pfx")?;
33    /// assert_eq!(stats.golden_count, batch.golden.len());
34    /// ```
35    pub fn write_jsonl_split(&self, out_dir: &str, prefix: &str) -> Result<SplitWriteStats> {
36        let dir = Path::new(out_dir);
37        std::fs::create_dir_all(dir)?;
38
39        let paths = BucketPaths::new(dir, prefix);
40        write_bucket(&paths.golden, &self.golden, "golden")?;
41        write_bucket(&paths.consensus, &self.consensus, "consensus")?;
42        write_reason_bucket(&paths.failed, &self.failed, "failed")?;
43        write_reason_bucket(&paths.unverified, &self.unverified, "unverified")?;
44
45        Ok(paths.into_stats(self))
46    }
47}
48
49struct BucketPaths {
50    golden: std::path::PathBuf,
51    consensus: std::path::PathBuf,
52    failed: std::path::PathBuf,
53    unverified: std::path::PathBuf,
54}
55
56impl BucketPaths {
57    fn new(dir: &Path, prefix: &str) -> Self {
58        Self {
59            golden: dir.join(format!("{prefix}.golden.jsonl")),
60            consensus: dir.join(format!("{prefix}.consensus.jsonl")),
61            failed: dir.join(format!("{prefix}.failed.jsonl")),
62            unverified: dir.join(format!("{prefix}.unverified.jsonl")),
63        }
64    }
65
66    fn into_stats(self, b: &BatchValidationStats) -> SplitWriteStats {
67        SplitWriteStats {
68            golden_path: self.golden.to_string_lossy().into(),
69            consensus_path: self.consensus.to_string_lossy().into(),
70            failed_path: self.failed.to_string_lossy().into(),
71            unverified_path: self.unverified.to_string_lossy().into(),
72            golden_count: b.golden.len(),
73            consensus_count: b.consensus.len(),
74            failed_count: b.failed.len(),
75            unverified_count: b.unverified.len(),
76        }
77    }
78}
79
80fn write_bucket(
81    path: &Path,
82    traces: &[super::trace_types::ValidatedTrace],
83    verdict: &str,
84) -> Result<()> {
85    let mut w = BufWriter::new(File::create(path)?);
86    for trace in traces {
87        let rec = OracleTraceRecord {
88            verdict: verdict.to_string(),
89            reason: None,
90            agreement_ratio: None,
91            trace: trace.clone(),
92        };
93        writeln!(w, "{}", serde_json::to_string(&rec)?)?;
94    }
95    w.flush()?;
96    Ok(())
97}
98
99fn write_reason_bucket(
100    path: &Path,
101    items: &[(super::trace_types::ValidatedTrace, String)],
102    verdict: &str,
103) -> Result<()> {
104    let mut w = BufWriter::new(File::create(path)?);
105    for (trace, reason) in items {
106        let rec = OracleTraceRecord {
107            verdict: verdict.to_string(),
108            reason: Some(reason.clone()),
109            agreement_ratio: None,
110            trace: trace.clone(),
111        };
112        writeln!(w, "{}", serde_json::to_string(&rec)?)?;
113    }
114    w.flush()?;
115    Ok(())
116}