Skip to main content

locus_sdk/application/
memory_find.rs

1use std::cmp::Ordering;
2use std::sync::Arc;
3
4use anyhow::Result;
5use locus_core_rs::domain::contracts::NodeStore;
6use locus_core_rs::domain::models::{NodeQuery, SttpNode};
7
8use crate::application::memory_filters::{build_session_filter, node_matches_common_filters};
9use crate::domain::memory::{
10    MemoryFindRequest, MemoryFindResult, MemorySortField, SortDirection, clamp_limit,
11};
12
13pub struct MemoryFindService {
14    store: Arc<dyn NodeStore>,
15}
16
17impl MemoryFindService {
18    /// Create a deterministic memory finder over a shared node store.
19    pub fn new(store: Arc<dyn NodeStore>) -> Self {
20        Self { store }
21    }
22
23    /// Run predicate-based retrieval with stable sorting and pagination semantics.
24    ///
25    /// This operation does not apply resonance scoring; it filters, sorts,
26    /// and truncates nodes based only on explicit request criteria.
27    pub async fn execute(&self, request: &MemoryFindRequest) -> Result<MemoryFindResult> {
28        let limit = clamp_limit(request.page.limit);
29        let query_limit = (limit.saturating_mul(5)).clamp(1, 5000);
30
31        let single_session = request
32            .scope
33            .session_ids
34            .as_deref()
35            .filter(|sessions| sessions.len() == 1)
36            .and_then(|sessions| sessions.first().cloned());
37
38        let mut nodes = self
39            .store
40            .query_nodes_async(NodeQuery {
41                limit: query_limit,
42                session_id: single_session,
43                from_utc: request.scope.from_utc,
44                to_utc: request.scope.to_utc,
45                tiers: request.scope.tiers.clone(),
46            })
47            .await?;
48
49        let session_filter = build_session_filter(&request.scope);
50
51        nodes.retain(|node| {
52            node_matches_common_filters(node, &request.scope, &request.filter, session_filter.as_ref())
53        });
54        sort_nodes(&mut nodes, request.sort.field, request.sort.direction);
55
56        let has_more = nodes.len() > limit;
57        nodes.truncate(limit);
58
59        let next_cursor = nodes
60            .last()
61            .map(|node| format!("{}|{}", node.updated_at.to_rfc3339(), node.sync_key));
62
63        Ok(MemoryFindResult {
64            retrieved: nodes.len(),
65            nodes,
66            has_more,
67            next_cursor,
68        })
69    }
70}
71
72fn sort_nodes(nodes: &mut [SttpNode], field: MemorySortField, direction: SortDirection) {
73    nodes.sort_by(|left, right| {
74        let ord = match field {
75            MemorySortField::Timestamp => left.timestamp.cmp(&right.timestamp),
76            MemorySortField::UpdatedAt => left.updated_at.cmp(&right.updated_at),
77            MemorySortField::Psi => left.psi.partial_cmp(&right.psi).unwrap_or(Ordering::Equal),
78            MemorySortField::Rho => left.rho.partial_cmp(&right.rho).unwrap_or(Ordering::Equal),
79            MemorySortField::Kappa => left
80                .kappa
81                .partial_cmp(&right.kappa)
82                .unwrap_or(Ordering::Equal),
83        };
84
85        match direction {
86            SortDirection::Asc => ord,
87            SortDirection::Desc => ord.reverse(),
88        }
89    });
90}