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}