car-ffi-common 0.26.0

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
//! JSON wrappers for the Agentic Harness Engineering Evolution Agent
//! (`car_memgine::harness_evolution`, survey §3.5/§5.2.3).
//!
//! Two stateless steps: diagnose telemetry into contract-bearing mutation
//! proposals, and regression-gate a candidate mutation against held-out
//! before/after metrics. Both are deterministic — same input, same output —
//! so the Evolution Agent's decisions are auditable.

use car_eventlog::harness_metrics::HarnessMetrics;
use car_memgine::harness_evolution::{
    EvolutionAgent, EvolutionConfig, Governance, HarnessConfig, HarnessMutation, PromotionDecision,
};

fn parse_metrics(json: &str, what: &str) -> Result<HarnessMetrics, String> {
    serde_json::from_str(json).map_err(|e| format!("invalid {what} metrics JSON: {e}"))
}

fn agent(config_json: Option<&str>) -> Result<EvolutionAgent, String> {
    match config_json {
        Some(c) => {
            let config: EvolutionConfig =
                serde_json::from_str(c).map_err(|e| format!("invalid evolution config JSON: {e}"))?;
            Ok(EvolutionAgent::with_config(config))
        }
        None => Ok(EvolutionAgent::new()),
    }
}

/// Diagnose harness-level telemetry into governed mutation proposals.
/// `metrics_json` is a `HarnessMetrics` (from `harness_metrics`);
/// `config_json` optionally overrides the diagnosis thresholds. Returns a
/// JSON array of `HarnessMutation`, each carrying its change contract.
pub fn diagnose(metrics_json: &str, config_json: Option<&str>) -> Result<String, String> {
    let metrics = parse_metrics(metrics_json, "harness")?;
    let mutations = agent(config_json)?.diagnose(&metrics);
    serde_json::to_string(&mutations).map_err(|e| e.to_string())
}

/// Regression-gate a candidate mutation. `mutation_json` is a
/// `HarnessMutation`; `baseline_json` / `candidate_json` are
/// `HarnessMetrics` measured before/after applying it on held-out
/// telemetry. Returns the `PromotionDecision` JSON (`promote` /
/// `needs_approval` / `reject`).
pub fn evaluate(
    mutation_json: &str,
    baseline_json: &str,
    candidate_json: &str,
    config_json: Option<&str>,
) -> Result<String, String> {
    let mutation: HarnessMutation =
        serde_json::from_str(mutation_json).map_err(|e| format!("invalid mutation JSON: {e}"))?;
    let baseline = parse_metrics(baseline_json, "baseline")?;
    let candidate = parse_metrics(candidate_json, "candidate")?;
    let decision = agent(config_json)?.evaluate(&mutation, &baseline, &candidate);
    serde_json::to_string(&decision).map_err(|e| e.to_string())
}

/// Apply a mutation's concrete patch to a harness config under governed
/// authorization (survey §3.5/§5.2.3). `config_json` is a `HarnessConfig`;
/// `mutation_json` a `HarnessMutation` (must carry a `patch`). Authorization:
/// `human_approved=true` applies under the HITL path (the only path that may
/// land a safety-affecting mutation); otherwise `decision_json` (a
/// `PromotionDecision`) must be `promote` and the mutation must be
/// non-safety. Returns `{ config, rollback }` — the updated config and the
/// inverse patch that restores it — or an error when refused.
///
/// Trust note: this cannot re-run the regression gate. `decision_json` is
/// trusted — pass the one returned by `evaluate`, not a synthesized one.
/// `human_approved=true` is an **unverified** caller assertion that a human
/// approved out of band; it bypasses the gate requirement, so callers MUST
/// gate it behind their own recorded approval (e.g. the `permission.*`
/// ledger). The blast radius is bounded to the five reversible non-safety
/// knobs — a safety boundary can never be mutated regardless.
pub fn apply(
    config_json: &str,
    mutation_json: &str,
    decision_json: Option<&str>,
    human_approved: bool,
) -> Result<String, String> {
    let mut config: HarnessConfig =
        serde_json::from_str(config_json).map_err(|e| format!("invalid config JSON: {e}"))?;
    let mutation: HarnessMutation =
        serde_json::from_str(mutation_json).map_err(|e| format!("invalid mutation JSON: {e}"))?;
    let governance = if human_approved {
        Governance::HumanApproved
    } else {
        let dj = decision_json
            .ok_or("non-approved apply requires a decision_json (a PromotionDecision)")?;
        let decision: PromotionDecision =
            serde_json::from_str(dj).map_err(|e| format!("invalid decision JSON: {e}"))?;
        Governance::Promoted(decision)
    };
    let rollback = config.apply(&mutation, governance)?;
    serde_json::to_string(&serde_json::json!({ "config": config, "rollback": rollback }))
        .map_err(|e| e.to_string())
}