//! [`CreateProject`] — create a project from a project root (ARCHITECTURE §6). //! //! Responsibilities (and *only* these): //! 1. validate the root (absolute path — enforced by [`ProjectPath`]) and name, //! 2. enforce the cross-aggregate uniqueness invariant `(remote, root)` — this //! lives here, not in the domain, because it requires knowledge of *all* //! projects (a repository concern, ARCHITECTURE §3.2), //! 3. create the `.ideai/` directory and write `project.json`, //! 4. register the project via the [`ProjectStore`], //! 5. publish [`DomainEvent::ProjectCreated`]. use std::sync::Arc; use domain::ports::{Clock, EventBus, FileSystem, IdGenerator, ProjectStore, RemotePath}; use domain::{DomainEvent, Project, ProjectId, ProjectPath, RemoteRef}; use crate::error::AppError; use super::meta::{join_root, to_json_bytes, ProjectMeta, IDEAI_DIR, PROJECT_FILE}; /// Input for [`CreateProject::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateProjectInput { /// Display name of the project. pub name: String, /// Absolute project root (validated into a [`ProjectPath`]). pub root: String, /// Where the project lives. Defaults to [`RemoteRef::Local`] when `None`. pub remote: Option, /// Default agent profile id, if already chosen. pub default_profile_id: Option, } /// Output of [`CreateProject::execute`]: the freshly-created project. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateProjectOutput { /// The created project. pub project: Project, } /// Creates a project: inits `.ideai/`, writes `project.json`, registers it. pub struct CreateProject { store: Arc, fs: Arc, ids: Arc, clock: Arc, events: Arc, } impl CreateProject { /// Builds the use case from its injected ports. #[must_use] pub fn new( store: Arc, fs: Arc, ids: Arc, clock: Arc, events: Arc, ) -> Self { Self { store, fs, ids, clock, events, } } /// Executes project creation. /// /// # Errors /// - [`AppError::Invalid`] if the name is empty or the root is not absolute, /// - [`AppError::Invalid`] if a project already exists for the same /// `(remote, root)` (uniqueness invariant), /// - [`AppError::FileSystem`] / [`AppError::Store`] on I/O failures. pub async fn execute( &self, input: CreateProjectInput, ) -> Result { let root = ProjectPath::new(input.root).map_err(|e| AppError::Invalid(e.to_string()))?; let remote = input.remote.unwrap_or(RemoteRef::Local); // (1+2) Uniqueness invariant on (remote, root) — repository-level. let existing = self.store.list_projects().await?; if existing .iter() .any(|p| p.remote == remote && p.root == root) { return Err(AppError::Invalid(format!( "a project already exists at {} for this remote", root.as_str() ))); } // Build the validated aggregate (enforces non-empty name). let id = ProjectId::from_uuid(self.ids.new_uuid()); let created_at = self.clock.now_millis(); let project = Project::new(id, input.name, root.clone(), remote, created_at) .map_err(|e| AppError::Invalid(e.to_string()))?; // (3) Materialise `.ideai/` and write `project.json`. let ideai_dir = join_root(&root, IDEAI_DIR); self.fs.create_dir_all(&RemotePath::new(ideai_dir)).await?; let meta = ProjectMeta::from_project(&project, input.default_profile_id); let meta_path = join_root(&root, &format!("{IDEAI_DIR}/{PROJECT_FILE}")); self.fs .write(&RemotePath::new(meta_path), &to_json_bytes(&meta)?) .await?; // (4) Register in the known-projects registry. self.store.save_project(&project).await?; // (5) Announce. self.events .publish(DomainEvent::ProjectCreated { project_id: id }); Ok(CreateProjectOutput { project }) } }