Skip to main content

sim_codec_bridge/
collab.rs

1use sim_kernel::{Error, Expr, NumberLiteral, Result, Symbol};
2use sim_value::build::entry;
3
4/// One axis in a structured collaboration vote.
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct BridgeScore {
7    /// Axis being scored, such as correctness or safety.
8    pub axis: Symbol,
9    /// Signed score for this axis.
10    pub value: i64,
11    /// Reviewer rationale for this axis.
12    pub reason: String,
13}
14
15impl BridgeScore {
16    /// Builds a vote score.
17    pub fn new(axis: Symbol, value: i64, reason: impl Into<String>) -> Self {
18        Self {
19            axis,
20            value,
21            reason: reason.into(),
22        }
23    }
24
25    /// Decodes a score expression.
26    pub fn from_expr(expr: &Expr) -> Result<Self> {
27        let fields = map_fields(expr, "bridge/Score")?;
28        reject_unknown(fields, &["axis", "value", "reason"])?;
29        Ok(Self::new(
30            required_symbol(fields, "axis")?.clone(),
31            required_i64(fields, "value")?,
32            required_string(fields, "reason")?.to_owned(),
33        ))
34    }
35
36    /// Encodes this score as an expression map.
37    pub fn to_expr(&self) -> Expr {
38        Expr::Map(vec![
39            entry("axis", Expr::Symbol(self.axis.clone())),
40            entry("value", i64_expr(self.value)),
41            entry("reason", Expr::String(self.reason.clone())),
42        ])
43    }
44}
45
46/// Review payload targeting one packet path.
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct BridgeReviewPayload {
49    /// Target packet path reviewed by this payload.
50    pub target: String,
51    /// Review text.
52    pub body: String,
53}
54
55impl BridgeReviewPayload {
56    /// Builds a review payload.
57    pub fn new(target: impl Into<String>, body: impl Into<String>) -> Self {
58        Self {
59            target: target.into(),
60            body: body.into(),
61        }
62    }
63
64    /// Decodes a review payload expression.
65    pub fn from_expr(expr: &Expr) -> Result<Self> {
66        let fields = map_fields(expr, "bridge/Review")?;
67        reject_unknown(fields, &["target", "body"])?;
68        Ok(Self::new(
69            required_string(fields, "target")?,
70            required_string(fields, "body")?,
71        ))
72    }
73
74    /// Encodes this payload as an expression map.
75    pub fn to_expr(&self) -> Expr {
76        Expr::Map(vec![
77            entry("target", Expr::String(self.target.clone())),
78            entry("body", Expr::String(self.body.clone())),
79        ])
80    }
81}
82
83/// Vote payload with one or more structured score axes.
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct BridgeVotePayload {
86    /// Target packet path or contribution id being voted on.
87    pub target: String,
88    /// Structured score axes.
89    pub scores: Vec<BridgeScore>,
90}
91
92impl BridgeVotePayload {
93    /// Builds a vote payload.
94    pub fn new(target: impl Into<String>, scores: Vec<BridgeScore>) -> Self {
95        Self {
96            target: target.into(),
97            scores,
98        }
99    }
100
101    /// Decodes a vote payload expression.
102    pub fn from_expr(expr: &Expr) -> Result<Self> {
103        let fields = map_fields(expr, "bridge/Vote")?;
104        reject_unknown(fields, &["target", "scores"])?;
105        let scores = required_vector(fields, "scores")?
106            .iter()
107            .map(BridgeScore::from_expr)
108            .collect::<Result<Vec<_>>>()?;
109        if scores.is_empty() {
110            return Err(Error::Eval(
111                "BRIDGE vote payload must carry at least one score".to_owned(),
112            ));
113        }
114        Ok(Self::new(required_string(fields, "target")?, scores))
115    }
116
117    /// Encodes this payload as an expression map.
118    pub fn to_expr(&self) -> Expr {
119        Expr::Map(vec![
120            entry("target", Expr::String(self.target.clone())),
121            entry(
122                "scores",
123                Expr::Vector(self.scores.iter().map(BridgeScore::to_expr).collect()),
124            ),
125        ])
126    }
127}
128
129/// Patch payload targeting an exact parent packet and path.
130#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct BridgePatchPayload {
132    /// Parent packet content id being patched.
133    pub parent_cid: String,
134    /// Target packet path.
135    pub target: String,
136    /// Replacement expression for the target path.
137    pub replacement: Expr,
138}
139
140impl BridgePatchPayload {
141    /// Builds a patch payload.
142    pub fn new(
143        parent_cid: impl Into<String>,
144        target: impl Into<String>,
145        replacement: Expr,
146    ) -> Self {
147        Self {
148            parent_cid: parent_cid.into(),
149            target: target.into(),
150            replacement,
151        }
152    }
153
154    /// Decodes a patch payload expression.
155    pub fn from_expr(expr: &Expr) -> Result<Self> {
156        let fields = map_fields(expr, "bridge/Patch")?;
157        reject_unknown(fields, &["parent-cid", "target", "replacement"])?;
158        Ok(Self::new(
159            required_string(fields, "parent-cid")?,
160            required_string(fields, "target")?,
161            required_field(fields, "replacement")?.clone(),
162        ))
163    }
164
165    /// Encodes this payload as an expression map.
166    pub fn to_expr(&self) -> Expr {
167        Expr::Map(vec![
168            entry("parent-cid", Expr::String(self.parent_cid.clone())),
169            entry("target", Expr::String(self.target.clone())),
170            entry("replacement", self.replacement.clone()),
171        ])
172    }
173}
174
175/// Evidence payload attached to a collaboration packet.
176#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct BridgeEvidencePayload {
178    /// Evidence source id or URI.
179    pub source: String,
180    /// Evidence body.
181    pub body: String,
182}
183
184impl BridgeEvidencePayload {
185    /// Builds an evidence payload.
186    pub fn new(source: impl Into<String>, body: impl Into<String>) -> Self {
187        Self {
188            source: source.into(),
189            body: body.into(),
190        }
191    }
192
193    /// Decodes an evidence payload expression.
194    pub fn from_expr(expr: &Expr) -> Result<Self> {
195        let fields = map_fields(expr, "bridge/Evidence")?;
196        reject_unknown(fields, &["source", "body"])?;
197        Ok(Self::new(
198            required_string(fields, "source")?,
199            required_string(fields, "body")?,
200        ))
201    }
202
203    /// Encodes this payload as an expression map.
204    pub fn to_expr(&self) -> Expr {
205        Expr::Map(vec![
206            entry("source", Expr::String(self.source.clone())),
207            entry("body", Expr::String(self.body.clone())),
208        ])
209    }
210}
211
212/// Receipt payload for an accepted or rejected collaboration step.
213#[derive(Clone, Debug, PartialEq, Eq)]
214pub struct BridgeReceiptPayload {
215    /// Receipt status.
216    pub status: Symbol,
217    /// Packet paths or content ids covered by the receipt.
218    pub refs: Vec<String>,
219}
220
221impl BridgeReceiptPayload {
222    /// Builds a receipt payload.
223    pub fn new(status: Symbol, refs: Vec<String>) -> Self {
224        Self { status, refs }
225    }
226
227    /// Decodes a receipt payload expression.
228    pub fn from_expr(expr: &Expr) -> Result<Self> {
229        let fields = map_fields(expr, "bridge/Receipt")?;
230        reject_unknown(fields, &["status", "refs"])?;
231        Ok(Self::new(
232            required_symbol(fields, "status")?.clone(),
233            string_vector(fields, "refs")?,
234        ))
235    }
236
237    /// Encodes this payload as an expression map.
238    pub fn to_expr(&self) -> Expr {
239        Expr::Map(vec![
240            entry("status", Expr::Symbol(self.status.clone())),
241            entry(
242                "refs",
243                Expr::Vector(self.refs.iter().cloned().map(Expr::String).collect()),
244            ),
245        ])
246    }
247}
248
249/// Attestation payload citing evidence for a claim.
250#[derive(Clone, Debug, PartialEq, Eq)]
251pub struct BridgeAttestPayload {
252    /// Subject packet path or content id.
253    pub subject: String,
254    /// Claim being attested.
255    pub claim: String,
256    /// Evidence ids supporting the claim.
257    pub evidence: Vec<String>,
258}
259
260impl BridgeAttestPayload {
261    /// Builds an attestation payload.
262    pub fn new(
263        subject: impl Into<String>,
264        claim: impl Into<String>,
265        evidence: Vec<String>,
266    ) -> Self {
267        Self {
268            subject: subject.into(),
269            claim: claim.into(),
270            evidence,
271        }
272    }
273
274    /// Decodes an attestation payload expression.
275    pub fn from_expr(expr: &Expr) -> Result<Self> {
276        let fields = map_fields(expr, "bridge/Attest")?;
277        reject_unknown(fields, &["subject", "claim", "evidence"])?;
278        Ok(Self::new(
279            required_string(fields, "subject")?,
280            required_string(fields, "claim")?,
281            string_vector(fields, "evidence")?,
282        ))
283    }
284
285    /// Encodes this payload as an expression map.
286    pub fn to_expr(&self) -> Expr {
287        Expr::Map(vec![
288            entry("subject", Expr::String(self.subject.clone())),
289            entry("claim", Expr::String(self.claim.clone())),
290            entry(
291                "evidence",
292                Expr::Vector(self.evidence.iter().cloned().map(Expr::String).collect()),
293            ),
294        ])
295    }
296}
297
298/// Validates a collaboration part payload for its registered part kind.
299pub fn validate_collab_payload(kind: &Symbol, payload: &Expr) -> Result<()> {
300    if kind == &part("Review") {
301        BridgeReviewPayload::from_expr(payload).map(drop)
302    } else if kind == &part("Vote") {
303        BridgeVotePayload::from_expr(payload).map(drop)
304    } else if kind == &part("Patch") {
305        BridgePatchPayload::from_expr(payload).map(drop)
306    } else if kind == &part("Evidence") {
307        BridgeEvidencePayload::from_expr(payload).map(drop)
308    } else if kind == &part("Receipt") {
309        BridgeReceiptPayload::from_expr(payload).map(drop)
310    } else if kind == &part("Attest") {
311        BridgeAttestPayload::from_expr(payload).map(drop)
312    } else {
313        Ok(())
314    }
315}
316
317fn map_fields<'a>(expr: &'a Expr, label: &str) -> Result<&'a [(Expr, Expr)]> {
318    match expr {
319        Expr::Map(fields) => Ok(fields),
320        _ => Err(Error::Eval(format!("{label} payload must be a map"))),
321    }
322}
323
324fn reject_unknown(fields: &[(Expr, Expr)], allowed: &[&str]) -> Result<()> {
325    for (key, _) in fields {
326        let Some(name) = field_name(key) else {
327            return Err(Error::Eval(
328                "BRIDGE collaboration field keys must be symbols".to_owned(),
329            ));
330        };
331        if !allowed.contains(&name.as_str()) {
332            return Err(Error::Eval(format!(
333                "unknown BRIDGE collaboration field {name}"
334            )));
335        }
336    }
337    Ok(())
338}
339
340fn required_field<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
341    fields
342        .iter()
343        .find_map(|(key, value)| (field_name(key).as_deref() == Some(name)).then_some(value))
344        .ok_or_else(|| Error::Eval(format!("BRIDGE collaboration record is missing {name}")))
345}
346
347fn required_symbol<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
348    match required_field(fields, name)? {
349        Expr::Symbol(symbol) => Ok(symbol),
350        _ => Err(Error::TypeMismatch {
351            expected: "symbol",
352            found: "non-symbol",
353        }),
354    }
355}
356
357fn required_string<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
358    match required_field(fields, name)? {
359        Expr::String(value) => Ok(value),
360        _ => Err(Error::TypeMismatch {
361            expected: "string",
362            found: "non-string",
363        }),
364    }
365}
366
367fn required_i64(fields: &[(Expr, Expr)], name: &str) -> Result<i64> {
368    match required_field(fields, name)? {
369        Expr::Number(number) => number.canonical.parse().map_err(|_| {
370            Error::Eval(format!(
371                "BRIDGE collaboration field {name} must be an i64 literal"
372            ))
373        }),
374        _ => Err(Error::TypeMismatch {
375            expected: "number",
376            found: "non-number",
377        }),
378    }
379}
380
381fn required_vector<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a [Expr]> {
382    match required_field(fields, name)? {
383        Expr::Vector(items) | Expr::List(items) => Ok(items),
384        _ => Err(Error::Eval(format!(
385            "BRIDGE collaboration field {name} must be a vector"
386        ))),
387    }
388}
389
390fn string_vector(fields: &[(Expr, Expr)], name: &str) -> Result<Vec<String>> {
391    required_vector(fields, name)?
392        .iter()
393        .map(|item| match item {
394            Expr::String(value) => Ok(value.clone()),
395            _ => Err(Error::TypeMismatch {
396                expected: "string",
397                found: "non-string",
398            }),
399        })
400        .collect()
401}
402
403fn i64_expr(value: i64) -> Expr {
404    Expr::Number(NumberLiteral {
405        domain: Symbol::qualified("numbers", "i64"),
406        canonical: value.to_string(),
407    })
408}
409
410fn field_name(expr: &Expr) -> Option<String> {
411    match expr {
412        Expr::Symbol(symbol) => Some(symbol.name.to_string()),
413        Expr::String(value) => Some(value.clone()),
414        _ => None,
415    }
416}
417
418fn part(name: &str) -> Symbol {
419    Symbol::qualified("bridge", name)
420}