Skip to main content

choir_differential/
choir-differential.rs

1//! Explicit D23 runner invoked by `choir-bridge queue`.
2//!
3//! The bridge supplies three isolated worktrees. This process runs one argv
4//! specification in all three — under an explicit environment, never this
5//! process's inherited one — appends the observation, and prints only a
6//! structured result. Interaction failures are data, not a nonzero exit.
7
8use std::path::Path;
9
10use choir_queue::differential::run_merged_vs_parents;
11use choir_queue::differential_ledger::{
12    adjudicate, effective_environment, environment_hash, load_command, record_observation, refresh,
13    Revisions,
14};
15
16fn usage() -> ! {
17    eprintln!(
18        "usage: choir-differential run <command-file> <state-dir> <parent-a-oid> <parent-a-dir> <parent-b-oid> <parent-b-dir> <merged-oid> <merged-dir>\n       choir-differential adjudicate <command-file> <state-dir> <observation-id> real|spurious\n       choir-differential refresh <command-file> <state-dir>"
19    );
20    std::process::exit(2);
21}
22
23fn run(args: &[String]) -> Result<serde_json::Value, String> {
24    let [command_file, state_dir, parent_a_oid, parent_a_dir, parent_b_oid, parent_b_dir, merged_oid, merged_dir] =
25        args
26    else {
27        usage();
28    };
29    let command = load_command(Path::new(command_file))?;
30    let environment = effective_environment(&command.env);
31    let report = run_merged_vs_parents(
32        &command.program,
33        &command.args,
34        Path::new(parent_a_dir),
35        Path::new(parent_b_dir),
36        Path::new(merged_dir),
37        &environment,
38        command.timeout_seconds.map(std::time::Duration::from_secs),
39    )?;
40    let revisions = Revisions {
41        parent_a: parent_a_oid.clone(),
42        parent_b: parent_b_oid.clone(),
43        merged: merged_oid.clone(),
44    };
45    let recorded = record_observation(
46        Path::new(state_dir),
47        &command.snapshot_hash,
48        &environment_hash(&environment),
49        &revisions,
50        &report,
51    )?;
52    Ok(serde_json::json!({
53        "format_version": 1,
54        "observation_id": recorded.observation_id,
55        "merge": merged_oid,
56        "report": recorded.report.to_json(),
57        "calibration": recorded.calibration,
58    }))
59}
60
61fn adjudicate_one(args: &[String]) -> Result<serde_json::Value, String> {
62    let [command_file, state_dir, observation_id, verdict] = args else {
63        usage();
64    };
65    let command = load_command(Path::new(command_file))?;
66    let observation_id = observation_id
67        .parse::<u64>()
68        .map_err(|_| "observation id must be an integer".to_string())?;
69    let real = match verdict.as_str() {
70        "real" => true,
71        "spurious" => false,
72        _ => return Err("adjudication must be real or spurious".to_string()),
73    };
74    adjudicate(
75        Path::new(state_dir),
76        &command.snapshot_hash,
77        observation_id,
78        real,
79    )
80}
81
82fn refresh_one(args: &[String]) -> Result<serde_json::Value, String> {
83    let [command_file, state_dir] = args else {
84        usage();
85    };
86    let command = load_command(Path::new(command_file))?;
87    refresh(Path::new(state_dir), &command.snapshot_hash)
88}
89
90fn main() {
91    let args: Vec<String> = std::env::args().skip(1).collect();
92    let Some((command, rest)) = args.split_first() else {
93        usage();
94    };
95    let result = match command.as_str() {
96        "run" => run(rest),
97        "adjudicate" => adjudicate_one(rest),
98        "refresh" => refresh_one(rest),
99        _ => usage(),
100    };
101    match result {
102        Ok(value) => println!("{value}"),
103        Err(error) => {
104            eprintln!("choir-differential: {error}");
105            std::process::exit(1);
106        }
107    }
108}