Skip to main content

sim_lib_bridge/
collab.rs

1use std::sync::Arc;
2
3use sim_codec_bridge::{
4    BridgeBook, BridgePacket, BridgePatchPayload, BridgeVotePayload, stamp_packet_cid,
5};
6use sim_kernel::{Cx, DefaultFactory, EagerPolicy, Error, Result, Symbol};
7
8use crate::rx_check;
9
10/// Declared policy for combining collaboration contributions.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub enum MergePolicy {
13    /// Accept one patch contribution.
14    Single,
15    /// Accept one patch contribution once enough vote records target it.
16    Quorum {
17        /// Minimum number of matching vote records.
18        min_votes: u32,
19    },
20    /// Accept a synthesizer patch contribution once enough vote records target it.
21    SynthesisThenVote {
22        /// Seat name of the synthesizer whose patch is eligible.
23        synthesizer: String,
24        /// Minimum number of matching vote records.
25        min_votes: u32,
26    },
27}
28
29/// Merge patch replies by exact parent content id and target path.
30///
31/// The selected packet is checked as a reply to `base`, so the root packet's
32/// `Return` contract still decides whether the merged payload is admissible.
33pub fn merge_bridge_replies(
34    base: &BridgePacket,
35    replies: &[BridgePacket],
36    policy: &MergePolicy,
37) -> Result<BridgePacket> {
38    let base_cid = base.header.cid.as_deref().ok_or_else(|| {
39        Error::Eval("BRIDGE collaboration merge requires a stamped base packet".to_owned())
40    })?;
41    let patches = patch_candidates(base_cid, replies)?;
42    if patches.is_empty() {
43        return Err(Error::Eval(
44            "BRIDGE collaboration merge found no patch for the base packet".to_owned(),
45        ));
46    }
47    require_one_target(&patches)?;
48    let selected = select_patch(base_cid, replies, &patches, policy)?;
49    let merged = stamp_packet_cid(&selected.packet.canonicalized())?;
50    check_merged_reply(base, &merged)?;
51    Ok(merged)
52}
53
54#[derive(Clone)]
55struct PatchCandidate<'a> {
56    packet: &'a BridgePacket,
57    patch: BridgePatchPayload,
58}
59
60fn patch_candidates<'a>(
61    base_cid: &str,
62    replies: &'a [BridgePacket],
63) -> Result<Vec<PatchCandidate<'a>>> {
64    let mut patches = Vec::new();
65    for packet in replies {
66        if !packet
67            .header
68            .parents
69            .iter()
70            .any(|parent| parent == base_cid)
71        {
72            continue;
73        }
74        for part in &packet.body {
75            if part.kind != Symbol::qualified("bridge", "Patch") {
76                continue;
77            }
78            let patch = BridgePatchPayload::from_expr(&part.payload)?;
79            if patch.parent_cid == base_cid {
80                patches.push(PatchCandidate { packet, patch });
81            }
82        }
83    }
84    Ok(patches)
85}
86
87fn require_one_target(patches: &[PatchCandidate<'_>]) -> Result<()> {
88    let target = patches[0].patch.target.as_str();
89    if patches
90        .iter()
91        .all(|candidate| candidate.patch.target == target)
92    {
93        return Ok(());
94    }
95    Err(Error::Eval(
96        "BRIDGE collaboration merge patches must share one exact target path".to_owned(),
97    ))
98}
99
100fn select_patch<'a>(
101    base_cid: &str,
102    replies: &'a [BridgePacket],
103    patches: &[PatchCandidate<'a>],
104    policy: &MergePolicy,
105) -> Result<PatchCandidate<'a>> {
106    match policy {
107        MergePolicy::Single => {
108            if patches.len() == 1 {
109                Ok(patches[0].clone())
110            } else {
111                Err(Error::Eval(
112                    "BRIDGE single merge requires exactly one patch".to_owned(),
113                ))
114            }
115        }
116        MergePolicy::Quorum { min_votes } => {
117            select_quorum_patch(base_cid, replies, patches, *min_votes)
118        }
119        MergePolicy::SynthesisThenVote {
120            synthesizer,
121            min_votes,
122        } => {
123            if synthesizer.is_empty() {
124                return Err(Error::Eval(
125                    "BRIDGE synthesis merge requires a synthesizer seat".to_owned(),
126                ));
127            }
128            let synthesized = patches
129                .iter()
130                .filter(|candidate| candidate.packet.header.from == *synthesizer)
131                .cloned()
132                .collect::<Vec<_>>();
133            select_quorum_patch(base_cid, replies, &synthesized, *min_votes)
134        }
135    }
136}
137
138fn select_quorum_patch<'a>(
139    base_cid: &str,
140    replies: &[BridgePacket],
141    patches: &[PatchCandidate<'a>],
142    min_votes: u32,
143) -> Result<PatchCandidate<'a>> {
144    if min_votes == 0 {
145        return Err(Error::Eval(
146            "BRIDGE quorum merge requires at least one vote".to_owned(),
147        ));
148    }
149    let eligible = patches
150        .iter()
151        .filter(|candidate| {
152            votes_for_target(base_cid, replies, &candidate.patch.target)
153                .map(|votes| votes >= min_votes)
154                .unwrap_or(false)
155        })
156        .cloned()
157        .collect::<Vec<_>>();
158    if eligible.len() == 1 {
159        Ok(eligible[0].clone())
160    } else if eligible.is_empty() {
161        Err(Error::Eval(
162            "BRIDGE quorum merge found no patch with enough votes".to_owned(),
163        ))
164    } else {
165        Err(Error::Eval(
166            "BRIDGE quorum merge selected more than one patch".to_owned(),
167        ))
168    }
169}
170
171fn votes_for_target(base_cid: &str, replies: &[BridgePacket], target: &str) -> Result<u32> {
172    let mut votes = 0u32;
173    for packet in replies {
174        if !packet
175            .header
176            .parents
177            .iter()
178            .any(|parent| parent == base_cid)
179        {
180            continue;
181        }
182        for part in &packet.body {
183            if part.kind != Symbol::qualified("bridge", "Vote") {
184                continue;
185            }
186            let vote = BridgeVotePayload::from_expr(&part.payload)?;
187            if vote.target == target {
188                votes += 1;
189            }
190        }
191    }
192    Ok(votes)
193}
194
195fn check_merged_reply(base: &BridgePacket, merged: &BridgePacket) -> Result<()> {
196    let book = BridgeBook::standard();
197    let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
198    let report = rx_check(&mut cx, &book, merged, Some(base))?;
199    if report.accepted() {
200        return Ok(());
201    }
202    Err(Error::Eval(format!(
203        "BRIDGE merged reply failed receive check: {:?}",
204        report.obligations
205    )))
206}