xz-skill-core 0.2.0

Core abstractions for the xz-skill plugin system: SkillExecutor, SkillLoader, SkillRegistry, and shared types
Documentation
use async_trait::async_trait;
use std::fmt::Debug;

use crate::error::SkillError;
use crate::types::filter::SkillFilter;
use crate::types::output::SkillSummary;
use crate::types::skill::{Skill, UpsertResult};

/// Skill lifecycle management trait.
///
/// `SkillRegistry` defines the core abstraction for managing skills throughout
/// their lifecycle: registration, lookup, search, disable/enable, and removal.
///
/// Implementations may be backed by in-memory collections, file-system
/// manifests, SQLite databases, or any other storage layer. All operations
/// are async to avoid blocking the runtime on I/O.
///
/// # Required bounds
///
/// `Send + Sync` so the registry can be shared across tasks via `Arc`.
/// `Debug` so tracing and logging can inspect the registry for diagnostics.
///
/// # Disabled skills
///
/// When [`enable`](Self::enable) is set to `false`, the skill is considered
/// disabled. Disabled skills MAY still appear in [`get`](Self::get) results
/// but MUST NOT be executed. Implementations SHOULD exclude disabled skills
/// from [`list`](Self::list) results by default (use [`SkillFilter::include_disabled`]
/// to override).
///
/// # Thread safety
///
/// All methods take `&self` (not `&mut self`), allowing shared references
/// to be used concurrently. Implementations must use interior mutability
/// (e.g. `tokio::sync::RwLock`, `std::sync::RwLock`) for mutable state.
///
/// # Examples
///
/// ```ignore
/// use xz_skill_core::registry::SkillRegistry;
/// use xz_skill_core::types::skill::{Skill, UpsertResult};
///
/// async fn example(registry: &dyn SkillRegistry) -> Result<(), SkillError> {
///     let count = registry.count().await?;
///     assert_eq!(count, 0);
///
///     let skill = Skill { id: "my-skill".into(), ..Default::default() };
///     let result = registry.register(skill).await?;
///     assert!(matches!(result, UpsertResult::Created));
///
///     let retrieved = registry.get("my-skill").await?;
///     assert!(retrieved.is_some());
///
///     registry.enable("my-skill", false).await?;
///     registry.unregister("my-skill").await?;
///     Ok(())
/// }
/// ```
#[async_trait]
pub trait SkillRegistry: Send + Sync + Debug {
    /// Register a new skill or update an existing one.
    ///
    /// If a skill with the same ID already exists, it is replaced and the
    /// method returns [`UpsertResult::Updated`]. Otherwise, a new entry is
    /// created and [`UpsertResult::Created`] is returned. If the incoming
    /// skill is identical to the stored skill, implementations SHOULD return
    /// [`UpsertResult::Unchanged`] and avoid unnecessary writes.
    async fn register(&self, skill: Skill) -> Result<UpsertResult, SkillError>;

    /// Remove a skill by its unique ID.
    ///
    /// If the ID is not found this is a no-op and returns `Ok(())`.
    /// Implementations MUST NOT treat unknown IDs as errors.
    async fn unregister(&self, id: &str) -> Result<(), SkillError>;

    /// Retrieve the full [`Skill`] by ID.
    ///
    /// Returns `Ok(None)` when the skill does not exist. Disabled skills
    /// are still returned — use [`SkillFilter::include_disabled`] or
    /// check the enabled flag on the result.
    async fn get(&self, id: &str) -> Result<Option<Skill>, SkillError>;

    /// List skill summaries matching the given filter.
    ///
    /// Returns lightweight [`SkillSummary`] records suitable for display in
    /// registries, marketplaces, or CLI listings. The filter supports
    /// category scoping, enabled/disabled toggles, and pagination.
    ///
    /// By default, disabled skills are excluded. Set
    /// [`SkillFilter::include_disabled`] to `true` to include them.
    async fn list(&self, filter: &SkillFilter) -> Result<Vec<SkillSummary>, SkillError>;

    /// Search across skill names, descriptions, tags, and author metadata.
    ///
    /// The `query` string is matched as a free-text / substring search.
    /// Results are returned as [`SkillSummary`] records ordered by relevance
    /// (implementations decide the ranking strategy).
    ///
    /// Disabled skills are excluded from search results unless the
    /// implementation's policy explicitly includes them.
    async fn search(&self, query: &str) -> Result<Vec<SkillSummary>, SkillError>;

    /// Enable or disable a skill by ID.
    ///
    /// Disabled skills cannot be loaded or executed. If the skill ID is not
    /// found, implementations SHOULD return a [`SkillError`] (e.g.
    /// `SkillError::NotFound`) rather than silently succeeding.
    ///
    /// Toggling a skill to the same enabled state it already has is a no-op.
    async fn enable(&self, id: &str, enabled: bool) -> Result<(), SkillError>;

    /// Return the total number of registered skills (including disabled ones).
    ///
    /// This count is meant for monitoring and statistics. Use
    /// [`list`](Self::list) with [`SkillFilter::include_disabled`] for
    /// breakdowns by status.
    async fn count(&self) -> Result<usize, SkillError>;
}