car-ffi-common 0.32.0

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
//! JSON wrapper for conflict-free replicated state merge (arXiv 2510.18893).
//!
//! Stateless helper, binding-only like the verify family — see
//! `docs/proposals/convergent-shared-state.md`. Merges divergent replicas of a
//! versioned key→value state (concurrent agents, or offline devices) into the
//! single state they all converge to, with strong eventual consistency.

use car_state::crdt::{
    claim_owners, export_lww, materialize, merge_claims_many, merge_many, ClaimRegistry, LwwMap,
};
use serde_json::Value;
use std::collections::HashMap;

/// Merge replicas of a CRDT shared state. `replicas_json` is a JSON array of
/// LWW maps, each `{ "<key>": { "value": <any>, "version": <u64>, "replica":
/// "<id>" } }`. Returns `{ "registers": <merged LWW map>, "state": <key→value>
/// }` — the merged CRDT (with tags, for further merging) and a materialized
/// plain state (for `verify`/`simulate`). Order-independent and idempotent: the
/// same replicas in any order yield the same result, with zero merge failures.
pub fn merge(replicas_json: &str) -> Result<String, String> {
    let replicas: Vec<LwwMap> = crate::from_json("replicas", replicas_json)?;
    let merged = merge_many(&replicas);
    let state = materialize(&merged);
    crate::to_json(&serde_json::json!({
        "registers": merged,
        "state": state,
    }))
}

/// Export a device/agent's state as a CRDT LWW map for replication (the export
/// half of multi-device sync). `snapshot_json` is the plain state `{ key: value
/// }` (e.g. from the state store's snapshot); `versions_json` is `{ key:
/// version }` (per-key versions); `replica` is this device/agent id. Returns the
/// LWW map JSON `{ "<key>": { value, version, replica } }`, ready to exchange and
/// feed to `crdt_merge`. Keys absent from `versions_json` default to version 0.
pub fn export(
    snapshot_json: &str,
    versions_json: &str,
    replica: &str,
) -> Result<String, String> {
    let snapshot: HashMap<String, Value> = crate::from_json("snapshot", snapshot_json)?;
    let versions: HashMap<String, u64> = crate::from_json("versions", versions_json)?;
    let map = export_lww(&snapshot, &versions, replica);
    crate::to_json(&map)
}

/// Merge replicas of a first-claim-wins `ClaimRegistry` for observation-driven
/// task coordination (CodeCRDT, Slice 3). `registries_json` is a JSON array of
/// registries, each `{ "<task>": { claimant, version, replica } }`. Per shared
/// task the *earliest* `(version, replica)` claim wins. Returns `{ "registry":
/// <merged>, "owners": { task: claimant } }` — the merged CRDT plus the resolved
/// one-owner-per-task view agents read to decide who executes. Order-independent
/// and idempotent.
pub fn merge_claims(registries_json: &str) -> Result<String, String> {
    let registries: Vec<ClaimRegistry> = crate::from_json("registries", registries_json)?;
    let merged = merge_claims_many(&registries);
    let owners = claim_owners(&merged);
    crate::to_json(&serde_json::json!({ "registry": merged, "owners": owners }))
}

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

    #[test]
    fn merge_converges_and_materializes() {
        let replicas = r#"[
            { "x": { "value": 1, "version": 2, "replica": "r1" },
              "shared": { "value": "a", "version": 1, "replica": "r1" } },
            { "y": { "value": 2, "version": 1, "replica": "r2" },
              "shared": { "value": "b", "version": 3, "replica": "r2" } }
        ]"#;
        let out = merge(replicas).unwrap();
        let v: Value = serde_json::from_str(&out).unwrap();
        // shared resolves to the higher version (r2's "b"); disjoint keys kept.
        assert_eq!(v["state"]["shared"], "b");
        assert_eq!(v["state"]["x"], 1);
        assert_eq!(v["state"]["y"], 2);
        // registers retain tags for further merging.
        assert_eq!(v["registers"]["shared"]["replica"], "r2");
    }

    #[test]
    fn empty_is_empty() {
        let out = merge("[]").unwrap();
        let v: Value = serde_json::from_str(&out).unwrap();
        assert_eq!(v["state"], serde_json::json!({}));
    }

    #[test]
    fn invalid_json_errs() {
        assert!(merge("not json").is_err());
    }

    #[test]
    fn merge_claims_resolves_owner() {
        // Two replicas claimed "t"; earliest wins, surfaced in `owners`.
        let regs = r#"[
            {"t":{"claimant":"a2","version":2,"replica":"r2"}},
            {"t":{"claimant":"a1","version":1,"replica":"r1"}}
        ]"#;
        let out = merge_claims(regs).unwrap();
        let v: Value = serde_json::from_str(&out).unwrap();
        assert_eq!(v["owners"]["t"], "a1");
        assert_eq!(v["registry"]["t"]["claimant"], "a1");
    }

    #[test]
    fn export_then_merge_via_ffi() {
        let a = export(r#"{"k":"a"}"#, r#"{"k":1}"#, "A").unwrap();
        let b = export(r#"{"k":"b"}"#, r#"{"k":2}"#, "B").unwrap();
        let replicas = format!("[{a},{b}]");
        let out = merge(&replicas).unwrap();
        let v: Value = serde_json::from_str(&out).unwrap();
        assert_eq!(v["state"]["k"], "b"); // higher version wins
    }
}