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