Skip to main content

stasis/application/use_cases/
identity_memory_service.rs

1use std::sync::Arc;
2
3use crate::domain::errors::Result;
4use crate::ports::outbound::memory::identity_memory_models::{
5    CommitEntityUpdateRequest, CommitEntityUpdateResponse, GetIdentityContextRequest,
6    GetIdentityContextResponse, ListEntityHistoryRequest, ListEntityHistoryResponse,
7    ProposeEntityUpdateRequest, ProposeEntityUpdateResponse, RollbackEntityVersionRequest,
8    RollbackEntityVersionResponse, UpdateTier,
9};
10use crate::ports::outbound::memory::identity_memory_store::IdentityMemoryStore;
11
12#[derive(Clone)]
13pub struct IdentityMemoryService {
14    store: Arc<dyn IdentityMemoryStore>,
15}
16
17#[derive(Clone, Debug, Default)]
18pub struct ProposeAndCommitRequest {
19    pub proposal_request: ProposeEntityUpdateRequest,
20    pub expected_version: i32,
21    pub approver: Option<String>,
22}
23
24#[derive(Clone, Debug, Default)]
25pub struct ProposeAndCommitResponse {
26    pub proposed: ProposeEntityUpdateResponse,
27    pub commits: Vec<CommitEntityUpdateResponse>,
28}
29
30impl IdentityMemoryService {
31    pub fn new(store: Arc<dyn IdentityMemoryStore>) -> Self {
32        Self { store }
33    }
34
35    pub async fn get_identity_context(
36        &self,
37        request: &GetIdentityContextRequest,
38    ) -> Result<GetIdentityContextResponse> {
39        self.store.get_identity_context(request).await
40    }
41
42    pub async fn propose_entity_update(
43        &self,
44        request: &ProposeEntityUpdateRequest,
45    ) -> Result<ProposeEntityUpdateResponse> {
46        self.store.propose_entity_update(request).await
47    }
48
49    pub async fn commit_entity_update(
50        &self,
51        request: &CommitEntityUpdateRequest,
52    ) -> Result<CommitEntityUpdateResponse> {
53        self.store.commit_entity_update(request).await
54    }
55
56    pub async fn list_entity_history(
57        &self,
58        request: &ListEntityHistoryRequest,
59    ) -> Result<ListEntityHistoryResponse> {
60        self.store.list_entity_history(request).await
61    }
62
63    pub async fn rollback_entity_version(
64        &self,
65        request: &RollbackEntityVersionRequest,
66    ) -> Result<RollbackEntityVersionResponse> {
67        self.store.rollback_entity_version(request).await
68    }
69
70    pub async fn propose_and_commit_autocommit(
71        &self,
72        request: &ProposeAndCommitRequest,
73    ) -> Result<ProposeAndCommitResponse> {
74        let proposed = self
75            .store
76            .propose_entity_update(&request.proposal_request)
77            .await?;
78
79        let mut expected_version = request.expected_version;
80        let mut commits = Vec::new();
81
82        for (idx, proposal_id) in proposed.proposal_ids.iter().enumerate() {
83            let tier = proposed
84                .tiers
85                .get(idx)
86                .copied()
87                .unwrap_or(UpdateTier::ApprovalRequired);
88            if !matches!(tier, UpdateTier::AutoCommit) {
89                continue;
90            }
91
92            let commit = self
93                .store
94                .commit_entity_update(&CommitEntityUpdateRequest {
95                    proposal_id: proposal_id.clone(),
96                    expected_version,
97                    approver: request.approver.clone(),
98                })
99                .await?;
100
101            if commit.committed && let Some(new_version) = commit.new_version {
102                expected_version = new_version;
103            }
104
105            commits.push(commit);
106        }
107
108        Ok(ProposeAndCommitResponse { proposed, commits })
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use std::sync::Arc;
115
116    use chrono::Utc;
117    use serde_json::json;
118
119    use super::{IdentityMemoryService, ProposeAndCommitRequest};
120    use crate::infrastructure::memory::in_memory_identity_memory_store::InMemoryIdentityMemoryStore;
121    use crate::ports::outbound::memory::identity_memory_models::{
122        EntityRef, IdentityEntityType, ProposeEntityUpdateRequest, RelationshipEntity,
123        RelationshipStatus, UpdateSource,
124    };
125
126    #[tokio::test]
127    async fn service_autocommits_only_auto_tier_proposals() {
128        let store = Arc::new(InMemoryIdentityMemoryStore::default());
129        store
130            .upsert_relationship(RelationshipEntity {
131                relationship_id: "rel-1".to_string(),
132                source_entity_ref: EntityRef {
133                    entity_type: "PersonaEntity".to_string(),
134                    entity_id: "p1".to_string(),
135                },
136                target_entity_ref: EntityRef {
137                    entity_type: "UserEntity".to_string(),
138                    entity_id: "u1".to_string(),
139                },
140                relationship_kind: "assistant_user".to_string(),
141                status: RelationshipStatus::Active,
142                trust_level: 0.50,
143                confidence: 0.80,
144                strength_score: 0.80,
145                recency_score: 0.60,
146                autonomy_scope: Default::default(),
147                approval_profile_id: None,
148                interruption_policy: Default::default(),
149                escalation_policy: Default::default(),
150                policy_tags: vec![],
151                provenance: UpdateSource::UserDirect,
152                parent_relationship_id: None,
153                governing_relationship_ids: vec![],
154                derived_from_relationship_id: None,
155                last_transition_reason: None,
156                transition_receipt_id: None,
157                version: 1,
158                created_at: Utc::now(),
159                updated_at: Utc::now(),
160            })
161            .expect("seed relationship should succeed");
162
163        let service = IdentityMemoryService::new(store);
164
165        let result = service
166            .propose_and_commit_autocommit(&ProposeAndCommitRequest {
167                proposal_request: ProposeEntityUpdateRequest {
168                    entity_type: IdentityEntityType::RelationshipEntity,
169                    entity_id: "rel-1".to_string(),
170                    patch: json!({
171                        "recency_score": 0.9,
172                        "autonomy_scope.allow": ["external_posting"]
173                    }),
174                    source: UpdateSource::ModelInferred,
175                    confidence: 0.82,
176                    reason: "mixed update".to_string(),
177                    actor: "model".to_string(),
178                    receipt_id: None,
179                    expires_at: None,
180                },
181                expected_version: 1,
182                approver: Some("owner".to_string()),
183            })
184            .await
185            .expect("workflow should succeed");
186
187        assert_eq!(result.proposed.proposal_ids.len(), 2);
188        assert_eq!(result.commits.len(), 1);
189        assert!(result.commits[0].committed);
190    }
191}