rusty_beads/context/
types.rs

1//! Context store types.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Namespace for context keys.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum Namespace {
11    /// File-level context (summaries, AST, symbols).
12    File,
13    /// Symbol definitions (functions, classes, types).
14    Symbol,
15    /// Project-level context (architecture, patterns).
16    Project,
17    /// Session context (recent decisions, working files).
18    Session,
19    /// Agent-specific context (preferences, patterns).
20    Agent,
21    /// Custom namespace.
22    Custom,
23}
24
25impl Namespace {
26    /// Get the string prefix for this namespace.
27    pub fn prefix(&self) -> &'static str {
28        match self {
29            Namespace::File => "file:",
30            Namespace::Symbol => "symbol:",
31            Namespace::Project => "project:",
32            Namespace::Session => "session:",
33            Namespace::Agent => "agent:",
34            Namespace::Custom => "custom:",
35        }
36    }
37
38    /// Parse namespace from a key.
39    pub fn from_key(key: &str) -> (Self, &str) {
40        if let Some(rest) = key.strip_prefix("file:") {
41            (Namespace::File, rest)
42        } else if let Some(rest) = key.strip_prefix("symbol:") {
43            (Namespace::Symbol, rest)
44        } else if let Some(rest) = key.strip_prefix("project:") {
45            (Namespace::Project, rest)
46        } else if let Some(rest) = key.strip_prefix("session:") {
47            (Namespace::Session, rest)
48        } else if let Some(rest) = key.strip_prefix("agent:") {
49            (Namespace::Agent, rest)
50        } else if let Some(rest) = key.strip_prefix("custom:") {
51            (Namespace::Custom, rest)
52        } else {
53            (Namespace::Custom, key)
54        }
55    }
56}
57
58/// A context entry in the store.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ContextEntry {
61    /// The full key (including namespace prefix).
62    pub key: String,
63
64    /// The value (JSON).
65    pub value: serde_json::Value,
66
67    /// When the entry was created.
68    pub created_at: DateTime<Utc>,
69
70    /// When the entry was last updated.
71    pub updated_at: DateTime<Utc>,
72
73    /// When the entry expires (optional TTL).
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub expires_at: Option<DateTime<Utc>>,
76
77    /// Git commit hash when entry was created (for invalidation).
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub git_commit: Option<String>,
80
81    /// File path this entry relates to (for file-level context).
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub file_path: Option<String>,
84
85    /// File modification time when entry was created.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub file_mtime: Option<i64>,
88
89    /// Custom metadata.
90    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
91    pub metadata: HashMap<String, String>,
92}
93
94impl ContextEntry {
95    /// Create a new context entry.
96    pub fn new(key: impl Into<String>, value: serde_json::Value) -> Self {
97        let now = Utc::now();
98        Self {
99            key: key.into(),
100            value,
101            created_at: now,
102            updated_at: now,
103            expires_at: None,
104            git_commit: None,
105            file_path: None,
106            file_mtime: None,
107            metadata: HashMap::new(),
108        }
109    }
110
111    /// Set TTL for this entry.
112    pub fn with_ttl(mut self, ttl_secs: i64) -> Self {
113        self.expires_at = Some(Utc::now() + chrono::Duration::seconds(ttl_secs));
114        self
115    }
116
117    /// Set git commit for invalidation.
118    pub fn with_git_commit(mut self, commit: impl Into<String>) -> Self {
119        self.git_commit = Some(commit.into());
120        self
121    }
122
123    /// Set file path and mtime for invalidation.
124    pub fn with_file_info(mut self, path: impl Into<String>, mtime: i64) -> Self {
125        self.file_path = Some(path.into());
126        self.file_mtime = Some(mtime);
127        self
128    }
129
130    /// Add metadata.
131    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
132        self.metadata.insert(key.into(), value.into());
133        self
134    }
135
136    /// Check if this entry has expired.
137    pub fn is_expired(&self) -> bool {
138        if let Some(expires_at) = self.expires_at {
139            Utc::now() > expires_at
140        } else {
141            false
142        }
143    }
144
145    /// Get the namespace of this entry.
146    pub fn namespace(&self) -> Namespace {
147        Namespace::from_key(&self.key).0
148    }
149}
150
151/// File context - structured context about a source file.
152#[derive(Debug, Clone, Default, Serialize, Deserialize)]
153pub struct FileContext {
154    /// File path.
155    pub path: String,
156
157    /// Brief summary of the file's purpose.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub summary: Option<String>,
160
161    /// Programming language.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub language: Option<String>,
164
165    /// Symbols defined in this file.
166    #[serde(default, skip_serializing_if = "Vec::is_empty")]
167    pub symbols: Vec<SymbolInfo>,
168
169    /// Imports/dependencies.
170    #[serde(default, skip_serializing_if = "Vec::is_empty")]
171    pub imports: Vec<String>,
172
173    /// Exports (for modules).
174    #[serde(default, skip_serializing_if = "Vec::is_empty")]
175    pub exports: Vec<String>,
176
177    /// Line count.
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub line_count: Option<usize>,
180
181    /// Complexity metrics.
182    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
183    pub metrics: HashMap<String, f64>,
184
185    /// Related files.
186    #[serde(default, skip_serializing_if = "Vec::is_empty")]
187    pub related_files: Vec<String>,
188
189    /// Tags/labels.
190    #[serde(default, skip_serializing_if = "Vec::is_empty")]
191    pub tags: Vec<String>,
192}
193
194/// Symbol information.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct SymbolInfo {
197    /// Symbol name.
198    pub name: String,
199
200    /// Symbol kind (function, class, struct, enum, etc.).
201    pub kind: SymbolKind,
202
203    /// Line number where symbol is defined.
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub line: Option<usize>,
206
207    /// Brief description.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub description: Option<String>,
210
211    /// Signature (for functions).
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub signature: Option<String>,
214
215    /// Visibility (public, private, etc.).
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub visibility: Option<String>,
218}
219
220/// Kind of symbol.
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
222#[serde(rename_all = "snake_case")]
223pub enum SymbolKind {
224    Function,
225    Method,
226    Class,
227    Struct,
228    Enum,
229    Interface,
230    Trait,
231    Type,
232    Constant,
233    Variable,
234    Module,
235    Other,
236}
237
238/// Project context - high-level project information.
239#[derive(Debug, Clone, Default, Serialize, Deserialize)]
240pub struct ProjectContext {
241    /// Project name.
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub name: Option<String>,
244
245    /// Project description.
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub description: Option<String>,
248
249    /// Primary language(s).
250    #[serde(default, skip_serializing_if = "Vec::is_empty")]
251    pub languages: Vec<String>,
252
253    /// Frameworks/libraries used.
254    #[serde(default, skip_serializing_if = "Vec::is_empty")]
255    pub frameworks: Vec<String>,
256
257    /// Architecture pattern.
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub architecture: Option<String>,
260
261    /// Key directories and their purposes.
262    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
263    pub directories: HashMap<String, String>,
264
265    /// Coding conventions/patterns observed.
266    #[serde(default, skip_serializing_if = "Vec::is_empty")]
267    pub conventions: Vec<String>,
268
269    /// Entry points.
270    #[serde(default, skip_serializing_if = "Vec::is_empty")]
271    pub entry_points: Vec<String>,
272
273    /// Test patterns.
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub test_pattern: Option<String>,
276
277    /// Build commands.
278    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
279    pub build_commands: HashMap<String, String>,
280}
281
282/// Session context - current working context for an agent.
283#[derive(Debug, Clone, Default, Serialize, Deserialize)]
284pub struct SessionContext {
285    /// Session ID.
286    pub session_id: String,
287
288    /// Agent ID.
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub agent_id: Option<String>,
291
292    /// Files currently being worked on.
293    #[serde(default, skip_serializing_if = "Vec::is_empty")]
294    pub working_files: Vec<String>,
295
296    /// Recent decisions made.
297    #[serde(default, skip_serializing_if = "Vec::is_empty")]
298    pub decisions: Vec<Decision>,
299
300    /// Current task/goal.
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub current_task: Option<String>,
303
304    /// Relevant issue IDs.
305    #[serde(default, skip_serializing_if = "Vec::is_empty")]
306    pub related_issues: Vec<String>,
307
308    /// Accumulated learnings from this session.
309    #[serde(default, skip_serializing_if = "Vec::is_empty")]
310    pub learnings: Vec<String>,
311
312    /// Session start time.
313    pub started_at: DateTime<Utc>,
314
315    /// Last activity time.
316    pub last_activity: DateTime<Utc>,
317}
318
319/// A decision made during a session.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct Decision {
322    /// What was decided.
323    pub decision: String,
324
325    /// Why it was decided.
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub rationale: Option<String>,
328
329    /// When it was decided.
330    pub timestamp: DateTime<Utc>,
331
332    /// Related files/symbols.
333    #[serde(default, skip_serializing_if = "Vec::is_empty")]
334    pub context: Vec<String>,
335}
336
337/// Query options for listing context entries.
338#[derive(Debug, Clone, Default)]
339pub struct ContextQuery {
340    /// Filter by namespace.
341    pub namespace: Option<Namespace>,
342
343    /// Filter by key prefix (within namespace).
344    pub prefix: Option<String>,
345
346    /// Include expired entries.
347    pub include_expired: bool,
348
349    /// Maximum number of results.
350    pub limit: Option<usize>,
351
352    /// Offset for pagination.
353    pub offset: Option<usize>,
354}
355
356impl ContextQuery {
357    /// Create a new query.
358    pub fn new() -> Self {
359        Self::default()
360    }
361
362    /// Filter by namespace.
363    pub fn namespace(mut self, ns: Namespace) -> Self {
364        self.namespace = Some(ns);
365        self
366    }
367
368    /// Filter by key prefix.
369    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
370        self.prefix = Some(prefix.into());
371        self
372    }
373
374    /// Include expired entries.
375    pub fn include_expired(mut self) -> Self {
376        self.include_expired = true;
377        self
378    }
379
380    /// Limit results.
381    pub fn limit(mut self, limit: usize) -> Self {
382        self.limit = Some(limit);
383        self
384    }
385}