aimx/
workspace.rs

1//! # Workspace Management
2//!
3//! Provides a global workspace for managing agentic workflows within a hierarchical file structure.
4//! The workspace coordinates concurrent access to workflows and maintains the relationship
5//! between workflow nodes and their corresponding file system structure.
6//!
7//! ## Overview
8//!
9//! The workspace serves as a singleton container that manages the active workflow and its associated
10//! file system paths. It implements a multi-version concurrency control (MVCC) pattern where:
11//!
12//! - **Non-blocking reads**: Multiple readers can access workflow snapshots concurrently
13//! - **Atomic writes**: Writers operate on isolated copies before publishing changes
14//! - **Global coordination**: Centralized management of workflow file operations
15//!
16//! ## Architecture
17//!
18//! The workspace maintains:
19//! - A single global `Workflow` instance representing the current state
20//! - Filesystem path mappings for workflow nodes and their journal files
21//! - Concurrency-safe access patterns using `Arc<RwLock>`
22//!
23//! ## File Structure
24//!
25//! Workflows are stored in a hierarchical directory structure where each node corresponds
26//! to a workflow file with its own version history:
27//!
28//! ```text
29//! workspace.aim     # Root workspace workflow
30//! workspace.jnl     # Root workspace journal
31//! workspace/
32//! ├── parent.aim    # Parent workflow node
33//! ├── parent.jnl    # Parent journal
34//! ├── parent/
35//! │   ├── child.aim # Child workflow node
36//! │   ├── child.jnl # Child journal
37//! │   ├── sibling.aim
38//! │   └── sibling.jnl
39//! ├── uncle.aim
40//! └── uncle.jnl
41//! ```
42//!
43//! # Examples
44//!
45//! ```rust
46//! use std::path::Path;
47//! use aimx::workspace::{load_workspace, get_workspace, create_path, add_node_rule};
48//! use aimx::Reference;
49//!
50//! // Load a workspace from a file
51//! // load_workspace(Path::new("my_workspace.aim")).unwrap();
52//!
53//! // Get a read-only snapshot of the current workspace
54//! let workflow = get_workspace();
55//! let rule_count = workflow.rule_count();
56//!
57//! // Create a path for a workflow reference
58//! let reference = Reference::new("my_node");
59//! // let path = create_path(&reference).unwrap();
60//!
61//! // Add a new node rule to the workspace
62//! // add_node_rule("new_node").unwrap();
63//! ```
64
65use std::path::{Path, PathBuf};
66use std::sync::{Arc, RwLock};
67use once_cell::sync::Lazy;
68use anyhow::Result;
69use crate::{
70    Workflow, WorkflowLike, Reference,
71};
72
73/// Internal workspace structure that contains the active workflow and path information.
74/// 
75/// This struct is not exposed publicly to maintain encapsulation and ensure
76/// thread-safe access patterns through the public API functions.
77/// 
78/// The workspace uses a multi-version concurrency control (MVCC) pattern where:
79/// - The `content` field is wrapped in `Arc<RwLock<Workflow>>` for concurrent access
80/// - Multiple readers can access workflow snapshots concurrently
81/// - Writers operate on isolated copies before publishing changes
82struct Workspace {
83    /// The current workflow content, wrapped in Arc<RwLock> for concurrent access
84    content: Arc<RwLock<Workflow>>,
85    /// The base path of the workspace, used for resolving relative references
86    path: RwLock<PathBuf>,
87}
88
89impl Workspace {
90    /// Creates a new empty workspace.
91    /// 
92    /// Initializes an empty workflow and default path structure.
93    /// The workspace uses `Arc<RwLock<Workflow>>` for thread-safe concurrent access
94    /// and `RwLock<PathBuf>` for thread-safe path management.
95    /// 
96    /// # Returns
97    /// A new `Workspace` instance with empty content and path
98    pub fn new() -> Self {
99        Workspace {
100            content: Arc::new(RwLock::new(Workflow::new())),
101            path: RwLock::new(PathBuf::new()),
102        }
103    }
104
105    /// Loads a workflow file into the workspace.
106    /// 
107    /// Updates the workspace path to the parent directory of the loaded file
108    /// and loads the workflow content from the specified file.
109    /// 
110    /// This function performs two operations atomically:
111    /// 1. Updates the workspace's base path to the parent directory of the loaded file
112    /// 2. Loads the workflow content from the specified file path
113    /// 
114    /// # Arguments
115    /// * `path` - Path to the workflow file to load
116    /// 
117    /// # Returns
118    /// * `Result<()>` - Success or error indicating if the operation completed
119    pub fn load(&self, path: &Path) -> anyhow::Result<()> {
120        // Update the path
121        {
122            let mut path_guard = self.path.write().unwrap();
123            *path_guard = path.parent().unwrap_or(Path::new("")).to_path_buf();
124        }
125        
126        // Load the workflow
127        {
128            let mut content_guard = self.content.write().unwrap();
129            content_guard.load(path)?;
130        }
131        
132        Ok(())
133    }
134
135    /// Creates a file system path for a workflow reference.
136    /// 
137    /// Combines the current workspace path with the reference's path
138    /// structure to create the full filesystem path for the workflow file.
139    /// 
140    /// This function ensures that the necessary directory structure is created
141    /// before returning the path, making it suitable for file operations.
142    /// 
143    /// # Arguments
144    /// * `reference` - The workflow reference
145    /// 
146    /// # Returns
147    /// * `Result<PathBuf>` - The full path to the workflow file
148    pub fn create_path(&self, reference: &Reference) -> Result<PathBuf> {
149        let path_guard = self.path.read().unwrap();
150        reference.create_path(&*path_guard)
151    }
152}
153
154/// Global workspace singleton instance.
155/// 
156/// Uses `once_cell::sync::Lazy` to ensure thread-safe lazy initialization.
157/// This singleton pattern ensures that there is exactly one workspace instance
158/// throughout the application lifecycle, providing centralized management
159/// of workflow resources and file system operations.
160/// 
161/// The workspace is wrapped in `RwLock` to allow concurrent read access
162/// while ensuring exclusive write access when needed.
163static GLOBAL_WORKSPACE: Lazy<RwLock<Workspace>> = Lazy::new(|| RwLock::new(Workspace::new()));
164
165/// Returns a reference to the global workspace singleton.
166/// 
167/// This function provides internal access to the workspace instance.
168/// It is used by the public API functions to access the underlying
169/// workspace functionality while maintaining encapsulation.
170/// 
171/// # Returns
172/// A static reference to the global workspace singleton
173fn workspace() -> &'static RwLock<Workspace> {
174    &GLOBAL_WORKSPACE
175}
176
177/// Returns a read-only snapshot of the current workspace.
178/// 
179/// This function provides concurrent access to the workspace by returning
180/// a cloned snapshot of the workflow wrapped in an `Arc<dyn WorkflowLike>`.
181/// Multiple readers can access the workspace simultaneously without blocking.
182/// 
183/// # Returns
184/// * `Arc<dyn WorkflowLike>` - A thread-safe, read-only reference to the workflow
185/// 
186/// # Examples
187/// 
188/// ```rust
189/// use aimx::workspace::get_workspace;
190/// 
191/// let workflow = get_workspace();
192/// let rule_count = workflow.rule_count();
193/// println!("Workspace contains {} rules", rule_count);
194/// ```
195pub fn get_workspace() -> Arc<dyn WorkflowLike> {
196    // Instead of trying to wrap the RwLock, we can clone the underlying Workflow
197    // which gives us a read-only snapshot -- exactly what we need for WorkflowLike
198    let workspace_guard = workspace().read().unwrap();
199    let workflow_guard = workspace_guard.content.read().unwrap();
200    // Clone the workflow for the trait object
201    Arc::new(workflow_guard.clone())
202}
203
204/// Loads a workspace file from disk.
205/// 
206/// This function replaces the current workspace content with the workflow
207/// loaded from the specified file path. It updates the workspace's base path
208/// to the parent directory of the loaded file.
209/// 
210/// # Arguments
211/// * `path` - Path to the workspace file to load
212/// 
213/// # Returns
214/// * `Result<()>` - Success or error indicating if the operation completed
215/// 
216/// # Examples
217/// 
218/// ```rust
219/// use std::path::Path;
220/// use aimx::workspace::load_workspace;
221/// 
222/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
223/// // Load a workspace from a file
224/// // load_workspace(Path::new("my_workspace.aim"))?;
225/// # Ok(())
226/// # }
227/// ``` 
228pub fn load_workspace(path: &Path) -> anyhow::Result<()> {
229    let workspace_guard = workspace().write().unwrap();
230    workspace_guard.load(path)
231}
232
233/// Creates a filesystem path for a workflow reference.
234/// 
235/// Combines the current workspace path with the reference's path structure
236/// to create the full filesystem path for a workflow file.
237/// 
238/// # Arguments
239/// * `reference` - The workflow reference
240/// 
241/// # Returns
242/// * `Result<PathBuf>` - The full path to the workflow file
243/// 
244/// # Examples
245/// 
246/// ```rust
247/// use aimx::workspace::create_path;
248/// use aimx::expressions::Reference;
249/// 
250/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
251/// let reference = Reference::new("my_node");
252/// let path = create_path(&reference)?;
253/// println!("Workflow path: {:?}", path);
254/// # Ok(())
255/// # }
256/// ```
257pub fn create_path(reference: &Reference) -> Result<PathBuf> {
258    let workspace_guard = workspace().read().unwrap();
259    workspace_guard.create_path(reference)
260}
261
262/// Adds a new Node Rule to the workspace, creating the necessary file structure.
263/// 
264/// This function creates the underlying file structure for a workflow node by:
265/// 1. Creating the appropriate directory structure
266/// 2. Creating an empty `.aim` file if it doesn't exist
267/// 
268/// Node Rules represent workflow nodes that can contain their own rules and
269/// sub-nodes in a hierarchical structure.
270/// 
271/// # Arguments
272/// * `identifier` - The identifier for the new Node Rule
273/// 
274/// # Returns
275/// * `Result<()>` - Success or error indicating if the operation completed
276/// 
277/// # Examples
278/// 
279/// ```rust
280/// use aimx::workspace::add_node_rule;
281/// 
282/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
283/// // Add a new node rule to the workspace
284/// // add_node_rule("new_workflow_node")?;
285/// # Ok(())
286/// # }
287/// ``` 
288pub fn add_node_rule(identifier: &str) -> Result<()> {
289    let reference = Reference::new(identifier);
290    let workspace_guard = workspace().read().unwrap();
291    
292    // Create the path structure for the node
293    let aim_path = workspace_guard.create_path(&reference)?;
294    
295    // Create an empty .aim file if it doesn't exist
296    if !aim_path.exists() {
297        std::fs::File::create(&aim_path)?;
298    }
299    
300    Ok(())
301}
302
303/// Renames a Node Rule, updating its associated files and directory structure.
304/// 
305/// This operation updates:
306/// - The `.aim` file name
307/// - The `.jnl` journal file name  
308/// - The directory name (if it exists)
309/// 
310/// # Arguments
311/// * `old_identifier` - The current identifier of the Node Rule
312/// * `new_identifier` - The new identifier for the Node Rule
313/// 
314/// # Returns
315/// * `Result<()>` - Success or error indicating if the operation completed
316/// 
317/// # Examples
318/// 
319/// ```rust
320/// use aimx::workspace::rename_node_rule;
321/// 
322/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
323/// // Rename a node rule
324/// // rename_node_rule("old_name", "new_name")?;
325/// # Ok(())
326/// # }
327/// ``` 
328pub fn rename_node_rule(old_identifier: &str, new_identifier: &str) -> Result<()> {
329    let old_reference = Reference::new(old_identifier);
330    let new_reference = Reference::new(new_identifier);
331    let workspace_guard = workspace().read().unwrap();
332    
333    // Get the current workspace path
334    let workspace_path = workspace_guard.path.read().unwrap().clone();
335    
336    // Get the old and new file paths
337    let old_aim_path = old_reference.to_path(&workspace_path);
338    let old_jnl_path = crate::aim::to_journal_path(&old_aim_path)?;
339    let new_aim_path = new_reference.to_path(&workspace_path);
340    let new_jnl_path = crate::aim::to_journal_path(&new_aim_path)?;
341    
342    // Rename .aim file if it exists
343    if old_aim_path.exists() {
344        std::fs::rename(&old_aim_path, &new_aim_path)?;
345    }
346    
347    // Rename .jnl file if it exists
348    if old_jnl_path.exists() {
349        std::fs::rename(&old_jnl_path, &new_jnl_path)?;
350    }
351
352    let old_dir = workspace_path.join(old_identifier);
353    let new_dir = workspace_path.join(new_identifier);
354    if old_dir.exists() {
355        std::fs::rename(old_dir, new_dir)?;
356    }
357
358   Ok(())
359}
360
361/// Deletes a Node Rule and its associated files and directory structure.
362/// 
363/// This operation removes:
364/// - The `.aim` file (if it exists)
365/// - The `.jnl` journal file (if it exists)
366/// - The directory (if it exists and is empty)
367/// 
368/// # Arguments
369/// * `identifier` - The identifier of the Node Rule to delete
370/// 
371/// # Returns
372/// * `Result<()>` - Success or error indicating if the operation completed
373/// 
374/// # Examples
375/// 
376/// ```rust
377/// use aimx::workspace::delete_node_rule;
378/// 
379/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
380/// // Delete a node rule
381/// // delete_node_rule("unwanted_node")?;
382/// # Ok(())
383/// # }
384/// ``` 
385pub fn delete_node_rule(identifier: &str) -> Result<()> {
386    let reference = Reference::new(identifier);
387    let workspace_guard = workspace().read().unwrap();
388    
389    // Get the current workspace path
390    let workspace_path = workspace_guard.path.read().unwrap().clone();
391    
392    // Get the file paths
393    let aim_path = reference.to_path(&workspace_path);
394    let jnl_path = crate::aim::to_journal_path(&aim_path)?;
395    
396    // Delete .aim file if it exists
397    if aim_path.exists() {
398        std::fs::remove_file(&aim_path)?;
399    }
400    
401    // Delete .jnl file if it exists
402    if jnl_path.exists() {
403        std::fs::remove_file(&jnl_path)?;
404    }
405
406    let dir_path = workspace_path.join(identifier);
407
408    // Remove the .aim file's directory if empty
409    if dir_path.exists() {
410        std::fs::remove_dir(&dir_path)?;
411    }
412
413    Ok(())
414}