fix: fix some displays and features
This commit is contained in:
21
crates/application/src/skill/mod.rs
Normal file
21
crates/application/src/skill/mod.rs
Normal file
@ -0,0 +1,21 @@
|
||||
//! Skill use cases (ARCHITECTURE §14.2; L12).
|
||||
//!
|
||||
//! Skills are reusable, model-agnostic workflows (IdeA's universal equivalent of
|
||||
//! a CLI slash-command). This module owns their CRUD across both scopes
|
||||
//! ([`domain::skill::SkillScope`]) and the agent↔skill assignment that records a
|
||||
//! [`domain::skill::SkillRef`] in the project manifest. The actual injection of
|
||||
//! an assigned skill's body into the generated convention file happens at agent
|
||||
//! activation (L6).
|
||||
//!
|
||||
//! Every use case talks only to ports ([`domain::ports::SkillStore`],
|
||||
//! [`domain::ports::AgentContextStore`], [`domain::ports::IdGenerator`],
|
||||
//! [`domain::ports::EventBus`]).
|
||||
|
||||
mod usecases;
|
||||
|
||||
pub use usecases::{
|
||||
AssignSkillToAgent, AssignSkillToAgentInput, CreateSkill, CreateSkillInput, CreateSkillOutput,
|
||||
DeleteSkill, DeleteSkillInput, ListSkills, ListSkillsInput, ListSkillsOutput,
|
||||
UnassignSkillFromAgent, UnassignSkillFromAgentInput, UpdateSkill, UpdateSkillInput,
|
||||
UpdateSkillOutput,
|
||||
};
|
||||
339
crates/application/src/skill/usecases.rs
Normal file
339
crates/application/src/skill/usecases.rs
Normal file
@ -0,0 +1,339 @@
|
||||
//! 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<dyn SkillStore>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
}
|
||||
|
||||
impl CreateSkill {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(skills: Arc<dyn SkillStore>, ids: Arc<dyn IdGenerator>) -> 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<CreateSkillOutput, AppError> {
|
||||
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<dyn SkillStore>,
|
||||
}
|
||||
|
||||
impl UpdateSkill {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(skills: Arc<dyn SkillStore>) -> 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<UpdateSkillOutput, AppError> {
|
||||
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<Skill>,
|
||||
}
|
||||
|
||||
/// Lists the skills in one scope.
|
||||
pub struct ListSkills {
|
||||
skills: Arc<dyn SkillStore>,
|
||||
}
|
||||
|
||||
impl ListSkills {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(skills: Arc<dyn SkillStore>) -> Self {
|
||||
Self { skills }
|
||||
}
|
||||
|
||||
/// Lists skills in `input.scope`.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Store`] on persistence failure.
|
||||
pub async fn execute(&self, input: ListSkillsInput) -> Result<ListSkillsOutput, AppError> {
|
||||
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<dyn SkillStore>,
|
||||
}
|
||||
|
||||
impl DeleteSkill {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(skills: Arc<dyn SkillStore>) -> 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<dyn AgentContextStore>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl AssignSkillToAgent {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(contexts: Arc<dyn AgentContextStore>, events: Arc<dyn EventBus>) -> 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<dyn AgentContextStore>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl UnassignSkillFromAgent {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(contexts: Arc<dyn AgentContextStore>, events: Arc<dyn EventBus>) -> 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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user