Skip to main content

kcode_k1_access_kmap/
lib.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use kcode_k1_access::{AccessCheck, K1Access, SubsystemId, Target};
5use kcode_k1_kmap::{
6    ConnectionMeasurement, ConnectionSpec as RawConnectionSpec, K1Kmap,
7    LoadedNode as RawLoadedNode, NodeId,
8};
9
10pub use kcode_k1_access::{AccessContext, AccessId, AccessPolicy, AccessRevision};
11pub use kcode_k1_kmap::{ConnectionTier, MeasurementImportance, OpenMode, TxId, Weight};
12
13const SUBSYSTEM_NAME: &str = "k1-kmap";
14const NODE_UNAVAILABLE: &str = "node unavailable";
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct ConnectionSpec {
18    pub target: AccessId,
19    pub tier: ConnectionTier,
20}
21
22#[derive(Clone, Debug, PartialEq)]
23pub struct Connection {
24    pub target: AccessId,
25    pub tier: ConnectionTier,
26    pub weight: Weight,
27}
28
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub struct Measurement {
31    pub source: AccessId,
32    pub target: AccessId,
33    pub useful: bool,
34    pub importance: MeasurementImportance,
35}
36
37#[derive(Clone, Debug, PartialEq)]
38pub struct Node {
39    pub access_id: AccessId,
40    pub title: String,
41    pub navigation_hint: String,
42    pub narrative: String,
43    pub connections: Vec<Connection>,
44}
45
46#[derive(Clone, Debug, PartialEq)]
47pub struct LoadedNode {
48    pub access_id: AccessId,
49    pub source: Option<AccessId>,
50    pub title: String,
51    pub navigation_hint: String,
52    pub narrative: Option<String>,
53}
54
55#[derive(Clone, Debug, PartialEq)]
56pub struct OpenResult {
57    pub nodes: Vec<LoadedNode>,
58    pub automatic_attention_spent: f64,
59}
60
61pub struct K1AccessKmap {
62    access: Arc<K1Access>,
63    kmap: Arc<K1Kmap>,
64}
65
66impl K1AccessKmap {
67    pub fn open(access: Arc<K1Access>, kmap: Arc<K1Kmap>) -> Result<Self, String> {
68        SubsystemId::from_str(SUBSYSTEM_NAME)
69            .map_err(|error| with_context("validate kmap subsystem", error))?;
70        Ok(Self { access, kmap })
71    }
72
73    #[allow(clippy::too_many_arguments)]
74    pub fn create_node(
75        &self,
76        context: &AccessContext,
77        policy: AccessPolicy,
78        title: String,
79        navigation_hint: String,
80        narrative: String,
81        connections: Vec<ConnectionSpec>,
82    ) -> Result<AccessRevision, String> {
83        if context.filter().contains(policy.authority()) {
84            return Err(unavailable());
85        }
86        let access_ids = connections
87            .iter()
88            .map(|connection| connection.target)
89            .collect::<Vec<_>>();
90        let require_edit = vec![false; access_ids.len()];
91        let raw_ids = self.authorize_nodes(context, &access_ids, &require_edit)?;
92        let raw_connections = connections
93            .into_iter()
94            .zip(raw_ids)
95            .map(|(connection, target)| RawConnectionSpec {
96                target,
97                tier: connection.tier,
98            })
99            .collect();
100        let revision = self
101            .kmap
102            .create_node(title, navigation_hint, narrative, raw_connections)
103            .map_err(|error| with_context("create kmap node", error))?;
104        self.access
105            .create(context, target_from_node(NodeId::from(revision)), policy)
106            .map_err(|error| {
107                with_context(
108                    "create node access after raw creation (possible inaccessible orphan)",
109                    error,
110                )
111            })
112    }
113
114    pub fn get_node(&self, context: &AccessContext, node: AccessId) -> Result<Node, String> {
115        let raw_id = self
116            .authorize_nodes(context, &[node], &[false])?
117            .into_iter()
118            .next()
119            .ok_or_else(inconsistent)?;
120        let raw = self
121            .kmap
122            .get_node(raw_id)
123            .map_err(|error| with_context("get kmap node", error))?
124            .ok_or_else(unavailable)?;
125        let targets = raw
126            .connections
127            .iter()
128            .map(|connection| target_from_node(connection.target))
129            .collect::<Vec<_>>();
130        let resolved = self.resolve_targets(context, &targets)?;
131        let connections = raw
132            .connections
133            .into_iter()
134            .zip(resolved)
135            .filter_map(|(connection, access_id)| {
136                access_id.map(|target| Connection {
137                    target,
138                    tier: connection.tier,
139                    weight: connection.weight,
140                })
141            })
142            .collect();
143        Ok(Node {
144            access_id: node,
145            title: raw.title,
146            navigation_hint: raw.navigation_hint,
147            narrative: raw.narrative,
148            connections,
149        })
150    }
151
152    pub fn open_node(
153        &self,
154        context: &AccessContext,
155        root: AccessId,
156        budget: f64,
157        temperature: f64,
158        mode: OpenMode,
159    ) -> Result<OpenResult, String> {
160        let raw_root = self
161            .authorize_nodes(context, &[root], &[false])?
162            .into_iter()
163            .next()
164            .ok_or_else(inconsistent)?;
165        let mut visible = HashMap::from([(raw_root, root)]);
166        let raw = self
167            .kmap
168            .open_node(
169                raw_root,
170                budget,
171                temperature,
172                mode,
173                |candidates: &[NodeId]| {
174                    let targets = candidates
175                        .iter()
176                        .copied()
177                        .map(target_from_node)
178                        .collect::<Vec<_>>();
179                    let resolved = self.resolve_targets(context, &targets)?;
180                    let mut allowed = Vec::new();
181                    for (candidate, access_id) in candidates.iter().copied().zip(resolved) {
182                        if let Some(access_id) = access_id {
183                            visible.insert(candidate, access_id);
184                            allowed.push(candidate);
185                        }
186                    }
187                    Ok(allowed)
188                },
189            )
190            .map_err(|error| with_context("open kmap node", error))?;
191        let nodes = raw
192            .nodes
193            .into_iter()
194            .map(|loaded: RawLoadedNode| {
195                let access_id = visible.get(&loaded.node_id).copied().ok_or_else(|| {
196                    with_context(
197                        "open kmap node",
198                        "dependency returned a loaded node without an authorized mapping",
199                    )
200                })?;
201                let source = loaded
202                    .source
203                    .map(|source| {
204                        visible.get(&source).copied().ok_or_else(|| {
205                            with_context(
206                                "open kmap node",
207                                "dependency returned a loaded node source without an authorized mapping",
208                            )
209                        })
210                    })
211                    .transpose()?;
212                Ok(LoadedNode {
213                    access_id,
214                    source,
215                    title: loaded.title,
216                    navigation_hint: loaded.navigation_hint,
217                    narrative: loaded.narrative,
218                })
219            })
220            .collect::<Result<Vec<_>, String>>()?;
221        Ok(OpenResult {
222            nodes,
223            automatic_attention_spent: raw.automatic_attention_spent,
224        })
225    }
226
227    #[allow(clippy::too_many_arguments)]
228    pub fn update_node(
229        &self,
230        context: &AccessContext,
231        node: AccessId,
232        title: Option<String>,
233        navigation_hint: Option<String>,
234        narrative: Option<String>,
235        connection_updates: Vec<ConnectionSpec>,
236    ) -> Result<TxId, String> {
237        let mut access_ids = Vec::with_capacity(connection_updates.len() + 1);
238        access_ids.push(node);
239        access_ids.extend(connection_updates.iter().map(|update| update.target));
240        let mut require_edit = vec![false; access_ids.len()];
241        require_edit[0] = true;
242        let raw_ids = self.authorize_nodes(context, &access_ids, &require_edit)?;
243        let (source, targets) = raw_ids.split_first().ok_or_else(inconsistent)?;
244        let updates = connection_updates
245            .into_iter()
246            .zip(targets.iter().copied())
247            .map(|(update, target)| RawConnectionSpec {
248                target,
249                tier: update.tier,
250            })
251            .collect();
252        self.kmap
253            .update_node(*source, title, navigation_hint, narrative, updates)
254            .map_err(|error| with_context("update kmap node", error))
255    }
256
257    pub fn apply_measurements(
258        &self,
259        context: &AccessContext,
260        measurements: Vec<Measurement>,
261    ) -> Result<TxId, String> {
262        let access_ids = measurements
263            .iter()
264            .flat_map(|measurement| [measurement.source, measurement.target])
265            .collect::<Vec<_>>();
266        let require_edit = measurements
267            .iter()
268            .flat_map(|_| [true, false])
269            .collect::<Vec<_>>();
270        let raw_ids = self.authorize_nodes(context, &access_ids, &require_edit)?;
271        let raw_measurements = measurements
272            .into_iter()
273            .zip(raw_ids.chunks_exact(2))
274            .map(|(measurement, ids)| {
275                ConnectionMeasurement::new(
276                    ids[0],
277                    ids[1],
278                    measurement.useful,
279                    measurement.importance,
280                )
281            })
282            .collect();
283        self.kmap
284            .apply_measurements(raw_measurements)
285            .map_err(|error| with_context("apply kmap measurements", error))
286    }
287
288    fn authorize_nodes(
289        &self,
290        context: &AccessContext,
291        access_ids: &[AccessId],
292        require_edit: &[bool],
293    ) -> Result<Vec<NodeId>, String> {
294        if access_ids.len() != require_edit.len() {
295            return Err(inconsistent());
296        }
297        let checks = self
298            .access
299            .check_many(context, access_ids, subsystem())
300            .map_err(|error| with_context("check kmap access", error))?;
301        if checks.len() != access_ids.len() {
302            return Err(inconsistent());
303        }
304        checks
305            .iter()
306            .zip(require_edit.iter().copied())
307            .map(|(check, edit)| checked_node_id(check, edit))
308            .collect()
309    }
310
311    fn resolve_targets(
312        &self,
313        context: &AccessContext,
314        targets: &[Target],
315    ) -> Result<Vec<Option<AccessId>>, String> {
316        let resolved = self
317            .access
318            .resolve_visible_targets(context, targets, subsystem())
319            .map_err(|error| with_context("resolve visible kmap targets", error))?;
320        if resolved.len() != targets.len() {
321            return Err(inconsistent());
322        }
323        Ok(resolved)
324    }
325}
326
327fn subsystem() -> SubsystemId {
328    SubsystemId::from_str(SUBSYSTEM_NAME).expect("validated subsystem literal")
329}
330
331fn target_from_node(node_id: NodeId) -> Target {
332    Target::new(subsystem(), node_id.0.to_vec())
333}
334
335fn node_id_from_target(target: &Target) -> Result<NodeId, String> {
336    if target.subsystem() != subsystem() {
337        return Err(unavailable());
338    }
339    let bytes = target.object_id().try_into().map_err(|_| unavailable())?;
340    Ok(NodeId(bytes))
341}
342
343fn checked_node_id(check: &AccessCheck, require_edit: bool) -> Result<NodeId, String> {
344    if !check.can_view() || (require_edit && !check.can_edit()) {
345        return Err(unavailable());
346    }
347    node_id_from_target(check.target().ok_or_else(unavailable)?)
348}
349
350fn unavailable() -> String {
351    NODE_UNAVAILABLE.to_owned()
352}
353
354fn inconsistent() -> String {
355    "dependency returned an inconsistent positional response".to_owned()
356}
357
358fn with_context(operation: &str, error: impl std::fmt::Display) -> String {
359    format!("{operation}: {error}")
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn target_conversion_is_exact_and_safe() {
368        let node = NodeId([7; 12]);
369        let target = target_from_node(node);
370        assert_eq!(target.object_id(), &[7; 12]);
371        assert_eq!(node_id_from_target(&target), Ok(node));
372        let malformed = Target::new(subsystem(), vec![7; 11]);
373        assert_eq!(node_id_from_target(&malformed), Err(unavailable()));
374    }
375}