1mod controls;
9
10use sim_codec_bridge::{
11 BridgeBook, BridgePacket, BridgePatchPayload, BridgeReceiptPayload, BridgeReviewPayload,
12 BridgeScore, BridgeVotePayload, expr_to_packet, packet_to_expr, validate_collab_payload,
13};
14use sim_kernel::{Cx, Error, Expr, Result, Symbol};
15use sim_lib_view::codec::reduce_for_caps;
16use sim_lib_view::{Draft, Operation, SurfaceCaps, SurfaceCodec};
17use sim_value::access::field;
18use sim_value::build::{entry, list, map, text};
19
20pub const BRIDGE_PACKET_SURFACE_CODEC_ID: &str = "surface:bridge-packet";
22
23#[derive(Clone, Copy, Debug, Default)]
25pub struct BridgePacketSurfaceCodec;
26
27impl BridgePacketSurfaceCodec {
28 pub fn new() -> Self {
30 Self
31 }
32}
33
34impl SurfaceCodec for BridgePacketSurfaceCodec {
35 fn encode(&self, _cx: &mut Cx, value: &Expr, caps: &SurfaceCaps) -> Result<Expr> {
36 let packet = expr_to_packet(value)?;
37 if let Some(scene) = crate::glasses_review::glasses_warrant_scene_for_caps(&packet, caps)? {
38 return Ok(scene);
39 }
40 let scene = packet_scene(&packet, caps)?;
41 Ok(reduce_for_caps(&scene, caps))
42 }
43
44 fn decode(&self, _cx: &mut Cx, value: &Expr, intent: &Expr) -> Result<Draft> {
45 sim_lib_intent::validate_intent(intent)
46 .map_err(|error| Error::HostError(format!("invalid intent: {error}")))?;
47 require_intent_kind(intent, "edit-field")?;
48 let packet = expr_to_packet(value)?;
49 require_packet_target(&packet, intent)?;
50 let path = path_segments(intent)?;
51 let action = match path.as_slice() {
52 [lane, action] if lane == "bridge-collab" => action.as_str(),
53 _ => {
54 return Err(Error::Eval(
55 "BRIDGE edit must target bridge-collab action".to_owned(),
56 ));
57 }
58 };
59 let part = collaboration_part(&packet, action, required_field(intent, "value")?)?;
60 validate_bridge_part(&part)?;
61 Ok(Draft::clean(value.clone(), part))
62 }
63
64 fn commit(&self, _cx: &mut Cx, draft: &Draft) -> Result<Operation> {
65 Ok(Operation::new(map(vec![
66 (
67 "op",
68 Expr::Symbol(Symbol::qualified("bridge", "surface-edit")),
69 ),
70 ("value", draft.proposed.clone()),
71 ])))
72 }
73}
74
75pub fn bridge_packet_view(cx: &mut Cx, packet: &BridgePacket, caps: &SurfaceCaps) -> Result<Expr> {
77 BridgePacketSurfaceCodec::new().encode(cx, &packet_to_expr(packet), caps)
78}
79
80pub fn bridge_packet_edit(cx: &mut Cx, packet: &BridgePacket, intent: &Expr) -> Result<Expr> {
82 let value = packet_to_expr(packet);
83 let codec = BridgePacketSurfaceCodec::new();
84 let draft = codec.decode(cx, &value, intent)?;
85 if !draft.committable {
86 return Err(Error::Eval(
87 "BRIDGE packet edit produced a rejected draft".to_owned(),
88 ));
89 }
90 Ok(draft.proposed)
91}
92
93fn packet_scene(packet: &BridgePacket, caps: &SurfaceCaps) -> Result<Expr> {
94 let profiles = BridgeBook::standard()
95 .profiles
96 .matching_profiles(packet)
97 .into_iter()
98 .map(|profile| profile.as_qualified_str())
99 .collect::<Vec<_>>()
100 .join(", ");
101 let cid = packet.header.cid.as_deref().unwrap_or("unstamped");
102 let mut children = vec![
103 sim_lib_scene::badge("surface", BRIDGE_PACKET_SURFACE_CODEC_ID),
104 sim_lib_scene::build::text_node(format!("cid {cid}")),
105 sim_lib_scene::build::text_node(format!(
106 "move {} from {}",
107 packet.header.move_kind.as_qualified_str(),
108 packet.header.from
109 )),
110 sim_lib_scene::build::text_node(format!(
111 "profiles {}",
112 if profiles.is_empty() {
113 "none".to_owned()
114 } else {
115 profiles
116 }
117 )),
118 sim_lib_scene::build::text_node(format!("surface {}", caps.preset_name())),
119 ];
120 children.extend(packet.body.iter().map(|part| {
121 sim_lib_scene::box_(
122 "part",
123 vec![
124 sim_lib_scene::badge("kind", &part.kind.as_qualified_str()),
125 sim_lib_scene::build::text_node(format!("id {}", part.id.as_qualified_str())),
126 sim_lib_scene::build::text_node(payload_summary(&part.payload)),
127 ],
128 )
129 }));
130 children.push(controls::collaboration_controls(packet));
131 let scene = sim_lib_scene::stack("column", children);
132 sim_lib_scene::validate_scene(&scene)
133 .map_err(|error| Error::HostError(format!("invalid BRIDGE packet scene: {error}")))?;
134 Ok(scene)
135}
136
137fn collaboration_part(packet: &BridgePacket, action: &str, value: &Expr) -> Result<Expr> {
138 match action {
139 "patch" => {
140 reject_unknown_fields(value, &["target", "replacement"], "bridge/Patch edit")?;
141 let patch = BridgePatchPayload::new(
142 packet_cid(packet)?,
143 required_non_empty_string(value, "target")?,
144 required_field(value, "replacement")?.clone(),
145 );
146 Ok(part_expr("P1", "Patch", patch.to_expr()))
147 }
148 "review" => {
149 reject_unknown_fields(value, &["target", "body"], "bridge/Review edit")?;
150 let review = BridgeReviewPayload::new(
151 required_non_empty_string(value, "target")?,
152 required_non_empty_string(value, "body")?,
153 );
154 Ok(part_expr("R1", "Review", review.to_expr()))
155 }
156 "vote" => {
157 reject_unknown_fields(value, &["target", "scores"], "bridge/Vote edit")?;
158 let vote =
159 BridgeVotePayload::new(required_non_empty_string(value, "target")?, scores(value)?);
160 Ok(part_expr("V1", "Vote", vote.to_expr()))
161 }
162 "receipt" => {
163 reject_unknown_fields(value, &["status", "refs"], "bridge/Receipt edit")?;
164 let receipt =
165 BridgeReceiptPayload::new(required_symbol_like(value, "status")?, refs(value)?);
166 Ok(part_expr("Rc1", "Receipt", receipt.to_expr()))
167 }
168 other => Err(Error::Eval(format!(
169 "unknown BRIDGE collaboration edit action {other}"
170 ))),
171 }
172}
173
174fn part_expr(id: &str, kind: &str, payload: Expr) -> Expr {
175 Expr::Map(vec![
176 entry("id", Expr::Symbol(Symbol::new(id))),
177 entry("kind", Expr::Symbol(Symbol::qualified("bridge", kind))),
178 entry("payload", payload),
179 ])
180}
181
182fn scores(value: &Expr) -> Result<Vec<BridgeScore>> {
183 let scores = required_vector(value, "scores")?
184 .iter()
185 .map(score)
186 .collect::<Result<Vec<_>>>()?;
187 if scores.is_empty() {
188 return Err(Error::Eval(
189 "BRIDGE packet vote edit requires at least one score".to_owned(),
190 ));
191 }
192 Ok(scores)
193}
194
195fn score(value: &Expr) -> Result<BridgeScore> {
196 if let Ok(score) = BridgeScore::from_expr(value) {
197 return Ok(score);
198 }
199 reject_unknown_fields(value, &["axis", "value", "reason"], "bridge/Score edit")?;
200 Ok(BridgeScore::new(
201 required_symbol_like(value, "axis")?,
202 required_i64_like(value, "value")?,
203 required_non_empty_string(value, "reason")?,
204 ))
205}
206
207fn refs(value: &Expr) -> Result<Vec<String>> {
208 required_vector(value, "refs")?
209 .iter()
210 .map(|item| match item {
211 Expr::String(value) => Ok(value.clone()),
212 _ => Err(Error::TypeMismatch {
213 expected: "string",
214 found: "non-string",
215 }),
216 })
217 .collect()
218}
219
220fn require_intent_kind(intent: &Expr, expected: &str) -> Result<()> {
221 let kind = sim_lib_intent::intent_kind_of(intent)
222 .ok_or_else(|| Error::Eval("intent is missing kind".to_owned()))?;
223 let expected_kind = sim_lib_intent::intent_kind(expected);
224 if kind != expected_kind {
225 return Err(Error::Eval(format!("expected intent/{expected}")));
226 }
227 Ok(())
228}
229
230fn require_packet_target(packet: &BridgePacket, intent: &Expr) -> Result<()> {
231 let target = required_field(intent, "target")?;
232 if target_matches(target, "bridge-packet")
233 || packet
234 .header
235 .cid
236 .as_deref()
237 .is_some_and(|cid| target_matches(target, cid))
238 {
239 return Ok(());
240 }
241 Err(Error::Eval(
242 "BRIDGE edit target must be bridge-packet or the packet cid".to_owned(),
243 ))
244}
245
246fn target_matches(target: &Expr, expected: &str) -> bool {
247 match target {
248 Expr::String(value) => value == expected,
249 Expr::Symbol(symbol) => symbol.as_qualified_str() == expected,
250 _ => false,
251 }
252}
253
254fn validate_bridge_part(part: &Expr) -> Result<()> {
255 let kind = required_symbol(part, "kind")?;
256 let payload = required_field(part, "payload")?;
257 validate_collab_payload(kind, payload)
258}
259
260fn packet_cid(packet: &BridgePacket) -> Result<String> {
261 packet.header.cid.clone().ok_or_else(|| {
262 Error::Eval("BRIDGE packet surface edits require a stamped packet".to_owned())
263 })
264}
265
266fn path_segments(intent: &Expr) -> Result<Vec<String>> {
267 required_vector(intent, "path")?
268 .iter()
269 .map(|segment| match segment {
270 Expr::String(value) => Ok(value.clone()),
271 Expr::Symbol(symbol) => Ok(symbol.name.to_string()),
272 _ => Err(Error::Eval(
273 "BRIDGE packet edit path segments must be strings or symbols".to_owned(),
274 )),
275 })
276 .collect()
277}
278
279fn required_field<'a>(expr: &'a Expr, name: &str) -> Result<&'a Expr> {
280 field(expr, name).ok_or_else(|| Error::Eval(format!("missing field {name}")))
281}
282
283fn required_string<'a>(expr: &'a Expr, name: &str) -> Result<&'a str> {
284 match required_field(expr, name)? {
285 Expr::String(value) => Ok(value),
286 _ => Err(Error::TypeMismatch {
287 expected: "string",
288 found: "non-string",
289 }),
290 }
291}
292
293fn required_non_empty_string<'a>(expr: &'a Expr, name: &str) -> Result<&'a str> {
294 let value = required_string(expr, name)?;
295 if value.trim().is_empty() {
296 return Err(Error::Eval(format!("field {name} must not be empty")));
297 }
298 Ok(value)
299}
300
301fn required_symbol<'a>(expr: &'a Expr, name: &str) -> Result<&'a Symbol> {
302 match required_field(expr, name)? {
303 Expr::Symbol(value) => Ok(value),
304 _ => Err(Error::TypeMismatch {
305 expected: "symbol",
306 found: "non-symbol",
307 }),
308 }
309}
310
311fn required_symbol_like(expr: &Expr, name: &str) -> Result<Symbol> {
312 match required_field(expr, name)? {
313 Expr::Symbol(value) => Ok(value.clone()),
314 Expr::String(value) if !value.trim().is_empty() => Ok(Symbol::new(value.clone())),
315 Expr::String(_) => Err(Error::Eval(format!("field {name} must not be empty"))),
316 _ => Err(Error::TypeMismatch {
317 expected: "symbol or string",
318 found: "non-symbol",
319 }),
320 }
321}
322
323fn required_i64_like(expr: &Expr, name: &str) -> Result<i64> {
324 match required_field(expr, name)? {
325 Expr::Number(number) => number
326 .canonical
327 .parse()
328 .map_err(|_| Error::Eval(format!("field {name} must be an i64 literal"))),
329 Expr::String(value) => value
330 .parse()
331 .map_err(|_| Error::Eval(format!("field {name} must be an i64 literal"))),
332 _ => Err(Error::TypeMismatch {
333 expected: "number or numeric string",
334 found: "non-number",
335 }),
336 }
337}
338
339fn required_vector<'a>(expr: &'a Expr, name: &str) -> Result<&'a [Expr]> {
340 match required_field(expr, name)? {
341 Expr::List(items) | Expr::Vector(items) => Ok(items),
342 _ => Err(Error::Eval(format!("field {name} must be a list"))),
343 }
344}
345
346fn reject_unknown_fields(expr: &Expr, allowed: &[&str], label: &str) -> Result<()> {
347 let Expr::Map(fields) = expr else {
348 return Err(Error::Eval(format!("{label} must be a map")));
349 };
350 for (key, _) in fields {
351 let Some(name) = field_name(key) else {
352 return Err(Error::Eval(format!("{label} field keys must be symbols")));
353 };
354 if !allowed.contains(&name.as_str()) {
355 return Err(Error::Eval(format!("unknown {label} field {name}")));
356 }
357 }
358 Ok(())
359}
360
361fn field_name(expr: &Expr) -> Option<String> {
362 match expr {
363 Expr::Symbol(symbol) => Some(symbol.name.to_string()),
364 Expr::String(value) => Some(value.clone()),
365 _ => None,
366 }
367}
368
369fn payload_summary(payload: &Expr) -> String {
370 let rendered = format!("{payload:?}");
371 if rendered.len() <= 96 {
372 rendered
373 } else {
374 format!("{}...", &rendered[..96])
375 }
376}
377
378pub fn patch_edit_intent(target: &str, replacement: Expr, origin: sim_lib_intent::Origin) -> Expr {
380 sim_lib_intent::intent(
381 "edit-field",
382 origin,
383 vec![
384 ("target", Expr::Symbol(Symbol::new("bridge-packet"))),
385 ("path", list(vec![text("bridge-collab"), text("patch")])),
386 (
387 "value",
388 map(vec![("target", text(target)), ("replacement", replacement)]),
389 ),
390 ],
391 )
392}
393
394pub fn review_edit_intent(target: &str, body: &str, origin: sim_lib_intent::Origin) -> Expr {
396 sim_lib_intent::intent(
397 "edit-field",
398 origin,
399 vec![
400 ("target", Expr::Symbol(Symbol::new("bridge-packet"))),
401 ("path", list(vec![text("bridge-collab"), text("review")])),
402 (
403 "value",
404 map(vec![("target", text(target)), ("body", text(body))]),
405 ),
406 ],
407 )
408}
409
410pub fn vote_edit_intent(
412 target: &str,
413 scores: Vec<BridgeScore>,
414 origin: sim_lib_intent::Origin,
415) -> Expr {
416 sim_lib_intent::intent(
417 "edit-field",
418 origin,
419 vec![
420 ("target", Expr::Symbol(Symbol::new("bridge-packet"))),
421 ("path", list(vec![text("bridge-collab"), text("vote")])),
422 (
423 "value",
424 map(vec![
425 ("target", text(target)),
426 (
427 "scores",
428 Expr::Vector(scores.iter().map(BridgeScore::to_expr).collect()),
429 ),
430 ]),
431 ),
432 ],
433 )
434}
435
436pub fn receipt_edit_intent(
438 status: Symbol,
439 refs: Vec<String>,
440 origin: sim_lib_intent::Origin,
441) -> Expr {
442 sim_lib_intent::intent(
443 "edit-field",
444 origin,
445 vec![
446 ("target", Expr::Symbol(Symbol::new("bridge-packet"))),
447 ("path", list(vec![text("bridge-collab"), text("receipt")])),
448 (
449 "value",
450 map(vec![
451 ("status", Expr::Symbol(status)),
452 ("refs", list(refs.into_iter().map(text).collect::<Vec<_>>())),
453 ]),
454 ),
455 ],
456 )
457}