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}