1use crate::{KhiveRuntime, NamespaceToken, RuntimeResult, VerbRegistry};
2use khive_storage::{Edge, Entity, Note};
3use sha2::{Digest, Sha256};
4use std::collections::{HashMap, HashSet};
5use uuid::Uuid;
6
7#[derive(Clone, Debug)]
9pub struct InspectedLintEdge {
10 pub edge: Edge,
11 pub source_live: bool,
12 pub target_live: bool,
13 pub source_soft_deleted: bool,
14 pub target_soft_deleted: bool,
15 pub contract_valid: Option<bool>,
16}
17pub struct LintCapture {
19 pub(super) token: NamespaceToken,
20 pub(super) entities: Vec<Entity>,
21 pub(super) notes: Vec<Note>,
22 pub(super) edges: Vec<InspectedLintEdge>,
23 pub(super) owners: Vec<KhiveRuntime>,
24 pub(super) record_owners: HashMap<Uuid, Vec<usize>>,
25 pub(super) all_entities: HashMap<Uuid, Entity>,
26 pub(super) all_notes: HashMap<Uuid, Note>,
27 pub(super) all_edges: HashMap<Uuid, Edge>,
28 pub(super) edge_guards: HashMap<Uuid, khive_storage::SqlStatement>,
29}
30impl LintCapture {
31 pub fn entities(&self) -> &[Entity] {
32 &self.entities
33 }
34 pub fn notes(&self) -> &[Note] {
35 &self.notes
36 }
37 pub fn edges(&self) -> &[InspectedLintEdge] {
38 &self.edges
39 }
40}
41
42const PAGE: u32 = 256;
43fn active() -> RuntimeResult<()> {
44 khive_storage::ensure_request_read_active("knowledge.lint capture").map_err(Into::into)
45}
46fn visible(token: &NamespaceToken, namespace: &str) -> bool {
47 token
48 .visible_namespaces()
49 .iter()
50 .any(|ns| ns.as_str() == namespace)
51}
52fn canonical<T: serde::Serialize>(value: &T) -> RuntimeResult<[u8; 32]> {
55 let bytes = serde_json::to_vec(value)
56 .map_err(|error| crate::RuntimeError::Internal(error.to_string()))?;
57 let mut digest = [0u8; 32];
58 digest.copy_from_slice(&Sha256::digest(bytes));
59 Ok(digest)
60}
61fn register<T: serde::Serialize>(
62 identities: &mut HashMap<Uuid, (&'static str, [u8; 32])>,
63 owners: &mut HashMap<Uuid, Vec<usize>>,
64 id: Uuid,
65 substrate: &'static str,
66 value: &T,
67 owner: usize,
68) -> RuntimeResult<bool> {
69 let value = canonical(value)?;
70 if let Some((prior_substrate, prior)) = identities.get(&id) {
71 if *prior_substrate != substrate || *prior != value {
72 return Err(crate::RuntimeError::InvalidInput(format!(
73 "contradictory visible graph records for {id}"
74 )));
75 }
76 let ids = owners.entry(id).or_default();
77 if !ids.contains(&owner) {
78 ids.push(owner);
79 }
80 return Ok(false);
81 }
82 identities.insert(id, (substrate, value));
83 owners.insert(id, vec![owner]);
84 Ok(true)
85}
86impl VerbRegistry {
87 pub async fn capture_graph_lint(
90 &self,
91 runtime: &KhiveRuntime,
92 token: &NamespaceToken,
93 ) -> RuntimeResult<LintCapture> {
94 khive_storage::await_request_read_phase(
95 "knowledge.lint capture",
96 self.capture_graph_lint_inner(runtime, token),
97 )
98 .await?
99 }
100 async fn capture_graph_lint_inner(
101 &self,
102 runtime: &KhiveRuntime,
103 token: &NamespaceToken,
104 ) -> RuntimeResult<LintCapture> {
105 let registered = self.graph_lint_runtimes(runtime);
106 let backend_aliases: HashSet<_> = registered
107 .iter()
108 .map(|owner| owner.backend_id().as_str().to_owned())
109 .collect();
110 let mut owners: Vec<KhiveRuntime> = Vec::new();
111 for owner in registered {
112 if !owners
113 .iter()
114 .any(|existing| existing.shares_backend_storage_with(&owner))
115 {
116 owners.push(owner);
117 }
118 }
119 let rules = self.all_edge_rules();
120 let mut capture = LintCapture {
121 token: token.clone(),
122 entities: Vec::new(),
123 notes: Vec::new(),
124 edges: Vec::new(),
125 owners,
126 record_owners: HashMap::new(),
127 all_entities: HashMap::new(),
128 all_notes: HashMap::new(),
129 all_edges: HashMap::new(),
130 edge_guards: HashMap::new(),
131 };
132 let mut identities = HashMap::new();
133 let mut all_edges = HashMap::new();
134 let mut hidden = HashSet::new();
135 let mailbox = crate::MailboxView {
136 actor_id: token.actor().id.clone(),
137 delegated: false,
138 };
139 for (owner, runtime) in capture.owners.iter().enumerate() {
140 let mut after = None;
141 loop {
142 active()?;
143 let filter = khive_storage::EntityFilter {
144 tombstones: khive_storage::entity::EntityTombstones::All,
145 ..Default::default()
146 };
147 let (entities, next) = runtime
148 .list_entities_after_filtered(token, filter, after, PAGE)
149 .await?;
150 for entity in entities {
151 if register(
152 &mut identities,
153 &mut capture.record_owners,
154 entity.id,
155 "entity",
156 &entity,
157 owner,
158 )? {
159 if entity.deleted_at.is_none() {
160 capture.entities.push(entity.clone());
161 }
162 capture.all_entities.insert(entity.id, entity);
163 }
164 }
165 match next {
166 None => break,
167 Some(next) if Some(next) == after => {
168 return Err(crate::RuntimeError::Internal(
169 "entity lint cursor did not advance".into(),
170 ))
171 }
172 Some(next) => after = Some(next),
173 }
174 }
175 let store = runtime.notes(token)?;
176 let filter = khive_storage::note::NoteFilter {
177 namespaces: token
178 .visible_namespaces()
179 .iter()
180 .map(|ns| ns.as_str().to_owned())
181 .collect(),
182 include_deleted: true,
183 ..Default::default()
184 };
185 let mut after = None;
186 loop {
187 active()?;
188 let page = store
189 .query_notes_filtered_after(token.namespace().as_str(), &filter, after, PAGE)
190 .await?;
191 for note in page.items {
192 if !mailbox.permits_message_note(token, ¬e) {
193 hidden.insert(note.id);
194 continue;
195 }
196 if note.salience.is_some_and(|v| !v.is_finite())
197 || note.decay_factor.is_some_and(|v| !v.is_finite())
198 {
199 return Err(crate::RuntimeError::InvalidInput(
200 "nonfinite visible note value".into(),
201 ));
202 }
203 if register(
204 &mut identities,
205 &mut capture.record_owners,
206 note.id,
207 "note",
208 ¬e,
209 owner,
210 )? {
211 if note.deleted_at.is_none() {
212 capture.notes.push(note.clone());
213 }
214 capture.all_notes.insert(note.id, note);
215 }
216 }
217 match page.next_after {
218 None => break,
219 Some(next) if Some(next) == after => {
220 return Err(crate::RuntimeError::Internal(
221 "note lint cursor did not advance".into(),
222 ))
223 }
224 Some(next) => after = Some(next),
225 }
226 }
227 for ns in token.visible_namespaces() {
228 let mut after: Option<String> = None;
229 loop {
230 active()?;
231 let rows = runtime
232 .sql()
233 .reader()
234 .await?
235 .query_all(khive_db::graph_lint::edge_page(
236 ns.as_str(),
237 after.as_deref(),
238 PAGE,
239 ))
240 .await?;
241 if rows.is_empty() {
242 break;
243 }
244 for row in rows {
245 let edge = khive_db::graph_lint::decode_edge(&row)?;
246 let id = Uuid::from(edge.id);
247 if register(
248 &mut identities,
249 &mut capture.record_owners,
250 id,
251 "edge",
252 &edge,
253 owner,
254 )? {
255 capture
256 .edge_guards
257 .insert(id, khive_db::graph_lint::edge_witness(&row)?);
258 all_edges.insert(id, edge);
259 }
260 after = Some(
261 row.text("id")
262 .map_err(|error| crate::RuntimeError::Internal(error.to_string()))?
263 .to_owned(),
264 );
265 }
266 }
267 }
268 }
269 let endpoint_ids: HashSet<_> = all_edges
272 .values()
273 .flat_map(|edge| [edge.source_id, edge.target_id])
274 .collect();
275 let mut events = HashMap::new();
276 for id in endpoint_ids {
277 active()?;
278 let listed = identities.contains_key(&id);
282 for (owner, runtime) in capture.owners.iter().enumerate() {
283 if !listed {
284 if let Some(entity) = runtime.get_entity_including_deleted(token, id).await? {
285 if !visible(token, &entity.namespace) {
286 hidden.insert(id);
287 continue;
288 }
289 register(
290 &mut identities,
291 &mut capture.record_owners,
292 id,
293 "entity",
294 &entity,
295 owner,
296 )?;
297 capture.all_entities.entry(id).or_insert(entity);
298 }
299 if let Some(note) = runtime.get_note_including_deleted(token, id).await? {
300 if !visible(token, ¬e.namespace)
301 || !mailbox.permits_message_note(token, ¬e)
302 {
303 hidden.insert(id);
304 continue;
305 }
306 register(
307 &mut identities,
308 &mut capture.record_owners,
309 id,
310 "note",
311 ¬e,
312 owner,
313 )?;
314 capture.all_notes.entry(id).or_insert(note);
315 }
316 if let Some(edge) = runtime.get_edge_including_deleted(token, id).await? {
317 if !visible(token, &edge.namespace) {
318 hidden.insert(id);
319 continue;
320 }
321 register(
322 &mut identities,
323 &mut capture.record_owners,
324 id,
325 "edge",
326 &edge,
327 owner,
328 )?;
329 }
330 }
331 if let Some(event) = runtime.events(token)?.get_event(id).await? {
332 if !visible(token, &event.namespace) {
333 hidden.insert(id);
334 continue;
335 }
336 register(
337 &mut identities,
338 &mut capture.record_owners,
339 id,
340 "event",
341 &event,
342 owner,
343 )?;
344 events.insert(id, event);
345 }
346 }
347 }
348 loop {
350 let before = hidden.len();
351 for (id, edge) in &all_edges {
352 if hidden.contains(&edge.source_id) || hidden.contains(&edge.target_id) {
353 hidden.insert(*id);
354 }
355 }
356 if before == hidden.len() {
357 break;
358 }
359 }
360 for edge in all_edges.values().filter(|edge| edge.deleted_at.is_none()) {
361 let id = Uuid::from(edge.id);
362 if hidden.contains(&id) {
363 continue;
364 }
365 if edge
366 .target_backend
367 .as_ref()
368 .is_some_and(|name| !backend_aliases.contains(name))
369 {
370 return Err(crate::RuntimeError::Unconfigured(
371 "lint edge endpoint backend is unavailable".into(),
372 ));
373 }
374 let resolved = |id| {
375 capture
376 .all_entities
377 .get(&id)
378 .cloned()
379 .map(crate::Resolved::Entity)
380 .or_else(|| {
381 capture
382 .all_notes
383 .get(&id)
384 .cloned()
385 .map(crate::Resolved::Note)
386 })
387 .or_else(|| events.get(&id).cloned().map(crate::Resolved::Event))
388 };
389 let live = |id| {
390 capture
391 .all_entities
392 .get(&id)
393 .is_some_and(|record| record.deleted_at.is_none())
394 || capture
395 .all_notes
396 .get(&id)
397 .is_some_and(|record| record.deleted_at.is_none())
398 || all_edges
399 .get(&id)
400 .is_some_and(|record| record.deleted_at.is_none())
401 || events.contains_key(&id)
402 };
403 let deleted = |id| {
404 capture
405 .all_entities
406 .get(&id)
407 .is_some_and(|record| record.deleted_at.is_some())
408 || capture
409 .all_notes
410 .get(&id)
411 .is_some_and(|record| record.deleted_at.is_some())
412 || all_edges
413 .get(&id)
414 .is_some_and(|record| record.deleted_at.is_some())
415 };
416 let src = resolved(edge.source_id);
417 let tgt = resolved(edge.target_id);
418 let validator = &capture.owners[capture.record_owners[&id][0]];
419 let contract_valid = if edge.relation == khive_storage::EdgeRelation::Annotates
420 && all_edges.contains_key(&edge.target_id)
421 {
422 Some(
426 validator
427 .validate_link_endpoints_by_resolved_with_rules(
428 edge.source_id,
429 edge.target_id,
430 edge.relation,
431 src.as_ref(),
432 src.as_ref(),
433 &rules,
434 )
435 .is_ok(),
436 )
437 } else if (all_edges.contains_key(&edge.source_id)
438 && (tgt.is_some() || all_edges.contains_key(&edge.target_id)))
439 || (all_edges.contains_key(&edge.target_id) && src.is_some())
440 {
441 Some(false)
444 } else if src.is_some() && tgt.is_some() {
445 Some(
446 validator
447 .validate_link_endpoints_by_resolved_with_rules(
448 edge.source_id,
449 edge.target_id,
450 edge.relation,
451 src.as_ref(),
452 tgt.as_ref(),
453 &rules,
454 )
455 .is_ok(),
456 )
457 } else {
458 None
459 };
460 capture.edges.push(InspectedLintEdge {
461 edge: edge.clone(),
462 source_live: live(edge.source_id),
463 target_live: live(edge.target_id),
464 source_soft_deleted: deleted(edge.source_id),
465 target_soft_deleted: deleted(edge.target_id),
466 contract_valid,
467 });
468 }
469 capture.entities.sort_by_key(|record| record.id);
470 capture.notes.sort_by_key(|record| record.id);
471 capture
472 .edges
473 .sort_by_key(|record| Uuid::from(record.edge.id));
474 capture.all_edges = all_edges;
475 active()?;
476 Ok(capture)
477 }
478}