aethellib 0.9.6

Composable text generation primitives over target-specific TOML corpora with provenance tracking.
Documentation
//! # engine
//!
//! typed plan-based execution for text generation.
//!
//! this module provides a compileable expression pipeline:
//! - model generation logic with typed [`RuleExpr`] values
//! - register named outputs in [`PlanBuilder`]
//! - validate graph structure and pool references before runtime
//! - compile once into [`CompiledPlan`] and generate repeatedly
//!
//! ## Mental Model
//!
//! - a [`RuleExpr`] is one generation expression.
//! - a [`PlanBuilder`] assembles named expressions.
//! - a [`CompiledPlan`] executes expressions in dependency order.
//! - each generated output is stored in [`GenerationContext`].
//!
//! ## Key Types
//!
//! - [`PlanBuilder`]: collects named expressions
//! - [`RuleKey`]: typed identifier for one generated output
//! - [`RuleExpr`]: expression tree used for generation
//! - [`CompiledPlan`]: reusable compiled execution artefact
//! - [`GenerationContext`]: shared read state and prior results
//! - [`ComposedValue`]: generated string plus provenance
//!
//! ## Combinators
//!
//! see [`combinators`] for expression helpers such as `pick`, `join`,
//! `chance`, `when`, and `weighted`.
//!
//! ## Determinism
//!
//! deterministic output depends on the RNG you pass to
//! [`CompiledPlan::generate`]. use a seeded RNG for reproducibility.
//!
//! [`Corpus`]: crate::corpus::Corpus
//! [`RuleExpr`]: crate::engine::RuleExpr
//! [`PlanBuilder`]: crate::engine::PlanBuilder
//! [`RuleKey`]: crate::engine::RuleKey
//! [`CompiledPlan`]: crate::engine::CompiledPlan
//! [`GenerationContext`]: crate::engine::GenerationContext
//! [`ComposedValue`]: crate::engine::ComposedValue
//! [`combinators`]: crate::engine::combinators
//! [`CompiledPlan::generate`]: crate::engine::CompiledPlan::generate

pub mod combinators;
pub mod error;
pub mod plan;
pub(crate) mod validation;

use std::collections::HashMap;

use crate::corpus::{Corpus, ValueProvenance};
pub use combinators::RuleExpr;
pub use error::AethelError;
pub use error::{PlanError, PlanErrorReport};
pub use plan::{CompiledPlan, PlanBuilder, PoolRef, RuleKey, ValidatedPlan};

// == ComposedValue ============================================================

#[derive(Debug, Clone)]
/// the final or intermediate result of a generation step.
pub struct ComposedValue {
    pub value: String,
    pub provenance: Vec<ValueProvenance>,
}

impl ComposedValue {
    /// merges two composed values, appending the string and extending the provenance vector.
    pub fn merge(mut self, other: ComposedValue) -> Self {
        self.value.push_str(&other.value);
        self.provenance.extend(other.provenance);
        self
    }
}

// == GenerationContext ========================================================

/// the shared state blackboard passed to every generation rule during execution.
pub struct GenerationContext<'a> {
    /// the corpus available to all rules.
    pub corpus: &'a Corpus,
    /// results of previously executed rules keyed by typed rule keys.
    pub history: HashMap<RuleKey, ComposedValue>,
}

impl<'a> GenerationContext<'a> {
    /// creates an empty generation context for one corpus.
    pub fn new(corpus: &'a Corpus) -> Self {
        Self {
            corpus,
            history: HashMap::new(),
        }
    }

    /// returns the result for one typed rule key, if present.
    pub fn get(&self, key: impl AsRef<RuleKey>) -> Option<&ComposedValue> {
        self.history.get(key.as_ref())
    }

    /// returns a result by raw key for migration convenience.
    pub fn get_str(&self, key: &str) -> Option<&ComposedValue> {
        let key = RuleKey::new(key).ok()?;
        self.history.get(&key)
    }

    /// returns a required typed value or a dependency error.
    pub fn require(&self, key: impl AsRef<RuleKey>) -> Result<&ComposedValue, AethelError> {
        let key = key.as_ref();
        self.history
            .get(key)
            .ok_or_else(|| AethelError::MissingDependency(key.as_str().to_string()))
    }

    /// inserts a typed result into context history.
    pub(crate) fn insert_typed(&mut self, key: RuleKey, value: ComposedValue) {
        self.history.insert(key, value);
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        corpus::ValueProvenance,
        engine::{ComposedValue, GenerationContext, RuleKey},
    };

    #[test]
    fn composed_value_merge_appends_string_and_provenance() {
        fn provenance(source_id: &str) -> ValueProvenance {
            ValueProvenance {
                source_id: source_id.to_string(),
                document_title: "doc".to_string(),
                section: "name".to_string(),
                field: "first".to_string(),
            }
        }

        let left = ComposedValue {
            value: "A".to_string(),
            provenance: vec![provenance("left")],
        };
        let right = ComposedValue {
            value: "B".to_string(),
            provenance: vec![provenance("right")],
        };

        let merged = left.merge(right);

        assert_eq!(merged.value, "AB");
        assert_eq!(merged.provenance.len(), 2);
        assert_eq!(merged.provenance[0].source_id, "left");
        assert_eq!(merged.provenance[1].source_id, "right");
    }

    #[test]
    fn generation_context_get_previous_returns_inserted_value() {
        let corpus = crate::corpus::Corpus::builder("weapon")
            .add_str(
                "ctx-test",
                r#"
[header]
title = "ctx"
target = "weapon"

[name]
first = ["ash"]
"#,
            )
            .build()
            .expect("corpus should build");

        let mut ctx = GenerationContext::new(&corpus);
        let key = RuleKey::new("greeting").expect("rule key should be valid");

        ctx.insert_typed(
            key.clone(),
            ComposedValue {
                value: "hello".to_string(),
                provenance: vec![],
            },
        );

        let found = ctx.get(&key).expect("key should exist");
        assert_eq!(found.value, "hello");
        assert!(ctx.get_str("greeting").is_some());
        assert!(ctx.get_str("missing").is_none());
    }
}