Skip to main content

weavatrix_memory/extraction/linker/
catalog.rs

1use super::{EntityLinker, scoring::exact_key};
2use crate::{
3    domain::MemoryView,
4    error::{MemoryError, Result},
5    extraction::key::normalized,
6};
7use std::{collections::HashMap, hash::Hash};
8
9#[derive(Debug, Clone)]
10pub(super) enum IndexBucket {
11    One(usize),
12    Many(Vec<usize>),
13}
14
15impl EntityLinker {
16    /// Builds reusable exact-match indexes from one temporal memory view.
17    ///
18    /// # Errors
19    ///
20    /// Rejects invalid catalog nodes, duplicate identifiers, and malformed
21    /// aliases or external identifiers.
22    pub fn from_view(view: &MemoryView) -> Result<Self> {
23        let mut linker = Self {
24            nodes: Vec::with_capacity(view.nodes.len()),
25            by_id: HashMap::with_capacity(view.nodes.len()),
26            by_label: HashMap::with_capacity(view.nodes.len()),
27            by_alias: HashMap::with_capacity(view.nodes.len()),
28            by_external_id: HashMap::with_capacity(view.nodes.len()),
29        };
30        for (index, node) in view.nodes.iter().enumerate() {
31            node.validate()?;
32            if linker.by_id.insert(node.id.clone(), index).is_some() {
33                return Err(MemoryError::InvalidValue {
34                    field: "entity_linker.catalog",
35                    reason: "entity identifiers must be unique",
36                });
37            }
38            let kind = normalized(&node.kind);
39            linker.nodes.push(super::CatalogEntity {
40                id: node.id.clone(),
41                kind: kind.clone(),
42                repository: node.repository.clone(),
43                branch: node.branch.clone(),
44            });
45            insert_index(
46                &mut linker.by_label,
47                (kind.clone(), normalized(&node.label)),
48                index,
49            );
50            for (key, value) in &node.attributes {
51                if key == "alias" || key.starts_with("alias.") {
52                    crate::domain::validate_text("entity_linker.alias", value)?;
53                    insert_index(
54                        &mut linker.by_alias,
55                        (kind.clone(), normalized(value)),
56                        index,
57                    );
58                } else if key.starts_with("external_id.") {
59                    crate::domain::validate_text("entity_linker.external_id", value)?;
60                    insert_index(
61                        &mut linker.by_external_id,
62                        (kind.clone(), key.clone(), exact_key(value)),
63                        index,
64                    );
65                }
66            }
67        }
68        Ok(linker)
69    }
70}
71
72fn insert_index<K: Eq + Hash>(index: &mut HashMap<K, IndexBucket>, key: K, value: usize) {
73    match index.entry(key) {
74        std::collections::hash_map::Entry::Vacant(entry) => {
75            entry.insert(IndexBucket::One(value));
76        }
77        std::collections::hash_map::Entry::Occupied(mut entry) => match entry.get_mut() {
78            IndexBucket::One(first) => {
79                let first = *first;
80                entry.insert(IndexBucket::Many(vec![first, value]));
81            }
82            IndexBucket::Many(values) => values.push(value),
83        },
84    }
85}