harn_cli/commands/
eval_context.rs1use std::fs;
25use std::io::Write as _;
26use std::path::{Path, PathBuf};
27
28use harn_vm::orchestration::{
29 context_eval_default_output_dir, evaluate_context_eval_manifest, load_context_eval_manifest,
30 ContextEvalReport, ContextEvalRunReport,
31};
32
33use crate::cli::EvalContextArgs;
34use crate::dispatch;
35use crate::env_guard::ScopedEnvVar;
36
37const CONTEXT_REPORT_ENV: &str = "HARN_EVAL_CONTEXT_REPORT_JSON";
42
43const CONTEXT_OUTPUT_MODE_ENV: &str = "HARN_EVAL_CONTEXT_OUTPUT_MODE";
47
48static DISPATCH_RENDER_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
60
61pub async fn run(args: EvalContextArgs) -> i32 {
62 let report = match aggregate_report(&args) {
63 Ok(report) => report,
64 Err(code) => return code,
65 };
66
67 let output_dir = args.output.unwrap_or_else(context_eval_default_output_dir);
68 if let Err(error) = fs::create_dir_all(&output_dir) {
69 eprintln!("error: failed to create {}: {error}", output_dir.display());
70 return 1;
71 }
72
73 if let Err(error) = write_json_artifacts(&output_dir, &report) {
78 eprintln!("error: failed to write context eval outputs: {error}");
79 return 1;
80 }
81
82 match write_markdown_dispatch(&output_dir, &report).await {
83 Ok(()) => {}
84 Err(code) => return code,
85 }
86 announce_output_paths(&output_dir);
87 if args.json {
88 if let Err(code) = print_json_dispatch(&report).await {
89 return code;
90 }
91 } else if let Err(code) = print_summary_dispatch(&report).await {
92 return code;
93 }
94 post_render_exit_code(&report)
95}
96
97fn aggregate_report(args: &EvalContextArgs) -> Result<ContextEvalReport, i32> {
99 let manifest = match load_context_eval_manifest(&args.manifest) {
100 Ok(manifest) => manifest,
101 Err(error) => {
102 eprintln!("error: {error}");
103 return Err(1);
104 }
105 };
106 let report = match evaluate_context_eval_manifest(&manifest) {
107 Ok(report) => report,
108 Err(error) => {
109 eprintln!("error: {error}");
110 return Err(1);
111 }
112 };
113 Ok(report)
114}
115
116fn post_render_exit_code(report: &ContextEvalReport) -> i32 {
117 i32::from(!report.pass)
118}
119
120fn announce_output_paths(output_dir: &Path) {
121 eprintln!(
122 "wrote {}, {}, and {}",
123 output_dir.join("summary.json").display(),
124 output_dir.join("per_run.jsonl").display(),
125 output_dir.join("summary.md").display()
126 );
127}
128
129fn write_json_artifacts(output_dir: &Path, report: &ContextEvalReport) -> Result<(), String> {
130 write_json(output_dir.join("summary.json"), report)?;
131 write_jsonl(output_dir.join("per_run.jsonl"), &report.runs)
132}
133
134fn write_json(path: PathBuf, report: &ContextEvalReport) -> Result<(), String> {
135 let payload = serde_json::to_string_pretty(report).map_err(|error| error.to_string())?;
136 fs::write(path, payload).map_err(|error| error.to_string())
137}
138
139fn write_jsonl(path: PathBuf, runs: &[ContextEvalRunReport]) -> Result<(), String> {
140 let mut file = fs::File::create(path).map_err(|error| error.to_string())?;
141 for run in runs {
142 let line = serde_json::to_string(run).map_err(|error| error.to_string())?;
143 file.write_all(line.as_bytes())
144 .map_err(|error| error.to_string())?;
145 file.write_all(b"\n").map_err(|error| error.to_string())?;
146 }
147 Ok(())
148}
149
150async fn write_markdown_dispatch(output_dir: &Path, report: &ContextEvalReport) -> Result<(), i32> {
153 let payload = render_via_dispatch(report, "markdown").await?;
154 if let Err(error) = fs::write(output_dir.join("summary.md"), payload) {
155 eprintln!("error: failed to write context eval markdown: {error}");
156 return Err(1);
157 }
158 Ok(())
159}
160
161async fn print_summary_dispatch(report: &ContextEvalReport) -> Result<(), i32> {
162 let payload = render_via_dispatch(report, "summary").await?;
163 print!("{payload}");
164 if !payload.ends_with('\n') {
165 println!();
166 }
167 Ok(())
168}
169
170async fn print_json_dispatch(report: &ContextEvalReport) -> Result<(), i32> {
171 let payload = render_via_dispatch(report, "json").await?;
172 print!("{payload}");
173 if !payload.ends_with('\n') {
174 println!();
175 }
176 Ok(())
177}
178
179async fn render_via_dispatch(report: &ContextEvalReport, mode: &str) -> Result<String, i32> {
188 let report_json = match serde_json::to_string(report) {
189 Ok(json) => json,
190 Err(error) => {
191 eprintln!("error: failed to serialise ContextEvalReport for dispatch: {error}");
192 return Err(1);
193 }
194 };
195
196 let _guard = DISPATCH_RENDER_LOCK.lock().await;
197 let _report = ScopedEnvVar::set(CONTEXT_REPORT_ENV, &report_json);
198 let _mode = ScopedEnvVar::set(CONTEXT_OUTPUT_MODE_ENV, mode);
199
200 let outcome = dispatch::run_embedded_script("eval/context", Vec::new(), false).await;
201 if !outcome.stderr.is_empty() {
202 use std::io::Write as _;
203 let _ = std::io::stderr().write_all(outcome.stderr.as_bytes());
204 }
205 if outcome.exit_code != 0 {
206 return Err(outcome.exit_code);
207 }
208 Ok(outcome.stdout)
209}