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