1use crate::critique::Critic;
22use crate::quality::{Critique, QualityMetadata};
23use crate::refine::Refiner;
24use crate::SpecError;
25
26pub const DEFAULT_MAX_ITERATIONS: u8 = 5;
28
29pub struct QualityLoop<T, C, R>
31where
32 T: Send + Sync,
33 C: Critic<T>,
34 R: Refiner<T>,
35{
36 critic: C,
37 refiner: R,
38 max_iterations: u8,
39 _phantom: std::marker::PhantomData<fn() -> T>,
40}
41
42impl<T, C, R> QualityLoop<T, C, R>
43where
44 T: Send + Sync,
45 C: Critic<T>,
46 R: Refiner<T>,
47{
48 pub fn new(critic: C, refiner: R) -> Self {
51 Self {
52 critic,
53 refiner,
54 max_iterations: DEFAULT_MAX_ITERATIONS,
55 _phantom: std::marker::PhantomData,
56 }
57 }
58
59 pub fn with_max_iterations(mut self, n: u8) -> Self {
62 self.max_iterations = n;
63 self
64 }
65
66 pub async fn run(&self, mut t: T) -> Result<(T, QualityMetadata), SpecError> {
71 if self.max_iterations == 0 {
72 return Err(SpecError::Config("max_iterations must be > 0".into()));
73 }
74
75 #[allow(unused_assignments)]
76 let mut last_critique: Option<Critique> = None;
77 let mut iter: u8 = 0;
78 loop {
79 let critique = self.critic.evaluate(&t, iter).await?.with_iteration(iter);
80
81 if critique.pass {
82 tracing::debug!(target: "klieo_spec.loop", iter, "critique passed");
83 return Ok((
84 t,
85 QualityMetadata {
86 iterations_used: iter,
87 max_iterations: self.max_iterations,
88 passed: true,
89 final_critique: Some(critique),
90 },
91 ));
92 }
93
94 tracing::debug!(
95 target: "klieo_spec.loop",
96 iter,
97 feedback = %critique.feedback,
98 "critique failed; refining"
99 );
100 last_critique = Some(critique.clone());
101 iter = iter.saturating_add(1);
102 if iter >= self.max_iterations {
103 tracing::warn!(
104 target: "klieo_spec.loop",
105 max = self.max_iterations,
106 "loop exhausted without passing"
107 );
108 return Ok((
109 t,
110 QualityMetadata {
111 iterations_used: iter,
112 max_iterations: self.max_iterations,
113 passed: false,
114 final_critique: last_critique,
115 },
116 ));
117 }
118 t = self.refiner.refine(t, &critique, iter - 1).await?;
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::Critique;
127 use async_trait::async_trait;
128
129 struct AlwaysPass;
130 #[async_trait]
131 impl Critic<String> for AlwaysPass {
132 async fn evaluate(&self, _t: &String, _i: u8) -> Result<Critique, SpecError> {
133 Ok(Critique::pass("ok"))
134 }
135 }
136 struct PadRefiner;
137 #[async_trait]
138 impl Refiner<String> for PadRefiner {
139 async fn refine(&self, t: String, _c: &Critique, _i: u8) -> Result<String, SpecError> {
140 Ok(format!("{t}-x"))
141 }
142 }
143
144 struct AlwaysFail;
145 #[async_trait]
146 impl Critic<String> for AlwaysFail {
147 async fn evaluate(&self, _t: &String, _i: u8) -> Result<Critique, SpecError> {
148 Ok(Critique::fail("bad"))
149 }
150 }
151
152 #[tokio::test]
153 async fn passes_first_iteration() {
154 let l = QualityLoop::new(AlwaysPass, PadRefiner);
155 let (out, meta) = l.run("hello".into()).await.unwrap();
156 assert_eq!(out, "hello");
157 assert!(meta.passed);
158 assert_eq!(meta.iterations_used, 0);
159 }
160
161 #[tokio::test]
162 async fn exhausts_iterations_when_critic_never_passes() {
163 let l = QualityLoop::new(AlwaysFail, PadRefiner).with_max_iterations(3);
164 let (out, meta) = l.run("a".into()).await.unwrap();
165 assert!(!meta.passed);
166 assert_eq!(meta.iterations_used, 3);
167 assert_eq!(meta.max_iterations, 3);
168 assert_eq!(out, "a-x-x");
171 }
172
173 #[tokio::test]
174 async fn zero_max_iterations_rejected() {
175 let l = QualityLoop::new(AlwaysPass, PadRefiner).with_max_iterations(0);
176 let err = l.run("x".into()).await.unwrap_err();
177 assert!(matches!(err, SpecError::Config(_)));
178 }
179}