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}