//! Skill use cases (ARCHITECTURE ยง14.2; L12). //! //! - **CRUD** in either scope: [`CreateSkill`], [`UpdateSkill`], [`DeleteSkill`], //! [`ListSkills`]. //! - **Assignment**: [`AssignSkillToAgent`] / [`UnassignSkillFromAgent`] mutate //! the project manifest entry's `skills` and announce //! [`DomainEvent::SkillAssigned`]. Both are idempotent. use std::sync::Arc; use domain::ports::{AgentContextStore, EventBus, IdGenerator, SkillStore}; use domain::{ AgentId, AgentManifest, DomainEvent, MarkdownDoc, Project, ProjectPath, Skill, SkillId, SkillRef, SkillScope, }; use crate::error::AppError; // --------------------------------------------------------------------------- // CreateSkill // --------------------------------------------------------------------------- /// Input for [`CreateSkill::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateSkillInput { /// Display name (also the `.md` stem on disk). pub name: String, /// Initial Markdown body. pub content: String, /// Scope the skill is created in (selects its backing store). pub scope: SkillScope, /// Active project root (used only for [`SkillScope::Project`]). pub project_root: ProjectPath, } /// Output of [`CreateSkill::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateSkillOutput { /// The created skill. pub skill: Skill, } /// Creates a skill in the store of its [`SkillScope`]. pub struct CreateSkill { skills: Arc, ids: Arc, } impl CreateSkill { /// Builds the use case from its ports. #[must_use] pub fn new(skills: Arc, ids: Arc) -> Self { Self { skills, ids } } /// Executes creation. /// /// # Errors /// - [`AppError::Invalid`] if `name`/`content` is empty, /// - [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: CreateSkillInput) -> Result { let id = SkillId::from_uuid(self.ids.new_uuid()); let skill = Skill::new(id, input.name, MarkdownDoc::new(input.content), input.scope) .map_err(|e| AppError::Invalid(e.to_string()))?; self.skills.save(&skill, &input.project_root).await?; Ok(CreateSkillOutput { skill }) } } // --------------------------------------------------------------------------- // UpdateSkill // --------------------------------------------------------------------------- /// Input for [`UpdateSkill::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdateSkillInput { /// Scope the skill lives in. pub scope: SkillScope, /// Skill to update. pub skill_id: SkillId, /// New Markdown body. pub content: String, /// Active project root (used only for [`SkillScope::Project`]). pub project_root: ProjectPath, } /// Output of [`UpdateSkill::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdateSkillOutput { /// The updated skill. pub skill: Skill, } /// Replaces a skill's content (re-validating the non-empty invariant). pub struct UpdateSkill { skills: Arc, } impl UpdateSkill { /// Builds the use case. #[must_use] pub fn new(skills: Arc) -> Self { Self { skills } } /// Executes the update. /// /// # Errors /// - [`AppError::NotFound`] if the skill is unknown in that scope, /// - [`AppError::Invalid`] if the new content is empty, /// - [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: UpdateSkillInput) -> Result { let current = self .skills .get(input.scope, &input.project_root, input.skill_id) .await?; let updated = current .with_content(MarkdownDoc::new(input.content)) .map_err(|e| AppError::Invalid(e.to_string()))?; self.skills.save(&updated, &input.project_root).await?; Ok(UpdateSkillOutput { skill: updated }) } } // --------------------------------------------------------------------------- // ListSkills / DeleteSkill // --------------------------------------------------------------------------- /// Input for [`ListSkills::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListSkillsInput { /// Scope to list. pub scope: SkillScope, /// Active project root (used only for [`SkillScope::Project`]). pub project_root: ProjectPath, } /// Output of [`ListSkills::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListSkillsOutput { /// All skills in the requested scope. pub skills: Vec, } /// Lists the skills in one scope. pub struct ListSkills { skills: Arc, } impl ListSkills { /// Builds the use case. #[must_use] pub fn new(skills: Arc) -> Self { Self { skills } } /// Lists skills in `input.scope`. /// /// # Errors /// [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: ListSkillsInput) -> Result { Ok(ListSkillsOutput { skills: self.skills.list(input.scope, &input.project_root).await?, }) } } /// Input for [`DeleteSkill::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeleteSkillInput { /// Scope the skill lives in. pub scope: SkillScope, /// Skill to delete. pub skill_id: SkillId, /// Active project root (used only for [`SkillScope::Project`]). pub project_root: ProjectPath, } /// Deletes a skill from its scope's store. /// /// Agents that referenced it keep their [`SkillRef`]; the injection step simply /// finds nothing to resolve for the now-absent skill and skips it. pub struct DeleteSkill { skills: Arc, } impl DeleteSkill { /// Builds the use case. #[must_use] pub fn new(skills: Arc) -> Self { Self { skills } } /// Deletes the skill. /// /// # Errors /// - [`AppError::NotFound`] if the skill is unknown in that scope, /// - [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: DeleteSkillInput) -> Result<(), AppError> { self.skills .delete(input.scope, &input.project_root, input.skill_id) .await?; Ok(()) } } // --------------------------------------------------------------------------- // AssignSkillToAgent / UnassignSkillFromAgent // --------------------------------------------------------------------------- /// Input for [`AssignSkillToAgent::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AssignSkillToAgentInput { /// The owning project. pub project: Project, /// The agent receiving the skill. pub agent_id: AgentId, /// The skill to assign. pub skill: SkillRef, } /// Assigns a skill to an agent by recording a [`SkillRef`] in its manifest entry. /// Idempotent: re-assigning the same skill is a no-op (no duplicate). pub struct AssignSkillToAgent { contexts: Arc, events: Arc, } impl AssignSkillToAgent { /// Builds the use case from its ports. #[must_use] pub fn new(contexts: Arc, events: Arc) -> Self { Self { contexts, events } } /// Executes the assignment. /// /// # Errors /// - [`AppError::NotFound`] if the agent is unknown to the project, /// - [`AppError::Invalid`] if the resulting manifest is invalid, /// - [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: AssignSkillToAgentInput) -> Result<(), AppError> { let mut manifest = self.contexts.load_manifest(&input.project).await?; let entry = manifest .entries .iter_mut() .find(|e| e.agent_id == input.agent_id) .ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?; // Mutate through the domain entity so the dedup invariant is enforced in // one place, then fold the result back into the manifest entry. let mut agent = entry .to_agent() .map_err(|e| AppError::Invalid(e.to_string()))?; let changed = agent.assign_skill(input.skill); *entry = domain::ManifestEntry::from_agent(&agent); if changed { self.persist_and_announce( &input.project, manifest, input.agent_id, input.skill.skill_id, true, ) .await?; } Ok(()) } /// Saves the manifest and announces the assignment change. async fn persist_and_announce( &self, project: &Project, manifest: AgentManifest, agent_id: AgentId, skill_id: SkillId, assigned: bool, ) -> Result<(), AppError> { let manifest = AgentManifest::new(manifest.version, manifest.entries) .map_err(|e| AppError::Invalid(e.to_string()))?; self.contexts.save_manifest(project, &manifest).await?; self.events.publish(DomainEvent::SkillAssigned { agent_id, skill_id, assigned, }); Ok(()) } } /// Input for [`UnassignSkillFromAgent::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UnassignSkillFromAgentInput { /// The owning project. pub project: Project, /// The agent losing the skill. pub agent_id: AgentId, /// The skill to unassign. pub skill_id: SkillId, } /// Removes a skill assignment from an agent. Idempotent: unassigning a skill the /// agent does not carry is a no-op. pub struct UnassignSkillFromAgent { contexts: Arc, events: Arc, } impl UnassignSkillFromAgent { /// Builds the use case from its ports. #[must_use] pub fn new(contexts: Arc, events: Arc) -> Self { Self { contexts, events } } /// Executes the un-assignment. /// /// # Errors /// - [`AppError::NotFound`] if the agent is unknown to the project, /// - [`AppError::Invalid`] if the resulting manifest is invalid, /// - [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: UnassignSkillFromAgentInput) -> Result<(), AppError> { let mut manifest = self.contexts.load_manifest(&input.project).await?; let entry = manifest .entries .iter_mut() .find(|e| e.agent_id == input.agent_id) .ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?; let mut agent = entry .to_agent() .map_err(|e| AppError::Invalid(e.to_string()))?; let changed = agent.unassign_skill(input.skill_id); *entry = domain::ManifestEntry::from_agent(&agent); if changed { let manifest = AgentManifest::new(manifest.version, manifest.entries) .map_err(|e| AppError::Invalid(e.to_string()))?; self.contexts .save_manifest(&input.project, &manifest) .await?; self.events.publish(DomainEvent::SkillAssigned { agent_id: input.agent_id, skill_id: input.skill_id, assigned: false, }); } Ok(()) } }