ryo-analysis 0.1.0

Code graph and discovery engine for the RYO project
Documentation
//! DiscoveryResult - Results from symbol discovery.

use crate::symbol::{FileSpan, SymbolId, SymbolPath, Uuid, Visibility};
use crate::SymbolKind;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Result of a discovery query.
#[derive(Debug, Clone, Default)]
pub struct DiscoveryResult {
    /// Discovered symbols.
    pub symbols: Vec<DiscoveredSymbol>,
    /// Relation graph (if relations were requested).
    pub relations: Option<RelationGraph>,
    /// Total matches before limit was applied.
    pub total_matches: usize,
    /// Whether results were truncated by limit.
    pub truncated: bool,
}

impl DiscoveryResult {
    /// Create an empty result.
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a discovered symbol.
    pub fn add(&mut self, symbol: DiscoveredSymbol) {
        self.symbols.push(symbol);
    }

    /// Get the number of symbols found.
    pub fn len(&self) -> usize {
        self.symbols.len()
    }

    /// Check if no symbols were found.
    pub fn is_empty(&self) -> bool {
        self.symbols.is_empty()
    }

    /// Get the first symbol.
    pub fn first(&self) -> Option<&DiscoveredSymbol> {
        self.symbols.first()
    }

    /// Iterate over symbols.
    pub fn iter(&self) -> impl Iterator<Item = &DiscoveredSymbol> {
        self.symbols.iter()
    }

    /// Get symbols as paths.
    pub fn paths(&self) -> impl Iterator<Item = &SymbolPath> {
        self.symbols.iter().map(|s| &s.path)
    }

    /// Get symbols as IDs.
    pub fn ids(&self) -> impl Iterator<Item = SymbolId> + '_ {
        self.symbols.iter().map(|s| s.id)
    }
}

/// A discovered symbol with metadata.
#[derive(Debug, Clone, Serialize)]
pub struct DiscoveredSymbol {
    /// Symbol ID (session-volatile).
    pub id: SymbolId,
    /// Persistent UUID for cross-session tracking.
    ///
    /// This UUID survives server restarts and symbol renames.
    /// Returns `None` if the symbol hasn't been assigned a persistent ID.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub uuid: Option<Uuid>,
    /// Symbol path.
    pub path: SymbolPath,
    /// Symbol kind.
    pub kind: SymbolKind,
    /// File location (if available).
    pub span: Option<FileSpan>,
    /// Visibility (if available).
    pub visibility: Option<Visibility>,
    /// Match score (for ranking).
    pub score: f32,
    /// Reference count (how often this symbol is used).
    pub ref_count: usize,
    /// Impl count (number of implementations for traits).
    pub impl_count: usize,
}

impl DiscoveredSymbol {
    /// Create a new discovered symbol.
    pub fn new(id: SymbolId, path: SymbolPath, kind: SymbolKind) -> Self {
        Self {
            id,
            uuid: None,
            path,
            kind,
            span: None,
            visibility: None,
            score: 1.0,
            ref_count: 0,
            impl_count: 0,
        }
    }

    /// Set the persistent UUID.
    pub fn with_uuid(mut self, uuid: Uuid) -> Self {
        self.uuid = Some(uuid);
        self
    }

    /// Set the file span.
    pub fn with_span(mut self, span: FileSpan) -> Self {
        self.span = Some(span);
        self
    }

    /// Set the visibility.
    pub fn with_visibility(mut self, visibility: Visibility) -> Self {
        self.visibility = Some(visibility);
        self
    }

    /// Set the match score.
    pub fn with_score(mut self, score: f32) -> Self {
        self.score = score;
        self
    }

    /// Set the reference count.
    pub fn with_ref_count(mut self, ref_count: usize) -> Self {
        self.ref_count = ref_count;
        self
    }

    /// Set the impl count.
    pub fn with_impl_count(mut self, impl_count: usize) -> Self {
        self.impl_count = impl_count;
        self
    }

