ryo-suggest 0.1.0

[experimental] Pattern-based suggestion engine for RYO
Documentation
//! IntentLock - Dirty read prevention for concurrent LLM operations
//!
//! When an Intent is being executed, other LLM agents should not read stale state
//! or queue conflicting operations. This prevents dirty reads and ensures consistency.
//!
//! # Design Overview
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────────┐
//! │                     IntentLock Flow                              │
//! ├─────────────────────────────────────────────────────────────────┤
//! │                                                                  │
//! │  LLM Agent 1                    LLM Agent 2                      │
//! │       │                              │                           │
//! │       │ try_acquire(symbols)         │                           │
//! │       ▼                              │                           │
//! │  ┌─────────┐                         │                           │
//! │  │ LOCKED  │◄────────────────────────┤ try_acquire(same symbols) │
//! │  │SymbolA  │                         │      → Blocked!           │
//! │  │SymbolB  │                         │                           │
//! │  └────┬────┘                         │                           │
//! │       │                              │                           │
//! │       │ execute mutation             │                           │
//! │       │                              │                           │
//! │       │ drop(guard)                  │                           │
//! │       ▼                              ▼                           │
//! │  ┌─────────┐                    ┌─────────┐                      │
//! │  │UNLOCKED │                    │ LOCKED  │ ← Now can acquire    │
//! │  └─────────┘                    └─────────┘                      │
//! │                                                                  │
//! └─────────────────────────────────────────────────────────────────┘
//! ```
//!
//! # Usage
//!
//! ```ignore
//! use ryo_suggest::intent_lock::{IntentLock, IntentId};
//!
//! let lock = IntentLock::new();
//!
//! // Try to acquire lock for symbols
//! let guard = lock.try_acquire(IntentId::new(), &target_symbols)?;
//!
//! // Execute with lock held
//! executor.execute(mutation).await?;
//!
//! // Lock released when guard drops
//! drop(guard);
//! ```
//!
//! # Integration with SuggestService
//!
//! When querying suggestions, the IntentLock should be consulted to filter out
//! suggestions targeting locked symbols:
//!
//! ```ignore
//! impl SuggestService {
//!     pub fn top_unlocked(
//!         &self,
//!         n: usize,
//!         intent_lock: &impl IntentLockQuery,
//!     ) -> Vec<RankedSuggestion> {
//!         self.store.read()
//!             .iter()
//!             .filter(|(_, sug)| !intent_lock.is_locked(&sug.opportunity.targets))
//!             .take(n)
//!             .collect()
//!     }
//! }
//! ```
//!
//! # Implementation Notes
//!
//! This module provides traits for IntentLock. The actual implementation lives
//! in the server layer (ryo-server) where the full concurrent execution context
//! is available.

use ryo_analysis::SymbolId;
use serde::{Deserialize, Serialize};
use std::fmt;

/// Unique identifier for an Intent (mutation request)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IntentId(pub u64);

impl IntentId {
    /// Create a new IntentId
    pub fn new(id: u64) -> Self {
        Self(id)
    }

    /// Get the raw ID value
    pub fn as_u64(self) -> u64 {
        self.0
    }
}

impl fmt::Display for IntentId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "I{:06}", self.0)
    }
}

/// Error when acquiring an IntentLock
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LockError {
    /// Lock acquisition blocked by another intent
    Blocked {
        /// The symbol that is locked
        symbol: SymbolId,
        /// The intent holding the lock
        blocking_intent: IntentId,
    },
}

impl fmt::Display for LockError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Blocked {
                symbol,
                blocking_intent,
            } => {
                write!(
                    f,
                    "Symbol {:?} is locked by intent {}",
                    symbol, blocking_intent
                )
            }
        }
    }
}

impl std::error::Error for LockError {}

/// Query interface for checking lock status
///
/// This trait allows checking if symbols are locked without requiring
/// write access to the lock state.
pub trait IntentLockQuery: Send + Sync {
    /// Check if any of the given symbols are currently locked
    fn is_locked(&self, symbols: &[SymbolId]) -> bool;

    /// Get the intent holding the lock on a symbol (if any)
    fn locked_by(&self, symbol: &SymbolId) -> Option<IntentId>;

    /// Get all currently locked symbols
    fn locked_symbols(&self) -> Vec<SymbolId>;
}

/// Full interface for IntentLock operations
///
/// Extends IntentLockQuery with mutation capabilities.
/// Implementations should use interior mutability (e.g., RwLock)
/// to allow concurrent query access.
pub trait IntentLockOps: IntentLockQuery {
    /// Guard type returned when lock is acquired
    type Guard<'a>
    where
        Self: 'a;

    /// Try to acquire locks for the given symbols
    ///
    /// Returns:
    /// - `Ok(guard)` if all symbols were successfully locked
    /// - `Err(LockError::Blocked)` if any symbol is already locked
    ///
    /// The guard should release the locks when dropped.
    fn try_acquire(
        &self,
        intent_id: IntentId,
        symbols: &[SymbolId],
    ) -> Result<Self::Guard<'_>, LockError>;

    /// Force release all locks for an intent (for cleanup/timeout)
    fn force_release(&self, intent_id: IntentId);
}

/// A no-op implementation for single-threaded or testing contexts
#[derive(Debug, Default)]
pub struct NoOpIntentLock;

impl IntentLockQuery for NoOpIntentLock {
    fn is_locked(&self, _symbols: &[SymbolId]) -> bool {
        false
    }

    fn locked_by(&self, _symbol: &SymbolId) -> Option<IntentId> {
        None
    }

    fn locked_symbols(&self) -> Vec<SymbolId> {
        vec![]
    }
}

impl IntentLockOps for NoOpIntentLock {
    type Guard<'a> = ();

    fn try_acquire(
        &self,
        _intent_id: IntentId,
        _symbols: &[SymbolId],
    ) -> Result<Self::Guard<'_>, LockError> {
        Ok(())
    }

    fn force_release(&self, _intent_id: IntentId) {}
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_intent_id_display() {
        let id = IntentId::new(42);
        assert_eq!(id.to_string(), "I000042");
    }

    #[test]
    fn test_lock_error_display() {
        let err = LockError::Blocked {
            symbol: SymbolId::parse("100v1").unwrap(),
            blocking_intent: IntentId::new(1),
        };
        assert!(err.to_string().contains("locked by intent"));
    }

    #[test]
    fn test_noop_lock() {
        let lock = NoOpIntentLock;
        let sym = SymbolId::parse("100v1").unwrap();

        assert!(!lock.is_locked(&[sym]));
        assert!(lock.try_acquire(IntentId::new(1), &[sym]).is_ok());
    }
}