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