    /// Check if this symbol is public.
    pub fn is_public(&self) -> bool {
        self.visibility.as_ref().is_some_and(|v| v.is_public())
    }
}

/// Graph of symbol relations.
#[derive(Debug, Clone, Default)]
pub struct RelationGraph {
    /// Relations keyed by source symbol ID.
    relations: HashMap<SymbolId, Vec<Relation>>,
}

/// A relation between symbols.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Relation {
    /// Target symbol ID.
    pub target: SymbolId,
    /// Kind of relation.
    pub kind: RelationKind,
}

/// Kind of relation between symbols.
///
/// Three axes of symbol usage:
/// - **Call**: `Calls` / `CalledBy` — function call relationships (from CodeGraphV2)
/// - **Type**: `TypeReferences` / `TypeReferencedBy` — type usage relationships (from TypeFlowGraphV2)
/// - **Trait**: `Implements` / `ImplementedBy` — trait implementation relationships (from CodeGraphV2)
///
/// Plus structural containment: `Contains` / `ContainedBy`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RelationKind {
    /// Symbol calls target (function call).
    Calls,
    /// Symbol is called by target.
    CalledBy,
    /// Symbol references target as a type (field type, param type, return type, etc.).
    TypeReferences,
    /// Symbol's type is referenced by target.
    TypeReferencedBy,
    /// Symbol implements target (trait).
    Implements,
    /// Symbol is implemented by target.
    ImplementedBy,
    /// Symbol contains target (parent-child).
    Contains,
    /// Symbol is contained by target.
    ContainedBy,
}

impl RelationGraph {
    /// Create a new empty relation graph.
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a relation.
    pub fn add(&mut self, source: SymbolId, target: SymbolId, kind: RelationKind) {
        self.relations
            .entry(source)
            .or_default()
            .push(Relation { target, kind });
    }

    /// Get relations for a symbol.
    pub fn get(&self, id: SymbolId) -> &[Relation] {
        self.relations.get(&id).map_or(&[], |v| v.as_slice())
    }

    /// Get relations of a specific kind.
    pub fn get_by_kind(&self, id: SymbolId, kind: RelationKind) -> Vec<SymbolId> {
        self.get(id)
            .iter()
            .filter(|r| r.kind == kind)
            .map(|r| r.target)
            .collect()
    }

    /// Check if there are any relations.
    pub fn is_empty(&self) -> bool {
        self.relations.is_empty()
    }

    /// Get the number of symbols with relations.
    pub fn len(&self) -> usize {
        self.relations.len()
    }

    /// Iterate over all relations.
    pub fn iter(&self) -> impl Iterator<Item = (SymbolId, &[Relation])> {
        self.relations
            .iter()
            .map(|(id, rels)| (*id, rels.as_slice()))
    }

    /// Get all source symbol IDs.
    pub fn sources(&self) -> impl Iterator<Item = SymbolId> + '_ {
        self.relations.keys().copied()
    }
}

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

    #[test]
    fn test_discovery_result() {
        let result = DiscoveryResult::new();
        assert!(result.is_empty());

        // We need a SymbolId, but for testing we can't easily create one
        // So we'll just test the empty case
        assert_eq!(result.len(), 0);
    }

    #[test]
    fn test_relation_graph() {
        use slotmap::SlotMap;

        let mut slots: SlotMap<SymbolId, ()> = SlotMap::with_key();
        let id1 = slots.insert(());
        let id2 = slots.insert(());

        let mut graph = RelationGraph::new();
        graph.add(id1, id2, RelationKind::Calls);

        assert!(!graph.is_empty());
        assert_eq!(graph.get(id1).len(), 1);
        assert_eq!(graph.get_by_kind(id1, RelationKind::Calls), vec![id2]);
    }

    #[test]
    fn test_relation_kind() {
        assert_ne!(RelationKind::Calls, RelationKind::CalledBy);
    }
}