1use std::collections::BTreeMap;
2use std::fmt;
3
4use chrono::{DateTime, Utc};
5use serde::de::{self, Visitor};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use serde_json::Value;
8
9#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
10pub enum IdentityEntityType {
11 PersonaEntity,
12 UserEntity,
13 ContactEntity,
14 ChannelProfileEntity,
15 PolicyProfileEntity,
16 #[default]
17 RelationshipEntity,
18}
19
20#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Hash)]
21pub enum UpdateTier {
22 #[default]
23 AutoCommit,
24 ConfirmRequired,
25 ApprovalRequired,
26}
27
28#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
29pub enum ProposalState {
30 Proposed,
31 Committed,
32 Rejected,
33 Expired,
34}
35
36#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
37pub enum UpdateSource {
38 UserDirect,
39 #[default]
40 ModelInferred,
41 SystemEvent,
42}
43
44#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
45pub enum RelationshipStatus {
46 #[default]
47 Proposed,
48 Active,
49 Suspended,
50 Deprecated,
51 Revoked,
52}
53
54#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
55#[serde(rename_all = "snake_case")]
56pub enum IdentityContextMode {
57 #[default]
58 Full,
59 Policy,
60 Cognitive,
61}
62
63#[derive(Clone, Debug, Eq, PartialEq, Hash)]
64pub enum RelationshipKind {
65 AssistantUser,
66 UserChannel,
67 Knows,
68 Prefers,
69 Delegation,
70 Colleague,
71 Legacy(String),
72}
73
74impl RelationshipKind {
75 pub fn parse(value: &str) -> Self {
76 match value {
77 "assistant_user" => Self::AssistantUser,
78 "user_channel" => Self::UserChannel,
79 "knows" => Self::Knows,
80 "prefers" => Self::Prefers,
81 "delegation" => Self::Delegation,
82 "colleague" => Self::Colleague,
83 other => Self::Legacy(other.to_string()),
84 }
85 }
86
87 pub fn as_str(&self) -> &str {
88 match self {
89 Self::AssistantUser => "assistant_user",
90 Self::UserChannel => "user_channel",
91 Self::Knows => "knows",
92 Self::Prefers => "prefers",
93 Self::Delegation => "delegation",
94 Self::Colleague => "colleague",
95 Self::Legacy(value) => value.as_str(),
96 }
97 }
98
99 pub fn is_structural(&self) -> bool {
100 matches!(self, Self::AssistantUser | Self::UserChannel)
101 }
102
103 pub fn is_social(&self) -> bool {
104 matches!(
105 self,
106 Self::Knows | Self::Prefers | Self::Delegation | Self::Colleague
107 )
108 }
109}
110
111impl Default for RelationshipKind {
112 fn default() -> Self {
113 Self::AssistantUser
114 }
115}
116
117impl Serialize for RelationshipKind {
118 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
119 where
120 S: Serializer,
121 {
122 serializer.serialize_str(self.as_str())
123 }
124}
125
126impl<'de> Deserialize<'de> for RelationshipKind {
127 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
128 where
129 D: Deserializer<'de>,
130 {
131 struct RelationshipKindVisitor;
132
133 impl Visitor<'_> for RelationshipKindVisitor {
134 type Value = RelationshipKind;
135
136 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137 formatter.write_str("a relationship kind string")
138 }
139
140 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
141 where
142 E: de::Error,
143 {
144 Ok(RelationshipKind::parse(value))
145 }
146
147 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
148 where
149 E: de::Error,
150 {
151 Ok(RelationshipKind::parse(&value))
152 }
153 }
154
155 deserializer.deserialize_str(RelationshipKindVisitor)
156 }
157}
158
159#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
160pub struct EntityRef {
161 pub entity_type: String,
162 pub entity_id: String,
163}
164
165#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
166pub struct EscalationPolicy {
167 pub mode: Option<String>,
168 pub fallback: Option<String>,
169}
170
171#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
172pub struct InterruptionPolicy {
173 pub quiet_hours: Option<String>,
174 pub allow_urgent_only: Option<bool>,
175 pub urgent_threshold: Option<f32>,
176}
177
178#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
179pub struct AutonomyScope {
180 pub allow: Vec<String>,
181 pub deny: Vec<String>,
182 pub approval_required: Vec<String>,
183}
184
185#[derive(Clone, Debug, Default, Serialize, Deserialize)]
186pub struct PersonaEntity {
187 pub persona_id: String,
188 pub display_name: String,
189 pub status: String,
190 pub version: i32,
191 pub updated_at: DateTime<Utc>,
192}
193
194#[derive(Clone, Debug, Default, Serialize, Deserialize)]
195pub struct UserEntity {
196 pub user_id: String,
197 pub timezone: String,
198 pub language_variant: Option<String>,
199 #[serde(default)]
200 pub preferences: BTreeMap<String, Value>,
201 pub status: String,
202 pub version: i32,
203 pub updated_at: DateTime<Utc>,
204}
205
206#[derive(Clone, Debug, Default, Serialize, Deserialize)]
207pub struct ContactEntity {
208 pub contact_id: String,
209 pub display_name: String,
210 #[serde(default)]
211 pub aliases: Vec<String>,
212 pub status: String,
213 pub version: i32,
214 pub updated_at: DateTime<Utc>,
215}
216
217#[derive(Clone, Debug, Default, Serialize, Deserialize)]
218pub struct ChannelProfileEntity {
219 pub channel_id: String,
220 pub channel_type: String,
221 pub proactive_allowed: bool,
222 pub status: String,
223 pub version: i32,
224 pub updated_at: DateTime<Utc>,
225}
226
227#[derive(Clone, Debug, Default, Serialize, Deserialize)]
228pub struct PolicyProfileEntity {
229 pub policy_profile_id: String,
230 pub graph_max_depth: usize,
231 pub trust_delta_max_per_window: f32,
232 pub status: String,
233 pub version: i32,
234 pub updated_at: DateTime<Utc>,
235}
236
237#[derive(Clone, Debug, Default, Serialize, Deserialize)]
238pub struct RelationshipEntity {
239 pub relationship_id: String,
240 pub source_entity_ref: EntityRef,
241 pub target_entity_ref: EntityRef,
242 pub relationship_kind: RelationshipKind,
243 pub status: RelationshipStatus,
244 pub trust_level: f32,
245 pub confidence: f32,
246 pub strength_score: f32,
247 pub recency_score: f32,
248 pub autonomy_scope: AutonomyScope,
249 pub approval_profile_id: Option<String>,
250 pub interruption_policy: InterruptionPolicy,
251 pub escalation_policy: EscalationPolicy,
252 pub policy_tags: Vec<String>,
253 pub provenance: UpdateSource,
254 pub parent_relationship_id: Option<String>,
255 pub governing_relationship_ids: Vec<String>,
256 pub derived_from_relationship_id: Option<String>,
257 pub last_transition_reason: Option<String>,
258 pub transition_receipt_id: Option<String>,
259 pub version: i32,
260 pub created_at: DateTime<Utc>,
261 pub updated_at: DateTime<Utc>,
262}
263
264#[derive(Clone, Debug, Default, Serialize, Deserialize)]
265pub struct FlattenedPolicyClaim {
266 pub claim_id: String,
267 pub source_relationship_ids: Vec<String>,
268 pub summary: String,
269 pub confidence: f32,
270 pub timestamp: DateTime<Utc>,
271}
272
273#[derive(Clone, Debug, Default, Serialize, Deserialize)]
274pub struct GetIdentityContextRequest {
275 pub user_id: String,
276 pub persona_id: String,
277 pub channel_id: String,
278 pub relationship_limit: usize,
279 #[serde(default)]
280 pub mode: IdentityContextMode,
281}
282
283#[derive(Clone, Debug, Default, Serialize, Deserialize)]
284pub struct GetIdentityContextResponse {
285 pub persona: Option<PersonaEntity>,
286 pub user: Option<UserEntity>,
287 pub channel: Option<ChannelProfileEntity>,
288 #[serde(default)]
289 pub contacts: Vec<ContactEntity>,
290 pub relationships: Vec<RelationshipEntity>,
291 pub policy_profiles: Vec<PolicyProfileEntity>,
292 pub graph_depth_used: usize,
293 pub flattened_claims: Vec<FlattenedPolicyClaim>,
294}
295
296#[derive(Clone, Debug, Default, Serialize, Deserialize)]
297pub struct ProposeEntityUpdateRequest {
298 pub entity_type: IdentityEntityType,
299 pub entity_id: String,
300 pub patch: Value,
301 pub source: UpdateSource,
302 pub confidence: f32,
303 pub reason: String,
304 pub actor: String,
305 pub receipt_id: Option<String>,
306 pub expires_at: Option<DateTime<Utc>>,
307}
308
309#[derive(Clone, Debug, Default, Serialize, Deserialize)]
310pub struct ProposeEntityUpdateResponse {
311 pub proposal_ids: Vec<String>,
312 pub tiers: Vec<UpdateTier>,
313 pub requires_approval: bool,
314 pub split_patch: bool,
315 pub policy_notes: Vec<String>,
316}
317
318#[derive(Clone, Debug, Serialize, Deserialize)]
319pub struct EntityUpdateProposalRecord {
320 pub proposal_id: String,
321 pub entity_type: IdentityEntityType,
322 pub entity_id: String,
323 pub patch: Value,
324 pub tier: UpdateTier,
325 pub source: UpdateSource,
326 pub confidence: f32,
327 pub reason: String,
328 pub state: ProposalState,
329 pub approver: Option<String>,
330 pub actor: String,
331 pub receipt_id: Option<String>,
332 pub expires_at: Option<DateTime<Utc>>,
333 pub created_at: DateTime<Utc>,
334 pub updated_at: DateTime<Utc>,
335}
336
337#[derive(Clone, Debug, Default, Serialize, Deserialize)]
338pub struct CommitEntityUpdateRequest {
339 pub proposal_id: String,
340 pub expected_version: i32,
341 pub approver: Option<String>,
342}
343
344#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
345pub enum CommitOutcomeCode {
346 Ok,
347 StaleState,
348 ApprovalRequired,
349 PolicyDenied,
350 InvalidPatch,
351 ExpiredProposal,
352 NotFound,
353}
354
355#[derive(Clone, Debug, Default, Serialize, Deserialize)]
356pub struct CommitEntityUpdateResponse {
357 pub committed: bool,
358 pub code: Option<CommitOutcomeCode>,
359 pub entity_type: Option<IdentityEntityType>,
360 pub entity_id: Option<String>,
361 pub new_version: Option<i32>,
362 pub receipt_id: Option<String>,
363 pub transition_event_id: Option<String>,
364 pub sttp_bridge_node: Option<String>,
365 pub sttp_bridge_reason: Option<String>,
366 pub rationale: Option<String>,
367}
368
369#[derive(Clone, Debug, Serialize, Deserialize)]
370pub struct RelationshipTransitionEvent {
371 pub event_id: String,
372 pub relationship_id: String,
373 pub from_status: Option<RelationshipStatus>,
374 pub to_status: RelationshipStatus,
375 pub reason: String,
376 pub actor: String,
377 pub receipt_id: Option<String>,
378 pub occurred_at: DateTime<Utc>,
379 pub metadata: Option<Value>,
380}
381
382#[derive(Clone, Debug, Default, Serialize, Deserialize)]
383pub struct ListEntityHistoryRequest {
384 pub entity_type: IdentityEntityType,
385 pub entity_id: String,
386 pub limit: usize,
387}
388
389#[derive(Clone, Debug, Default, Serialize, Deserialize)]
390pub struct ListEntityHistoryResponse {
391 pub proposals: Vec<EntityUpdateProposalRecord>,
392 pub transitions: Vec<RelationshipTransitionEvent>,
393}
394
395#[derive(Clone, Debug, Default, Serialize, Deserialize)]
396pub struct RollbackEntityVersionRequest {
397 pub entity_type: IdentityEntityType,
398 pub entity_id: String,
399 pub target_version: i32,
400 pub reason: String,
401 pub approver: String,
402}
403
404#[derive(Clone, Debug, Default, Serialize, Deserialize)]
405pub struct RollbackEntityVersionResponse {
406 pub rolled_back: bool,
407 pub new_version: Option<i32>,
408 pub rollback_receipt_id: Option<String>,
409 pub rationale: Option<String>,
410}
411
412#[cfg(test)]
413mod tests {
414 use super::{IdentityContextMode, RelationshipKind};
415
416 #[test]
417 fn relationship_kind_deserializes_legacy_strings() {
418 let kind: RelationshipKind =
419 serde_json::from_value(serde_json::json!("assistant_user")).expect("deserialize");
420 assert_eq!(kind, RelationshipKind::AssistantUser);
421
422 let social: RelationshipKind =
423 serde_json::from_value(serde_json::json!("knows")).expect("deserialize");
424 assert!(social.is_social());
425 }
426
427 #[test]
428 fn relationship_kind_serializes_snake_case() {
429 let encoded = serde_json::to_string(&RelationshipKind::Delegation).expect("serialize");
430 assert_eq!(encoded, "\"delegation\"");
431 }
432
433 #[test]
434 fn identity_context_mode_defaults_to_full() {
435 let request: super::GetIdentityContextRequest = serde_json::from_value(serde_json::json!({
436 "user_id": "u1",
437 "persona_id": "p1",
438 "channel_id": "c1",
439 "relationship_limit": 4
440 }))
441 .expect("request should default mode to full");
442 assert_eq!(request.mode, IdentityContextMode::Full);
443 }
444}