kcode-k1-access-kmap 0.2.0

Access-controlled facade for K1 Kmap nodes
Documentation
use std::collections::HashMap;
use std::sync::Arc;

use kcode_k1_access::{AccessCheck, K1Access, SubsystemId, Target};
use kcode_k1_kmap::{
    ConnectionMeasurement, ConnectionSpec as RawConnectionSpec, K1Kmap,
    LoadedNode as RawLoadedNode, NodeId,
};

pub use kcode_k1_access::{AccessContext, AccessId, AccessPolicy, AccessRevision};
pub use kcode_k1_kmap::{ConnectionTier, MeasurementImportance, OpenMode, TxId, Weight};

const SUBSYSTEM_NAME: &str = "k1-kmap";
const NODE_UNAVAILABLE: &str = "node unavailable";

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ConnectionSpec {
    pub target: AccessId,
    pub tier: ConnectionTier,
}

#[derive(Clone, Debug, PartialEq)]
pub struct Connection {
    pub target: AccessId,
    pub tier: ConnectionTier,
    pub weight: Weight,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Measurement {
    pub source: AccessId,
    pub target: AccessId,
    pub useful: bool,
    pub importance: MeasurementImportance,
}

#[derive(Clone, Debug, PartialEq)]
pub struct Node {
    pub access_id: AccessId,
    pub title: String,
    pub navigation_hint: String,
    pub narrative: String,
    pub connections: Vec<Connection>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct LoadedNode {
    pub access_id: AccessId,
    pub title: String,
    pub navigation_hint: String,
    pub narrative: Option<String>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct OpenResult {
    pub nodes: Vec<LoadedNode>,
    pub automatic_attention_spent: f64,
}

pub struct K1AccessKmap {
    access: Arc<K1Access>,
    kmap: Arc<K1Kmap>,
}

impl K1AccessKmap {
    pub fn open(access: Arc<K1Access>, kmap: Arc<K1Kmap>) -> Result<Self, String> {
        SubsystemId::from_str(SUBSYSTEM_NAME)
            .map_err(|error| with_context("validate kmap subsystem", error))?;
        Ok(Self { access, kmap })
    }

    #[allow(clippy::too_many_arguments)]
    pub fn create_node(
        &self,
        context: &AccessContext,
        policy: AccessPolicy,
        title: String,
        navigation_hint: String,
        narrative: String,
        connections: Vec<ConnectionSpec>,
    ) -> Result<AccessRevision, String> {
        if context.filter().contains(policy.authority()) {
            return Err(unavailable());
        }
        let access_ids = connections
            .iter()
            .map(|connection| connection.target)
            .collect::<Vec<_>>();
        let require_edit = vec![false; access_ids.len()];
        let raw_ids = self.authorize_nodes(context, &access_ids, &require_edit)?;
        let raw_connections = connections
            .into_iter()
            .zip(raw_ids)
            .map(|(connection, target)| RawConnectionSpec {
                target,
                tier: connection.tier,
            })
            .collect();
        let revision = self
            .kmap
            .create_node(title, navigation_hint, narrative, raw_connections)
            .map_err(|error| with_context("create kmap node", error))?;
        self.access
            .create(context, target_from_node(NodeId::from(revision)), policy)
            .map_err(|error| {
                with_context(
                    "create node access after raw creation (possible inaccessible orphan)",
                    error,
                )
            })
    }

    pub fn get_node(&self, context: &AccessContext, node: AccessId) -> Result<Node, String> {
        let raw_id = self
            .authorize_nodes(context, &[node], &[false])?
            .into_iter()
            .next()
            .ok_or_else(inconsistent)?;
        let raw = self
            .kmap
            .get_node(raw_id)
            .map_err(|error| with_context("get kmap node", error))?
            .ok_or_else(unavailable)?;
        let targets = raw
            .connections
            .iter()
            .map(|connection| target_from_node(connection.target))
            .collect::<Vec<_>>();
        let resolved = self.resolve_targets(context, &targets)?;
        let connections = raw
            .connections
            .into_iter()
            .zip(resolved)
            .filter_map(|(connection, access_id)| {
                access_id.map(|target| Connection {
                    target,
                    tier: connection.tier,
                    weight: connection.weight,
                })
            })
            .collect();
        Ok(Node {
            access_id: node,
            title: raw.title,
            navigation_hint: raw.navigation_hint,
            narrative: raw.narrative,
            connections,
        })
    }

    pub fn open_node(
        &self,
        context: &AccessContext,
        root: AccessId,
        budget: f64,
        temperature: f64,
        mode: OpenMode,
    ) -> Result<OpenResult, String> {
        let raw_root = self
            .authorize_nodes(context, &[root], &[false])?
            .into_iter()
            .next()
            .ok_or_else(inconsistent)?;
        let mut visible = HashMap::from([(raw_root, root)]);
        let raw = self
            .kmap
            .open_node(
                raw_root,
                budget,
                temperature,
                mode,
                |candidates: &[NodeId]| {
                    let targets = candidates
                        .iter()
                        .copied()
                        .map(target_from_node)
                        .collect::<Vec<_>>();
                    let resolved = self.resolve_targets(context, &targets)?;
                    let mut allowed = Vec::new();
                    for (candidate, access_id) in candidates.iter().copied().zip(resolved) {
                        if let Some(access_id) = access_id {
                            visible.insert(candidate, access_id);
                            allowed.push(candidate);
                        }
                    }
                    Ok(allowed)
                },
            )
            .map_err(|error| with_context("open kmap node", error))?;
        let nodes = raw
            .nodes
            .into_iter()
            .map(|loaded: RawLoadedNode| {
                let access_id = visible.get(&loaded.node_id).copied().ok_or_else(|| {
                    with_context(
                        "open kmap node",
                        "dependency returned a loaded node without an authorized mapping",
                    )
                })?;
                Ok(LoadedNode {
                    access_id,
                    title: loaded.title,
                    navigation_hint: loaded.navigation_hint,
                    narrative: loaded.narrative,
                })
            })
            .collect::<Result<Vec<_>, String>>()?;
        Ok(OpenResult {
            nodes,
            automatic_attention_spent: raw.automatic_attention_spent,
        })
    }

    #[allow(clippy::too_many_arguments)]
    pub fn update_node(
        &self,
        context: &AccessContext,
        node: AccessId,
        title: Option<String>,
        navigation_hint: Option<String>,
        narrative: Option<String>,
        connection_updates: Vec<ConnectionSpec>,
    ) -> Result<TxId, String> {
        let mut access_ids = Vec::with_capacity(connection_updates.len() + 1);
        access_ids.push(node);
        access_ids.extend(connection_updates.iter().map(|update| update.target));
        let mut require_edit = vec![false; access_ids.len()];
        require_edit[0] = true;
        let raw_ids = self.authorize_nodes(context, &access_ids, &require_edit)?;
        let (source, targets) = raw_ids.split_first().ok_or_else(inconsistent)?;
        let updates = connection_updates
            .into_iter()
            .zip(targets.iter().copied())
            .map(|(update, target)| RawConnectionSpec {
                target,
                tier: update.tier,
            })
            .collect();
        self.kmap
            .update_node(*source, title, navigation_hint, narrative, updates)
            .map_err(|error| with_context("update kmap node", error))
    }

    pub fn apply_measurements(
        &self,
        context: &AccessContext,
        measurements: Vec<Measurement>,
    ) -> Result<TxId, String> {
        let access_ids = measurements
            .iter()
            .flat_map(|measurement| [measurement.source, measurement.target])
            .collect::<Vec<_>>();
        let require_edit = measurements
            .iter()
            .flat_map(|_| [true, false])
            .collect::<Vec<_>>();
        let raw_ids = self.authorize_nodes(context, &access_ids, &require_edit)?;
        let raw_measurements = measurements
            .into_iter()
            .zip(raw_ids.chunks_exact(2))
            .map(|(measurement, ids)| {
                ConnectionMeasurement::new(
                    ids[0],
                    ids[1],
                    measurement.useful,
                    measurement.importance,
                )
            })
            .collect();
        self.kmap
            .apply_measurements(raw_measurements)
            .map_err(|error| with_context("apply kmap measurements", error))
    }

    fn authorize_nodes(
        &self,
        context: &AccessContext,
        access_ids: &[AccessId],
        require_edit: &[bool],
    ) -> Result<Vec<NodeId>, String> {
        if access_ids.len() != require_edit.len() {
            return Err(inconsistent());
        }
        let checks = self
            .access
            .check_many(context, access_ids, subsystem())
            .map_err(|_| unavailable())?;
        if checks.len() != access_ids.len() {
            return Err(inconsistent());
        }
        checks
            .iter()
            .zip(require_edit.iter().copied())
            .map(|(check, edit)| checked_node_id(check, edit))
            .collect()
    }

    fn resolve_targets(
        &self,
        context: &AccessContext,
        targets: &[Target],
    ) -> Result<Vec<Option<AccessId>>, String> {
        let resolved = self
            .access
            .resolve_visible_targets(context, targets, subsystem())
            .map_err(|_| unavailable())?;
        if resolved.len() != targets.len() {
            return Err(inconsistent());
        }
        Ok(resolved)
    }
}

fn subsystem() -> SubsystemId {
    SubsystemId::from_str(SUBSYSTEM_NAME).expect("validated subsystem literal")
}

fn target_from_node(node_id: NodeId) -> Target {
    Target::new(subsystem(), node_id.0.to_vec())
}

fn node_id_from_target(target: &Target) -> Result<NodeId, String> {
    if target.subsystem() != subsystem() {
        return Err(unavailable());
    }
    let bytes = target.object_id().try_into().map_err(|_| unavailable())?;
    Ok(NodeId(bytes))
}

fn checked_node_id(check: &AccessCheck, require_edit: bool) -> Result<NodeId, String> {
    if !check.can_view() || (require_edit && !check.can_edit()) {
        return Err(unavailable());
    }
    node_id_from_target(check.target().ok_or_else(unavailable)?)
}

fn unavailable() -> String {
    NODE_UNAVAILABLE.to_owned()
}

fn inconsistent() -> String {
    "dependency returned an inconsistent positional response".to_owned()
}

fn with_context(operation: &str, error: impl std::fmt::Display) -> String {
    format!("{operation}: {error}")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn target_conversion_is_exact_and_safe() {
        let node = NodeId([7; 12]);
        let target = target_from_node(node);
        assert_eq!(target.object_id(), &[7; 12]);
        assert_eq!(node_id_from_target(&target), Ok(node));
        let malformed = Target::new(subsystem(), vec![7; 11]);
        assert_eq!(node_id_from_target(&malformed), Err(unavailable()));
    }
}