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