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#[derive(Clone, Debug, PartialEq, Eq)]
12pub enum MergePolicy {
13 Single,
15 Quorum {
17 min_votes: u32,
19 },
20 SynthesisThenVote {
22 synthesizer: String,
24 min_votes: u32,
26 },
27}
28
29pub 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}