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