Skip to main content

weavatrix_memory/extraction/linker/
catalog.rs

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