feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
17
crates/application/src/template/mod.rs
Normal file
17
crates/application/src/template/mod.rs
Normal file
@ -0,0 +1,17 @@
|
||||
//! Template & synchronisation use cases (ARCHITECTURE §6, §8; L7).
|
||||
//!
|
||||
//! Templates are reusable agent contexts stored in the global IDE store, with a
|
||||
//! monotonic version. This module owns their CRUD, the template→agent
|
||||
//! instantiation, and the drift-detection / synchronisation flow that keeps
|
||||
//! `synchronized` agents in step with their template.
|
||||
|
||||
mod usecases;
|
||||
|
||||
pub use usecases::{
|
||||
AgentDrift, CreateAgentFromTemplate, CreateAgentFromTemplateInput,
|
||||
CreateAgentFromTemplateOutput, CreateTemplate, CreateTemplateInput, CreateTemplateOutput,
|
||||
DeleteTemplate, DeleteTemplateInput, DetectAgentDrift, DetectAgentDriftInput,
|
||||
DetectAgentDriftOutput, ListTemplates, ListTemplatesOutput, SyncAgentWithTemplate,
|
||||
SyncAgentWithTemplateInput, SyncAgentWithTemplateOutput, UpdateTemplate, UpdateTemplateInput,
|
||||
UpdateTemplateOutput,
|
||||
};
|
||||
495
crates/application/src/template/usecases.rs
Normal file
495
crates/application/src/template/usecases.rs
Normal file
@ -0,0 +1,495 @@
|
||||
//! Template & synchronisation use cases (ARCHITECTURE §6, §8; L7).
|
||||
//!
|
||||
//! Two concerns live here:
|
||||
//! - **Templates** (global IDE store): CRUD + monotonic versioning
|
||||
//! ([`CreateTemplate`], [`UpdateTemplate`], [`ListTemplates`], [`DeleteTemplate`]).
|
||||
//! - **Template → agent link**: instantiating an agent from a template
|
||||
//! ([`CreateAgentFromTemplate`]), detecting when a synchronized agent is behind
|
||||
//! its template ([`DetectAgentDrift`]), and applying the update
|
||||
//! ([`SyncAgentWithTemplate`]).
|
||||
//!
|
||||
//! Every use case talks only to ports ([`TemplateStore`], [`AgentContextStore`],
|
||||
//! [`IdGenerator`], [`EventBus`]).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::{AgentContextStore, EventBus, IdGenerator, StoreError, TemplateStore};
|
||||
use domain::{
|
||||
Agent, AgentId, AgentManifest, AgentOrigin, AgentTemplate, DomainEvent, ManifestEntry,
|
||||
MarkdownDoc, ProfileId, Project, TemplateId, TemplateVersion,
|
||||
};
|
||||
|
||||
use crate::agent::unique_md_path;
|
||||
use crate::error::AppError;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CreateTemplate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`CreateTemplate::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CreateTemplateInput {
|
||||
/// Display name.
|
||||
pub name: String,
|
||||
/// Initial Markdown content.
|
||||
pub content: String,
|
||||
/// Default runtime profile for agents created from this template.
|
||||
pub default_profile_id: ProfileId,
|
||||
}
|
||||
|
||||
/// Output of [`CreateTemplate::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CreateTemplateOutput {
|
||||
/// The created template (at [`TemplateVersion::INITIAL`]).
|
||||
pub template: AgentTemplate,
|
||||
}
|
||||
|
||||
/// Creates a template in the global store at the initial version.
|
||||
pub struct CreateTemplate {
|
||||
templates: Arc<dyn TemplateStore>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
}
|
||||
|
||||
impl CreateTemplate {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(templates: Arc<dyn TemplateStore>, ids: Arc<dyn IdGenerator>) -> Self {
|
||||
Self { templates, ids }
|
||||
}
|
||||
|
||||
/// Executes creation.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::Invalid`] if the name is empty,
|
||||
/// - [`AppError::Store`] on persistence failure.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: CreateTemplateInput,
|
||||
) -> Result<CreateTemplateOutput, AppError> {
|
||||
let id = TemplateId::from_uuid(self.ids.new_uuid());
|
||||
let template = AgentTemplate::new(
|
||||
id,
|
||||
input.name,
|
||||
MarkdownDoc::new(input.content),
|
||||
input.default_profile_id,
|
||||
)
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
self.templates.save(&template).await?;
|
||||
Ok(CreateTemplateOutput { template })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UpdateTemplate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`UpdateTemplate::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UpdateTemplateInput {
|
||||
/// Template to update.
|
||||
pub template_id: TemplateId,
|
||||
/// New Markdown content.
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
/// Output of [`UpdateTemplate::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UpdateTemplateOutput {
|
||||
/// The updated template (version bumped by one).
|
||||
pub template: AgentTemplate,
|
||||
}
|
||||
|
||||
/// Updates a template's content and **bumps its version** (monotonic, §8.1),
|
||||
/// announcing [`DomainEvent::TemplateUpdated`] so drift can be re-evaluated.
|
||||
pub struct UpdateTemplate {
|
||||
templates: Arc<dyn TemplateStore>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl UpdateTemplate {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(templates: Arc<dyn TemplateStore>, events: Arc<dyn EventBus>) -> Self {
|
||||
Self { templates, events }
|
||||
}
|
||||
|
||||
/// Executes the update.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::NotFound`] if the template is unknown,
|
||||
/// - [`AppError::Store`] on persistence failure.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: UpdateTemplateInput,
|
||||
) -> Result<UpdateTemplateOutput, AppError> {
|
||||
let current = self.templates.get(input.template_id).await?;
|
||||
let updated = current.with_updated_content(MarkdownDoc::new(input.content));
|
||||
self.templates.save(&updated).await?;
|
||||
self.events.publish(DomainEvent::TemplateUpdated {
|
||||
template_id: updated.id,
|
||||
version: updated.version,
|
||||
});
|
||||
Ok(UpdateTemplateOutput { template: updated })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ListTemplates / DeleteTemplate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Output of [`ListTemplates::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListTemplatesOutput {
|
||||
/// All templates in the global store.
|
||||
pub templates: Vec<AgentTemplate>,
|
||||
}
|
||||
|
||||
/// Lists the templates in the global store.
|
||||
pub struct ListTemplates {
|
||||
templates: Arc<dyn TemplateStore>,
|
||||
}
|
||||
|
||||
impl ListTemplates {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(templates: Arc<dyn TemplateStore>) -> Self {
|
||||
Self { templates }
|
||||
}
|
||||
|
||||
/// Lists templates.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Store`] on persistence failure.
|
||||
pub async fn execute(&self) -> Result<ListTemplatesOutput, AppError> {
|
||||
Ok(ListTemplatesOutput {
|
||||
templates: self.templates.list().await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for [`DeleteTemplate::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DeleteTemplateInput {
|
||||
/// Template to delete.
|
||||
pub template_id: TemplateId,
|
||||
}
|
||||
|
||||
/// Deletes a template from the global store.
|
||||
///
|
||||
/// Agents previously created from it keep their `.md` (their `origin` still
|
||||
/// references the now-absent template; drift detection simply finds nothing to
|
||||
/// compare against).
|
||||
pub struct DeleteTemplate {
|
||||
templates: Arc<dyn TemplateStore>,
|
||||
}
|
||||
|
||||
impl DeleteTemplate {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(templates: Arc<dyn TemplateStore>) -> Self {
|
||||
Self { templates }
|
||||
}
|
||||
|
||||
/// Deletes the template.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::NotFound`] if the template is unknown,
|
||||
/// - [`AppError::Store`] on persistence failure.
|
||||
pub async fn execute(&self, input: DeleteTemplateInput) -> Result<(), AppError> {
|
||||
self.templates.delete(input.template_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CreateAgentFromTemplate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`CreateAgentFromTemplate::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CreateAgentFromTemplateInput {
|
||||
/// The project that owns the agent.
|
||||
pub project: Project,
|
||||
/// Source template.
|
||||
pub template_id: TemplateId,
|
||||
/// Optional agent name; defaults to the template's name.
|
||||
pub name: Option<String>,
|
||||
/// Whether the agent tracks the template (`true` ⇒ future syncs apply).
|
||||
pub synchronized: bool,
|
||||
}
|
||||
|
||||
/// Output of [`CreateAgentFromTemplate::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CreateAgentFromTemplateOutput {
|
||||
/// The created agent.
|
||||
pub agent: Agent,
|
||||
}
|
||||
|
||||
/// Instantiates a project agent from a template: copies the template's
|
||||
/// `content_md` into the agent's `.md`, links the origin to the template at its
|
||||
/// current version, and records the manifest entry.
|
||||
pub struct CreateAgentFromTemplate {
|
||||
templates: Arc<dyn TemplateStore>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl CreateAgentFromTemplate {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
templates: Arc<dyn TemplateStore>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> Self {
|
||||
Self {
|
||||
templates,
|
||||
contexts,
|
||||
ids,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes creation from a template.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::NotFound`] if the template is unknown,
|
||||
/// - [`AppError::Invalid`] if the resulting agent/manifest is invalid,
|
||||
/// - [`AppError::Store`] on persistence failure.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: CreateAgentFromTemplateInput,
|
||||
) -> Result<CreateAgentFromTemplateOutput, AppError> {
|
||||
let template = self.templates.get(input.template_id).await?;
|
||||
let manifest = self.contexts.load_manifest(&input.project).await?;
|
||||
|
||||
let name = input.name.unwrap_or_else(|| template.name.clone());
|
||||
let id = AgentId::from_uuid(self.ids.new_uuid());
|
||||
let md_path = unique_md_path(&name, &manifest);
|
||||
let origin = AgentOrigin::FromTemplate {
|
||||
template_id: template.id,
|
||||
synced_template_version: template.version,
|
||||
};
|
||||
let agent = Agent::new(
|
||||
id,
|
||||
name,
|
||||
md_path,
|
||||
template.default_profile_id,
|
||||
origin,
|
||||
input.synchronized,
|
||||
)
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
|
||||
let mut entries = manifest.entries;
|
||||
entries.push(ManifestEntry::from_agent(&agent));
|
||||
let manifest = AgentManifest::new(manifest.version, entries)
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
self.contexts.save_manifest(&input.project, &manifest).await?;
|
||||
|
||||
// Seed the agent context with the template content.
|
||||
self.contexts
|
||||
.write_context(&input.project, &agent.id, &template.content_md)
|
||||
.await?;
|
||||
|
||||
self.events.publish(DomainEvent::LayoutChanged {
|
||||
project_id: input.project.id,
|
||||
});
|
||||
|
||||
Ok(CreateAgentFromTemplateOutput { agent })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DetectAgentDrift
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One drifting agent: its template moved ahead of the agent's synced version.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AgentDrift {
|
||||
/// The drifting agent.
|
||||
pub agent_id: AgentId,
|
||||
/// Version the agent is currently synced to.
|
||||
pub from: TemplateVersion,
|
||||
/// Version available from the template.
|
||||
pub to: TemplateVersion,
|
||||
}
|
||||
|
||||
/// Input for [`DetectAgentDrift::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DetectAgentDriftInput {
|
||||
/// The project whose agents to check.
|
||||
pub project: Project,
|
||||
}
|
||||
|
||||
/// Output of [`DetectAgentDrift::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DetectAgentDriftOutput {
|
||||
/// Agents whose template has a newer version (only `synchronized` ones).
|
||||
pub drifts: Vec<AgentDrift>,
|
||||
}
|
||||
|
||||
/// Detects which synchronized agents are behind their template
|
||||
/// (`template.version > synced_template_version`, §8.2), announcing
|
||||
/// [`DomainEvent::AgentDriftDetected`] for each.
|
||||
pub struct DetectAgentDrift {
|
||||
templates: Arc<dyn TemplateStore>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl DetectAgentDrift {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
templates: Arc<dyn TemplateStore>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> Self {
|
||||
Self {
|
||||
templates,
|
||||
contexts,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the drift set.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Store`] on persistence failure (a *deleted* template is not an
|
||||
/// error — that agent simply has nothing to drift against).
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: DetectAgentDriftInput,
|
||||
) -> Result<DetectAgentDriftOutput, AppError> {
|
||||
let manifest = self.contexts.load_manifest(&input.project).await?;
|
||||
let mut drifts = Vec::new();
|
||||
for entry in &manifest.entries {
|
||||
// Only synchronized, template-backed agents can drift.
|
||||
if !entry.synchronized {
|
||||
continue;
|
||||
}
|
||||
let (Some(template_id), Some(synced)) =
|
||||
(entry.template_id, entry.synced_template_version)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let template = match self.templates.get(template_id).await {
|
||||
Ok(t) => t,
|
||||
Err(StoreError::NotFound) => continue,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
if template.version > synced {
|
||||
let drift = AgentDrift {
|
||||
agent_id: entry.agent_id,
|
||||
from: synced,
|
||||
to: template.version,
|
||||
};
|
||||
self.events.publish(DomainEvent::AgentDriftDetected {
|
||||
agent_id: drift.agent_id,
|
||||
from: drift.from,
|
||||
to: drift.to,
|
||||
});
|
||||
drifts.push(drift);
|
||||
}
|
||||
}
|
||||
Ok(DetectAgentDriftOutput { drifts })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SyncAgentWithTemplate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`SyncAgentWithTemplate::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SyncAgentWithTemplateInput {
|
||||
/// The owning project.
|
||||
pub project: Project,
|
||||
/// The agent to bring up to date.
|
||||
pub agent_id: AgentId,
|
||||
}
|
||||
|
||||
/// Output of [`SyncAgentWithTemplate::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SyncAgentWithTemplateOutput {
|
||||
/// Whether an update was applied (`false` for a non-synchronized or
|
||||
/// scratch agent — those are intentionally left untouched, §8.4).
|
||||
pub synced: bool,
|
||||
/// The version the agent is now at, when a sync happened.
|
||||
pub version: Option<TemplateVersion>,
|
||||
}
|
||||
|
||||
/// Applies a template update to a synchronized agent: **replaces** the agent's
|
||||
/// `.md` with the template content (the context of a synchronized agent is
|
||||
/// "owned" by the template, §8.3) and records the new synced version. Agents
|
||||
/// that are not `synchronized` (or are `scratch`) are left untouched.
|
||||
pub struct SyncAgentWithTemplate {
|
||||
templates: Arc<dyn TemplateStore>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl SyncAgentWithTemplate {
|
||||
/// Builds the use case from its ports.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
templates: Arc<dyn TemplateStore>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> Self {
|
||||
Self {
|
||||
templates,
|
||||
contexts,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the sync for one agent.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::NotFound`] if the agent or its template is unknown,
|
||||
/// - [`AppError::Store`] on persistence failure.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: SyncAgentWithTemplateInput,
|
||||
) -> Result<SyncAgentWithTemplateOutput, AppError> {
|
||||
let mut manifest = self.contexts.load_manifest(&input.project).await?;
|
||||
let idx = manifest
|
||||
.entries
|
||||
.iter()
|
||||
.position(|e| e.agent_id == input.agent_id)
|
||||
.ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?;
|
||||
let entry = &manifest.entries[idx];
|
||||
|
||||
// Non-synchronized / scratch agents are never auto-updated (§8.4).
|
||||
let Some(template_id) = entry.template_id.filter(|_| entry.synchronized) else {
|
||||
return Ok(SyncAgentWithTemplateOutput {
|
||||
synced: false,
|
||||
version: None,
|
||||
});
|
||||
};
|
||||
|
||||
let template = self.templates.get(template_id).await?;
|
||||
manifest.entries[idx].synced_template_version = Some(template.version);
|
||||
|
||||
// Persist the manifest (revalidated) and overwrite the agent context.
|
||||
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.contexts
|
||||
.write_context(&input.project, &input.agent_id, &template.content_md)
|
||||
.await?;
|
||||
|
||||
self.events.publish(DomainEvent::AgentSynced {
|
||||
agent_id: input.agent_id,
|
||||
to: template.version,
|
||||
});
|
||||
|
||||
Ok(SyncAgentWithTemplateOutput {
|
||||
synced: true,
|
||||
version: Some(template.version),
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user