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