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