sim-lib-bridge 0.1.0

Checked BRIDGE packet runtime for SIM model exchanges.
Documentation
use std::sync::Mutex;

use sim_codec_bridge::{
    BridgeBook, BridgeFramePayload, BridgeHeader, BridgePacket, BridgePart, BridgeProvenance,
    BridgeWeavePayload, BridgeWeaveRow, stamp_packet_cid,
};
use sim_kernel::{Cx, Error, EvalFabric, EvalReply, EvalRequest, Expr, Result, Symbol};
use sim_lib_agent_runner_core::{
    ModelRequest, ModelResponse, OUTPUT_GRAMMAR_EXTRA, OUTPUT_GRAMMAR_REQUIRED_EXTRA,
};
use sim_value::build::entry;

use crate::{next_frontier_menu, rx_check, validate_weave, validate_woven_row, weave_row_by_row};

use super::{cx, text_content};

struct RowFabric {
    rows: Mutex<Vec<BridgeWeaveRow>>,
    requests: Mutex<Vec<ModelRequest>>,
}

impl RowFabric {
    fn new(rows: Vec<BridgeWeaveRow>) -> Self {
        Self {
            rows: Mutex::new(rows),
            requests: Mutex::new(Vec::new()),
        }
    }

    fn requests(&self) -> Vec<ModelRequest> {
        self.requests.lock().unwrap().clone()
    }
}

impl EvalFabric for RowFabric {
    fn realize(&self, cx: &mut Cx, request: EvalRequest) -> Result<EvalReply> {
        self.requests
            .lock()
            .unwrap()
            .push(ModelRequest::try_from(request.expr.clone())?);
        let row = {
            let mut rows = self.rows.lock().unwrap();
            if rows.is_empty() {
                return Err(Error::Eval("row fabric is exhausted".to_owned()));
            }
            rows.remove(0)
        };
        let response = ModelResponse::new(
            Symbol::qualified("runner", "fixture"),
            "fixture",
            vec![
                text_content("draft".to_owned()),
                Expr::Map(vec![
                    entry(
                        "type",
                        Expr::Symbol(Symbol::qualified("bridge", "WeaveRow")),
                    ),
                    entry("row", row.to_expr()),
                ]),
            ],
            Symbol::new("stop"),
        );
        Ok(EvalReply {
            value: cx.factory().expr(Expr::from(response))?,
            diagnostics: Vec::new(),
            trace: None,
        })
    }
}

fn row(slot: &str, head: &str, roles: Vec<(&str, Expr)>) -> BridgeWeaveRow {
    BridgeWeaveRow::new(
        slot,
        Symbol::new(head),
        roles
            .into_iter()
            .map(|(role, value)| (Symbol::new(role), value))
            .collect(),
    )
}

fn weave_packet(rows: Vec<BridgeWeaveRow>) -> BridgePacket {
    BridgePacket {
        header: BridgeHeader {
            cid: None,
            move_kind: Symbol::new("request"),
            from: "sim".to_owned(),
            to: vec!["model:drafter".to_owned()],
            role: Symbol::new("implementer"),
            parents: Vec::new(),
            task: Symbol::new("W1"),
            output: Symbol::new("O1"),
            ceiling: vec![Symbol::qualified("ai", "run")],
            context: vec![Symbol::new("T1")],
            provenance: BridgeProvenance::default(),
        },
        body: vec![
            BridgePart {
                id: Symbol::new("T1"),
                kind: Symbol::qualified("bridge", "Frame"),
                payload: BridgeFramePayload::new(Symbol::qualified("bridge", "proposal")).to_expr(),
            },
            BridgePart {
                id: Symbol::new("W1"),
                kind: Symbol::qualified("bridge", "Weave"),
                payload: BridgeWeavePayload::new(rows).to_expr(),
            },
            BridgePart {
                id: Symbol::new("O1"),
                kind: Symbol::qualified("bridge", "Return"),
                payload: Expr::Map(vec![
                    entry("codec", Expr::Symbol(Symbol::qualified("codec", "bridge"))),
                    entry("shape", Expr::Symbol(Symbol::qualified("core", "Map"))),
                ]),
            },
        ],
        warrant: None,
    }
}

fn first_weave(packet: &BridgePacket) -> BridgeWeavePayload {
    BridgeWeavePayload::from_expr(&packet.body[1].payload).unwrap()
}

fn extra<'a>(request: &'a ModelRequest, name: &str) -> Option<&'a Expr> {
    request.extra.iter().find_map(|(key, value)| {
        matches!(key, Expr::Symbol(symbol) if symbol.name.as_ref() == name).then_some(value)
    })
}

#[test]
fn valid_weave_fixture_passes() {
    let mut cx = cx();
    let book = BridgeBook::standard();
    let packet = stamp_packet_cid(&weave_packet(vec![row(
        "answer",
        "reply",
        vec![("input", Expr::Symbol(Symbol::new("T1")))],
    )]))
    .unwrap();
    let report = rx_check(&mut cx, &book, &packet, None).unwrap();
    let loom_report = validate_weave(&mut cx, &packet, &first_weave(&packet)).unwrap();
    let menu = next_frontier_menu(&mut cx, &packet).unwrap();

    assert!(report.accepted());
    assert!(loom_report.accepted());
    assert!(format!("{:?}", menu.heads).contains("reply"));
}

