Skip to main content

klieo_spec/
quality.rs

1//! Quality types — outcomes of a single critique run and the metadata
2//! emitted by [`crate::QualityLoop`] when it finishes (or gives up).
3//!
4//! Generalised from `klieo_domain::quality::{Critique, QualityMetadata}`
5//! but with the domain-specific `remaining_issues: Vec<String>` field
6//! dropped. Agent authors who need richer feedback embed their own
7//! payload in `Critique::feedback` (free-form string) or wrap the
8//! type — `Critique` is intentionally minimal.
9
10use serde::{Deserialize, Serialize};
11
12/// Outcome of a single critique evaluation.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[non_exhaustive]
15pub struct Critique {
16    /// Whether the candidate passes the critic's quality bar.
17    pub pass: bool,
18    /// Free-form feedback. Refiners may parse this as JSON or plain
19    /// text — `klieo-spec` does not interpret it.
20    pub feedback: String,
21    /// Iteration index this critique was produced for (0-based).
22    /// `QualityLoop` populates this; manual constructors can leave it
23    /// at the `Critique::pass` / `Critique::fail` default of `0`.
24    #[serde(default)]
25    pub iteration: u8,
26}
27
28impl Critique {
29    /// Build a passing critique.
30    pub fn pass(feedback: impl Into<String>) -> Self {
31        Self {
32            pass: true,
33            feedback: feedback.into(),
34            iteration: 0,
35        }
36    }
37
38    /// Build a failing critique.
39    pub fn fail(feedback: impl Into<String>) -> Self {
40        Self {
41            pass: false,
42            feedback: feedback.into(),
43            iteration: 0,
44        }
45    }
46
47    /// Concrete-`&str` variant of [`Self::pass`]. Useful at call sites
48    /// where `impl Into<String>` inference is ambiguous on a bare
49    /// string literal (`Critique::pass("...".into())` → `E0283`).
50    pub fn pass_str(feedback: &str) -> Self {
51        Self::pass(feedback.to_string())
52    }
53
54    /// Concrete-`&str` variant of [`Self::fail`]. See [`Self::pass_str`].
55    pub fn fail_str(feedback: &str) -> Self {
56        Self::fail(feedback.to_string())
57    }
58
59    /// Stamp the iteration index. Internal use by [`crate::QualityLoop`].
60    pub(crate) fn with_iteration(mut self, iter: u8) -> Self {
61        self.iteration = iter;
62        self
63    }
64}
65
66/// Metadata about a [`crate::QualityLoop`] run, returned alongside the
67/// final value. Useful for telemetry — push it into a runlog or
68/// emit as a tracing event.
69#[derive(Debug, Clone, Serialize, Deserialize, Default)]
70#[non_exhaustive]
71pub struct QualityMetadata {
72    /// Iterations consumed before exit.
73    pub iterations_used: u8,
74    /// Maximum iterations the loop was configured with.
75    pub max_iterations: u8,
76    /// Whether the loop converged on a passing critique.
77    pub passed: bool,
78    /// The last critique observed. `None` if the loop short-circuited
79    /// before any critique ran.
80    pub final_critique: Option<Critique>,
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn pass_fail_constructors() {
89        let c = Critique::pass("good");
90        assert!(c.pass);
91        assert_eq!(c.iteration, 0);
92
93        let c = Critique::fail("bad");
94        assert!(!c.pass);
95    }
96
97    #[test]
98    fn with_iteration_stamps_index() {
99        let c = Critique::pass("ok").with_iteration(3);
100        assert_eq!(c.iteration, 3);
101    }
102
103    #[test]
104    fn fail_str_accepts_literal_without_inference_ambiguity() {
105        // Concrete `&str` ctor avoids the `impl Into<String>` ambiguity
106        // some call sites hit on bare literals.
107        let c = Critique::fail_str("too short");
108        assert!(!c.pass);
109        assert_eq!(c.feedback, "too short");
110    }
111
112    #[test]
113    fn pass_str_accepts_literal() {
114        let c = Critique::pass_str("ok");
115        assert!(c.pass);
116        assert_eq!(c.feedback, "ok");
117    }
118
119    #[test]
120    fn round_trip_json() {
121        let c = Critique::pass("hi");
122        let s = serde_json::to_string(&c).unwrap();
123        let c2: Critique = serde_json::from_str(&s).unwrap();
124        assert_eq!(c.feedback, c2.feedback);
125    }
126}