rusty-beads 0.1.0

Git-backed graph issue tracker for AI coding agents - a Rust implementation with context store, dependency tracking, and semantic compaction
Documentation
//! Context store types.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Namespace for context keys.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Namespace {
    /// File-level context (summaries, AST, symbols).
    File,
    /// Symbol definitions (functions, classes, types).
    Symbol,
    /// Project-level context (architecture, patterns).
    Project,
    /// Session context (recent decisions, working files).
    Session,
    /// Agent-specific context (preferences, patterns).
    Agent,
    /// Custom namespace.
    Custom,
}

impl Namespace {
    /// Get the string prefix for this namespace.
    pub fn prefix(&self) -> &'static str {
        match self {
            Namespace::File => "file:",
            Namespace::Symbol => "symbol:",
            Namespace::Project => "project:",
            Namespace::Session => "session:",
            Namespace::Agent => "agent:",
            Namespace::Custom => "custom:",
        }
    }

    /// Parse namespace from a key.
    pub fn from_key(key: &str) -> (Self, &str) {
        if let Some(rest) = key.strip_prefix("file:") {
            (Namespace::File, rest)
        } else if let Some(rest) = key.strip_prefix("symbol:") {
            (Namespace::Symbol, rest)
        } else if let Some(rest) = key.strip_prefix("project:") {
            (Namespace::Project, rest)
        } else if let Some(rest) = key.strip_prefix("session:") {
            (Namespace::Session, rest)
        } else if let Some(rest) = key.strip_prefix("agent:") {
            (Namespace::Agent, rest)
        } else if let Some(rest) = key.strip_prefix("custom:") {
            (Namespace::Custom, rest)
        } else {
            (Namespace::Custom, key)
        }
    }
}

/// A context entry in the store.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextEntry {
    /// The full key (including namespace prefix).
    pub key: String,

    /// The value (JSON).
    pub value: serde_json::Value,

    /// When the entry was created.
    pub created_at: DateTime<Utc>,

    /// When the entry was last updated.
    pub updated_at: DateTime<Utc>,

    /// When the entry expires (optional TTL).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires_at: Option<DateTime<Utc>>,

    /// Git commit hash when entry was created (for invalidation).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub git_commit: Option<String>,

    /// File path this entry relates to (for file-level context).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub file_path: Option<String>,

    /// File modification time when entry was created.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub file_mtime: Option<i64>,

    /// Custom metadata.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub metadata: HashMap<String, String>,
}

impl ContextEntry {
    /// Create a new context entry.
    pub fn new(key: impl Into<String>, value: serde_json::Value) -> Self {
        let now = Utc::now();
        Self {
            key: key.into(),
            value,
            created_at: now,
            updated_at: now,
            expires_at: None,
            git_commit: None,
            file_path: None,
            file_mtime: None,
            metadata: HashMap::new(),
        }
    }

    /// Set TTL for this entry.
    pub fn with_ttl(mut self, ttl_secs: i64) -> Self {
        self.expires_at = Some(Utc::now() + chrono::Duration::seconds(ttl_secs));
        self
    }

    /// Set git commit for invalidation.
    pub fn with_git_commit(mut self, commit: impl Into<String>) -> Self {
        self.git_commit = Some(commit.into());
        self
    }

    /// Set file path and mtime for invalidation.
    pub fn with_file_info(mut self, path: impl Into<String>, mtime: i64) -> Self {
        self.file_path = Some(path.into());
        self.file_mtime = Some(mtime);
        self
    }

    /// Add metadata.
    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.metadata.insert(key.into(), value.into());
        self
    }

    /// Check if this entry has expired.
    pub fn is_expired(&self) -> bool {
        if let Some(expires_at) = self.expires_at {
            Utc::now() > expires_at
        } else {
            false
        }
    }

    /// Get the namespace of this entry.
    pub fn namespace(&self) -> Namespace {
        Namespace::from_key(&self.key).0
    }
}

