1pub mod entities;
9pub mod import;
10pub mod memory;
11pub mod requests;
12pub mod responses;
13pub mod sources;
14
15pub use entities::{
17 Entity, EntityDetail, EntitySearchResult, EntitySuggestion, Observation, RecentRelation,
18 Relation, RelationWithEntity,
19};
20pub use memory::{
21 ActivityBadge, ActivityKind, AgentActivityRow, AgentConnection, ConceptChange,
22 ConceptChangeKind, DomainInfo, EnrichmentStatusResponse, EnrichmentStepStatus, HomeStats,
23 IndexedFileInfo, MemoryItem, MemoryStats, MemoryVersionItem, Profile, RecentActivityItem,
24 RejectionRecord, RetrievalEvent, SearchResult, SessionSnapshot, SnapshotCapture,
25 SnapshotCaptureWithContent, Space, TopMemory, TypeBreakdown,
26};
27pub use sources::{MemoryType, RawDocument, SourceType, StabilityTier, SyncStatus};
28
29use serde::{Deserialize, Serialize};
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ChangelogEntry {
34 pub version: i64,
35 pub at: i64,
37 pub delta: String,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub source_agent: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub incoming_source_id: Option<String>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ConceptSource {
50 pub concept_id: String,
51 pub memory_source_id: String,
52 pub linked_at: i64,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub link_reason: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ConceptSourceWithMemory {
61 pub source: ConceptSource,
62 pub memory: Option<crate::memory::MemoryItem>,
63}
64
65pub fn version() -> &'static str {
67 env!("CARGO_PKG_VERSION")
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn version_is_set() {
76 assert!(!version().is_empty());
77 }
78
79 #[test]
80 fn memory_type_roundtrip() {
81 for variant in [
82 MemoryType::Identity,
83 MemoryType::Preference,
84 MemoryType::Decision,
85 MemoryType::Fact,
86 MemoryType::Goal,
87 ] {
88 let s = variant.to_string();
89 let parsed: MemoryType = s.parse().unwrap();
90 assert_eq!(parsed, variant);
91 }
92 }
93
94 #[test]
95 fn search_result_serializes() {
96 let sr = SearchResult {
97 id: "1".into(),
98 content: "test".into(),
99 source: "memory".into(),
100 source_id: "mem_abc".into(),
101 title: "Test".into(),
102 url: None,
103 chunk_index: 0,
104 last_modified: 1000,
105 score: 0.9,
106 chunk_type: None,
107 language: None,
108 semantic_unit: None,
109 memory_type: Some("fact".into()),
110 domain: None,
111 source_agent: None,
112 confidence: Some(0.8),
113 confirmed: Some(true),
114 stability: None,
115 supersedes: None,
116 summary: None,
117 entity_id: None,
118 entity_name: None,
119 quality: None,
120 is_archived: false,
121 is_recap: false,
122 structured_fields: None,
123 retrieval_cue: None,
124 source_text: None,
125 raw_score: 0.0,
126 };
127 let json = serde_json::to_string(&sr).unwrap();
128 assert!(json.contains("mem_abc"));
129 assert!(!json.contains("entity_id"));
131 }
132
133 #[test]
134 fn raw_document_default() {
135 let doc = RawDocument::default();
136 assert_eq!(doc.enrichment_status, "raw");
137 assert_eq!(doc.supersede_mode, "hide");
138 assert!(!doc.pending_revision);
139 assert!(!doc.is_recap);
140 }
141
142 #[test]
143 fn stability_tier_mapping() {
144 use sources::stability_tier;
145 assert_eq!(stability_tier(Some("identity")), StabilityTier::Protected);
146 assert_eq!(stability_tier(Some("preference")), StabilityTier::Protected);
147 assert_eq!(stability_tier(Some("fact")), StabilityTier::Standard);
148 assert_eq!(stability_tier(Some("decision")), StabilityTier::Standard);
149 assert_eq!(stability_tier(Some("goal")), StabilityTier::Ephemeral);
150 assert_eq!(stability_tier(None), StabilityTier::Ephemeral);
151 }
152}
153
154#[cfg(test)]
155mod retrieval_event_tests {
156 use super::*;
157
158 #[test]
159 fn retrieval_event_roundtrips() {
160 let e = RetrievalEvent {
161 timestamp_ms: 1_700_000_000_000,
162 agent_name: "claude-code".into(),
163 query: Some("origin positioning".into()),
164 concept_titles: vec!["Origin positioning".into(), "Daemon architecture".into()],
165 concept_ids: vec![],
166 memory_snippets: vec![],
167 };
168 let s = serde_json::to_string(&e).unwrap();
169 let back: RetrievalEvent = serde_json::from_str(&s).unwrap();
170 assert_eq!(back.agent_name, "claude-code");
171 assert_eq!(back.concept_titles.len(), 2);
172 assert_eq!(back.query.as_deref(), Some("origin positioning"));
173 }
174
175 #[test]
176 fn retrieval_event_omits_none_query() {
177 let e = RetrievalEvent {
178 timestamp_ms: 1_700_000_000_000,
179 agent_name: "claude-code".into(),
180 query: None,
181 concept_titles: vec![],
182 concept_ids: vec![],
183 memory_snippets: vec![],
184 };
185 let s = serde_json::to_string(&e).unwrap();
186 assert!(
187 !s.contains("\"query\""),
188 "expected None query to be skipped on the wire, got: {s}",
189 );
190 let back: RetrievalEvent = serde_json::from_str(&s).unwrap();
191 assert_eq!(back.query, None);
192 assert!(back.concept_titles.is_empty());
193 }
194
195 #[test]
196 fn concept_change_roundtrips() {
197 let c = ConceptChange {
198 concept_id: "concept_abc".into(),
199 title: "Wiki-style prose concepts".into(),
200 change_kind: ConceptChangeKind::Revised,
201 changed_at_ms: 1_700_000_000_000,
202 };
203 let s = serde_json::to_string(&c).unwrap();
204 assert!(
205 s.contains("\"change_kind\":\"revised\""),
206 "expected snake_case change_kind on the wire, got: {s}",
207 );
208 let back: ConceptChange = serde_json::from_str(&s).unwrap();
209 assert_eq!(back.change_kind, ConceptChangeKind::Revised);
210 }
211}