Skip to main content

kcode_k1_kmap_format/
lib.rs

1use kcode_k1_transaction_id::TxId;
2use serde::{Deserialize, Serialize};
3use std::{collections::HashSet, error::Error as StdError, fmt};
4
5pub const FORMAT_VERSION: u8 = 2;
6pub const EWA_HALF_LIFE_MASS: f64 = 30.0;
7pub const PRUNE_EPSILON: f64 = 0.1;
8pub const MAX_NAVIGATION_CONNECTIONS: usize = 12;
9
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct KmapError(pub String);
12
13impl fmt::Display for KmapError {
14    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15        f.write_str(&self.0)
16    }
17}
18
19impl StdError for KmapError {}
20pub type Result<T> = std::result::Result<T, KmapError>;
21
22fn error(message: &str) -> KmapError {
23    KmapError(message.to_owned())
24}
25
26#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
27pub struct NodeId(pub [u8; 12]);
28
29impl From<TxId> for NodeId {
30    fn from(value: TxId) -> Self {
31        Self(*value.as_bytes())
32    }
33}
34
35impl From<NodeId> for TxId {
36    fn from(value: NodeId) -> Self {
37        Self::from_bytes(value.0)
38    }
39}
40
41#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
42pub enum ConnectionTier {
43    Navigation,
44    Automated,
45}
46
47#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
48pub struct Weight {
49    pub value: f64,
50    pub mass: f64,
51}
52
53impl Weight {
54    pub fn new(value: f64, mass: f64) -> Result<Self> {
55        validate_value_mass(value, mass)?;
56        Ok(Self { value, mass })
57    }
58
59    pub const fn initial() -> Self {
60        Self {
61            value: 1.0,
62            mass: 3.0,
63        }
64    }
65
66    pub fn update(self, value: f64, mass: f64) -> Result<Option<Self>> {
67        validate_value_mass(self.value, self.mass)?;
68        validate_value_mass(value, mass)?;
69        let aged_mass = self.mass * (-mass / EWA_HALF_LIFE_MASS).exp2();
70        let new_mass = aged_mass + mass;
71        if !new_mass.is_finite() || new_mass <= 0.0 {
72            return Err(error("updated mass is not representable"));
73        }
74        let new_value = (value - self.value)
75            .mul_add(mass / new_mass, self.value)
76            .clamp(0.0, 1.0);
77        let updated = Self::new(new_value, new_mass)?;
78        Ok((updated.value >= PRUNE_EPSILON).then_some(updated))
79    }
80}
81
82#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
83pub struct Connection {
84    pub target: NodeId,
85    pub tier: ConnectionTier,
86    pub weight: Weight,
87}
88
89impl Connection {
90    pub fn new(target: NodeId, tier: ConnectionTier) -> Self {
91        Self {
92            target,
93            tier,
94            weight: Weight::initial(),
95        }
96    }
97}
98
99#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
100pub struct ConnectionSpec {
101    pub target: NodeId,
102    pub tier: ConnectionTier,
103}
104
105#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
106pub enum MeasurementImportance {
107    Critical,
108    NonCritical,
109}
110
111#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
112pub struct ConnectionMeasurement {
113    pub source: NodeId,
114    pub target: NodeId,
115    pub useful: bool,
116    pub importance: MeasurementImportance,
117}
118
119impl ConnectionMeasurement {
120    pub const fn new(
121        source: NodeId,
122        target: NodeId,
123        useful: bool,
124        importance: MeasurementImportance,
125    ) -> Self {
126        Self {
127            source,
128            target,
129            useful,
130            importance,
131        }
132    }
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136pub enum MeasurementOutcome {
137    Updated,
138    Pruned,
139    Absent,
140}
141
142#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
143pub struct Node {
144    pub title: String,
145    pub navigation_hint: String,
146    pub narrative: String,
147    pub connections: Vec<Connection>,
148}
149
150impl Node {
151    pub fn new(
152        title: impl Into<String>,
153        navigation_hint: impl Into<String>,
154        narrative: impl Into<String>,
155        connections: Vec<Connection>,
156    ) -> Result<Self> {
157        let node = Self {
158            title: title.into(),
159            navigation_hint: navigation_hint.into(),
160            narrative: narrative.into(),
161            connections,
162        };
163        node.validate()?;
164        Ok(node)
165    }
166
167    pub fn from_specs(
168        title: impl Into<String>,
169        navigation_hint: impl Into<String>,
170        narrative: impl Into<String>,
171        specs: Vec<ConnectionSpec>,
172    ) -> Result<Self> {
173        validate_specs(&specs)?;
174        Self::new(
175            title,
176            navigation_hint,
177            narrative,
178            specs
179                .into_iter()
180                .map(|spec| Connection::new(spec.target, spec.tier))
181                .collect(),
182        )
183    }
184
185    pub fn validate(&self) -> Result<()> {
186        ensure_unique(self.connections.iter().map(|connection| connection.target))?;
187        for connection in &self.connections {
188            validate_value_mass(connection.weight.value, connection.weight.mass)?;
189        }
190        validate_navigation(self.connections.iter().map(|connection| connection.tier))
191    }
192
193    pub fn apply_connection_updates(&mut self, updates: &[ConnectionSpec]) -> Result<()> {
194        ensure_unique(updates.iter().map(|update| update.target))?;
195        let mut connections = self.connections.clone();
196        for update in updates {
197            if let Some(connection) = connections
198                .iter_mut()
199                .find(|connection| connection.target == update.target)
200            {
201                connection.tier = update.tier;
202            } else {
203                connections.push(Connection::new(update.target, update.tier));
204            }
205        }
206        let replacement = Self::new(
207            self.title.clone(),
208            self.navigation_hint.clone(),
209            self.narrative.clone(),
210            connections,
211        )?;
212        self.connections = replacement.connections;
213        Ok(())
214    }
215
216    pub fn apply_measurement(
217        &mut self,
218        target: NodeId,
219        useful: bool,
220        importance: MeasurementImportance,
221    ) -> Result<MeasurementOutcome> {
222        let Some(index) = self
223            .connections
224            .iter()
225            .position(|connection| connection.target == target)
226        else {
227            return Ok(MeasurementOutcome::Absent);
228        };
229        let value = if useful { 1.0 } else { 0.0 };
230        let mass = match importance {
231            MeasurementImportance::Critical => 3.0,
232            MeasurementImportance::NonCritical => 1.0,
233        };
234        match self.connections[index].weight.update(value, mass)? {
235            Some(weight) => {
236                self.connections[index].weight = weight;
237                Ok(MeasurementOutcome::Updated)
238            }
239            None => {
240                self.connections.remove(index);
241                Ok(MeasurementOutcome::Pruned)
242            }
243        }
244    }
245
246    pub fn encode(&self) -> Result<Vec<u8>> {
247        self.validate()?;
248        encode(self)
249    }
250
251    pub fn decode(bytes: &[u8]) -> Result<Self> {
252        let value: Self = decode(bytes)?;
253        value.validate()?;
254        Ok(value)
255    }
256}
257
258#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
259pub enum KmapAction {
260    CreateNode {
261        title: String,
262        navigation_hint: String,
263        narrative: String,
264        connections: Vec<ConnectionSpec>,
265    },
266    UpdateNode {
267        node_id: NodeId,
268        title: Option<String>,
269        navigation_hint: Option<String>,
270        narrative: Option<String>,
271        connection_updates: Vec<ConnectionSpec>,
272    },
273    ApplyMeasurements {
274        measurements: Vec<ConnectionMeasurement>,
275    },
276}
277
278impl KmapAction {
279    pub fn create_node(
280        title: impl Into<String>,
281        navigation_hint: impl Into<String>,
282        narrative: impl Into<String>,
283        connections: Vec<ConnectionSpec>,
284    ) -> Result<Self> {
285        let value = Self::CreateNode {
286            title: title.into(),
287            navigation_hint: navigation_hint.into(),
288            narrative: narrative.into(),
289            connections,
290        };
291        value.validate()?;
292        Ok(value)
293    }
294
295    pub fn update_node(
296        node_id: NodeId,
297        title: Option<String>,
298        navigation_hint: Option<String>,
299        narrative: Option<String>,
300        connection_updates: Vec<ConnectionSpec>,
301    ) -> Result<Self> {
302        let value = Self::UpdateNode {
303            node_id,
304            title,
305            navigation_hint,
306            narrative,
307            connection_updates,
308        };
309        value.validate()?;
310        Ok(value)
311    }
312
313    pub fn apply_measurements(measurements: Vec<ConnectionMeasurement>) -> Result<Self> {
314        let value = Self::ApplyMeasurements { measurements };
315        value.validate()?;
316        Ok(value)
317    }
318
319    pub fn validate(&self) -> Result<()> {
320        match self {
321            Self::CreateNode { connections, .. } => validate_specs(connections),
322            Self::UpdateNode {
323                connection_updates, ..
324            } => ensure_unique(connection_updates.iter().map(|update| update.target)),
325            Self::ApplyMeasurements { .. } => Ok(()),
326        }
327    }
328
329    pub fn encode(&self) -> Result<Vec<u8>> {
330        self.validate()?;
331        encode(self)
332    }
333
334    pub fn decode(bytes: &[u8]) -> Result<Self> {
335        let value: Self = decode(bytes)?;
336        value.validate()?;
337        Ok(value)
338    }
339}
340
341fn validate_value_mass(value: f64, mass: f64) -> Result<()> {
342    if !value.is_finite() || !(0.0..=1.0).contains(&value) {
343        return Err(error("value must be finite and in [0, 1]"));
344    }
345    if !mass.is_finite() || mass <= 0.0 {
346        return Err(error("mass must be finite and positive"));
347    }
348    Ok(())
349}
350
351fn validate_specs(specs: &[ConnectionSpec]) -> Result<()> {
352    ensure_unique(specs.iter().map(|spec| spec.target))?;
353    validate_navigation(specs.iter().map(|spec| spec.tier))
354}
355
356fn ensure_unique(targets: impl IntoIterator<Item = NodeId>) -> Result<()> {
357    let mut seen = HashSet::new();
358    if targets.into_iter().all(|target| seen.insert(target)) {
359        Ok(())
360    } else {
361        Err(error("duplicate connection target"))
362    }
363}
364
365fn validate_navigation(tiers: impl IntoIterator<Item = ConnectionTier>) -> Result<()> {
366    if tiers
367        .into_iter()
368        .filter(|tier| *tier == ConnectionTier::Navigation)
369        .count()
370        > MAX_NAVIGATION_CONNECTIONS
371    {
372        Err(error("too many Navigation connections"))
373    } else {
374        Ok(())
375    }
376}
377
378fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>> {
379    let mut bytes = vec![FORMAT_VERSION];
380    bytes.extend(postcard::to_stdvec(value).map_err(|_| error("wire encoding failed"))?);
381    Ok(bytes)
382}
383
384fn decode<T: for<'a> Deserialize<'a>>(bytes: &[u8]) -> Result<T> {
385    let Some((&version, payload)) = bytes.split_first() else {
386        return Err(error("empty wire value"));
387    };
388    if version != FORMAT_VERSION {
389        return Err(error("unsupported wire version"));
390    }
391    let (value, trailing) =
392        postcard::take_from_bytes(payload).map_err(|_| error("malformed wire payload"))?;
393    if trailing.is_empty() {
394        Ok(value)
395    } else {
396        Err(error("trailing wire bytes"))
397    }
398}