/// File context - structured context about a source file.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FileContext {
    /// File path.
    pub path: String,

    /// Brief summary of the file's purpose.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary: Option<String>,

    /// Programming language.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub language: Option<String>,

    /// Symbols defined in this file.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub symbols: Vec<SymbolInfo>,

    /// Imports/dependencies.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub imports: Vec<String>,

    /// Exports (for modules).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub exports: Vec<String>,

    /// Line count.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub line_count: Option<usize>,

    /// Complexity metrics.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub metrics: HashMap<String, f64>,

    /// Related files.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub related_files: Vec<String>,

    /// Tags/labels.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,
}

/// Symbol information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolInfo {
    /// Symbol name.
    pub name: String,

    /// Symbol kind (function, class, struct, enum, etc.).
    pub kind: SymbolKind,

    /// Line number where symbol is defined.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub line: Option<usize>,

    /// Brief description.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// Signature (for functions).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub signature: Option<String>,

    /// Visibility (public, private, etc.).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub visibility: Option<String>,
}

/// Kind of symbol.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SymbolKind {
    Function,
    Method,
    Class,
    Struct,
    Enum,
    Interface,
    Trait,
    Type,
    Constant,
    Variable,
    Module,
    Other,
}

/// Project context - high-level project information.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProjectContext {
    /// Project name.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,

    /// Project description.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// Primary language(s).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub languages: Vec<String>,

    /// Frameworks/libraries used.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub frameworks: Vec<String>,

    /// Architecture pattern.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub architecture: Option<String>,

    /// Key directories and their purposes.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub directories: HashMap<String, String>,

    /// Coding conventions/patterns observed.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub conventions: Vec<String>,

    /// Entry points.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub entry_points: Vec<String>,

    /// Test patterns.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub test_pattern: Option<String>,

    /// Build commands.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub build_commands: HashMap<String, String>,
}

/// Session context - current working context for an agent.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SessionContext {
    /// Session ID.
    pub session_id: String,

    /// Agent ID.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub agent_id: Option<String>,

    /// Files currently being worked on.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub working_files: Vec<String>,

    /// Recent decisions made.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub decisions: Vec<Decision>,

    /// Current task/goal.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub current_task: Option<String>,

    /// Relevant issue IDs.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub related_issues: Vec<String>,

    /// Accumulated learnings from this session.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub learnings: Vec<String>,

    /// Session start time.
    pub started_at: DateTime<Utc>,

    /// Last activity time.
    pub last_activity: DateTime<Utc>,
}

/// A decision made during a session.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Decision {
    /// What was decided.
    pub decision: String,

    /// Why it was decided.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rationale: Option<String>,

    /// When it was decided.
    pub timestamp: DateTime<Utc>,

    /// Related files/symbols.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub context: Vec<String>,
}

/// Query options for listing context entries.
#[derive(Debug, Clone, Default)]
pub struct ContextQuery {
    /// Filter by namespace.
    pub namespace: Option<Namespace>,

    /// Filter by key prefix (within namespace).
    pub prefix: Option<String>,

    /// Include expired entries.
    pub include_expired: bool,

    /// Maximum number of results.
    pub limit: Option<usize>,

    /// Offset for pagination.
    pub offset: Option<usize>,
}

impl ContextQuery {
    /// Create a new query.
    pub fn new() -> Self {
        Self::default()
    }

    /// Filter by namespace.
    pub fn namespace(mut self, ns: Namespace) -> Self {
        self.namespace = Some(ns);
        self
    }

    /// Filter by key prefix.
    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
        self.prefix = Some(prefix.into());
        self
    }

    /// Include expired entries.
    pub fn include_expired(mut self) -> Self {
        self.include_expired = true;
        self
    }

    /// Limit results.
    pub fn limit(mut self, limit: usize) -> Self {
        self.limit = Some(limit);
        self
    }
}