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