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