Skip to main content

remem/retrieval/graph/
types.rs

1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2pub enum GraphTraversalStatus {
3    Ready,
4    MissingTable,
5    EmptyGraph,
6    NoSeed,
7    NoExpansion,
8}
9
10impl GraphTraversalStatus {
11    pub const fn disabled_reason(self) -> Option<&'static str> {
12        match self {
13            Self::Ready => None,
14            Self::MissingTable => Some("graph_edges table is unavailable"),
15            Self::EmptyGraph => Some("graph_edges table is empty"),
16            Self::NoSeed => Some("no eligible FTS/vector graph seeds"),
17            Self::NoExpansion => Some("no eligible trusted graph expansion"),
18        }
19    }
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
23pub enum GraphPathKind {
24    Supersedes,
25    Mentions,
26    TouchesFile,
27}
28
29impl GraphPathKind {
30    pub const fn as_str(self) -> &'static str {
31        match self {
32            Self::Supersedes => "supersedes",
33            Self::Mentions => "mentions",
34            Self::TouchesFile => "touches_file",
35        }
36    }
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct GraphTraversalHit {
41    pub memory_id: i64,
42    pub hop_count: u8,
43    pub path_kind: GraphPathKind,
44    pub min_confidence: f64,
45    pub seed_rank: usize,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct GraphTraversalLimits {
50    pub max_seeds: usize,
51    pub max_degree_per_node: usize,
52    pub max_edges_scanned: usize,
53    pub max_candidates: usize,
54}
55
56impl Default for GraphTraversalLimits {
57    fn default() -> Self {
58        Self {
59            max_seeds: 32,
60            max_degree_per_node: 64,
61            max_edges_scanned: 2_048,
62            max_candidates: 120,
63        }
64    }
65}
66
67impl GraphTraversalLimits {
68    pub fn for_search(fetch_limit: i64) -> Self {
69        Self {
70            max_candidates: usize::try_from(fetch_limit.max(1)).unwrap_or(120),
71            ..Self::default()
72        }
73    }
74
75    pub(super) fn validate(self) -> anyhow::Result<()> {
76        anyhow::ensure!(self.max_seeds > 0, "graph max_seeds must be positive");
77        anyhow::ensure!(
78            self.max_degree_per_node > 0,
79            "graph max_degree_per_node must be positive"
80        );
81        anyhow::ensure!(
82            self.max_edges_scanned > 0,
83            "graph max_edges_scanned must be positive"
84        );
85        anyhow::ensure!(
86            self.max_candidates > 0,
87            "graph max_candidates must be positive"
88        );
89        Ok(())
90    }
91}
92
93#[derive(Debug, Clone, Copy)]
94pub struct GraphTraversalRequest<'a> {
95    pub seed_memory_ids: &'a [i64],
96    pub project: Option<&'a str>,
97    pub memory_type: Option<&'a str>,
98    pub branch: Option<&'a str>,
99    pub include_inactive: bool,
100    pub reference_time_epoch: i64,
101    pub limits: GraphTraversalLimits,
102}
103
104#[derive(Debug, Clone, Default, PartialEq, Eq)]
105pub struct GraphTraversalDiagnostics {
106    pub edges_scanned: usize,
107    pub candidates_considered: usize,
108    pub targets_filtered: usize,
109    pub diagnostic_hint_edges: usize,
110    pub extracted_from_edges: usize,
111    pub ignored_trusted_edges: usize,
112}
113
114#[derive(Debug, Clone, PartialEq)]
115pub struct GraphTraversalOutcome {
116    pub status: GraphTraversalStatus,
117    pub hits: Vec<GraphTraversalHit>,
118    pub diagnostics: GraphTraversalDiagnostics,
119}
120
121impl GraphTraversalOutcome {
122    pub(super) fn empty(status: GraphTraversalStatus) -> Self {
123        Self {
124            status,
125            hits: Vec::new(),
126            diagnostics: GraphTraversalDiagnostics::default(),
127        }
128    }
129}