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:
2026-06-06 01:27:01 +02:00
parent 55b3bee2c8
commit 307ae71857
273 changed files with 48740 additions and 0 deletions

View 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,
};

View 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),
})
}
}