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, ConceptChange,
22    ConceptChangeKind, DomainInfo, HomeStats, IndexedFileInfo, MemoryItem, MemoryStats,
23    MemoryVersionItem, Profile, RecentActivityItem, RejectionRecord, RetrievalEvent, SearchResult,
24    SessionSnapshot, SnapshotCapture, SnapshotCaptureWithContent, Space, TopMemory, TypeBreakdown,
25};
26pub use sources::{MemoryType, RawDocument, SourceType, StabilityTier, SyncStatus};
27
28use serde::{Deserialize, Serialize};
29
30/// A single revision entry in a memory's changelog (topic-key upsert history).
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ChangelogEntry {
33    pub version: i64,
34    /// Unix timestamp of when this revision was written.
35    pub at: i64,
36    /// Human-readable one-liner describing what changed. May be empty when
37    /// the LLM delta hasn't been generated yet (async fill-in).
38    pub delta: String,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub source_agent: Option<String>,
41    /// The source_id of the incoming memory that triggered this upsert.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub incoming_source_id: Option<String>,
44}
45
46/// A link between a concept and one of its source memories (concept_sources join table).
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ConceptSource {
49    pub concept_id: String,
50    pub memory_source_id: String,
51    /// Unix timestamp of when this link was created.
52    pub linked_at: i64,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub link_reason: Option<String>,
55}
56
57/// Concept source enriched with the memory's metadata (for the API response).
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ConceptSourceWithMemory {
60    pub source: ConceptSource,
61    pub memory: Option<crate::memory::MemoryItem>,
62}
63
64/// Crate version.
65pub fn version() -> &'static str {
66    env!("CARGO_PKG_VERSION")
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn version_is_set() {
75        assert!(!version().is_empty());
76    }
77
78    #[test]
79    fn memory_type_roundtrip() {
80        for variant in [
81            MemoryType::Identity,
82            MemoryType::Preference,
83            MemoryType::Decision,
84            MemoryType::Fact,
85            MemoryType::Goal,
86        ] {
87            let s = variant.to_string();
88            let parsed: MemoryType = s.parse().unwrap();
89            assert_eq!(parsed, variant);
90        }
91    }
92
93    #[test]
94    fn search_result_serializes() {
95        let sr = SearchResult {
96            id: "1".into(),
97            content: "test".into(),
98            source: "memory".into(),
99            source_id: "mem_abc".into(),
100            title: "Test".into(),
101            url: None,
102            chunk_index: 0,
103            last_modified: 1000,
104            score: 0.9,
105            chunk_type: None,
106            language: None,
107            semantic_unit: None,
108            memory_type: Some("fact".into()),
109            domain: None,
110            source_agent: None,
111            confidence: Some(0.8),
112            confirmed: Some(true),
113            stability: None,
114            supersedes: None,
115            summary: None,
116            entity_id: None,
117            entity_name: None,
118            quality: None,
119            is_archived: false,
120            is_recap: false,
121            structured_fields: None,
122            retrieval_cue: None,
123            source_text: None,
124            raw_score: 0.0,
125        };
126        let json = serde_json::to_string(&sr).unwrap();
127        assert!(json.contains("mem_abc"));
128        // Verify skip_serializing_if works: None fields should be absent
129        assert!(!json.contains("entity_id"));
130    }
131
132    #[test]
133    fn raw_document_default() {
134        let doc = RawDocument::default();
135        assert_eq!(doc.enrichment_status, "raw");
136        assert_eq!(doc.supersede_mode, "hide");
137        assert!(!doc.pending_revision);
138        assert!(!doc.is_recap);
139    }
140
141    #[test]
142    fn stability_tier_mapping() {
143        use sources::stability_tier;
144        assert_eq!(stability_tier(Some("identity")), StabilityTier::Protected);
145        assert_eq!(stability_tier(Some("preference")), StabilityTier::Protected);
146        assert_eq!(stability_tier(Some("fact")), StabilityTier::Standard);
147        assert_eq!(stability_tier(Some("decision")), StabilityTier::Standard);
148        assert_eq!(stability_tier(Some("goal")), StabilityTier::Ephemeral);
149        assert_eq!(stability_tier(None), StabilityTier::Ephemeral);
150    }
151}
152
153#[cfg(test)]
154mod retrieval_event_tests {
155    use super::*;
156
157    #[test]
158    fn retrieval_event_roundtrips() {
159        let e = RetrievalEvent {
160            timestamp_ms: 1_700_000_000_000,
161            agent_name: "claude-code".into(),
162            query: Some("origin positioning".into()),
163            concept_titles: vec!["Origin positioning".into(), "Daemon architecture".into()],
164            concept_ids: vec![],
165            memory_snippets: vec![],
166        };
167        let s = serde_json::to_string(&e).unwrap();
168        let back: RetrievalEvent = serde_json::from_str(&s).unwrap();
169        assert_eq!(back.agent_name, "claude-code");
170        assert_eq!(back.concept_titles.len(), 2);
171        assert_eq!(back.query.as_deref(), Some("origin positioning"));
172    }
173
174    #[test]
175    fn retrieval_event_omits_none_query() {
176        let e = RetrievalEvent {
177            timestamp_ms: 1_700_000_000_000,
178            agent_name: "claude-code".into(),
179            query: None,
180            concept_titles: vec![],
181            concept_ids: vec![],
182            memory_snippets: vec![],
183        };
184        let s = serde_json::to_string(&e).unwrap();
185        assert!(
186            !s.contains("\"query\""),
187            "expected None query to be skipped on the wire, got: {s}",
188        );
189        let back: RetrievalEvent = serde_json::from_str(&s).unwrap();
190        assert_eq!(back.query, None);
191        assert!(back.concept_titles.is_empty());
192    }
193
194    #[test]
195    fn concept_change_roundtrips() {
196        let c = ConceptChange {
197            concept_id: "concept_abc".into(),
198            title: "Wiki-style prose concepts".into(),
199            change_kind: ConceptChangeKind::Revised,
200            changed_at_ms: 1_700_000_000_000,
201        };
202        let s = serde_json::to_string(&c).unwrap();
203        assert!(
204            s.contains("\"change_kind\":\"revised\""),
205            "expected snake_case change_kind on the wire, got: {s}",
206        );
207        let back: ConceptChange = serde_json::from_str(&s).unwrap();
208        assert_eq!(back.change_kind, ConceptChangeKind::Revised);
209    }
210}