klieo-spec 3.3.0

Generic decompose / critique / refine traits and a quality loop runner.
Documentation
//! Generic decomposition trait.
//!
//! A `Decomposer<I, O>` takes an input and emits a list of children.
//! Implementations may run an LLM call, traverse a structured input,
//! or hard-code rules — `klieo-spec` does not constrain the strategy.
//!
//! The trait is **separate from refinement** by design. A typical
//! agent pipeline is:
//!
//! 1. `Decomposer::decompose(parent)` -> `Vec<Child>`
//! 2. For each `child`, run a `QualityLoop` (`Critic` + `Refiner`)
//!    until it converges.
//!
//! Some agents only need step 1 (fan-out) or step 2 (single-item
//! refinement); those agents simply omit the trait they don't use.

use crate::error::SpecError;
use async_trait::async_trait;

/// Decompose `Input` into a list of `Output` children.
///
/// Implementations may produce any number of children — the trait
/// does not enforce a minimum. Callers that require >= 2 children
/// (the legacy `SpecDecompositionPort` contract) should validate
/// the returned `Vec` length themselves.
#[async_trait]
pub trait Decomposer<Input, Output>: Send + Sync
where
    Input: Send + Sync,
    Output: Send + Sync,
{
    /// Decompose `input` into children.
    async fn decompose(&self, input: &Input) -> Result<Vec<Output>, SpecError>;
}

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

    const _: fn() = || {
        let _: &dyn Decomposer<String, String>;
    };

    struct ToyDecomposer;
    #[async_trait]
    impl Decomposer<String, String> for ToyDecomposer {
        async fn decompose(&self, input: &String) -> Result<Vec<String>, SpecError> {
            Ok(input.split_whitespace().map(str::to_owned).collect())
        }
    }

    #[tokio::test]
    async fn toy_decomposer_splits_words() {
        let d = ToyDecomposer;
        let out = d.decompose(&"hello world".to_string()).await.unwrap();
        assert_eq!(out, vec!["hello", "world"]);
    }
}