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}