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