Skip to main content

wm_tools/expansion/
glyph.rs

1//! Glyph wire tools — Q34 sub-experiment 2 (2026-09-09, session 7f56e966).
2//!
3//! `glyph.encode` translates {route, args} into the compact {r, a} wire
4//! form (see the codebook in crate root: GLYPH_ROUTES / GLYPH_ARGS).
5//! Pure function, read-only effects, registered unconditionally — the
6//! trust-boundary gate lives on the DECODE seam (`WM_GLYPH=1` in the
7//! meta-tool), not here: emitting codes is harmless, parsing them is
8//! what Q09 reviews. Unknown names pass through unchanged (both sides),
9//! so partial books never corrupt.
10
11#![forbid(unsafe_code)]
12
13use async_trait::async_trait;
14
15use serde_json::{Value, json};
16use wm_core::{Context, EffectRow, Gana, Tool, ToolStats};
17
18/// `glyph.encode` — {route, args} → {r, a} compact form.
19/// Args: route (required), args (object, default {}).
20pub struct GlyphEncodeTool {
21    stats: ToolStats,
22    effects: EffectRow,
23}
24
25impl GlyphEncodeTool {
26    #[must_use]
27    pub fn new() -> Self {
28        Self {
29            stats: ToolStats::default(),
30            effects: EffectRow::pure(),
31        }
32    }
33}
34
35impl Default for GlyphEncodeTool {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41#[async_trait]
42impl Tool for GlyphEncodeTool {
43    fn name(&self) -> &str {
44        "glyph.encode"
45    }
46    fn gana(&self) -> Gana {
47        Gana::Horn
48    }
49    fn effects(&self) -> &EffectRow {
50        &self.effects
51    }
52    fn description(&self) -> &str {
53        "Encode a {route, args} dispatch into the compact glyph wire form {r, a}. Args: route (required), args (object, default {}). Pure translation — unknown names pass through; decode side (WM_GLYPH=1) reverses it."
54    }
55    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
56        let route = args
57            .get("route")
58            .and_then(Value::as_str)
59            .ok_or_else(|| wm_core::CoreError::InvalidArgs("route is required".into()))?;
60        let inner = args.get("args").cloned().unwrap_or_else(|| json!({}));
61        let encoded = crate::encode_glyph(route, &inner);
62        let raw_len =
63            serde_json::to_string(&json!({"route": route, "args": inner})).map_or(0, |s| s.len());
64        let enc_len = serde_json::to_string(&encoded).map_or(0, |s| s.len());
65        Ok(json!({
66            "status": "success",
67            "encoded": encoded,
68            "raw_length": raw_len,
69            "encoded_length": enc_len,
70            "saved_ratio": if raw_len > 0 {
71                1.0 - enc_len as f64 / raw_len as f64
72            } else {
73                0.0
74            },
75        }))
76    }
77    fn stats(&self) -> &ToolStats {
78        &self.stats
79    }
80}
81
82/// `glyph.decode` — {r, a} compact form → {route, args}. Debug/bench
83/// surface for the wire format; the live decode seam sits in the
84/// meta-tool (WM_GLYPH=1). Unknown route codes refuse (None → error).
85pub struct GlyphDecodeTool {
86    stats: ToolStats,
87    effects: EffectRow,
88}
89
90impl GlyphDecodeTool {
91    #[must_use]
92    pub fn new() -> Self {
93        Self {
94            stats: ToolStats::default(),
95            effects: EffectRow::pure(),
96        }
97    }
98}
99
100impl Default for GlyphDecodeTool {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106#[async_trait]
107impl Tool for GlyphDecodeTool {
108    fn name(&self) -> &str {
109        "glyph.decode"
110    }
111    fn gana(&self) -> Gana {
112        Gana::Horn
113    }
114    fn effects(&self) -> &EffectRow {
115        &self.effects
116    }
117    fn description(&self) -> &str {
118        "Decode a glyph wire object {r, a} back into {route, args}. Args: r (required route code), a (object of coded args, default {}). Unknown route codes are refused; unknown arg codes pass through."
119    }
120    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
121        let decoded = crate::decode_glyph(&args).ok_or_else(|| {
122            wm_core::CoreError::InvalidArgs(
123                "not a glyph object: need {r: <known route code>, a: {...}}".into(),
124            )
125        })?;
126        Ok(json!({ "status": "success", "decoded": decoded }))
127    }
128    fn stats(&self) -> &ToolStats {
129        &self.stats
130    }
131}
132
133/// Register the glyph & LKEP tools (3) — pure translators, no gating: the
134/// trust gate lives on the meta-tool's decode seam (WM_GLYPH=1).
135#[must_use]
136pub fn register_glyph(registry: &wm_dispatch::ToolRegistry) -> wm_dispatch::ToolRegistry {
137    registry
138        .register(std::sync::Arc::new(GlyphEncodeTool::new()))
139        .register(std::sync::Arc::new(GlyphDecodeTool::new()))
140        .register(std::sync::Arc::new(super::lkep::LkepExecTool::new()))
141}
142
143#[cfg(test)]
144mod glyph_tests {
145
146    #[test]
147    fn book_tables_cover_measured_routes() {
148        for route in [
149            "memory.search",
150            "memory.create",
151            "session.record",
152            "session.continuity",
153            "dharma.escalate",
154            "graph.walk",
155            "tools.list",
156            "citta.status",
157        ] {
158            assert!(
159                crate::glyph_lookup(crate::GLYPH_ROUTES, route).is_some(),
160                "missing {route}"
161            );
162        }
163    }
164}