Skip to main content

origin_types/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Shared types for the Origin memory system.
3//!
4//! This crate provides lightweight type definitions shared across
5//! origin-core, origin-server, and the Tauri app. Dependencies are
6//! limited to serde and serde_json -- no heavy runtime deps.
7
8pub mod entities;
9pub mod import;
10pub mod memory;
11pub mod requests;
12pub mod responses;
13pub mod sources;
14
15// Re-export commonly used types at crate root for convenience.
16pub use entities::{
17    Entity, EntityDetail, EntitySearchResult, EntitySuggestion, Observation, RecentRelation,
18    Relation, RelationWithEntity,
19};
20pub use memory::{
21    ActivityBadge, ActivityKind, AgentActivityRow, AgentConnection, DomainInfo,
22    EnrichmentStatusResponse, EnrichmentStepStatus, HomeStats, IndexedFileInfo, MemoryItem,
23    MemoryStats, MemoryVersionItem, PageChange, PageChangeKind, 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/// A single revision entry in a memory's changelog (topic-key upsert history).
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ChangelogEntry {
34    pub version: i64,
35    /// Unix timestamp of when this revision was written.
36    pub at: i64,
37    /// Human-readable one-liner describing what changed. May be empty when
38    /// the LLM delta hasn't been generated yet (async fill-in).
39    pub delta: String,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub source_agent: Option<String>,
42    /// The source_id of the incoming memory that triggered this upsert.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub incoming_source_id: Option<String>,
45}
46
47/// A link between a page and one of its source memories.
48/// (Backed by the `concept_sources` SQL table; rename deferred for back-compat.)
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct PageSource {
51    pub page_id: String,
52    pub memory_source_id: String,
53    /// Unix timestamp of when this link was created.
54    pub linked_at: i64,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub link_reason: Option<String>,
57}
58
59/// Page source enriched with the memory's metadata (for the API response).
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct PageSourceWithMemory {
62    pub source: PageSource,
63    pub memory: Option<crate::memory::MemoryItem>,
64}
65
66/// Crate version.
67pub fn version() -> &'static str {
68    env!("CARGO_PKG_VERSION")
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn version_is_set() {
77        assert!(!version().is_empty());
78    }
79
80    #[test]
81    fn memory_type_roundtrip() {
82        for variant in [
83            MemoryType::Identity,
84            MemoryType::Preference,
85            MemoryType::Decision,
86            MemoryType::Lesson,
87            MemoryType::Gotcha,
88            MemoryType::Fact,
89        ] {
90            let s = variant.to_string();
91            let parsed: MemoryType = s.parse().unwrap();
92            assert_eq!(parsed, variant);
93        }
94    }
95
96    #[test]
97    fn search_result_serializes() {
98        let sr = SearchResult {
99            id: "1".into(),
100            content: "test".into(),
101            source: "memory".into(),
102            source_id: "mem_abc".into(),
103            title: "Test".into(),
104            url: None,
105            chunk_index: 0,
106            last_modified: 1000,
107            score: 0.9,
108            chunk_type: None,
109            language: None,
110            semantic_unit: None,
111            memory_type: Some("fact".into()),
112            domain: None,
113            source_agent: None,
114            confidence: Some(0.8),
115            confirmed: Some(true),
116            stability: None,
117            supersedes: None,
118            summary: None,
119            entity_id: None,
120            entity_name: None,
121            quality: None,
122            is_archived: false,
123            is_recap: false,
124            structured_fields: None,
125            retrieval_cue: None,
126            source_text: None,
127            raw_score: 0.0,
128        };
129        let json = serde_json::to_string(&sr).unwrap();
130        assert!(json.contains("mem_abc"));
131        // Verify skip_serializing_if works: None fields should be absent
132        assert!(!json.contains("entity_id"));
133    }
134
135    #[test]
136    fn raw_document_default() {
137        let doc = RawDocument::default();
138        assert_eq!(doc.enrichment_status, "raw");
139        assert_eq!(doc.supersede_mode, "hide");
140        assert!(!doc.pending_revision);
141        assert!(!doc.is_recap);
142    }
143
144    #[test]
145    fn stability_tier_mapping() {
146        use sources::stability_tier;
147        assert_eq!(stability_tier(Some("identity")), StabilityTier::Protected);
148        assert_eq!(stability_tier(Some("preference")), StabilityTier::Protected);
149        assert_eq!(stability_tier(Some("fact")), StabilityTier::Standard);
150        assert_eq!(stability_tier(Some("decision")), StabilityTier::Standard);
151        assert_eq!(stability_tier(Some("lesson")), StabilityTier::Standard);
152        assert_eq!(stability_tier(Some("gotcha")), StabilityTier::Standard);
153        // Deprecated: legacy "goal" rows still in DB pre-migration map to
154        // Protected via Identity fold (aspirations = identity).
155        assert_eq!(stability_tier(Some("goal")), StabilityTier::Protected);
156        assert_eq!(stability_tier(None), StabilityTier::Ephemeral);
157    }
158}
159
160#[cfg(test)]
161mod retrieval_event_tests {
162    use super::*;
163
164    #[test]
165    fn retrieval_event_roundtrips() {
166        let e = RetrievalEvent {
167            timestamp_ms: 1_700_000_000_000,
168            agent_name: "claude-code".into(),
169            query: Some("origin positioning".into()),
170            page_titles: vec!["Origin positioning".into(), "Daemon architecture".into()],
171            page_ids: vec![],
172            memory_snippets: vec![],
173        };
174        let s = serde_json::to_string(&e).unwrap();
175        let back: RetrievalEvent = serde_json::from_str(&s).unwrap();
176        assert_eq!(back.agent_name, "claude-code");
177        assert_eq!(back.page_titles.len(), 2);
178        assert_eq!(back.query.as_deref(), Some("origin positioning"));
179    }
180
181    #[test]
182    fn retrieval_event_omits_none_query() {
183        let e = RetrievalEvent {
184            timestamp_ms: 1_700_000_000_000,
185            agent_name: "claude-code".into(),
186            query: None,
187            page_titles: vec![],
188            page_ids: vec![],
189            memory_snippets: vec![],
190        };
191        let s = serde_json::to_string(&e).unwrap();
192        assert!(
193            !s.contains("\"query\""),
194            "expected None query to be skipped on the wire, got: {s}",
195        );
196        let back: RetrievalEvent = serde_json::from_str(&s).unwrap();
197        assert_eq!(back.query, None);
198        assert!(back.page_titles.is_empty());
199    }
200
201    #[test]
202    fn page_change_roundtrips() {
203        let c = PageChange {
204            page_id: "page_abc".into(),
205            title: "Wiki-style prose pages".into(),
206            change_kind: PageChangeKind::Revised,
207            changed_at_ms: 1_700_000_000_000,
208        };
209        let s = serde_json::to_string(&c).unwrap();
210        assert!(
211            s.contains("\"change_kind\":\"revised\""),
212            "expected snake_case change_kind on the wire, got: {s}",
213        );
214        let back: PageChange = serde_json::from_str(&s).unwrap();
215        assert_eq!(back.change_kind, PageChangeKind::Revised);
216    }
217}