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