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 = 1;
6pub const EWA_HALF_LIFE_MASS: f64 = 30.0;
7pub const PRUNE_EPSILON: f64 = 0.01;
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, Debug, Deserialize, Eq, PartialEq, Serialize)]
106pub enum ConnectionChange {
107    Set(ConnectionSpec),
108    Remove(NodeId),
109}
110
111impl ConnectionChange {
112    fn target(&self) -> NodeId {
113        match self {
114            Self::Set(spec) => spec.target,
115            Self::Remove(target) => *target,
116        }
117    }
118}
119
120#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
121pub struct ConnectionMeasurement {
122    pub source: NodeId,
123    pub target: NodeId,
124    pub value: f64,
125    pub mass: f64,
126}
127
128impl ConnectionMeasurement {
129    pub fn new(source: NodeId, target: NodeId, value: f64, mass: f64) -> Result<Self> {
130        validate_value_mass(value, mass)?;
131        Ok(Self {
132            source,
133            target,
134            value,
135            mass,
136        })
137    }
138}
139
140#[derive(Clone, Copy, Debug, Eq, PartialEq)]
141pub enum MeasurementOutcome {
142    Updated,
143    Pruned,
144    Absent,
145}
146
147#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
148pub struct Node {
149    pub title: String,
150    pub navigation_hint: String,
151    pub narrative: String,
152    pub connections: Vec<Connection>,
153}
154
155impl Node {
156    pub fn new(
157        title: impl Into<String>,
158        navigation_hint: impl Into<String>,
159        narrative: impl Into<String>,
160        connections: Vec<Connection>,
161    ) -> Result<Self> {
162        let node = Self {
163            title: title.into(),
164            navigation_hint: navigation_hint.into(),
165            narrative: narrative.into(),
166            connections,
167        };
168        node.validate()?;
169        Ok(node)
170    }
171
172    pub fn from_specs(
173        title: impl Into<String>,
174        navigation_hint: impl Into<String>,
175        narrative: impl Into<String>,
176        specs: Vec<ConnectionSpec>,
177    ) -> Result<Self> {
178        validate_specs(&specs)?;
179        Self::new(
180            title,
181            navigation_hint,
182            narrative,
183            specs
184                .into_iter()
185                .map(|spec| Connection::new(spec.target, spec.tier))
186                .collect(),
187        )
188    }
189
190    pub fn validate(&self) -> Result<()> {
191        ensure_unique(self.connections.iter().map(|connection| connection.target))?;
192        for connection in &self.connections {
193            validate_value_mass(connection.weight.value, connection.weight.mass)?;
194        }
195        validate_navigation(self.connections.iter().map(|connection| connection.tier))
196    }
197
198    pub fn apply_connection_changes(&mut self, changes: &[ConnectionChange]) -> Result<()> {
199        ensure_unique(changes.iter().map(ConnectionChange::target))?;
200        let mut connections = self.connections.clone();
201        for change in changes {
202            match change {
203                ConnectionChange::Set(spec) => {
204                    if let Some(connection) = connections
205                        .iter_mut()
206                        .find(|connection| connection.target == spec.target)
207                    {
208                        connection.tier = spec.tier;
209                    } else {
210                        connections.push(Connection::new(spec.target, spec.tier));
211                    }
212                }
213                ConnectionChange::Remove(target) => {
214                    connections.retain(|connection| connection.target != *target);
215                }
216            }
217        }
218        let replacement = Self::new(
219            self.title.clone(),
220            self.navigation_hint.clone(),
221            self.narrative.clone(),
222            connections,
223        )?;
224        self.connections = replacement.connections;
225        Ok(())
226    }
227
228    pub fn apply_measurement(
229        &mut self,
230        target: NodeId,
231        value: f64,
232        mass: f64,
233    ) -> Result<MeasurementOutcome> {
234        validate_value_mass(value, mass)?;
235        let Some(index) = self
236            .connections
237            .iter()
238            .position(|connection| connection.target == target)
239        else {
240            return Ok(MeasurementOutcome::Absent);
241        };
242        match self.connections[index].weight.update(value, mass)? {
243            Some(weight) => {
244                self.connections[index].weight = weight;
245                Ok(MeasurementOutcome::Updated)
246            }
247            None => {
248                self.connections.remove(index);
249                Ok(MeasurementOutcome::Pruned)
250            }
251        }
252    }
253
254    pub fn encode(&self) -> Result<Vec<u8>> {
255        self.validate()?;
256        encode(self)
257    }
258
259    pub fn decode(bytes: &[u8]) -> Result<Self> {
260        let value: Self = decode(bytes)?;
261        value.validate()?;
262        Ok(value)
263    }
264}
265
266#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
267pub enum KmapAction {
268    CreateNode {
269        title: String,
270        navigation_hint: String,
271        narrative: String,
272        connections: Vec<ConnectionSpec>,
273    },
274    UpdateNode {
275        node_id: NodeId,
276        title: Option<String>,
277        navigation_hint: Option<String>,
278        narrative: Option<String>,
279        connection_changes: Vec<ConnectionChange>,
280    },
281    ApplyMeasurements {
282        measurements: Vec<ConnectionMeasurement>,
283    },
284}
285
286impl KmapAction {
287    pub fn create_node(
288        title: impl Into<String>,
289        navigation_hint: impl Into<String>,
290        narrative: impl Into<String>,
291        connections: Vec<ConnectionSpec>,
292    ) -> Result<Self> {
293        let value = Self::CreateNode {
294            title: title.into(),
295            navigation_hint: navigation_hint.into(),
296            narrative: narrative.into(),
297            connections,
298        };
299        value.validate()?;
300        Ok(value)
301    }
302
303    pub fn update_node(
304        node_id: NodeId,
305        title: Option<String>,
306        navigation_hint: Option<String>,
307        narrative: Option<String>,
308        connection_changes: Vec<ConnectionChange>,
309    ) -> Result<Self> {
310        let value = Self::UpdateNode {
311            node_id,
312            title,
313            navigation_hint,
314            narrative,
315            connection_changes,
316        };
317        value.validate()?;
318        Ok(value)
319    }
320
321    pub fn apply_measurements(measurements: Vec<ConnectionMeasurement>) -> Result<Self> {
322        let value = Self::ApplyMeasurements { measurements };
323        value.validate()?;
324        Ok(value)
325    }
326
327    pub fn validate(&self) -> Result<()> {
328        match self {
329            Self::CreateNode { connections, .. } => validate_specs(connections),
330            Self::UpdateNode {
331                connection_changes, ..
332            } => ensure_unique(connection_changes.iter().map(ConnectionChange::target)),
333            Self::ApplyMeasurements { measurements } => {
334                measurements.iter().try_for_each(|measurement| {
335                    validate_value_mass(measurement.value, measurement.mass)
336                })
337            }
338        }
339    }
340
341    pub fn encode(&self) -> Result<Vec<u8>> {
342        self.validate()?;
343        encode(self)
344    }
345
346    pub fn decode(bytes: &[u8]) -> Result<Self> {
347        let value: Self = decode(bytes)?;
348        value.validate()?;
349        Ok(value)
350    }
351}
352
353fn validate_value_mass(value: f64, mass: f64) -> Result<()> {
354    if !value.is_finite() || !(0.0..=1.0).contains(&value) {
355        return Err(error("value must be finite and in [0, 1]"));
356    }
357    if !mass.is_finite() || mass <= 0.0 {
358        return Err(error("mass must be finite and positive"));
359    }
360    Ok(())
361}
362
363fn validate_specs(specs: &[ConnectionSpec]) -> Result<()> {
364    ensure_unique(specs.iter().map(|spec| spec.target))?;
365    validate_navigation(specs.iter().map(|spec| spec.tier))
366}
367
368fn ensure_unique(targets: impl IntoIterator<Item = NodeId>) -> Result<()> {
369    let mut seen = HashSet::new();
370    if targets.into_iter().all(|target| seen.insert(target)) {
371        Ok(())
372    } else {
373        Err(error("duplicate connection target"))
374    }
375}
376
377fn validate_navigation(tiers: impl IntoIterator<Item = ConnectionTier>) -> Result<()> {
378    if tiers
379        .into_iter()
380        .filter(|tier| *tier == ConnectionTier::Navigation)
381        .count()
382        > MAX_NAVIGATION_CONNECTIONS
383    {
384        Err(error("too many Navigation connections"))
385    } else {
386        Ok(())
387    }
388}
389
390fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>> {
391    let mut bytes = vec![FORMAT_VERSION];
392    bytes.extend(postcard::to_stdvec(value).map_err(|_| error("wire encoding failed"))?);
393    Ok(bytes)
394}
395
396fn decode<T: for<'a> Deserialize<'a>>(bytes: &[u8]) -> Result<T> {
397    let Some((&version, payload)) = bytes.split_first() else {
398        return Err(error("empty wire value"));
399    };
400    if version != FORMAT_VERSION {
401        return Err(error("unsupported wire version"));
402    }
403    let (value, trailing) =
404        postcard::take_from_bytes(payload).map_err(|_| error("malformed wire payload"))?;
405    if trailing.is_empty() {
406        Ok(value)
407    } else {
408        Err(error("trailing wire bytes"))
409    }
410}