klieo-spec 3.4.0

Generic decompose / critique / refine traits and a quality loop runner.
Documentation
//! Quality types — outcomes of a single critique run and the metadata
//! emitted by [`crate::QualityLoop`] when it finishes (or gives up).
//!
//! Generalised from `klieo_domain::quality::{Critique, QualityMetadata}`
//! but with the domain-specific `remaining_issues: Vec<String>` field
//! dropped. Agent authors who need richer feedback embed their own
//! payload in `Critique::feedback` (free-form string) or wrap the
//! type — `Critique` is intentionally minimal.

use serde::{Deserialize, Serialize};

/// Outcome of a single critique evaluation.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Critique {
    /// Whether the candidate passes the critic's quality bar.
    pub pass: bool,
    /// Free-form feedback. Refiners may parse this as JSON or plain
    /// text — `klieo-spec` does not interpret it.
    pub feedback: String,
    /// Iteration index this critique was produced for (0-based).
    /// `QualityLoop` populates this; manual constructors can leave it
    /// at the `Critique::pass` / `Critique::fail` default of `0`.
    #[serde(default)]
    pub iteration: u8,
}

impl Critique {
    /// Build a passing critique.
    pub fn pass(feedback: impl Into<String>) -> Self {
        Self {
            pass: true,
            feedback: feedback.into(),
            iteration: 0,
        }
    }

    /// Build a failing critique.
    pub fn fail(feedback: impl Into<String>) -> Self {
        Self {
            pass: false,
            feedback: feedback.into(),
            iteration: 0,
        }
    }

    /// Concrete-`&str` variant of [`Self::pass`]. Useful at call sites
    /// where `impl Into<String>` inference is ambiguous on a bare
    /// string literal (`Critique::pass("...".into())` → `E0283`).
    pub fn pass_str(feedback: &str) -> Self {
        Self::pass(feedback.to_string())
    }

    /// Concrete-`&str` variant of [`Self::fail`]. See [`Self::pass_str`].
    pub fn fail_str(feedback: &str) -> Self {
        Self::fail(feedback.to_string())
    }

    /// Stamp the iteration index. Internal use by [`crate::QualityLoop`].
    pub(crate) fn with_iteration(mut self, iter: u8) -> Self {
        self.iteration = iter;
        self
    }
}

/// Metadata about a [`crate::QualityLoop`] run, returned alongside the
/// final value. Useful for telemetry — push it into a runlog or
/// emit as a tracing event.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub struct QualityMetadata {
    /// Iterations consumed before exit.
    pub iterations_used: u8,
    /// Maximum iterations the loop was configured with.
    pub max_iterations: u8,
    /// Whether the loop converged on a passing critique.
    pub passed: bool,
    /// The last critique observed. `None` if the loop short-circuited
    /// before any critique ran.
    pub final_critique: Option<Critique>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn pass_fail_constructors() {
        let c = Critique::pass("good");
        assert!(c.pass);
        assert_eq!(c.iteration, 0);

        let c = Critique::fail("bad");
        assert!(!c.pass);
    }

    #[test]
    fn with_iteration_stamps_index() {
        let c = Critique::pass("ok").with_iteration(3);
        assert_eq!(c.iteration, 3);
    }

    #[test]
    fn fail_str_accepts_literal_without_inference_ambiguity() {
        // Concrete `&str` ctor avoids the `impl Into<String>` ambiguity
        // some call sites hit on bare literals.
        let c = Critique::fail_str("too short");
        assert!(!c.pass);
        assert_eq!(c.feedback, "too short");
    }

    #[test]
    fn pass_str_accepts_literal() {
        let c = Critique::pass_str("ok");
        assert!(c.pass);
        assert_eq!(c.feedback, "ok");
    }

    #[test]
    fn round_trip_json() {
        let c = Critique::pass("hi");
        let s = serde_json::to_string(&c).unwrap();
        let c2: Critique = serde_json::from_str(&s).unwrap();
        assert_eq!(c.feedback, c2.feedback);
    }
}