car-ffi-common 0.32.1

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
//! JSON wrapper for the self-evolution governor (arXiv 2507.21046,
//! *A Survey of Self-Evolving Agents*).
//!
//! Stateless helper, binding-only — see
//! `docs/proposals/self-evolution-governor.md`. Decides the survey's *when +
//! what to evolve*: across every evolvable component (memory, skills, harness,
//! context, tools), which are due to evolve now, in what priority, under one
//! budget — evolving only under pressure and only with enough evidence.

use car_memgine::self_evolution::{plan_evolution, ComponentState, EvolutionPolicy};

#[derive(serde::Deserialize)]
struct EvolutionRequest {
    #[serde(default)]
    components: Vec<ComponentState>,
    #[serde(default)]
    policy: Option<EvolutionPolicy>,
}

/// Plan an evolution cycle. `request_json` is `{ components: [{ component:
/// "memory" | "skills" | "harness" | "context" | "tools", pressure?, evidence?,
/// min_evidence?, cost? }], policy?: { pressure_threshold?, budget? } }`. Returns
/// the `EvolutionPlan` JSON `{ decisions: [{ component, action: "evolve_now" |
/// "defer" | "skip", priority, defer_reason?, reason }], spent, evolve_now: [..] }`.
/// A component with pressure below threshold is skipped; one under pressure but
/// short on evidence defers (avoid overfitting); the rest are prioritized by
/// `pressure / cost` and packed into the budget.
pub fn plan(request_json: &str) -> Result<String, String> {
    let req: EvolutionRequest = crate::from_json("request", request_json)?;
    let policy = req.policy.unwrap_or_default();
    let plan = plan_evolution(&req.components, &policy);
    crate::to_json(&plan)
}

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

    #[test]
    fn skips_low_pressure_evolves_high() {
        let req = r#"{
            "components": [
                {"component":"memory","pressure":0.05,"evidence":100,"min_evidence":10,"cost":1.0},
                {"component":"skills","pressure":0.9,"evidence":100,"min_evidence":10,"cost":2.0}
            ]
        }"#;
        let v: Value = serde_json::from_str(&plan(req).unwrap()).unwrap();
        assert_eq!(v["evolve_now"], serde_json::json!(["skills"]));
    }

    #[test]
    fn thin_evidence_defers() {
        let req = r#"{"components":[{"component":"skills","pressure":0.9,"evidence":2,"min_evidence":20}]}"#;
        let v: Value = serde_json::from_str(&plan(req).unwrap()).unwrap();
        assert_eq!(v["decisions"][0]["action"], "defer");
        assert_eq!(v["decisions"][0]["defer_reason"], "insufficient_evidence");
    }

    #[test]
    fn bad_json_errors() {
        assert!(plan("nope").is_err());
    }
}