#[test]
fn off_menu_head_is_row_scoped_obligation() {
    let mut cx = cx();
    let book = BridgeBook::standard();
    let packet = stamp_packet_cid(&weave_packet(vec![row(
        "answer",
        "invented-head",
        vec![("input", Expr::Symbol(Symbol::new("T1")))],
    )]))
    .unwrap();
    let report = rx_check(&mut cx, &book, &packet, None).unwrap();

    assert!(!report.accepted());
    assert!(report.obligations.iter().any(|obligation| {
        obligation.path == "loom/rows/0/head" && obligation.reason.contains("off-menu")
    }));
}

#[test]
fn forward_ref_is_row_scoped_obligation() {
    let mut cx = cx();
    let book = BridgeBook::standard();
    let packet = stamp_packet_cid(&weave_packet(vec![
        row(
            "first",
            "reply",
            vec![("input", Expr::String("@second".to_owned()))],
        ),
        row(
            "second",
            "reply",
            vec![("input", Expr::String("@first".to_owned()))],
        ),
    ]))
    .unwrap();
    let report = rx_check(&mut cx, &book, &packet, None).unwrap();

    assert!(!report.accepted());
    assert!(report.obligations.iter().any(|obligation| {
        obligation.path == "loom/rows/0/roles/input"
            && obligation.reason.contains("forward reference")
    }));
}

#[test]
fn hand_written_result_shape_that_disagrees_rejects() {
    let mut cx = cx();
    let book = BridgeBook::standard();
    let mut packet = weave_packet(vec![row(
        "answer",
        "reply",
        vec![("input", Expr::Symbol(Symbol::new("T1")))],
    )]);
    let Expr::Map(fields) = &mut packet.body[1].payload else {
        panic!("weave payload must be a map");
    };
    for (key, value) in fields {
        if matches!(key, Expr::Symbol(symbol) if symbol.name.as_ref() == "result-shape") {
            *value = Expr::Symbol(Symbol::qualified("core", "String"));
        }
    }
    let packet = stamp_packet_cid(&packet).unwrap();
    let report = rx_check(&mut cx, &book, &packet, None).unwrap();

    assert!(!report.accepted());
    assert!(
        report
            .obligations
            .iter()
            .any(|obligation| obligation.actual.contains("result-shape disagrees"))
    );
}

#[test]
fn woven_mode_never_asks_unconstrained_row() {
    let mut cx = cx();
    let packet = stamp_packet_cid(&weave_packet(vec![row(
        "budget",
        "reply",
        vec![("input", Expr::Symbol(Symbol::new("T1")))],
    )]))
    .unwrap();
    let budget = first_weave(&packet);
    let fabric = RowFabric::new(vec![row(
        "answer",
        "reply",
        vec![("input", Expr::Symbol(Symbol::new("T1")))],
    )]);

    let completed = weave_row_by_row(&mut cx, &fabric, packet, &budget).unwrap();
    let requests = fabric.requests();

    assert_eq!(requests.len(), 1);
    assert!(format!("{:?}", extra(&requests[0], "bridge-frontier-heads")).contains("reply"));
    assert!(format!("{:?}", extra(&requests[0], "bridge-role-refs")).contains("T1"));
    assert_eq!(
        extra(&requests[0], OUTPUT_GRAMMAR_REQUIRED_EXTRA),
        Some(&Expr::Bool(true))
    );
    assert!(
        matches!(extra(&requests[0], OUTPUT_GRAMMAR_EXTRA), Some(Expr::String(grammar)) if grammar.contains("\"enum\""))
    );
    assert_eq!(first_weave(&completed).rows[0].slot, "answer");
}

#[test]
fn woven_and_validated_share_one_checker() {
    let mut cx = cx();
    let packet = stamp_packet_cid(&weave_packet(vec![row(
        "budget",
        "reply",
        vec![("input", Expr::Symbol(Symbol::new("T1")))],
    )]))
    .unwrap();
    let bad = row(
        "answer",
        "invented-head",
        vec![("input", Expr::Symbol(Symbol::new("T1")))],
    );
    let woven_report = validate_woven_row(&mut cx, &packet, &[], bad.clone()).unwrap();
    let validated_report =
        validate_weave(&mut cx, &packet, &BridgeWeavePayload::new(vec![bad])).unwrap();

    assert_eq!(woven_report.obligations, validated_report.obligations);
    assert!(
        woven_report
            .obligations
            .iter()
            .any(|obligation| obligation.path == "loom/rows/0/head")
    );
}

#[test]
fn repair_replaces_bad_row_by_path() {
    let mut cx = cx();
    let packet = stamp_packet_cid(&weave_packet(vec![row(
        "budget",
        "reply",
        vec![("input", Expr::Symbol(Symbol::new("T1")))],
    )]))
    .unwrap();
    let budget = first_weave(&packet);
    let fabric = RowFabric::new(vec![
        row(
            "bad",
            "invented-head",
            vec![("input", Expr::Symbol(Symbol::new("T1")))],
        ),
        row(
            "answer",
            "reply",
            vec![("input", Expr::Symbol(Symbol::new("T1")))],
        ),
    ]);

    let completed = weave_row_by_row(&mut cx, &fabric, packet, &budget).unwrap();
    let requests = fabric.requests();

    assert_eq!(requests.len(), 2);
    assert!(
        format!("{:?}", extra(&requests[1], "bridge-obligations")).contains("loom/rows/0/head")
    );
    assert_eq!(first_weave(&completed).rows[0].slot, "answer");
    assert_eq!(first_weave(&completed).rows[0].head, Symbol::new("reply"));
}