use crate::critique::Critic;
use crate::quality::{Critique, QualityMetadata};
use crate::refine::Refiner;
use crate::SpecError;
pub const DEFAULT_MAX_ITERATIONS: u8 = 5;
pub struct QualityLoop<T, C, R>
where
T: Send + Sync,
C: Critic<T>,
R: Refiner<T>,
{
critic: C,
refiner: R,
max_iterations: u8,
_phantom: std::marker::PhantomData<fn() -> T>,
}
impl<T, C, R> QualityLoop<T, C, R>
where
T: Send + Sync,
C: Critic<T>,
R: Refiner<T>,
{
pub fn new(critic: C, refiner: R) -> Self {
Self {
critic,
refiner,
max_iterations: DEFAULT_MAX_ITERATIONS,
_phantom: std::marker::PhantomData,
}
}
pub fn with_max_iterations(mut self, n: u8) -> Self {
self.max_iterations = n;
self
}
pub async fn run(&self, mut t: T) -> Result<(T, QualityMetadata), SpecError> {
if self.max_iterations == 0 {
return Err(SpecError::Config("max_iterations must be > 0".into()));
}
#[allow(unused_assignments)]
let mut last_critique: Option<Critique> = None;
let mut iter: u8 = 0;
loop {
let critique = self.critic.evaluate(&t, iter).await?.with_iteration(iter);
if critique.pass {
tracing::debug!(target: "klieo_spec.loop", iter, "critique passed");
return Ok((
t,
QualityMetadata {
iterations_used: iter,
max_iterations: self.max_iterations,
passed: true,
final_critique: Some(critique),
},
));
}
tracing::debug!(
target: "klieo_spec.loop",
iter,
feedback = %critique.feedback,
"critique failed; refining"
);
last_critique = Some(critique.clone());
iter = iter.saturating_add(1);
if iter >= self.max_iterations {
tracing::warn!(
target: "klieo_spec.loop",
max = self.max_iterations,
"loop exhausted without passing"
);
return Ok((
t,
QualityMetadata {
iterations_used: iter,
max_iterations: self.max_iterations,
passed: false,
final_critique: last_critique,
},
));
}
t = self.refiner.refine(t, &critique, iter - 1).await?;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Critique;
use async_trait::async_trait;
struct AlwaysPass;
#[async_trait]
impl Critic<String> for AlwaysPass {
async fn evaluate(&self, _t: &String, _i: u8) -> Result<Critique, SpecError> {
Ok(Critique::pass("ok"))
}
}
struct PadRefiner;
#[async_trait]
impl Refiner<String> for PadRefiner {
async fn refine(&self, t: String, _c: &Critique, _i: u8) -> Result<String, SpecError> {
Ok(format!("{t}-x"))
}
}
struct AlwaysFail;
#[async_trait]
impl Critic<String> for AlwaysFail {
async fn evaluate(&self, _t: &String, _i: u8) -> Result<Critique, SpecError> {
Ok(Critique::fail("bad"))
}
}
#[tokio::test]
async fn passes_first_iteration() {
let l = QualityLoop::new(AlwaysPass, PadRefiner);
let (out, meta) = l.run("hello".into()).await.unwrap();
assert_eq!(out, "hello");
assert!(meta.passed);
assert_eq!(meta.iterations_used, 0);
}
#[tokio::test]
async fn exhausts_iterations_when_critic_never_passes() {
let l = QualityLoop::new(AlwaysFail, PadRefiner).with_max_iterations(3);
let (out, meta) = l.run("a".into()).await.unwrap();
assert!(!meta.passed);
assert_eq!(meta.iterations_used, 3);
assert_eq!(meta.max_iterations, 3);
assert_eq!(out, "a-x-x");
}
#[tokio::test]
async fn zero_max_iterations_rejected() {
let l = QualityLoop::new(AlwaysPass, PadRefiner).with_max_iterations(0);
let err = l.run("x".into()).await.unwrap_err();
assert!(matches!(err, SpecError::Config(_)));
}
}