Skip to main content

sim_lib_view_codec/
multicodec.rs

1//! The multi-codec lens and round-trip probe.
2//!
3//! One value, several codecs, side by side. Each codec's rendering is a
4//! `scene/embed` carrying the encoded form and a round-trip badge. The probe
5//! encodes then decodes and reports whether the value survived and, if not,
6//! that there was loss. Read-construct and codec output position are surfaced
7//! where relevant; broad read-eval is never exposed here (it stays
8//! capability-gated elsewhere).
9
10use sim_codec::{
11    CodecPrism, Input, Output, PrismDiagnostic, PrismOutput, RuntimeCodecPrism, decode_with_codec,
12    encode_with_codec,
13};
14use sim_kernel::{Cx, EncodeOptions, EncodePosition, Expr, ReadPolicy, Symbol};
15use sim_lib_scene::{data_map, node, sym};
16
17/// The multi-codec lens id.
18pub const MULTI_CODEC_LENS: &str = "view:codec-multi";
19
20/// The SysEx comparison lens id.
21pub const SYSEX_COMPARISON_LENS: &str = "view:codec-sysex-comparison";
22
23/// The outcome of a round-trip probe through one codec.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct ProbeResult {
26    /// The encoded form, rendered for display.
27    pub encoded: String,
28    /// Whether decoding reproduced the original value.
29    pub lossless: bool,
30    /// Whether the Prism parse/encode/reparse proof preserved semantic id.
31    pub semantic_identity: bool,
32    /// Stable semantic id from the Prism parse pass.
33    pub semantic_id: Option<String>,
34    /// Number of source spans discovered by the Prism parse pass.
35    pub span_count: usize,
36    /// Loss and parse diagnostics collected by the Prism.
37    pub diagnostics: Vec<String>,
38    /// Whether the inspected input is treated as trusted executable input.
39    pub trusted_executable: bool,
40    /// Suggested alternate codec surfaces for comparison.
41    pub alternates: Vec<Symbol>,
42}
43
44impl ProbeResult {
45    /// Builds a passing probe result for callers that already have a proof.
46    pub fn lossless(encoded: impl Into<String>) -> Self {
47        Self {
48            encoded: encoded.into(),
49            lossless: true,
50            semantic_identity: true,
51            semantic_id: None,
52            span_count: 0,
53            diagnostics: Vec::new(),
54            trusted_executable: false,
55            alternates: Vec::new(),
56        }
57    }
58}
59
60/// Probe a value's round-trip through one codec.
61pub fn roundtrip_probe(cx: &mut Cx, codec: &Symbol, value: &Expr) -> ProbeResult {
62    match encode_with_codec(cx, codec, value, EncodeOptions::default()) {
63        Ok(output) => {
64            let (encoded, input, prism_output) = render_output(output);
65            let decoded_lossless = decode_with_codec(cx, codec, input, ReadPolicy::default())
66                .map(|decoded| decoded.canonical_eq(value))
67                .unwrap_or(false);
68            let prism = prism_for_codec(codec);
69            let proof = match prism_output {
70                PrismOutput::Text(text) => prism.round_trip(cx, &text, EncodePosition::Data),
71                PrismOutput::Bytes(bytes) => {
72                    prism.round_trip_bytes(cx, &bytes, EncodePosition::Data)
73                }
74            };
75            let semantic_identity = proof.loss_report.semantic_identity;
76            let diagnostics = diagnostic_text(&proof.loss_report.diagnostics);
77            ProbeResult {
78                encoded,
79                lossless: decoded_lossless && proof.loss_report.lossless,
80                semantic_identity,
81                semantic_id: proof.parse.semantic_id.as_ref().map(|id| id.stable.clone()),
82                span_count: proof.parse.span_map.len(),
83                diagnostics,
84                trusted_executable: proof.parse.inspection.trusted_executable,
85                alternates: suggested_alternates(codec),
86            }
87        }
88        Err(error) => ProbeResult {
89            encoded: format!("<encode error: {error}>"),
90            lossless: false,
91            semantic_identity: false,
92            semantic_id: None,
93            span_count: 0,
94            diagnostics: vec![format!("encode-error: {error}")],
95            trusted_executable: false,
96            alternates: suggested_alternates(codec),
97        },
98    }
99}
100
101fn render_output(output: Output) -> (String, Input, PrismOutput) {
102    match output {
103        Output::Text(text) => (
104            text.clone(),
105            Input::Text(text.clone()),
106            PrismOutput::Text(text),
107        ),
108        Output::Bytes(bytes) => {
109            let hex: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect();
110            let display = format!("{} bytes: {hex}", bytes.len());
111            (
112                display,
113                Input::Bytes(bytes.clone()),
114                PrismOutput::Bytes(bytes),
115            )
116        }
117    }
118}
119
120fn prism_for_codec(codec: &Symbol) -> RuntimeCodecPrism {
121    match (&codec.namespace, &*codec.name) {
122        (Some(namespace), "chat") if &**namespace == "codec" => {
123            RuntimeCodecPrism::domain(codec.clone(), "chat transcript")
124        }
125        (Some(namespace), "mcp") if &**namespace == "codec" => {
126            RuntimeCodecPrism::domain(codec.clone(), "MCP envelope")
127        }
128        (Some(namespace), "binary") if &**namespace == "codec" => {
129            RuntimeCodecPrism::binary(codec.clone())
130        }
131        (Some(namespace), "binary-base64") if &**namespace == "codec" => {
132            RuntimeCodecPrism::binary_base64(codec.clone())
133        }
134        _ => RuntimeCodecPrism::general(codec.clone()),
135    }
136}
137
138fn suggested_alternates(codec: &Symbol) -> Vec<Symbol> {
139    let installed = ["lisp", "algol", "json", "binary", "binary-base64"];
140    installed
141        .iter()
142        .map(|name| Symbol::qualified("codec", *name))
143        .filter(|candidate| candidate != codec)
144        .collect()
145}
146
147fn diagnostic_text(diagnostics: &[PrismDiagnostic]) -> Vec<String> {
148    diagnostics
149        .iter()
150        .map(|diagnostic| format!("{}: {}", diagnostic.code, diagnostic.message))
151        .collect()
152}
153
154/// Open one value through several codecs at once, each as a `scene/embed` with a
155/// round-trip badge.
156pub fn multi_codec_view(cx: &mut Cx, codecs: &[Symbol], value: &Expr) -> Expr {
157    let panels = codecs
158        .iter()
159        .map(|codec| codec_panel(cx, codec, value))
160        .collect();
161    node(
162        "stack",
163        vec![
164            ("role", sym("multi-codec")),
165            ("dir", sym("row")),
166            ("children", Expr::List(panels)),
167        ],
168    )
169}
170
171/// Compare the same SysEx payload as hex, binary, and Lisp data, with a
172/// round-trip probe result.
173pub fn sysex_comparison_view(hex: &str, bytes: &[u8], lisp: &str, probe: &ProbeResult) -> Expr {
174    let status = if probe.lossless { "ok" } else { "warn" };
175    node(
176        "stack",
177        vec![
178            ("lens", Expr::Symbol(Symbol::new(SYSEX_COMPARISON_LENS))),
179            ("role", sym("sysex-comparison-view")),
180            ("dir", sym("column")),
181            (
182                "children",
183                Expr::List(vec![
184                    node(
185                        "table",
186                        vec![
187                            ("role", sym("sysex-format-comparison")),
188                            (
189                                "rows",
190                                Expr::List(vec![
191                                    comparison_row("hex", hex),
192                                    comparison_row("binary", &binary_display(bytes)),
193                                    comparison_row("lisp", lisp),
194                                ]),
195                            ),
196                        ],
197                    ),
198                    node(
199                        "badge",
200                        vec![
201                            ("role", sym("round-trip-probe")),
202                            ("status", sym(status)),
203                            ("label", Expr::String(probe.encoded.clone())),
204                        ],
205                    ),
206                ]),
207            ),
208        ],
209    )
210}
211
212fn codec_panel(cx: &mut Cx, codec: &Symbol, value: &Expr) -> Expr {
213    let probe = roundtrip_probe(cx, codec, value);
214    let prism_report = prism_report(&probe);
215    let badge = node(
216        "badge",
217        vec![
218            ("status", sym(if probe.lossless { "ok" } else { "warn" })),
219            (
220                "label",
221                Expr::String(
222                    if probe.lossless {
223                        "round-trips"
224                    } else {
225                        "loss"
226                    }
227                    .to_owned(),
228                ),
229            ),
230        ],
231    );
232    let inner = node(
233        "box",
234        vec![
235            ("role", sym("codec-prism")),
236            (
237                "prism",
238                data_map(vec![
239                    (
240                        "semantic-id",
241                        Expr::String(probe.semantic_id.clone().unwrap_or_default()),
242                    ),
243                    ("semantic-identity", bool_expr(probe.semantic_identity)),
244                    ("span-count", int_expr(probe.span_count)),
245                    ("trusted-executable", bool_expr(probe.trusted_executable)),
246                    (
247                        "alternates",
248                        Expr::List(probe.alternates.iter().cloned().map(Expr::Symbol).collect()),
249                    ),
250                ]),
251            ),
252            (
253                "children",
254                Expr::List(vec![
255                    node("text", vec![("text", Expr::String(codec.to_string()))]),
256                    badge,
257                    prism_report,
258                    node(
259                        "field",
260                        vec![
261                            ("datatype", sym("text")),
262                            ("value", Expr::String(probe.encoded)),
263                        ],
264                    ),
265                ]),
266            ),
267        ],
268    );
269    node(
270        "embed",
271        vec![("lens", Expr::Symbol(codec.clone())), ("scene", inner)],
272    )
273}
274
275fn prism_report(probe: &ProbeResult) -> Expr {
276    node(
277        "stack",
278        vec![
279            ("role", sym("prism-diagnostics")),
280            ("dir", sym("column")),
281            (
282                "children",
283                Expr::List(vec![
284                    node(
285                        "badge",
286                        vec![
287                            ("role", sym("prism-loss-report")),
288                            ("status", sym(if probe.lossless { "ok" } else { "warn" })),
289                            (
290                                "label",
291                                Expr::String(
292                                    if probe.semantic_identity {
293                                        "semantic identity"
294                                    } else {
295                                        "semantic loss"
296                                    }
297                                    .to_owned(),
298                                ),
299                            ),
300                        ],
301                    ),
302                    data_map(vec![
303                        ("spans", int_expr(probe.span_count)),
304                        (
305                            "diagnostics",
306                            Expr::List(
307                                probe
308                                    .diagnostics
309                                    .iter()
310                                    .cloned()
311                                    .map(Expr::String)
312                                    .collect(),
313                            ),
314                        ),
315                    ]),
316                ]),
317            ),
318        ],
319    )
320}
321
322fn bool_expr(value: bool) -> Expr {
323    Expr::Symbol(Symbol::new(if value { "true" } else { "false" }))
324}
325
326fn int_expr(value: usize) -> Expr {
327    Expr::String(value.to_string())
328}
329
330fn comparison_row(format: &str, value: &str) -> Expr {
331    data_map(vec![
332        ("format", Expr::Symbol(Symbol::new(format))),
333        ("value", Expr::String(value.to_owned())),
334    ])
335}
336
337fn binary_display(bytes: &[u8]) -> String {
338    bytes
339        .iter()
340        .map(|byte| format!("{byte:08b}"))
341        .collect::<Vec<_>>()
342        .join(" ")
343}