car-ffi-common 0.32.0

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
//! JSON wrapper for runtime harness adaptation (arXiv 2605.22166, "Life-Harness").
//!
//! Diagnose recurring interaction failures in a JSONL event-log tail into typed,
//! reusable harness interventions. Stateless, binding-only like
//! `harness_metrics` — see `docs/proposals/runtime-harness-adaptation.md`.

/// Diagnose harness interventions from a JSONL string of events (one per line).
/// `min_occurrences` is the recurrence threshold (one-offs are noise; pass 2 for
/// "recurring"). Returns the `AdaptationReport` JSON `{ interventions: [{ layer,
/// target, trigger, intervention, evidence_count }], parse_errors }`, where
/// `layer` is `environment_contract | action_realization | trajectory_regulation
/// | procedural_skill`.
pub fn diagnose(events_jsonl: &str, min_occurrences: u32) -> Result<String, String> {
    let report = car_eventlog::harness_adapt::diagnose_from_jsonl(
        events_jsonl,
        min_occurrences.max(1) as usize,
    );
    serde_json::to_string(&report).map_err(|e| e.to_string())
}

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

    #[test]
    fn diagnoses_recurring_rejection() {
        let jsonl = [
            r#"{"kind":"action_rejected","action_id":"a1","data":{},"timestamp":"2026-06-28T00:00:00Z"}"#,
            r#"{"kind":"action_rejected","action_id":"a1","data":{},"timestamp":"2026-06-28T00:00:01Z"}"#,
        ]
        .join("\n");
        let out = diagnose(&jsonl, 2).unwrap();
        let v: Value = serde_json::from_str(&out).unwrap();
        assert_eq!(v["interventions"][0]["layer"], "environment_contract");
        assert_eq!(v["interventions"][0]["evidence_count"], 2);
    }

    #[test]
    fn one_off_below_threshold_is_empty() {
        let jsonl = r#"{"kind":"action_rejected","action_id":"a1","data":{},"timestamp":"2026-06-28T00:00:00Z"}"#;
        let out = diagnose(jsonl, 2).unwrap();
        let v: Value = serde_json::from_str(&out).unwrap();
        assert_eq!(v["interventions"].as_array().unwrap().len(), 0);
    }
}