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(|_| unavailable())?;
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(|_| unavailable())?;
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}