Skip to main content

origin_types/
sources.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Document source types -- MemoryType enum, RawDocument, SourceType, SyncStatus.
3
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Closed taxonomy of memory facets -- validated at API boundary.
8/// Stored as lowercase TEXT in SQLite.
9///
10/// Six user-facing types after taxonomy refactor:
11/// - Identity, Preference -- about the user (Protected tier)
12/// - Decision -- choices made (Standard tier)
13/// - Lesson -- positive learnings (Standard tier)
14/// - Gotcha -- traps/warnings/negative learnings (Standard tier)
15/// - Fact -- durable knowledge (Standard tier)
16///
17/// Goal is deprecated: incoming "goal" parses as Identity (aspirations
18/// are part of who the user is). Existing rows migrate via DB migration.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum MemoryType {
22    Identity,
23    Preference,
24    Decision,
25    Lesson,
26    Gotcha,
27    Fact,
28}
29
30impl MemoryType {
31    /// All valid lowercase string values (6 canonical types).
32    pub fn all_values() -> &'static [&'static str] {
33        &[
34            "identity",
35            "preference",
36            "decision",
37            "lesson",
38            "gotcha",
39            "fact",
40        ]
41    }
42
43    /// Check if input is the "profile" high-level alias (case-insensitive).
44    /// Used by the store flow to detect when async LLM sub-classification is needed.
45    pub fn is_profile_alias(s: &str) -> bool {
46        s.eq_ignore_ascii_case("profile")
47    }
48
49    /// Check if input is the "knowledge" high-level alias (case-insensitive).
50    /// Knowledge expands to fact | lesson | gotcha and needs sub-classification.
51    pub fn is_knowledge_alias(s: &str) -> bool {
52        s.eq_ignore_ascii_case("knowledge")
53    }
54}
55
56impl std::fmt::Display for MemoryType {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        let s = match self {
59            Self::Identity => "identity",
60            Self::Preference => "preference",
61            Self::Decision => "decision",
62            Self::Lesson => "lesson",
63            Self::Gotcha => "gotcha",
64            Self::Fact => "fact",
65        };
66        f.write_str(s)
67    }
68}
69
70impl std::str::FromStr for MemoryType {
71    type Err = String;
72    fn from_str(s: &str) -> Result<Self, Self::Err> {
73        match s.to_lowercase().as_str() {
74            "identity" => Ok(Self::Identity),
75            "preference" => Ok(Self::Preference),
76            "decision" => Ok(Self::Decision),
77            "lesson" => Ok(Self::Lesson),
78            "gotcha" => Ok(Self::Gotcha),
79            "fact" => Ok(Self::Fact),
80            // Deprecated: "goal" folds into Identity (aspirations = identity).
81            "goal" => Ok(Self::Identity),
82            // High-level alias: "profile" needs async LLM sub-classification
83            "profile" => Err(
84                "profile requires sub-classification into identity or preference -- use classify_memory_type".to_string()
85            ),
86            // High-level alias: "knowledge" needs sub-classification (fact | lesson | gotcha)
87            "knowledge" => Err(
88                "knowledge requires sub-classification into fact, lesson, or gotcha -- use classify_memory_type".to_string()
89            ),
90            // Backward compat: removed types map to Fact
91            "correction" | "custom" | "recap" => Ok(Self::Fact),
92            _ => Err(format!(
93                "invalid memory_type '{}', valid values: {}",
94                s,
95                Self::all_values().join(", ")
96            )),
97        }
98    }
99}
100
101/// Stability tiers determine supersede behavior, confidence defaults, and retrieval decay.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum StabilityTier {
104    /// identity, preference -- supersede requires human confirmation
105    Protected,
106    /// fact, decision, lesson, gotcha -- supersede auto-applies unconfirmed
107    Standard,
108    /// catch-all for unknown / removed types -- supersede auto-applies silently
109    Ephemeral,
110}
111
112/// Map a memory type string to its stability tier. NULL -> Ephemeral.
113pub fn stability_tier(memory_type: Option<&str>) -> StabilityTier {
114    match memory_type {
115        Some("identity") | Some("preference") => StabilityTier::Protected,
116        Some("fact") | Some("decision") | Some("lesson") | Some("gotcha") => {
117            StabilityTier::Standard
118        }
119        // Deprecated: "goal" still in DB rows pre-migration -> treat as Identity (Protected).
120        Some("goal") => StabilityTier::Protected,
121        _ => StabilityTier::Ephemeral,
122    }
123}
124
125/// A raw document fetched from any source, ready for chunking and embedding.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct RawDocument {
128    /// Source identifier ("gmail", "notion", "local_files", etc.)
129    pub source: String,
130    /// Unique ID within the source (message ID, page ID, file path)
131    pub source_id: String,
132    /// Document title (filename, subject line, page title)
133    pub title: String,
134    /// LLM-generated summary (stored separately from chunk content)
135    pub summary: Option<String>,
136    /// Plain text content
137    pub content: String,
138    /// Deep link back to the source (URL, file path)
139    pub url: Option<String>,
140    /// Unix timestamp of last modification
141    pub last_modified: i64,
142    /// Additional metadata
143    pub metadata: HashMap<String, String>,
144
145    // --- Memory layer fields (all optional for backward compat) ---
146    /// Memory category: "preference", "decision", "fact", "goal", "relationship"
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub memory_type: Option<String>,
149    /// Domain context: "work", "personal", "health", or "project:<name>"
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub domain: Option<String>,
152    /// Which AI agent stored this memory (e.g. "claude-code", "chatgpt")
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub source_agent: Option<String>,
155    /// Confidence score (0.0-1.0) assigned by the storing agent
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub confidence: Option<f32>,
158    /// Whether a human has confirmed this memory
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub confirmed: Option<bool>,
161    /// Stability tier: "new", "learned", or "confirmed"
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub stability: Option<String>,
164    /// source_id of the memory this entry supersedes (version chain)
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub supersedes: Option<String>,
167    /// Whether this is a pending revision awaiting human approval (Protected tier supersede)
168    #[serde(default)]
169    pub pending_revision: bool,
170    /// Link to a knowledge graph entity (nullable, cascade handled manually)
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub entity_id: Option<String>,
173    /// Quality assessment: "low", "medium", "high" (NULL = unassessed)
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub quality: Option<String>,
176    /// Whether this memory is a recap/summary of other memories
177    #[serde(default)]
178    pub is_recap: bool,
179    /// Deprecated: enrichment status is now derived from the `enrichment_steps` table.
180    /// This field is ignored on INSERT. Kept for API compatibility with downstream consumers.
181    #[serde(default = "default_enrichment_status")]
182    pub enrichment_status: String,
183    /// How superseded content is handled: "hide" (default) or "archive" (visible but muted)
184    #[serde(default = "default_supersede_mode")]
185    pub supersede_mode: String,
186    /// JSON object with type-specific structured fields (e.g. {"claim": "...", "context": "..."})
187    #[serde(default, skip_serializing_if = "Option::is_none")]
188    pub structured_fields: Option<String>,
189    /// LLM-generated question this memory answers -- embedded for vector search
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub retrieval_cue: Option<String>,
192    /// Original prose content, preserved when structured_fields are promoted to primary content
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub source_text: Option<String>,
195}
196
197fn default_enrichment_status() -> String {
198    "raw".to_string()
199}
200
201fn default_supersede_mode() -> String {
202    "hide".to_string()
203}
204
205impl Default for RawDocument {
206    fn default() -> Self {
207        Self {
208            source: String::new(),
209            source_id: String::new(),
210            title: String::new(),
211            summary: None,
212            content: String::new(),
213            url: None,
214            last_modified: 0,
215            metadata: HashMap::new(),
216            memory_type: None,
217            domain: None,
218            source_agent: None,
219            confidence: None,
220            confirmed: None,
221            stability: None,
222            supersedes: None,
223            pending_revision: false,
224            entity_id: None,
225            quality: None,
226            is_recap: false,
227            enrichment_status: "raw".to_string(),
228            supersede_mode: "hide".to_string(),
229            structured_fields: None,
230            retrieval_cue: None,
231            source_text: None,
232        }
233    }
234}
235
236/// Persisted source type.
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(rename_all = "lowercase")]
239pub enum SourceType {
240    Obsidian,
241    Directory,
242}
243
244impl SourceType {
245    pub fn as_str(&self) -> &'static str {
246        match self {
247            Self::Obsidian => "obsidian",
248            Self::Directory => "directory",
249        }
250    }
251}
252
253/// Sync status for a connected source.
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub enum SyncStatus {
256    Active,
257    Paused,
258    Error(String),
259}