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