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}