wm_tools/expansion/
glyph.rs1#![forbid(unsafe_code)]
12
13use async_trait::async_trait;
14
15use serde_json::{Value, json};
16use wm_core::{Context, EffectRow, Gana, Tool, ToolStats};
17
18pub 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
82pub 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#[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}