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