Skip to main content

wenlan_types/
lib.rs

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