subcog/models/
domain.rs

1//! Domain and namespace types.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Memory namespace categories.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum Namespace {
10    /// Architectural and design decisions.
11    #[default]
12    Decisions,
13    /// Discovered patterns and conventions.
14    Patterns,
15    /// Lessons learned from debugging or issues.
16    Learnings,
17    /// Important contextual information.
18    Context,
19    /// Technical debts and future improvements.
20    #[serde(alias = "techdebt", alias = "tech_debt")]
21    #[serde(rename = "tech-debt")]
22    TechDebt,
23    /// Blockers and impediments.
24    Blockers,
25    /// Work progress and milestones.
26    Progress,
27    /// API endpoints and contracts.
28    Apis,
29    /// Configuration and environment details.
30    Config,
31    /// Security-related information.
32    Security,
33    /// Performance optimizations and benchmarks.
34    Performance,
35    /// Testing strategies and edge cases.
36    Testing,
37    /// Built-in help content (read-only system namespace).
38    Help,
39    /// Reusable prompt templates with variable substitution.
40    Prompts,
41}
42
43impl Namespace {
44    /// Returns all namespace variants.
45    #[must_use]
46    pub const fn all() -> &'static [Self] {
47        &[
48            Self::Decisions,
49            Self::Patterns,
50            Self::Learnings,
51            Self::Context,
52            Self::TechDebt,
53            Self::Blockers,
54            Self::Progress,
55            Self::Apis,
56            Self::Config,
57            Self::Security,
58            Self::Performance,
59            Self::Testing,
60            Self::Help,
61            Self::Prompts,
62        ]
63    }
64
65    /// Returns user-facing namespaces (excludes system namespaces like Help).
66    #[must_use]
67    pub const fn user_namespaces() -> &'static [Self] {
68        &[
69            Self::Decisions,
70            Self::Patterns,
71            Self::Learnings,
72            Self::Context,
73            Self::TechDebt,
74            Self::Blockers,
75            Self::Progress,
76            Self::Apis,
77            Self::Config,
78            Self::Security,
79            Self::Performance,
80            Self::Testing,
81            Self::Prompts,
82        ]
83    }
84
85    /// Returns the namespace as a string slice.
86    #[must_use]
87    pub const fn as_str(&self) -> &'static str {
88        match self {
89            Self::Decisions => "decisions",
90            Self::Patterns => "patterns",
91            Self::Learnings => "learnings",
92            Self::Context => "context",
93            Self::TechDebt => "tech-debt",
94            Self::Blockers => "blockers",
95            Self::Progress => "progress",
96            Self::Apis => "apis",
97            Self::Config => "config",
98            Self::Security => "security",
99            Self::Performance => "performance",
100            Self::Testing => "testing",
101            Self::Help => "help",
102            Self::Prompts => "prompts",
103        }
104    }
105
106    /// Returns true if this is a system namespace (read-only).
107    #[must_use]
108    pub const fn is_system(&self) -> bool {
109        matches!(self, Self::Help)
110    }
111
112    /// Parses a namespace from a string.
113    #[must_use]
114    pub fn parse(s: &str) -> Option<Self> {
115        match s.to_lowercase().as_str() {
116            "decisions" => Some(Self::Decisions),
117            "patterns" => Some(Self::Patterns),
118            "learnings" => Some(Self::Learnings),
119            "context" => Some(Self::Context),
120            "tech-debt" | "techdebt" | "tech_debt" => Some(Self::TechDebt),
121            "blockers" => Some(Self::Blockers),
122            "progress" => Some(Self::Progress),
123            "apis" => Some(Self::Apis),
124            "config" => Some(Self::Config),
125            "security" => Some(Self::Security),
126            "performance" => Some(Self::Performance),
127            "testing" => Some(Self::Testing),
128            "help" => Some(Self::Help),
129            "prompts" => Some(Self::Prompts),
130            _ => None,
131        }
132    }
133}
134
135impl fmt::Display for Namespace {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        write!(f, "{}", self.as_str())
138    }
139}
140
141impl std::str::FromStr for Namespace {
142    type Err = String;
143
144    fn from_str(s: &str) -> Result<Self, Self::Err> {
145        Self::parse(s).ok_or_else(|| format!("unknown namespace: {s}"))
146    }
147}
148
149/// Domain separation for memories.
150#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
151pub struct Domain {
152    /// Organization or team identifier.
153    pub organization: Option<String>,
154    /// Project identifier.
155    pub project: Option<String>,
156    /// Repository identifier.
157    pub repository: Option<String>,
158}
159
160impl Domain {
161    /// Creates a new domain with all fields empty.
162    #[must_use]
163    pub const fn new() -> Self {
164        Self {
165            organization: None,
166            project: None,
167            repository: None,
168        }
169    }
170
171    /// Creates a domain based on the current working directory context.
172    ///
173    /// - If in a git repository: returns a project-scoped domain
174    /// - If NOT in a git repository: returns a user-scoped domain
175    ///
176    /// This ensures memories are routed to the appropriate storage backend:
177    /// - Project domains use `SQLite` storage with project faceting
178    /// - User domains use sqlite storage
179    #[must_use]
180    pub fn default_for_context() -> Self {
181        use crate::storage::index::is_in_git_repo;
182
183        if is_in_git_repo() {
184            // In a git repo - use project scope (user-level storage with facets)
185            Self::new()
186        } else {
187            // Not in a git repo - use user scope
188            Self::for_user()
189        }
190    }
191
192    /// Creates a user-scoped domain.
193    ///
194    /// User-scoped memories are stored in the user's personal sqlite database
195    /// and are accessible across all projects.
196    #[must_use]
197    pub fn for_user() -> Self {
198        Self {
199            organization: None,
200            project: Some("user".to_string()),
201            repository: None,
202        }
203    }
204
205    /// Creates an organization-scoped domain.
206    ///
207    /// Org-scoped memories are stored in a shared organization database
208    /// and are accessible to all team members.
209    #[must_use]
210    pub fn for_org() -> Self {
211        Self {
212            organization: Some("org".to_string()),
213            project: None,
214            repository: None,
215        }
216    }
217
218    /// Creates a domain for a specific repository.
219    #[must_use]
220    pub fn for_repository(org: impl Into<String>, repo: impl Into<String>) -> Self {
221        Self {
222            organization: Some(org.into()),
223            project: None,
224            repository: Some(repo.into()),
225        }
226    }
227
228    /// Returns true if this is a project-scoped domain (no org/repo restrictions).
229    #[must_use]
230    pub const fn is_project_scoped(&self) -> bool {
231        self.organization.is_none() && self.project.is_none() && self.repository.is_none()
232    }
233
234    /// Returns true if this is a user-scoped domain.
235    #[must_use]
236    pub fn is_user(&self) -> bool {
237        self.project.as_deref() == Some("user") && self.organization.is_none()
238    }
239
240    /// Returns the scope string for URN construction.
241    ///
242    /// - `"project"` for project-scoped (no org/repo restrictions)
243    /// - `"org/{name}"` for organization-scoped
244    /// - `"{org}/{repo}"` for repository-scoped
245    #[must_use]
246    pub fn to_scope_string(&self) -> String {
247        match (&self.organization, &self.repository) {
248            (Some(org), Some(repo)) => format!("{org}/{repo}"),
249            (Some(org), None) => format!("org/{org}"),
250            (None, Some(repo)) => repo.clone(),
251            (None, None) => "project".to_string(),
252        }
253    }
254}
255
256impl fmt::Display for Domain {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        match (&self.organization, &self.project, &self.repository) {
259            (Some(org), Some(proj), Some(repo)) => write!(f, "{org}/{proj}/{repo}"),
260            (Some(org), None, Some(repo)) => write!(f, "{org}/{repo}"),
261            (Some(org), Some(proj), None) => write!(f, "{org}/{proj}"),
262            (Some(org), None, None) => write!(f, "{org}"),
263            // User-scoped domain shows as "user"
264            (None, Some(proj), _) if proj == "user" => write!(f, "user"),
265            (None, Some(proj), _) => write!(f, "{proj}"),
266            (None, None, Some(repo)) => write!(f, "{repo}"),
267            // Project-scoped domain (no org/repo restrictions)
268            (None, None, None) => write!(f, "project"),
269        }
270    }
271}
272
273/// Status of a memory entry.
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
275pub enum MemoryStatus {
276    /// Active and searchable.
277    #[default]
278    Active,
279    /// Archived but still searchable.
280    Archived,
281    /// Superseded by another memory.
282    Superseded,
283    /// Pending review or approval.
284    Pending,
285    /// Marked for deletion.
286    Deleted,
287    /// Soft-deleted, hidden by default.
288    Tombstoned,
289    /// Consolidated into a summary memory.
290    ///
291    /// This status indicates that the memory has been included in a consolidation
292    /// operation and is now referenced by a summary memory. The original memory
293    /// remains searchable and is linked to its summary via edge relationships.
294    Consolidated,
295}
296
297impl MemoryStatus {
298    /// Returns the status as a string slice.
299    #[must_use]
300    pub const fn as_str(&self) -> &'static str {
301        match self {
302            Self::Active => "active",
303            Self::Archived => "archived",
304            Self::Superseded => "superseded",
305            Self::Pending => "pending",
306            Self::Deleted => "deleted",
307            Self::Tombstoned => "tombstoned",
308            Self::Consolidated => "consolidated",
309        }
310    }
311}
312
313impl fmt::Display for MemoryStatus {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        write!(f, "{}", self.as_str())
316    }
317}