aimx/
workflow.rs

1
2//! # Workflow Management
3//!
4//! This module provides the core workflow management functionality for AIMX.
5//! A workflow represents a collection of rules organized in a hierarchical structure,
6//! with support for versioning, concurrent access, and efficient rule management.
7//!
8//! ## Key Concepts
9//!
10//! - **Workflow**: A container for rules that represents a logical unit of work
11//! - **Rule**: Individual definitions with identifiers, types, expressions, and values
12//! - **Versioning**: Support for major (epoch) and minor (partial) version tracking
13//! - **Concurrent Access**: Thread-safe design supporting multiple readers and atomic writes
14//! - **Change Tracking**: Automatic detection of changes requiring partial or full saves
15//!
16//! ## Workflow Structure
17//!
18//! Workflows maintain rules in two complementary data structures:
19//! - `rows`: A vector maintaining rule order and empty slots
20//! - `lookup`: A hash map for O(1) identifier-based rule access
21//!
22//! This dual-structure approach provides both ordered access (for serialization)
23//! and fast lookup (for evaluation and rule management).
24//!
25//! ## Version Control
26//!
27//! Workflows support a sophisticated version control system:
28//! - **Epoch**: Major version numbers that increment on structural changes
29//! - **Partial**: Minor version numbers for incremental updates within an epoch
30//! - **Journaling**: External journal files track version offsets for efficient loading
31//! - **Change Tracking**: Automatic detection of change types (None, Partial, Version)
32//!
33//! ## Concurrency Model
34//!
35//! The workflow system uses a multi-version concurrency control (MVCC) model:
36//! - **Non-blocking Reads**: Unlimited concurrent readers access consistent snapshots
37//! - **Atomic Writes**: Writers operate on isolated copies before publishing changes
38//! - **Fine-grained Locking**: Per-workflow locking instead of global workspace locking
39//!
40//! This design ensures high performance for agentic workflows where many tasks
41//! may need to evaluate expressions simultaneously without blocking.
42//!
43//! ## Usage Examples
44//!
45//! ```rust
46//! use aimx::{Workflow, WorkflowLike, Rule, Typedef, Expression, Literal, Value};
47//!
48//! // Create a new workflow
49//! let mut workflow = Workflow::new();
50//!
51//! // Add rules to the workflow
52//! let rule = Rule::new("temperature".to_string(), Typedef::Number, 
53//!     Expression::Empty, Value::Literal(Literal::Number(72.0)));
54//! workflow.append_or_update(Some(rule));
55//!
56//! // Access rules by identifier
57//! let temp_rule = workflow.get_rule("temperature").unwrap();
58//! let temp = temp_rule.value().to_literal();
59//! assert_eq!(temp, &Literal::Number(72.0));
60//!
61//! // Check that we have rules
62//! assert!(workflow.rule_count() > 0);
63//! ```
64
65use std::{
66    any::Any,
67    fmt::Debug, // Import Debug trait for the trait bound
68    fs::OpenOptions,
69    io::Write,
70    path::{Path, PathBuf},
71    collections::HashMap,
72};
73use anyhow::{Result, anyhow};
74use crate::{
75    aim::{journal_save, parse_version, read_latest, read_version, Journal, Version}, create_path, literals::parse_unsigned, parse_rule, ContextLike, Reference, Rule, Writer
76};
77
78/// The central trait for interacting with workflow content.
79///
80/// This trait defines the interface for workflow management operations.
81/// It is designed to be thread-safe (`Send + Sync`) to support concurrent access
82/// in agentic workflow applications.
83///
84/// ## Concurrency Guarantees
85///
86/// - `Send`: The type can be safely sent (moved) to another thread
87/// - `Sync`: The type can be safely shared (`&T`) between threads
88/// - `Arc<T>` requires its contained type `T` to be `Send + Sync`
89///
90/// ## Implementation Notes
91///
92/// The `WorkflowLike` trait provides both read-only and mutable operations,
93/// with the mutable operations typically implemented through interior mutability
94/// patterns to maintain thread safety.
95///
96/// # Examples
97///
98/// ```rust
99/// use aimx::{WorkflowLike, Workflow};
100/// use std::sync::Arc;
101///
102/// // Create a workflow and wrap it in an Arc for thread-safe sharing
103/// let workflow = Arc::new(Workflow::new());
104/// 
105/// // Multiple threads can safely read from the workflow
106/// let workflow_clone = Arc::clone(&workflow);
107/// std::thread::spawn(move || {
108///     println!("Workflow version: {}", workflow_clone.version());
109/// });
110/// ```
111pub trait WorkflowLike: Send + Sync + Debug + 'static {
112    // --- Core Metadata ---
113    
114    /// Returns the major version (epoch) of the workflow.
115    ///
116    /// The epoch represents major structural changes to the workflow.
117    /// Increments when significant changes are made that require a new version.
118    fn version(&self) -> u32;
119    
120    /// Returns the minor version (partial) of the workflow.
121    ///
122    /// The partial represents incremental updates within the same epoch.
123    /// Increments for minor changes that don't require a new major version.
124    fn minor_version(&self) -> u32;
125    
126    /// Returns the reference identifier for this workflow.
127    ///
128    /// The reference uniquely identifies the workflow within the workspace.
129    fn reference(&self) -> &Reference;
130    
131    /// Returns the file system path to the workflow's AIM file.
132    ///
133    /// This path points to the `.aim` file that stores the workflow content.
134    fn path(&self) -> &Path;
135
136    // --- Rule Access ---
137    
138    /// Retrieves a rule by its row index.
139    ///
140    /// # Parameters
141    /// - `index`: The zero-based row index
142    ///
143    /// # Returns
144    /// `Some(Rule)` if the index is valid and contains a rule, `None` otherwise
145    fn get_row(&self, index: usize) -> Option<Rule>;
146    
147    /// Retrieves a rule by its identifier.
148    ///
149    /// # Parameters
150    /// - `identifier`: The rule's unique identifier
151    ///
152    /// # Returns
153    /// `Some(Rule)` if the identifier exists, `None` otherwise
154    fn get_rule(&self, identifier: &str) -> Option<Rule>;
155
156    // --- Introspection & Bulk Access ---
157    
158    /// Checks if a rule with the given identifier exists.
159    ///
160    /// # Parameters
161    /// - `identifier`: The identifier to check
162    ///
163    /// # Returns
164    /// `true` if the rule exists, `false` otherwise
165    fn contains(&self, identifier: &str) -> bool;
166    
167    /// Checks if a specific row index contains a rule.
168    ///
169    /// # Parameters
170    /// - `index`: The row index to check
171    ///
172    /// # Returns
173    /// `true` if the row contains a rule, `false` if empty or out of bounds
174    fn has_rule(&self, index: usize) -> bool;
175    
176    /// Returns the number of rules in the workflow.
177    ///
178    /// This counts only the actual rules, excluding empty rows.
179    fn rule_count(&self) -> usize;
180    
181    /// Returns the total number of rows in the workflow.
182    ///
183    /// This includes both rules and empty rows.
184    fn rule_rows(&self) -> usize;
185
186    /// Returns an iterator over all rows.
187    ///
188    /// The iterator yields `&Option<Rule>` items, including empty rows.
189    /// Use this when you need to preserve the exact row structure.
190    fn iter_rows<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Option<Rule>> + 'a>;
191    
192    /// Returns an iterator over all rules.
193    ///
194    /// The iterator yields `&Rule` items, skipping empty rows.
195    /// Use this when you only need the actual rules.
196    fn iter_rules<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Rule> + 'a>;
197
198    // --- Trait Object Helper ---
199    
200    /// Returns a reference to the workflow as a trait object.
201    ///
202    /// This method enables downcasting to the concrete workflow type
203    /// when working with trait objects.
204    fn as_any(&self) -> &dyn Any;
205}
206
207/// Represents the types of changes that can be made to an AIM structure.
208///
209/// This enum is used to determine what kind of save operation is needed.
210/// The change type affects how the workflow is persisted to disk.
211///
212/// ## Change Types
213///
214/// - `None`: No changes have been made, no save needed
215/// - `Partial`: Incremental changes that can be saved as a partial update
216/// - `Version`: Structural changes that require a new version
217///
218/// # Examples
219///
220/// ```rust
221/// use aimx::workflow::Changes;
222///
223/// let no_changes = Changes::None;
224/// let partial_changes = Changes::Partial;
225/// let version_changes = Changes::Version;
226/// 
227/// assert_ne!(no_changes, partial_changes);
228/// assert_ne!(partial_changes, version_changes);
229/// ```
230#[derive(Debug, Clone, PartialEq)]
231pub enum Changes {
232    /// No changes have been made
233    None,
234    /// Changes that only require a partial save
235    Partial,
236    /// Changes that require a full version change
237    Version,
238}
239
240/// Represents a workflow containing rules with version control and change tracking.
241///
242/// A workflow is the central container for rules in the AIMX system. It manages:
243/// - Rule storage and indexing for efficient access
244/// - Version control with epoch and partial tracking
245/// - Change detection for optimal persistence
246/// - File system integration for loading and saving
247///
248/// ## Internal Structure
249///
250/// The workflow maintains rules using two complementary data structures:
251/// - `rows`: A vector preserving rule order and empty slots
252/// - `lookup`: A hash map enabling O(1) identifier-based access
253///
254/// This dual-structure approach provides both ordered serialization and fast lookup.
255///
256/// ## Version Control
257///
258/// Workflows support sophisticated version control:
259/// - **Epoch**: Major version numbers for structural changes
260/// - **Partial**: Minor version numbers for incremental updates
261/// - **Journaling**: External files track version offsets for efficient loading
262///
263/// ## Change Tracking
264///
265/// The workflow automatically tracks changes to optimize persistence:
266/// - `None`: No changes, skip saving
267/// - `Partial`: Incremental changes, append partial update
268/// - `Version`: Structural changes, create new version
269///
270/// # Examples
271///
272/// ```rust
273/// use aimx::{Workflow, WorkflowLike, Rule, Typedef, Expression, Literal, Value};
274///
275/// // Create a new workflow
276/// let mut workflow = Workflow::new();
277/// 
278/// // Add a rule
279/// let rule = Rule::new("temperature".to_string(), Typedef::Number, 
280///     Expression::Empty, Value::Literal(Literal::Number(72.0)));
281/// workflow.append_or_update(Some(rule));
282/// 
283/// // Access rules by identifier
284/// let temp_rule = workflow.get_rule("temperature").unwrap();
285/// let temp = temp_rule.value().to_literal();
286/// assert_eq!(temp, &Literal::Number(72.0));
287/// ```
288#[derive(Debug, Clone)]
289pub struct Workflow {
290    /// Ordered storage of rules, with None representing empty slots
291    rows: Vec<Option<Rule>>,
292    /// Hash map for O(1) identifier-based lookups to row indices
293    lookup: HashMap<String, usize>,
294    /// Current version
295    version: Version,
296    /// Path to the workflow Aim file
297    path: PathBuf,
298    /// Reference to the workflow Node 
299    reference: Reference,
300    /// Version journal
301    journals: Vec<Journal>,
302    /// Type of changes that have been made
303    changes: Changes,
304
305    /// This flag allows workflows to track changes for partial saves.
306    /// This flag only affects `append_or_update`, `update_rule` and
307    /// `set_rule` which are used by `Workflow::parse()`. Normally this
308    /// flag should be set to `true` after parsing input, however it should
309    /// be set to `true` before loading a previous versions it ensures all
310    /// 'recovered' rules are saved.
311    track_changes: bool,
312}
313
314impl PartialEq<Workflow> for Reference {
315    fn eq(&self, other: &Workflow) -> bool {
316        self == &other.reference
317    }
318}
319
320impl PartialEq<Reference> for Workflow {
321    fn eq(&self, other: &Reference) -> bool {
322        &self.reference == other
323    }
324}
325
326impl PartialEq<Workflow> for Workflow {
327    fn eq(&self, other: &Workflow) -> bool {
328        &self.reference == &other.reference
329    }
330}
331
332impl Workflow {
333    /// Creates a new empty workflow.
334    ///
335    /// The workflow starts with:
336    /// - Empty rule storage
337    /// - Version 0.0
338    /// - Empty path and reference
339    /// - No changes pending
340    /// - Change tracking disabled
341    ///
342    /// # Returns
343    ///
344    /// A new `Workflow` instance ready for use.
345    ///
346    /// # Examples
347    ///
348    /// ```rust
349    /// use aimx::{Workflow, WorkflowLike};
350    ///
351    /// let workflow = Workflow::new();
352    /// assert_eq!(workflow.version(), 0);
353    /// assert_eq!(workflow.rule_count(), 0);
354    /// ```
355    pub fn new() -> Self {
356        Workflow {
357            rows: Vec::new(),
358            lookup: HashMap::new(),
359            version: Version::new(0, 0),
360            path: PathBuf::new(),
361            reference: Reference::new("_"),
362            journals: Vec::new(),
363            changes: Changes::None,
364            track_changes: false,
365        }
366    }
367
368    /// Creates a new workflow by parsing AIM content.
369    ///
370    /// This constructor parses the provided AIM content and creates a workflow
371    /// with the parsed rules. Change tracking is automatically enabled.
372    ///
373    /// # Parameters
374    ///
375    /// * `input` - The AIM content to parse
376    ///
377    /// # Returns
378    ///
379    /// A new `Workflow` instance containing the parsed rules.
380    ///
381    /// # Examples
382    ///
383    /// ```rust
384    /// use aimx::{Workflow, WorkflowLike};
385    ///
386    /// let workflow = Workflow::parse_new("[1]\ntemperature: Number = 72");
387    /// assert!(workflow.rule_count() > 0);
388    /// ```
389    pub fn parse_new(input: &str) -> Self {
390        let mut workflow = Self::new();
391        let _ = workflow.parse(input, None);
392        workflow.track_changes();
393        workflow
394    }
395
396    /// Creates a new workflow by loading from a reference.
397    ///
398    /// This constructor loads a workflow from the file system based on the
399    /// provided reference. If the file doesn't exist, an empty workflow
400    /// is created with the reference set.
401    ///
402    /// # Parameters
403    ///
404    /// * `reference` - The workflow reference to load
405    ///
406    /// # Returns
407    ///
408    /// A new `Workflow` instance loaded from the file system.
409    pub fn load_new(reference: &Reference) -> Self {
410        let mut workflow = Self::new();
411        if let Ok(path) = create_path(reference) {
412            workflow.reference = reference.clone();
413            // This error should really be reported somewhere
414            workflow.load(&path).map_err(|_| ()).unwrap();
415        }
416        workflow
417    }
418
419    /// Creates a new workflow by loading from a workspace path.
420    ///
421    /// This constructor loads a workflow from the specified file system path.
422    ///
423    /// # Parameters
424    ///
425    /// * `path` - The path to the AIM file to load
426    ///
427    /// # Returns
428    ///
429    /// A new `Workflow` instance loaded from the file system.
430    pub fn new_workspace(path: &Path) -> Self {
431        let mut workflow = Self::new();
432        // This error should really be reported somewhere
433        workflow.load(path).map_err(|_| ()).unwrap();
434        workflow
435    }
436
437    /// Checks if the workflow has unsaved changes.
438    ///
439    /// # Returns
440    ///
441    /// `true` if the workflow has changes that need to be saved, `false` otherwise.
442    pub fn is_touched(&self) -> bool {
443        self.changes != Changes::None
444    }
445
446    /// Flags changes that only require a partial save.
447    ///
448    /// This method escalates the change status from `None` to `Partial`.
449    /// If changes are already at `Partial` or `Version`, they remain unchanged.
450    pub fn set_partial_change(&mut self) {
451        // We only escalate partial changes from none
452        if self.changes == Changes::None {
453            self.changes = Changes::Partial;
454        }
455    }
456
457    /// Flags changes that require a version change on save.
458    ///
459    /// This method sets the change status to `Version`, indicating that
460    /// structural changes have been made that require a new version.
461    pub fn set_version_change(&mut self) {
462        self.changes = Changes::Version;
463    }
464
465    /// Clears the changes flag.
466    ///
467    /// This method is typically called after a successful save operation
468    /// to reset the change tracking state.
469    pub fn clear_changes(&mut self) {
470        self.changes = Changes::None;
471    }
472
473    /// Enables change tracking for partial saves.
474    ///
475    /// When enabled, the workflow will track changes made through specific
476    /// operations (`append_or_update`, `update_rule`, `set_rule`) and
477    /// automatically flag them for partial saving.
478    ///
479    /// This should be enabled before or after using `workflow.parse()` directly.
480    pub fn track_changes(&mut self) {
481        self.track_changes = true;
482    }
483
484    /// Gets the first version number from the journal.
485    ///
486    /// # Returns
487    ///
488    /// The epoch of the first version, or 0 if no versions exist.
489    pub fn first_version(&self) -> u32 {
490        if self.journals.len() == 0 {
491            0
492        } else {
493            self.journals[0].version()
494        }
495    }
496
497    /// Gets the latest version number from the journal.
498    ///
499    /// # Returns
500    ///
501    /// The epoch of the latest version, or 0 if no versions exist.
502    pub fn latest_version(&self) -> u32 {
503        if self.journals.len() > 0 {
504            self.journals[self.journals.len() - 1].version()
505        }
506        else {
507            0
508        }
509    }
510
511    /// Loads a specific version of an AIM file.
512    ///
513    /// This method loads a particular version (and optionally a specific partial)
514    /// from the AIM file. After loading, the workflow switches back to the latest
515    /// version but flags that a version change is needed since the loaded version
516    /// differs from the current one.
517    ///
518    /// # Parameters
519    ///
520    /// * `path` - The path to the AIM file
521    /// * `version` - The epoch version to load
522    /// * `partial` - Optional partial version within the epoch
523    ///
524    /// # Returns
525    ///
526    /// `Ok(())` if successful, or an error if the version cannot be loaded.
527    pub fn load_version(
528        &mut self,
529        path: &Path,
530        version: u32,
531        partial: Option<u32>,
532    ) -> Result<()> {
533        if self.path != *path {
534            self.path = path.to_path_buf();
535        }
536        // Read the contents of specific version section
537        let contents = read_version(&mut self.journals, &path, version)?;
538        self.version.set_epoc(version);
539        self.track_changes();
540        // IMPORTANT! parser will throw an error if version is not found
541        self.parse(&contents, partial)?;
542        // Switch back to the latest version
543        self.version.set_epoc(self.latest_version());
544        // This is different to the latest version so flag a version change
545        self.set_version_change();
546        Ok(())
547    }
548
549    /// Loads the latest version of an AIM file.
550    ///
551    /// This method loads the most recent version from the AIM file and
552    /// clears any pending changes. Change tracking is enabled after loading.
553    ///
554    /// # Parameters
555    ///
556    /// * `path` - The path to the AIM file
557    ///
558    /// # Returns
559    ///
560    /// `Ok(())` if successful, or an error if the file cannot be loaded.
561    pub fn load(&mut self, path: &Path) -> Result<()> {
562        if self.path != *path {
563            self.path = path.to_path_buf();
564        }
565        // Read the latest section
566        let contents = read_latest(&mut self.journals, &path)?;
567        self.version.set_epoc(self.latest_version());
568        self.parse(&contents, None)?;
569        self.clear_changes();
570        self.track_changes();
571        Ok(())
572    }
573
574    /// Saves the AIM structure to its associated file.
575    ///
576    /// This function saves changes to the AIM file based on the type of changes made:
577    /// - `Changes::Version`: Creates a new version with incremented epoch
578    /// - `Changes::Partial`: Creates a partial update with incremented partial counter
579    /// - `Changes::None`: No changes to save, returns Ok(())
580    ///
581    /// The function also updates the journal file with position information for fast lookup.
582    ///
583    /// # Returns
584    ///
585    /// `Ok(())` if successful, or an error if the save operation fails.
586    ///
587    /// # Examples
588    ///
589    /// ```rust
590    /// use aimx::{Workflow, Rule, Typedef, Expression, Literal, Value};
591    ///
592    /// let mut workflow = Workflow::new();
593    /// let rule = Rule::new("test".to_string(), Typedef::Number, 
594    ///     Expression::Empty, Value::Literal(Literal::Number(42.0)));
595    /// workflow.track_changes();
596    /// workflow.append_or_update(Some(rule));
597    /// 
598    /// // Workflow now has changes that could be saved
599    /// assert!(workflow.is_touched());
600    /// ```
601    pub fn save(&mut self) -> Result<()> {
602        let mut writer = Writer::formulizer();
603        let mut new_version = self.version.clone();
604        let mut update_journal = false;
605        // Guarantee a version header on the first save
606        if self.version.epoc() == 0 {
607            self.set_version_change();
608        }
609        match self.changes {
610            Changes::Version => {
611                new_version.increment_epoc();
612                writer.write_version(new_version.epoc(), new_version.partial());
613                self.iter_rows_mut().for_each(|opt_rule| match opt_rule {
614                    Some(rule) => {
615                        rule.save(&mut writer);
616                    }
617                    None => {
618                        writer.write_eol();
619                    }
620                });
621                update_journal = true;
622            }
623            Changes::Partial => {
624                new_version.increment_partial();
625                writer.write_version(new_version.epoc(), new_version.partial());
626                self.iter_mut_with_rows()
627                    .for_each(|(index, opt_rule)| match opt_rule {
628                        Some(rule) => {
629                            rule.partial_save(index, &mut writer);
630                        }
631                        None => {}
632                    });
633            }
634            Changes::None => return Ok(()),
635        }
636        // Open file for appending
637        let mut file = OpenOptions::new()
638            .create(true)
639            .append(true)
640            .open(&self.path)?;
641
642        if update_journal {
643            self.journals
644                .push(Journal::new(new_version.epoc(), file.metadata()?.len()));
645            journal_save(&self.path, &self.journals)?;
646        }
647        file.write(writer.finish().as_bytes())?;
648        self.version = new_version;
649        self.clear_changes();
650        Ok(())
651    }
652
653    /// Gets a mutable reference to a rule by identifier.
654    ///
655    /// # Parameters
656    ///
657    /// * `identifier` - The rule identifier
658    ///
659    /// # Returns
660    ///
661    /// `Some(&mut Rule)` if the rule exists, `None` otherwise.
662    pub fn get_rule_mut(&mut self, identifier: &str) -> Option<&mut Rule> {
663        self.lookup
664            .get(identifier)
665            .and_then(|&index| self.rows[index].as_mut())
666    }
667
668    /// Appends a rule to the workflow or updates an existing rule.
669    ///
670    /// If a rule with the same identifier already exists, it is updated.
671    /// Otherwise, the rule is appended to the end of the workflow.
672    ///
673    /// # Parameters
674    ///
675    /// * `row` - The rule to append or update
676    pub fn append_or_update(&mut self, row: Option<Rule>) {
677        if let Some(mut rule) = row {
678            // Update if the rule already exists
679            if self.lookup.contains_key(rule.identifier()) {
680                // Rule update guaranteed because identifier exists
681                self.update_rule(rule).unwrap();
682                return;
683            }
684            let index = self.rows.len();
685            self.lookup.insert(rule.identifier().to_string(), index);
686            if self.track_changes {
687                rule.touch();
688                self.set_partial_change();
689            }
690            self.rows.push(Some(rule));
691        } else {
692            if self.track_changes {
693                self.set_version_change();
694            }
695            self.rows.push(None);
696        }
697    }
698
699    /// Updates an existing rule.
700    ///
701    /// # Parameters
702    ///
703    /// * `rule` - The updated rule
704    ///
705    /// # Returns
706    ///
707    /// `Ok(())` if successful, or an error if the rule doesn't exist.
708    pub fn update_rule(&mut self, mut rule: Rule) -> Result<()> {
709        match self.lookup.get(rule.identifier()) {
710            Some(&index) => {
711                if self.track_changes {
712                    rule.touch();
713                    self.set_partial_change();
714                }
715                self.rows[index] = Some(rule);
716                Ok(())
717            }
718            None => Err(anyhow!("Rule {} not found", rule.identifier())),
719        }
720    }
721
722    /// Deletes a rule by identifier.
723    ///
724    /// # Parameters
725    ///
726    /// * `identifier` - The rule identifier to delete
727    ///
728    /// # Returns
729    ///
730    /// `Some(Rule)` if the rule was deleted, `None` if it didn't exist.
731    pub fn delete_rule(&mut self, identifier: &str) -> Option<Rule> {
732        match self.lookup.remove(identifier) {
733            Some(index) => {
734                self.set_version_change();
735                self.rows[index].take()
736            }
737            None => None,
738        }
739    }
740
741    // --- Indexed Rule Access --
742
743    /// Gets the row index of a rule by identifier.
744    ///
745    /// # Parameters
746    ///
747    /// * `identifier` - The rule identifier
748    ///
749    /// # Returns
750    ///
751    /// `Some(usize)` if the rule exists, `None` otherwise.
752    pub fn get_index(&self, identifier: &str) -> Option<usize> {
753        self.lookup.get(identifier).copied()
754    }
755
756    /// Sets a rule at a specific row index.
757    ///
758    /// If a rule already exists at the specified index with a different identifier,
759    /// this method will return an error. If the index is beyond the current size
760    /// of the workflow, the rows vector will be expanded to accommodate it.
761    ///
762    /// # Parameters
763    ///
764    /// * `index` - The zero-based row index where to place the rule
765    /// * `row` - The rule to set, or None to create an empty row
766    ///
767    /// # Returns
768    ///
769    /// `Ok(())` if successful, or an error if there's a conflict with an existing rule
770    pub fn set_row(&mut self, index: usize, row: Option<Rule>) -> Result<()> {
771        if let Some(mut rule) = row {
772            // Check if the rule identifier already exists
773            if self.lookup.contains_key(rule.identifier()) {
774                if index < self.rows.len() {
775                    // Get the rule at the specified index
776                    match self.rows[index].as_ref() {
777                        Some(old_rule) => {
778                            // Ensure the new rule and old rule identifiers match
779                            if rule.identifier() != old_rule.identifier() {
780                                return Err(anyhow!("Rule already exists"));
781                            }
782                            // Manage this special case
783                            if self.track_changes {
784                                rule.touch();
785                                self.set_partial_change();
786                            }
787                            self.rows[index] = Some(rule);
788                            return Ok(());
789                        }
790                        None => return Err(anyhow!("Rule already exists")),
791                    }
792                } else {
793                    return Err(anyhow!("Rule already exists"));
794                }
795            }
796            // Expand vector if needed
797            if index >= self.rows.len() {
798                self.rows.resize_with(index + 1, || None);
799            }
800            // Insert at specified position
801            self.lookup.insert(rule.identifier().to_string(), index);
802            if self.track_changes {
803                rule.touch();
804                self.set_partial_change();
805            }
806            self.rows[index] = Some(rule);
807        } else {
808            // Expand vector if needed
809            if index >= self.rows.len() {
810                self.rows.resize_with(index + 1, || None);
811            }
812            if self.track_changes {
813                self.set_partial_change();
814            }
815            self.rows[index] = None;
816        }
817        Ok(())
818    }
819
820    /// Inserts a rule at a specific row index.
821    ///
822    /// This method inserts a rule at the specified index, shifting existing rules
823    /// to higher indices. If the index is beyond the current size of the workflow,
824    /// empty rows will be created as needed. If a rule with the same identifier
825    /// already exists in the workflow, this method returns an error.
826    ///
827    /// # Parameters
828    ///
829    /// * `index` - The zero-based row index where to insert the rule
830    /// * `row` - The rule to insert, or None to create an empty row
831    ///
832    /// # Returns
833    ///
834    /// `Ok(())` if successful, or an error if a rule with the same identifier already exists
835    pub fn insert_row(&mut self, index: usize, row: Option<Rule>) -> Result<()> {
836        if let Some(rule) = row {
837            if self.lookup.contains_key(rule.identifier()) {
838                return Err(anyhow!("Rule already exists"));
839            }
840            if index >= self.rows.len() {
841                // Expand vector if needed
842                if index > self.rows.len() {
843                    self.rows.resize_with(index, || None);
844                }
845                // and append
846                self.lookup.insert(rule.identifier().to_string(), index);
847                self.rows.push(Some(rule));
848            } else {
849                // Insert at specified position
850                self.lookup.insert(rule.identifier().to_string(), index);
851                self.rows.insert(index, Some(rule));
852            }
853        } else if index >= self.rows.len() {
854            // Expand vector if needed
855            if index > self.rows.len() {
856                self.rows.resize_with(index, || None);
857            }
858            // and append
859            self.rows.push(None);
860        } else {
861            self.rows.insert(index, None);
862        }
863
864        // Update indices of all rows
865        self.reindex();
866        self.set_version_change();
867        Ok(())
868    }
869
870    /// Repositions a rule from one row index to another.
871    ///
872    /// This method moves a rule from the `from` index to the `to` index,
873    /// shifting other rules as needed. If either index is beyond the current
874    /// size of the workflow, the rows vector will be expanded to accommodate them.
875    ///
876    /// # Parameters
877    ///
878    /// * `from` - The current zero-based row index of the rule to move
879    /// * `to` - The target zero-based row index where to place the rule
880    ///
881    /// # Returns
882    ///
883    /// `Ok(())` if successful, or an error if the operation fails
884    pub fn reposition_row(&mut self, from: usize, to: usize) -> Result<()> {
885        if from != to {
886            // Expand vectors if needed
887            let max_row = from.max(to);
888            if max_row >= self.rows.len() {
889                self.rows.resize_with(max_row + 1, || None);
890            }
891            self.set_partial_change();
892
893            // Check if there's actually a rules to move
894            if self.rows[from].is_none() && self.rows[to].is_none() {
895                return Ok(());
896            }
897
898            let opt_rule = self.rows.remove(from);
899            self.rows.insert(to, opt_rule);
900
901            // Rebuild index mappings due to reordering
902            self.reindex();
903        }
904        Ok(())
905    }
906
907    /// Clears a row at a specific index, removing the rule if present.
908    ///
909    /// This method removes the rule at the specified index but keeps the row
910    /// structure intact. The row will become empty but still exist in the workflow.
911    /// If the index is out of bounds, this method returns None.
912    ///
913    /// # Parameters
914    ///
915    /// * `index` - The zero-based row index to clear
916    ///
917    /// # Returns
918    ///
919    /// `Some(Rule)` if a rule was present and removed, `None` if the row was already empty or index was out of bounds
920    pub fn clear_row(&mut self, index: usize) -> Option<Rule> {
921        // Check bounds
922        if index >= self.rows.len() {
923            return None;
924        }
925
926        match self.rows[index].take() {
927            Some(rule) => {
928                // Remove from index map
929                self.lookup.remove(rule.identifier());
930                self.set_version_change();
931                Some(rule)
932            }
933            None => None,
934        }
935    }
936
937    /// Removes a row at a specific index, removing the rule and shrinking the workflow.
938    ///
939    /// This method removes the rule at the specified index and removes the row
940    /// entirely from the workflow, shifting subsequent rows to lower indices.
941    /// If the index is out of bounds, this method returns None.
942    ///
943    /// # Parameters
944    ///
945    /// * `index` - The zero-based row index to remove
946    ///
947    /// # Returns
948    ///
949    /// `Some(Rule)` if a rule was present and removed, `None` if the index was out of bounds
950    pub fn remove_row(&mut self, index: usize) -> Option<Rule> {
951        if index >= self.rows.len() {
952            return None;
953        }
954
955        match self.rows.remove(index) {
956            Some(rule) => {
957                // Remove from index map
958                self.lookup.remove(rule.identifier());
959
960                // Rebuild indices since we have empty spaces now
961                self.reindex();
962                self.set_version_change();
963                Some(rule)
964            }
965            None => None,
966        }
967    }
968
969    /// Create a mutable iterator over the rows.
970    ///
971    /// This iterator yields mutable references to `Option<Rule>`, allowing
972    /// modification of both rules and empty rows.
973    pub fn iter_rows_mut<'a>(&'a mut self) -> Box<dyn Iterator<Item = &'a mut Option<Rule>> + 'a> {
974        Box::new(self.rows.iter_mut())
975    }
976    /// Create a mutable iterator over the rules.
977    ///
978    /// This iterator yields mutable references to `Rule`, skipping empty rows.
979    /// It's useful when you need to modify only the actual rules in the workflow.
980    pub fn iter_rules_mut<'a>(&'a mut self) -> Box<dyn Iterator<Item = &'a mut Rule> + 'a> {
981        Box::new(self.rows.iter_mut().filter_map(|opt_rule| opt_rule.as_mut()))
982    }
983
984    /// Create an iterator over the rules with their indices.
985    ///
986    /// This iterator yields tuples of `(index, &Option<Rule>)`, providing
987    /// both the position and content of each row including empty ones.
988    pub fn iter_with_rows(&self) -> impl Iterator<Item = (usize, &Option<Rule>)> {
989        self.rows
990            .iter()
991            .enumerate()
992            .map(|(index, opt_rule)| (index, opt_rule))
993    }
994
995    /// Create a mutable iterator over the rules with their indices.
996    ///
997    /// This iterator yields tuples of `(index, &mut Option<Rule>)`, providing
998    /// both the position and mutable content of each row including empty ones.
999    pub fn iter_mut_with_rows(&mut self) -> impl Iterator<Item = (usize, &mut Option<Rule>)> {
1000        self.rows
1001            .iter_mut()
1002            .enumerate()
1003            .map(|(index, opt_rule)| (index, opt_rule))
1004    }
1005
1006    /// Rebuilds the lookup mapping index.
1007    ///
1008    /// This helper function rebuilds the internal hash map that maps rule identifiers
1009    /// to their row indices. It's called automatically after operations that change
1010    /// the position of rules within the workflow.
1011    pub fn reindex(&mut self) {
1012        self.lookup.clear();
1013        for (index, opt_rule) in self.rows.iter().enumerate() {
1014            if let Some(rule) = opt_rule {
1015                self.lookup.insert(rule.identifier().to_string(), index);
1016            }
1017        }
1018    }
1019
1020    /// Evaluates all rules in the workflow.
1021    ///
1022    /// This method evaluates each rule's expression in the context and stores
1023    /// the result back in the context. It's used to compute the current values
1024    /// of all rules in the workflow.
1025    ///
1026    /// # Parameters
1027    ///
1028    /// * `context` - The evaluation context where results will be stored
1029    pub fn evaluate(&self, context: &mut dyn ContextLike) {
1030        for rule in self.rows.iter().filter_map(|opt_rule| opt_rule.as_ref()) {
1031            let expression = rule.expression();
1032            let reference = Reference::new(rule.identifier());
1033            let value = expression.invoke(context);
1034            let _ = context.set_referenced(&reference, value);
1035        }
1036    }
1037
1038    /// Parses AIM content and populates the workflow.
1039    ///
1040    /// This method parses AIM format content and creates rules based on the parsed data.
1041    /// It can optionally parse only up to a specific partial version. The method clears
1042    /// the current workflow contents before parsing.
1043    ///
1044    /// # Parameters
1045    ///
1046    /// * `input` - The AIM content to parse
1047    /// * `partial` - Optional partial version to parse up to
1048    ///
1049    /// # Returns
1050    ///
1051    /// `Ok(())` if parsing was successful, or an error if parsing failed
1052    pub fn parse(&mut self, input: &str, partial: Option<u32>) -> Result<()> {
1053        // Clear contents
1054        self.rows.clear();
1055        self.lookup.clear();
1056        self.clear_changes();
1057
1058        let lines: Vec<&str> = input.lines().collect();
1059        let mut is_version = false;
1060        let mut is_partial = false;
1061
1062        for line in lines {
1063            let mut opt_index: Option<u32> = None;
1064            let mut line = line.trim();
1065            // Append empty lines to maintain row spacing in first (non-partial) section
1066            if line.is_empty() {
1067                if is_version && self.version.partial() == 0 {
1068                    self.append_or_update(None);
1069                }
1070                continue;
1071            }
1072
1073            // Try to parse comment (lines starting with #)
1074            if line.starts_with('#') {
1075                continue;
1076            }
1077
1078            // Try to parse version headers (lines starting with [)
1079            if line.starts_with('[') {
1080                match parse_version(line) {
1081                    Ok(current) => {
1082                        // When version/epoc is 0, assign the first version header encountered
1083                        if self.version.epoc() == 0 {
1084                            is_version = true;
1085                            self.version = current;
1086                        }
1087                        // When epoc is known, merge partials
1088                        else if self.version.epoc() == current.epoc() {
1089                            is_version = true;
1090                            // We've move past the target partial, stop parsing
1091                            if is_partial {
1092                                break;
1093                            }
1094                            self.version.set_partial(current.partial());
1095                            if let Some(opt_partial) = partial {
1096                                if opt_partial == current.partial() {
1097                                    is_partial = true;
1098                                }
1099                            }
1100                        } else if is_version {
1101                            // We've moved past the target version, stop parsing
1102                            break;
1103                        }
1104                    }
1105                    // Skip lines with syntax errors
1106                    Err(_e) => {}
1107                }
1108                continue;
1109            }
1110
1111            // If we haven't found the target version yet, skip this line
1112            if !is_version {
1113                continue;
1114            }
1115
1116            /* if line.starts_with(?) {
1117                // The grammar can support additional extension here if needed in the future
1118                // with signifiers like: '"'|'!'|'@'|'$'|'%'|'^'|'&'|'*'|'('
1119            } */
1120
1121            // Try to parse leading number (rule index used by partial saves)
1122            if line.chars().next().map_or(false, |c| c.is_ascii_digit()) {
1123                match parse_unsigned(line) {
1124                    Ok((remainder, index)) => {
1125                        opt_index = Some(index);
1126                        line = remainder;
1127                    }
1128                    Err(_) => {}
1129                }
1130            }
1131
1132            // Try to parse rule
1133            match parse_rule(line) {
1134                Ok(rule) => {
1135                    if rule.is_node() {
1136                        todo!();                        
1137                    }
1138                    match opt_index {
1139                        Some(index) => {
1140                            // Set rule at index
1141                            if self.contains(&rule.identifier()) {
1142                                let _ = self.update_rule(rule);
1143                            } else {
1144                                let _ = self.set_row(index as usize, Some(rule));
1145                            }
1146                        }
1147                        None => {
1148                            // Append or overwrite existing rule
1149                            self.append_or_update(Some(rule));
1150                        }
1151                    }
1152                }
1153                Err(_) => {
1154                    // Ignore syntax errors, but maintain row spacing in (non-partial) sections
1155                    if self.version.partial() == 0 {
1156                        self.append_or_update(None);
1157                    }
1158                }
1159            }
1160        }
1161
1162        if !is_version && self.version.epoc() > 0 {
1163            return Err(anyhow!("Version {} not found", self.version.epoc()));
1164        }
1165
1166        Ok(())
1167    }
1168}
1169
1170impl WorkflowLike for Workflow {
1171    
1172    // --- Core Metadata ---
1173
1174    fn version(&self) -> u32 {
1175        self.version.epoc()
1176    }
1177
1178    fn minor_version(&self) -> u32 {
1179        self.version.partial()
1180    }
1181
1182    fn reference(&self) -> &Reference {
1183        &self.reference
1184    }
1185
1186    fn path(&self) -> &Path {
1187        self.path.as_path()
1188    }
1189
1190    // --- Rule Access ---
1191
1192    fn get_row(&self, index: usize) -> Option<Rule> {
1193        if index < self.rows.len() {
1194            self.rows[index].clone()
1195        } else {
1196            None
1197        }
1198    }
1199
1200    fn get_rule(&self, identifier: &str) -> Option<Rule> {
1201        self.lookup
1202            .get(identifier)
1203            .and_then(|&index| self.rows[index].clone())
1204    }
1205
1206    // --- Introspection & Bulk Access ---
1207
1208    fn contains(&self, identifier: &str) -> bool {
1209        self.lookup.contains_key(identifier)
1210    }
1211
1212    fn has_rule(&self, index: usize) -> bool {
1213        if index < self.rows.len() {
1214            self.rows[index].is_some()
1215        } else {
1216            false
1217        }
1218    }
1219
1220    fn rule_count(&self) -> usize {
1221        self.lookup.len()
1222    }
1223
1224    fn rule_rows(&self) -> usize {
1225        self.rows.len()
1226    }
1227
1228    fn iter_rows<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Option<Rule>> + 'a> {
1229        Box::new(self.rows.iter())
1230    }
1231
1232    fn iter_rules<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Rule> + 'a> {
1233        Box::new(self.rows.iter().filter_map(|opt_rule| opt_rule.as_ref()))
1234    }
1235
1236    // --- Trait Object Helper ---
1237
1238    fn as_any(&self) -> &dyn Any {
1239        self
1240    }
1241}