use std::{
io::{BufRead, BufReader},
path::{Path, PathBuf},
process::{Command, Stdio},
sync::mpsc,
thread,
};
use anyhow::Result;
use crate::{config::TruthMirrorConfig, ledger::LedgerStore, resolve, reviewer::ReviewQueue};
use super::data::{self, Snapshot};
fn ensure_enabled(config_path: &Path, state_dir: &Path) -> Result<bool> {
let config = TruthMirrorConfig::load_for_cli(Some(config_path), state_dir)
.map_err(|error| anyhow::anyhow!("failed to load runtime config: {error}"))?;
Ok(config.enabled)
}
pub fn remove_queue_item(
state_dir: &Path,
config_path: &Path,
run_id: &str,
sha: &str,
) -> Result<String> {
if !ensure_enabled(config_path, state_dir)? {
return Ok(
"truth-mirror: disabled by runtime configuration; skipped queue removal".to_owned(),
);
}
let queue = ReviewQueue::new(state_dir);
if !run_id.is_empty() {
queue
.remove_run_id(run_id)
.map_err(|error| anyhow::anyhow!(error.to_string()))?;
} else {
queue
.remove_sha(sha)
.map_err(|error| anyhow::anyhow!(error.to_string()))?;
}
Ok(format!("removed queue item {sha}"))
}
pub fn waive_rejection(
state_dir: &Path,
_config_path: &Path,
sha: &str,
reason: &str,
) -> Result<String> {
let store = LedgerStore::new(state_dir);
let entry = resolve::waive_human(&store, sha, reason)?;
Ok(format!("waived {} (human lane)", entry.commit_sha))
}
pub fn petition_rejection(
state_dir: &Path,
config_path: &Path,
rejection_sha: &str,
fix_sha: &str,
) -> Result<String> {
if !ensure_enabled(config_path, state_dir)? {
return Ok("truth-mirror: disabled by runtime configuration; skipped petition".to_owned());
}
let store = LedgerStore::new(state_dir);
resolve::petition_fix_with_config(&store, state_dir, rejection_sha, fix_sha, Some(config_path))
}
pub fn preview_config_edit(snapshot: &Snapshot, path: &str, value: &str) -> Result<String> {
let next = data::apply_config_edit(&snapshot.config_raw, path, value)?;
Ok(data::line_diff(&snapshot.config_raw, &next))
}
pub fn save_config_edit(
config_path: &Path,
snapshot: &Snapshot,
path: &str,
value: &str,
) -> Result<String> {
let next = data::apply_config_edit(&snapshot.config_raw, path, value)?;
data::write_config(config_path, &next)?;
Ok(format!("wrote {}", config_path.display()))
}
pub fn spawn_drain_once(
state_dir: &Path,
config_path: &Path,
explicit_config: bool,
tx: mpsc::Sender<DrainEvent>,
) -> Result<thread::JoinHandle<()>> {
if !ensure_enabled(config_path, state_dir)? {
let handle = thread::spawn(move || {
let _ = tx.send(DrainEvent::Done(0));
});
return Ok(handle);
}
let binary = drain_binary();
let state_dir = state_dir.to_path_buf();
let config_path = config_path.to_path_buf();
let binary_display = binary.display().to_string();
let state_display = state_dir.display().to_string();
let config_display = config_path.display().to_string();
let command_display = if explicit_config {
format!(" --config {config_display}")
} else {
String::new()
};
let _ = tx.send(DrainEvent::Line(format!(
"$ {binary_display} --state-dir {state_display}{command_display} watch --once"
)));
let handle = thread::spawn(move || {
let mut cmd = Command::new(&binary);
cmd.arg("--state-dir").arg(&state_dir);
if explicit_config {
cmd.arg("--config").arg(&config_path);
}
cmd.arg("watch")
.arg("--once")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = match cmd.spawn() {
Ok(child) => child,
Err(error) => {
let _ = tx.send(DrainEvent::Line(format!("failed to spawn drain: {error}")));
let _ = tx.send(DrainEvent::Done(1));
return;
}
};
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let tx_out = tx.clone();
let out_handle = thread::spawn(move || {
if let Some(out) = stdout {
for line in BufReader::new(out).lines().map_while(Result::ok) {
if tx_out.send(DrainEvent::Line(line)).is_err() {
break;
}
}
}
});
let tx_err = tx.clone();
let err_handle = thread::spawn(move || {
if let Some(err) = stderr {
for line in BufReader::new(err).lines().map_while(Result::ok) {
if tx_err
.send(DrainEvent::Line(format!("[err] {line}")))
.is_err()
{
break;
}
}
}
});
let code = match child.wait() {
Ok(status) => status.code().unwrap_or(1),
Err(error) => {
let _ = tx.send(DrainEvent::Line(format!("wait failed: {error}")));
1
}
};
let _ = out_handle.join();
let _ = err_handle.join();
let _ = tx.send(DrainEvent::Done(code));
});
Ok(handle)
}
fn drain_binary() -> PathBuf {
std::env::current_exe().unwrap_or_else(|_| PathBuf::from("truth-mirror"))
}
#[derive(Debug, PartialEq, Eq)]
pub enum DrainEvent {
Line(String),
Done(i32),
}
pub fn resolve_config_path(explicit: Option<&Path>, state_dir: &Path) -> PathBuf {
explicit.map_or_else(
|| crate::config::TruthMirrorConfig::default_path(state_dir),
Path::to_path_buf,
)
}
pub fn validate_and_normalize_edit(path: &str, raw: &str) -> Result<String> {
super::model::validate_config_edit(path, raw).map_err(anyhow::Error::msg)
}
pub fn validate_waiver_reason(raw: &str) -> Result<()> {
let trimmed = raw.trim();
if trimmed.is_empty() {
anyhow::bail!("waiver reason must not be empty");
}
if trimmed.contains("<why this rejection") || trimmed.contains("<what we are choosing") {
anyhow::bail!("replace the template placeholders before submitting the waiver");
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::{fs, sync::mpsc};
use super::*;
#[test]
fn disabled_queue_mutation_is_a_successful_noop() {
let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.toml");
fs::write(&config_path, "enabled = false\n").unwrap();
let queue = ReviewQueue::new(temp.path());
queue.enqueue("queued").unwrap();
let outcome = remove_queue_item(temp.path(), &config_path, "", "queued").unwrap();
assert_eq!(
outcome,
"truth-mirror: disabled by runtime configuration; skipped queue removal"
);
assert_eq!(queue.pending().unwrap().len(), 1);
}
#[test]
fn disabled_provider_spawn_is_a_successful_noop() {
let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.toml");
fs::write(&config_path, "enabled = false\n").unwrap();
let (tx, rx) = mpsc::channel();
let handle = spawn_drain_once(temp.path(), &config_path, false, tx).unwrap();
handle.join().unwrap();
assert!(matches!(rx.try_recv().unwrap(), DrainEvent::Done(0)));
}
#[test]
fn disabled_petition_is_a_successful_noop_and_waiver_still_works() {
use crate::ledger::{LedgerEntry, LedgerStore, ReviewerConfig, Verdict};
let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.toml");
fs::write(&config_path, "enabled = false\n").unwrap();
let petition = petition_rejection(temp.path(), &config_path, "rejection", "fix").unwrap();
assert_eq!(
petition,
"truth-mirror: disabled by runtime configuration; skipped petition"
);
let store = LedgerStore::new(temp.path());
store
.append_entry(&LedgerEntry::new(
"rejection",
Verdict::Reject,
"CLAIM: thing | verified: cargo test | evidence: tests:cargo-test",
vec!["tests:cargo-test".into()],
ReviewerConfig::new("claude", "claude-opus-4", false),
vec!["finding".into()],
))
.unwrap();
resolve::set_stdin_tty_override_for_testing(Some(true));
let waiver = waive_rejection(temp.path(), &config_path, "rejection", "reason").unwrap();
resolve::set_stdin_tty_override_for_testing(None);
assert_eq!(waiver, "waived rejection (human lane)");
}
#[test]
fn disabled_drain_does_not_emit_a_command_line() {
let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.toml");
fs::write(&config_path, "enabled = false\n").unwrap();
let (tx, rx) = mpsc::channel();
let handle = spawn_drain_once(temp.path(), &config_path, false, tx).unwrap();
handle.join().unwrap();
let events: Vec<_> = rx.try_iter().collect();
assert_eq!(
events,
vec![DrainEvent::Done(0)],
"disabled drain must not spawn a child or emit the command line"
);
}
}