aimx/aim/
workflow.rs

1//! Workflow container and versioned persistence.
2//!
3//! Provides rule storage, lookup, and journaled version management for AIMX
4//! workflows. Used as the concrete implementation behind [`WorkflowLike`].
5
6use crate::{
7    Aim, aim::{BasePath, Journal, Row, Rule, Typedef, Version, Writer, WriterLike, parse_rule, parse_version, read_latest, read_version, save_journal, version::now}, expressions::{Expression, Reference}, literals::parse_unsigned, values::{Errata, Instance, Node, Value}
8};
9use anyhow::{Result, anyhow};
10use jiff::civil::DateTime;
11use std::{
12    any::Any,
13    collections::HashMap,
14    fmt::Debug, // Import Debug trait for the trait bound
15    fs::OpenOptions,
16    io::Write,
17    path::{Path, PathBuf},
18    sync::Arc,
19    fmt,
20};
21
22/// Thread-safe abstraction over workflow rule storage and metadata.
23///
24/// Implemented by concrete workflow containers such as [`Workflow`].
25pub trait WorkflowLike: Send + Sync + Debug + 'static {
26    // --- Core Metadata ---
27
28    /// Returns the global locator (Reference) for this workflow.
29    ///
30    /// The locator uniquely identifies the workflow within the workspace.
31    fn locator(&self) -> Arc<Reference>;
32
33    /// Returns the file system path to the workflow's AIM file.
34    ///
35    /// This path points to the `.aim` file that stores the workflow content.
36    fn path(&self) -> Arc<PathBuf>;
37
38    fn date(&self) -> DateTime;
39
40    /// Returns the major version (epoch) of the workflow.
41    ///
42    /// The epoch represents major structural changes to the workflow.
43    /// Increments when significant changes are made that require a new version.
44    fn version(&self) -> u32;
45
46    /// Returns the minor version (partial) of the workflow.
47    ///
48    /// The partial represents incremental updates within the same epoch.
49    /// Increments for minor changes that don't require a new major version.
50    fn minor_version(&self) -> u32;
51
52    // --- Rule Access ---
53
54    /// Gets the row index of a rule by identifier.
55    ///
56    /// # Parameters
57    ///
58    /// * `identifier` - The rule identifier
59    ///
60    /// # Returns
61    ///
62    /// `Some(usize)` if the rule exists, `None` otherwise.
63    fn get_index(&self, identifier: &str) -> Option<usize>;
64
65    /// Retrieves a rule by its row index.
66    ///
67    /// # Parameters
68    /// - `index`: The zero-based row index
69    ///
70    /// # Returns
71    /// `Some(Rule)` if the index is valid and contains a rule, `None` otherwise
72    fn get_row(&self, index: usize) -> Row;
73
74    /// Retrieves a rule by its identifier.
75    ///
76    /// # Parameters
77    /// - `identifier`: The rule's unique identifier
78    ///
79    /// # Returns
80    /// `Some(Rule)` if the identifier exists, `None` otherwise
81    fn get_rule(&self, identifier: &str) -> Option<Arc<Rule>>;
82
83    // --- Introspection & Bulk Access ---
84
85    /// Checks if a rule with the given identifier exists.
86    ///
87    /// # Parameters
88    /// - `identifier`: The identifier to check
89    ///
90    /// # Returns
91    /// `true` if the rule exists, `false` otherwise
92    fn contains(&self, identifier: &str) -> bool;
93
94    /// Checks if a specific row index contains a rule.
95    ///
96    /// # Parameters
97    /// - `index`: The row index to check
98    ///
99    /// # Returns
100    /// `true` if the row contains a rule, `false` if empty or out of bounds
101    fn has_rule(&self, index: usize) -> bool;
102
103    /// Returns the number of rules in the workflow.
104    ///
105    /// This counts only the actual rules, excluding empty rows.
106    fn rule_count(&self) -> usize;
107
108    /// Returns the total number of rows in the workflow.
109    ///
110    /// This includes both rules and empty rows.
111    fn row_count(&self) -> usize;
112
113    /// Returns an iterator over all rows.
114    ///
115    /// The iterator yields `&Option<Rule>` items, including empty rows.
116    /// Use this when you need to preserve the exact row structure.
117    fn iter_rows<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Row> + 'a>;
118
119    /// Returns an iterator over all rules.
120    ///
121    /// The iterator yields `&Rule` items, skipping empty rows.
122    /// Use this when you only need the actual rules.
123    fn iter_rules(&self) -> Box<dyn Iterator<Item = Arc<Rule>> + '_>;
124
125    // --- Trait Object Helper ---
126
127    /// Returns a reference to the workflow as a trait object.
128    ///
129    /// This method enables downcasting to the concrete workflow type
130    /// when working with trait objects.
131    fn as_any(&self) -> &dyn Any;
132}
133
134/// Workflow execution result used by orchestrators to understand whether
135/// evaluation completed, suspended, or failed.
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum WorkflowStatus {
138    /// Evaluation ran to completion.
139    Completed,
140    /// Evaluation suspended at a rule identifier; may be resumed.
141    Suspended { state_id: Arc<str> },
142    /// Evaluation failed at or near `state_id` with optional error detail.
143    Failed {
144        state_id: Option<Arc<str>>,
145        errata: Option<Arc<Errata>>,
146    },
147}
148
149impl WorkflowStatus {
150    /// Returns true if the workflow completed successfully.
151    pub fn is_completed(&self) -> bool {
152        matches!(self, WorkflowStatus::Completed)
153    }
154
155    /// Returns the failure Errata when status is Failed.
156    pub fn error(&self) -> Option<&Errata> {
157        match self {
158            WorkflowStatus::Failed {
159                errata: Some(e), ..
160            } => Some(e),
161            _ => None,
162        }
163    }
164
165    /// Returns the identifier of the rule where execution last stopped
166    /// for both Suspended and Failed states.
167    pub fn state_id(&self) -> Option<Arc<str>> {
168        match self {
169            WorkflowStatus::Suspended { state_id }
170            | WorkflowStatus::Failed {
171                state_id: Some(state_id),
172                ..
173            } => Some(state_id.clone()),
174            _ => None,
175        }
176    }
177}
178
179/// Change classification used to decide how a workflow is persisted.
180///
181/// `None` → no-op, `Partial` → append partial update, `Version` → write full version.
182#[derive(Debug, Clone, PartialEq)]
183pub enum Changes {
184    /// No changes have been made
185    None,
186    /// Changes that only require a partial save
187    Partial,
188    /// Changes that require a full version change
189    Version,
190}
191
192/// In-memory workflow with journaled versioning for AIMX rules.
193///
194/// Maintains ordered rule rows plus identifier lookup, tracks changes as
195/// [`Changes`], and persists via versioned AIM/journal files.
196#[derive(Debug, Clone)]
197pub struct Workflow {
198    /// A global reference to this workflow's node.
199    locator: Arc<Reference>,
200    /// Path to the workflow `.aim` file.
201    path: Arc<PathBuf>,
202    /// Ordered storage of rules; `None` represents an empty row.
203    rows: Vec<Row>,
204    /// Hash map for identifier-based lookups to row indices.
205    lookup: HashMap<Arc<str>, usize>,
206    /// Current version
207    version: Version,
208    /// Version journal
209    journals: Vec<Arc<Journal>>,
210    /// Type of changes that have been made
211    changes: Changes,
212
213    /// This flag allows workflows to track changes for partial saves.
214    /// This flag only affects `append_or_update`, `update_rule` and
215    /// `set_rule` which are used by `Workflow::parse()`. Normally this
216    /// flag should be set to `true` after parsing input, however it should
217    /// be set to `true` before loading a previous versions it ensures all
218    /// 'recovered' rules are saved.
219    track_changes: bool,
220}
221
222impl PartialEq<Workflow> for Reference {
223    fn eq(&self, other: &Workflow) -> bool {
224        self == &*other.locator
225    }
226}
227
228impl PartialEq<Reference> for Workflow {
229    fn eq(&self, other: &Reference) -> bool {
230        &*self.locator == other
231    }
232}
233
234impl PartialEq<Workflow> for Workflow {
235    fn eq(&self, other: &Workflow) -> bool {
236        &self.locator == &other.locator
237    }
238}
239
240
241impl WorkflowLike for Workflow {
242    // --- Core Metadata ---
243
244    fn locator(&self) -> Arc<Reference> {
245        self.locator.clone()
246    }
247
248    fn path(&self) -> Arc<PathBuf> {
249        self.path.clone()
250    }
251
252    fn date(&self) -> DateTime {
253        self.version.date()
254    }
255
256    fn version(&self) -> u32 {
257        self.version.epoch()
258    }
259
260    fn minor_version(&self) -> u32 {
261        self.version.partial()
262    }
263
264    // --- Rule Access ---
265
266    fn get_index(&self, identifier: &str) -> Option<usize> {
267        self.lookup.get(identifier).copied()
268    }
269
270    fn has_rule(&self, index: usize) -> bool {
271        if index < self.rows.len() {
272            self.rows[index].is_rule()
273        } else {
274            false
275        }
276    }
277
278    // --- Introspection & Bulk Access ---
279
280    fn get_row(&self, index: usize) -> Row {
281        if index < self.rows.len() {
282            self.rows[index].clone()
283        } else {
284            Row::Empty
285        }
286    }
287
288    fn contains(&self, identifier: &str) -> bool {
289        self.lookup.contains_key(identifier)
290    }
291
292    fn get_rule(&self, identifier: &str) -> Option<Arc<Rule>> {
293        self.lookup
294            .get(identifier)
295            .and_then(|&index| self.rows[index].rule())
296    }
297
298    fn rule_count(&self) -> usize {
299        self.lookup.len()
300    }
301
302    fn row_count(&self) -> usize {
303        self.rows.len()
304    }
305
306    fn iter_rows<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Row> + 'a> {
307        Box::new(self.rows.iter())
308    }
309
310    fn iter_rules(&self) -> Box<dyn Iterator<Item = Arc<Rule>> + '_> {
311        Box::new(self.rows.iter().filter_map(|row| row.rule()))
312    }
313
314    // --- Trait Object Helper ---
315
316    fn as_any(&self) -> &dyn Any {
317        self
318    }
319}
320
321impl Workflow {
322    // -----------------------------------------------------------------------------
323    // Test Methods
324    // -----------------------------------------------------------------------------
325
326    /// Creates a new empty workflow.
327    ///
328    /// The workflow starts with:
329    /// - Empty rule storage
330    /// - Version 0.0
331    /// - Extract inference pattern
332    /// - Empty path and reference
333    /// - No changes pending
334    /// - Change tracking disabled
335    ///
336
337    pub fn for_testing(locator: Arc<Reference>) -> Result<Self> {
338        let path: Arc<PathBuf> = Arc::new(PathBuf::from("test.aim"));
339        Ok(Self::new_from(locator, path))
340    }
341
342    /// Creates a new workflow by parsing AIM content.
343    ///
344    /// This constructor parses the provided AIM content and creates a workflow
345    /// with the parsed rules. Change tracking is automatically enabled.
346    ///
347
348    pub fn parse_for_tests(input: &str) -> Result<Self> {
349        let mut workflow = Self::for_testing(Reference::workspace())?;
350        workflow.parse(input, None)?;
351        Ok(workflow)
352    }
353
354    // -----------------------------------------------------------------------------
355    // Api Methods
356    // -----------------------------------------------------------------------------
357
358    // Called exclusively by Workspace::create
359    pub fn new_from(locator: Arc<Reference>, path: Arc<PathBuf>) -> Self {
360        Workflow {
361            locator,
362            path,
363            rows: Vec::new(),
364            lookup: HashMap::new(),
365            version: Version::new(0, 0, now()),
366            journals: Vec::new(),
367            changes: Changes::None,
368            track_changes: true,
369        }
370    }
371
372    /// Creates a new workflow by loading from a global locator (Reference).
373    ///
374    /// This constructor loads a workflow from the file system based on the
375    /// provided locator. If the file doesn't exist, an empty workflow
376    /// is created with the locator set.
377    ///
378
379    pub fn open(locator: Arc<Reference>) -> Result<Self> {
380        let path = BasePath::convert(&locator)?;
381        let mut workflow = Self::new_from(locator, path);
382        if workflow.path.exists() {
383            workflow.load()?;
384        }
385        Ok(workflow)
386    }
387
388    /// Checks if the workflow has unsaved changes.
389    ///
390    /// # Returns
391    ///
392    /// `true` if the workflow has changes that need to be saved, `false` otherwise.
393    pub fn is_touched(&self) -> bool {
394        self.changes != Changes::None
395    }
396
397    /// Flags changes that only require a partial save.
398    ///
399    /// This method escalates the change status from `None` to `Partial`.
400    /// If changes are already at `Partial` or `Version`, they remain unchanged.
401    pub fn set_partial_change(&mut self) {
402        // We only escalate partial changes from none
403        if self.changes == Changes::None {
404            self.changes = Changes::Partial;
405        }
406    }
407
408    /// Flags changes that require a version change on save.
409    ///
410    /// This method sets the change status to `Version`, indicating that
411    /// structural changes have been made that require a new version.
412    pub fn set_version_change(&mut self) {
413        self.changes = Changes::Version;
414    }
415
416    /// Clears the changes flag.
417    ///
418    /// This method is typically called after a successful save operation
419    /// to reset the change tracking state.
420    pub fn clear_changes(&mut self) {
421        self.changes = Changes::None;
422    }
423
424    /// Gets the first version number from the journal.
425    ///
426    /// # Returns
427    ///
428    /// The epoch of the first version, or 0 if no versions exist.
429    pub fn first_version(&self) -> u32 {
430        if self.journals.is_empty() {
431            0
432        } else {
433            // first() guaranteed to exist
434            self.journals.first().unwrap().version()
435        }
436    }
437
438    /// Gets the latest version number from the journal.
439    ///
440    /// # Returns
441    ///
442    /// The epoch of the latest version, or 0 if no versions exist.
443    pub fn latest_version(&self) -> u32 {
444        if self.journals.is_empty() {
445            0
446        } else {
447            // last() guaranteed to exist
448            self.journals.last().unwrap().version()
449        }
450    }
451
452    /// Test-only: override the workflow file path.
453    pub fn set_path(&mut self, path: &Path) {
454        self.path = Arc::new(path.to_path_buf());
455    }
456
457    /// Loads a specific version of an AIM file.
458    ///
459    /// This method loads a particular version (and optionally a specific partial)
460    /// from the AIM file. After loading, the workflow switches back to the latest
461    /// version but flags that a version change is needed since the loaded version
462    /// differs from the current one.
463    ///
464    /// # Parameters
465    ///
466    /// * `path` - The path to the AIM file
467    /// * `version` - The epoch version to load
468    /// * `partial` - Optional partial version within the epoch
469    ///
470    /// # Returns
471    ///
472    /// `Ok(())` if successful, or an error if the version cannot be loaded.
473    pub fn load_version(&mut self, version: u32, partial: Option<u32>) -> Result<()> {
474        // Read the contents of specific version section
475        let contents = read_version(&mut self.journals, &self.path, version)?;
476        self.version.set_epoch(version);
477        // IMPORTANT! parser will throw an error if version is not found
478        self.parse(&contents, partial)?;
479        // Switch back to the latest version
480        self.version.set_epoch(self.latest_version());
481        // This is different to the latest version so flag a version change
482        self.set_version_change();
483        Ok(())
484    }
485
486    /// Loads the latest version of an AIM file.
487    ///
488    /// This method loads the most recent version from the AIM file and
489    /// clears any pending changes. Change tracking is enabled after loading.
490    ///
491    /// # Parameters
492    ///
493    /// * `path` - The path to the AIM file
494    ///
495    /// # Returns
496    ///
497    /// `Ok(())` if successful, or an error if the file cannot be loaded.
498    pub fn load(&mut self) -> Result<()> {
499        // Read the latest section
500        let contents = read_latest(&mut self.journals, &self.path)?;
501        self.version.set_epoch(self.latest_version());
502        self.track_changes = false;
503        self.parse(&contents, None)?;
504        self.clear_changes();
505        self.track_changes = true;
506        Ok(())
507    }
508
509    /// Saves the AIM structure to its associated file.
510    ///
511    /// This function saves changes to the AIM file based on the type of changes made:
512    /// - `Changes::Version`: Creates a new version with incremented epoch
513    /// - `Changes::Partial`: Creates a partial update with incremented partial counter
514    /// - `Changes::None`: No changes to save, returns Ok(())
515    ///
516    /// The function also updates the journal file with position information for fast lookup.
517    ///
518    /// # Returns
519    ///
520    /// `Ok(())` if successful, or an error if the save operation fails.
521    ///
522    pub fn save(&mut self) -> Result<()> {
523        let mut writer = Writer::formulizer();
524        let mut new_version = self.version.clone();
525        let mut update_journal = false;
526        // Guarantee a version header on the first save
527        if self.version.epoch() == 0 {
528            self.set_version_change();
529        }
530        match self.changes {
531            Changes::Version => {
532                new_version.increment_epoch();
533                new_version.print(&mut writer);
534                // Collect touched rows first to avoid borrowing conflicts
535                let mut touched_rows = Vec::new();
536                for (index, row) in self.iter_with_rows() {
537                    let untouched = row.save(&mut writer);
538                    if let Some(row) = untouched {
539                        touched_rows.push((index, row));
540                    }
541                }
542                // Now update the rows after the iteration is complete
543                for (index, row) in touched_rows {
544                    self.rows[index] = row;
545                }
546                update_journal = true;
547            }
548            Changes::Partial => {
549                new_version.increment_partial();
550                new_version.print(&mut writer);
551                // Collect touched rows first to avoid borrowing conflicts
552                let mut touched_rows = Vec::new();
553                for (index, row) in self.iter_with_rows() {
554                    let untouched = row.partial_save(index, &mut writer);
555                    if let Some(row) = untouched {
556                        touched_rows.push((index, row));
557                    }
558                }
559                // Now update the rows after the iteration is complete
560                for (index, row) in touched_rows {
561                    self.rows[index] = row;
562                }
563            }
564            Changes::None => return Ok(()),
565        }
566        // Open file for appending
567        let mut file = OpenOptions::new()
568            .create(true)
569            .append(true)
570            .open(&*self.path)?;
571
572        if update_journal {
573            self.journals
574                .push(Arc::new(Journal::new(new_version.epoch(), file.metadata()?.len(), now())));
575            save_journal(&self.path, &self.journals)?;
576        }
577        file.write(writer.finish().as_bytes())?;
578        self.version = new_version;
579        self.clear_changes();
580        Ok(())
581    }
582
583    pub fn save_all(&mut self) -> Result<()> {
584        // Recursively save child nodes and instances
585        for rule in self.iter_rules() {
586            if let Some(node) = rule.get_node() {
587                node.save_all()?;
588            // Also handle instances within this workflow
589            } else if let Some(instance) = rule.get_instance() {
590                instance.save_all()?;
591            }
592        }
593        self.save()
594    }
595
596    pub fn check_identifier_unique(&self, identifier: &str) -> Result<()> {
597        Aim::check_identifier(identifier)?;
598        if self.contains(&identifier) {
599            return Err(anyhow!("Rule {} exists in {}", identifier, self.locator));
600        }
601        Ok(())
602    }
603
604    pub fn add_node(&mut self, identifier: Arc<str>, node: Node) -> Result<Arc<Reference>> {
605        self.check_identifier_unique(&identifier)?;
606
607        // If this is the workspace make a new top level reference
608        let reference = if self.locator.contains_root() {
609            Reference::new(identifier.clone())
610        } else {
611            Arc::new(self.locator.add_child(identifier.clone())?)
612        };
613
614        // Initialize as Unloaded
615        node.init(reference.clone());
616
617        let rule = Rule::new(
618            identifier,
619            Typedef::Node,
620            Expression::Empty,
621            Arc::new(Value::Node(node))
622        );
623        self.append_or_update(Row::Rule(Arc::new(rule)));
624        Ok(reference)
625    }
626
627    pub fn copy_node(&mut self, from: &Node, to: Arc<str>) -> Result<()> {
628        self.check_identifier_unique(&to)?;
629        let from_workflow = from.get_workflow_like()?;
630        let from_path = from_workflow.path();
631
632        // Create and add new node reference
633        let to_reference = if &*self.locator == "_" {
634            Reference::new(to.clone())
635        } else {
636            Arc::new(self.locator.add_child(to.clone())?)
637        };
638
639        // Construct target path for the new node
640        let base_path = self.path.with_extension("");
641        if !base_path.exists() {
642            std::fs::create_dir_all(&base_path)?;
643        }
644        let to_path = base_path.join(format!("{}.aim", to));
645
646        // Copy AIM file if it exists
647        if from_path.exists() {
648            std::fs::copy(&*from_path, &*to_path)?;
649        }
650        
651        // Copy JNL file if it exists
652        let from_jnl_path = from_path.with_extension("jnl");
653        let to_jnl_path = to_path.with_extension("jnl");
654        if from_jnl_path.exists() {
655            std::fs::copy(&from_jnl_path, &to_jnl_path)?;
656        }
657        
658        // Copy directory contents recursively if it exists (for child nodes)
659        let from_dir_path = from_path.with_extension("");
660        let to_dir_path = to_path.with_extension("");
661        if from_dir_path.exists() && from_dir_path.is_dir() {
662            if to_dir_path.exists() {
663                std::fs::remove_dir_all(&to_dir_path)?;
664            }
665            Self::copy_dir_all(&from_dir_path, &to_dir_path)?;
666        }
667        
668        let node = Node::init_new(to_reference, from.inference());
669        self.add_node(to.clone(), node)?;
670        Ok(())
671    }
672
673    /// Recursively copy directory contents
674    fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
675        std::fs::create_dir_all(dst)?;
676        for entry in std::fs::read_dir(src)? {
677            let entry = entry?;
678            let file_type = entry.file_type()?;
679            let dst_path = dst.join(entry.file_name());
680            
681            if file_type.is_dir() {
682                Self::copy_dir_all(&entry.path(), &dst_path)?;
683            } else {
684                std::fs::copy(entry.path(), dst_path)?;
685            }
686        }
687        Ok(())
688    }
689
690    pub fn get_node(&self, identifier: &str) -> Result<Node> {
691        if let Some(rule) = self.get_rule(identifier) {
692            if let Some(node) = rule.get_node() {
693                return Ok(node);
694            }
695        }
696        return Err(anyhow!("Node {} not found", identifier));
697    }
698
699    pub fn rename_node(&mut self, from: &str, to: Arc<str>) -> Result<()> {
700        self.check_identifier_unique(&to)?;
701        if let Some(from_rule) = self.get_rule(from) {
702            // Save the 'from' workflow before renaming its node
703            if let Some(from_node) = from_rule.get_node() {
704                let from_workflow = from_node.get_workflow_like()?;
705                let from_path = from_workflow.path();
706
707                // Rename the workflow node
708                if let Some(index) = self.get_index(from) {
709                    match self.get_row(index) {
710                        Row::Rule(from_rule) 
711                        | Row::Touched(from_rule) => {
712                            let to_rule = Arc::new(from_rule.clone().rename_node(to.clone())?);
713                            // Clear the previous row
714                            self.replace_row(index, Row::Touched(to_rule.clone()))?;
715
716                            // Rename associated files if they exist
717                            if from_path.exists() {
718                                // Rename the .aim file
719                                let to_path = from_path.with_file_name(format!("{}.aim", to));
720                                std::fs::rename(&*from_path, &to_path)?;
721                                
722                                // Rename the .jnl file
723                                let from_jnl_path = from_path.with_extension("jnl");
724                                if from_jnl_path.exists() {
725                                    let to_jnl_path = to_path.with_extension("jnl");
726                                    std::fs::rename(from_jnl_path, to_jnl_path)?;
727                                }
728                                
729                                // Rename the directory if it exists (for child nodes)
730                                let from_dir_path = from_path.with_extension("");
731                                if from_dir_path.exists() && from_dir_path.is_dir() {
732                                    let to_dir_path = to_path.with_extension("");
733                                    std::fs::rename(from_dir_path, to_dir_path)?;
734                                }
735                            }
736                            return Ok(())
737                        }
738                        _ => unreachable!(),
739                    }
740                }
741            }
742        }
743        Err(anyhow!("Node {} not found", from))
744    }
745
746    pub fn delete_node(&mut self, identifier: &str) -> Result<()> {
747        if let Some(rule) = self.get_rule(identifier) {
748            // Save the 'from' workflow before renaming its node
749            if let Some(node) = rule.get_node() {
750                let workflow = node.get_workflow_like()?;
751                let node_path = workflow.path();
752                
753                self.delete_rule(identifier);
754                // Remove associated files
755
756                // Remove the .aim file
757                if node_path.exists() {
758                    std::fs::remove_file(&*node_path)?;
759                }
760                
761                // Remove the .jnl file
762                let jnl_path = node_path.with_extension("jnl");
763                if jnl_path.exists() {
764                    std::fs::remove_file(jnl_path)?;
765                }
766                
767                // Remove the directory if it exists (for child nodes)
768                let dir_path = node_path.with_extension("");
769                if dir_path.exists() && dir_path.is_dir() {
770                    std::fs::remove_dir_all(dir_path)?;
771                }
772                return Ok(())
773            }
774        }
775        return Err(anyhow!("Node {} not found", identifier));
776    }
777
778    pub fn add_instance(&mut self, identifier: Arc<str>, source: Arc<Reference>, target: Arc<Reference>) -> Result<()> {
779        self.check_identifier_unique(&identifier)?;
780        let instance = Instance::new(source, target);
781        let rule = Rule::new(
782            identifier,
783            Typedef::Instance,
784            Expression::Empty,
785            Arc::new(Value::Instance(instance))
786        );
787        Ok(self.append_or_update(Row::Rule(Arc::new(rule))))
788    }
789
790    /// Appends a row to the workflow or updates an existing rule.
791    ///
792    /// If a rule with the same identifier already exists, it is updated.
793    /// Otherwise, the rule is appended to the end of the workflow.
794    ///
795    /// # Parameters
796    ///
797    /// * `row` - The rule to append or update
798    pub fn append_or_update(&mut self, row: Row) {
799        match row {
800            Row::Rule(rule) | Row::Touched(rule) => {
801                // Update if the rule already exists
802                if self.lookup.contains_key(&rule.identifier()) {
803                    // Rule update guaranteed because identifier exists
804                    self.update_rule(rule).unwrap();
805                    return;
806                }
807                if Aim::check_identifier(&rule.identifier()).is_ok() {
808                    let index = self.rows.len();
809                    self.lookup.insert(rule.identifier().clone(), index);
810                    if self.track_changes {
811                        self.set_partial_change();
812                        self.rows.push(Row::Touched(rule));
813                    } else {
814                        self.rows.push(Row::Rule(rule));
815                    }
816                }
817            }
818            _ => {
819                if self.track_changes {
820                    self.set_version_change();
821                }
822                self.rows.push(row);
823            }
824        }
825    }
826
827    /// Updates an existing rule.
828    ///
829    /// # Parameters
830    ///
831    /// * `rule` - The updated rule
832    ///
833    /// # Returns
834    ///
835    /// `Ok(())` if successful, or an error if the rule doesn't exist.
836    pub fn update_rule(&mut self, rule: Arc<Rule>) -> Result<()> {
837        match self.lookup.get(&rule.identifier()) {
838            Some(&index) => {
839                if self.track_changes {
840                    self.set_partial_change();
841                    self.rows[index] = Row::Touched(rule);
842                } else {
843                    self.rows[index] = Row::Rule(rule);
844                }
845                Ok(())
846            }
847            None => Err(anyhow!("Rule {} not found in {}", rule.identifier(), self.locator())),
848        }
849    }
850
851    /// Updates an existing rule value.
852    ///
853    /// # Parameters
854    ///
855    /// * `identifier` = The rule identifier
856    /// * `value` - The new value
857    ///
858    /// # Returns
859    ///
860    /// `Ok(())` if successful, or an error if the rule doesn't exist.
861    pub fn update_value(&mut self, identifier: &str, value: Arc<Value>) -> Result<()> {
862        if let Some(&index) = self.lookup.get(identifier) {
863            let arc_rule = self.rows[index].rule().unwrap();
864            if arc_rule.value().as_ref() != &*value {
865                let mut new_rule = (*arc_rule).clone();
866                let _ = new_rule.set_value(value);
867                if self.track_changes {
868                    self.set_partial_change();
869                    self.rows[index] = Row::Touched(Arc::new(new_rule));
870                } else {
871                    self.rows[index] = Row::Rule(Arc::new(new_rule));
872                }
873            }
874            Ok(())
875        } else {
876            Err(anyhow!("Rule {} not found in {}", identifier, self.locator()))
877        }
878    }
879
880    /// Deletes a rule by identifier.
881    ///
882    /// # Parameters
883    ///
884    /// * `identifier` - The rule identifier to delete
885    ///
886    /// # Returns
887    ///
888    /// `Some(Rule)` if the rule was deleted, `None` if it didn't exist.
889    pub fn delete_rule(&mut self, identifier: &str) -> Row {
890        match self.lookup.remove(identifier) {
891            Some(index) => {
892                self.set_version_change();
893                let row = self.rows.remove(index);
894                self.reindex();
895                row
896            }
897            None => Row::Empty,
898        }
899    }
900
901    // --- Indexed Rule Access --
902
903    /// Sets a rule at a specific row index.
904    ///
905    /// If a rule already exists at the specified index with a different identifier,
906    /// this method will return an error. If the index is beyond the current size
907    /// of the workflow, the rows vector will be expanded to accommodate it.
908    ///
909    /// # Parameters
910    ///
911    /// * `index` - The zero-based row index where to place the rule
912    /// * `row` - The rule to set, or None to create an empty row
913    ///
914    /// # Returns
915    ///
916    /// `Ok(())` if successful, or an error if there's a conflict with an existing rule
917    pub fn set_row(&mut self, index: usize, row: Row) -> Result<()> {
918        if let Row::Rule(rule) | Row::Touched(rule) = row {
919            Aim::check_identifier(&rule.identifier())?;
920            // Check if the rule identifier already exists
921            if self.lookup.contains_key(&rule.identifier()) {
922                if index < self.rows.len() {
923                    // Get the rule at the specified index
924                    match &self.rows[index] {
925                        Row::Rule(old_rule) | Row::Touched(old_rule) => {
926                            // Ensure the new rule and old rule identifiers match
927                            if rule.identifier() != old_rule.identifier() {
928                                return Err(anyhow!("Rule mismatch {} at {}[{}], found {}", rule.identifier(), self.locator(), index, old_rule.identifier()));
929                            }
930                            // Manage this special case
931                            if self.track_changes {
932                                self.set_partial_change();
933                                self.rows[index] = Row::Touched(rule);
934                            } else {
935                                self.rows[index] = Row::Rule(rule);
936                            }
937                            return Ok(());
938                        }
939                        _ => {
940                            return Err(anyhow!("No Rule {} at {}[{}]", rule.identifier(), self.locator(), index));
941                        }
942                    }
943                } else {
944                    return Err(anyhow!("Rule {} is missing at {}[{}]", rule.identifier(), self.locator(), index));
945                }
946            }
947            // Expand vector if needed
948            if index >= self.rows.len() {
949                self.rows.resize_with(index + 1, || Row::Empty);
950            }
951            // Insert at specified position
952            self.lookup.insert(rule.identifier().clone(), index);
953            if self.track_changes {
954                self.set_partial_change();
955                self.rows[index] = Row::Touched(rule);
956            } else {
957                self.rows[index] = Row::Rule(rule);
958            }
959        } else {
960            // Expand vector if needed
961            if index >= self.rows.len() {
962                self.rows.resize_with(index + 1, || Row::Empty);
963            }
964            if self.track_changes {
965                self.set_partial_change();
966            }
967            self.rows[index] = row;
968        }
969        Ok(())
970    }
971
972    /// Inserts a rule at a specific row index.
973    ///
974    /// This method inserts a rule at the specified index, shifting existing rules
975    /// to higher indices. If the index is beyond the current size of the workflow,
976    /// empty rows will be created as needed. If a rule with the same identifier
977    /// already exists in the workflow, this method returns an error.
978    ///
979    /// # Parameters
980    ///
981    /// * `index` - The zero-based row index where to insert the rule
982    /// * `row` - The rule to insert, or None to create an empty row
983    ///
984    /// # Returns
985    ///
986    /// `Ok(())` if successful, or an error if a rule with the same identifier already exists
987    pub fn insert_row(&mut self, index: usize, row: Row) -> Result<()> {
988        if let Row::Rule(rule) | Row::Touched(rule) = &row {
989            Aim::check_identifier(&rule.identifier())?;
990            if self.lookup.contains_key(&rule.identifier()) {
991                return Err(anyhow!("Rule {} exists in {}", rule.identifier(), self.locator));
992            }
993            if index >= self.rows.len() {
994                // Expand vector if needed
995                if index > self.rows.len() {
996                    self.rows.resize_with(index, || Row::Empty);
997                }
998                // and append
999                self.lookup.insert(rule.identifier().clone(), index);
1000                self.rows.push(row);
1001            } else {
1002                // Insert at specified position
1003                self.lookup.insert(rule.identifier().clone(), index);
1004                self.rows.insert(index, row);
1005            }
1006        } else if index >= self.rows.len() {
1007            // Expand vector if needed
1008            if index > self.rows.len() {
1009                self.rows.resize_with(index, || Row::Empty);
1010            }
1011            // and append
1012            self.rows.push(row);
1013        } else {
1014            self.rows.insert(index, row);
1015        }
1016
1017        // Update indices of all rows
1018        self.reindex();
1019        self.set_version_change();
1020        Ok(())
1021    }
1022
1023    /// Repositions a rule from one row index to another.
1024    ///
1025    /// This method moves a rule from the `from` index to the `to` index,
1026    /// shifting other rules as needed. If either index is beyond the current
1027    /// size of the workflow, the rows vector will be expanded to accommodate them.
1028    ///
1029    /// # Parameters
1030    ///
1031    /// * `from` - The current zero-based row index of the rule to move
1032    /// * `to` - The target zero-based row index where to place the rule
1033    ///
1034    /// # Returns
1035    ///
1036    /// `Ok(())` if successful, or an error if the operation fails
1037    pub fn reposition_row(&mut self, from: usize, to: usize) -> Result<()> {
1038        if from != to {
1039            // Expand vectors if needed
1040            let max_row = from.max(to);
1041            if max_row >= self.rows.len() {
1042                self.rows.resize_with(max_row + 1, || Row::Empty);
1043            }
1044            self.set_partial_change();
1045
1046            // Check if there's actually a rules to move
1047            if self.rows[from].is_empty() && self.rows[to].is_empty() {
1048                return Ok(());
1049            }
1050
1051            let row = self.rows.remove(from);
1052            self.rows.insert(to, row);
1053
1054            // Rebuild index mappings due to reordering
1055            self.reindex();
1056        }
1057        Ok(())
1058    }
1059
1060    pub fn replace_row(&mut self, index: usize, row: Row) -> Result<()> {
1061        if let Row::Rule(new_rule) | Row::Touched(new_rule) = row {
1062            Aim::check_identifier(&new_rule.identifier())?;
1063            // Check if the rule identifier already exists
1064            if self.lookup.contains_key(&new_rule.identifier()) {
1065                if index < self.rows.len() {
1066                    // Get the rule at the specified index
1067                    match &self.rows[index] {
1068                        // Check if the old row is a rule
1069                        Row::Rule(old_rule) | Row::Touched(old_rule) => {
1070                            // Ensure the new rule and old rule identifiers match
1071                            if new_rule.identifier() != old_rule.identifier() {
1072                                return Err(anyhow!("Rule {} already exists in {}", new_rule.identifier(), self.locator()));
1073                            }
1074                            // Manage changes
1075                            if self.track_changes {
1076                                self.set_partial_change();
1077                                self.rows[index] = Row::Touched(new_rule);
1078                            } else {
1079                                self.rows[index] = Row::Rule(new_rule);
1080                            }
1081                            return Ok(());
1082                        }
1083                        _ => {}
1084                    }
1085                } else {
1086                    return Err(anyhow!("Rule {} is missing at {}[{}]", new_rule.identifier(), self.locator(), index));
1087                }
1088            }
1089            // Expand vector if needed
1090            if index >= self.rows.len() {
1091                self.rows.resize_with(index + 1, || Row::Empty);
1092            }
1093            // Insert at specified position
1094            self.lookup.insert(new_rule.identifier().clone(), index);
1095            if self.track_changes {
1096                self.set_partial_change();
1097                self.rows[index] = Row::Touched(new_rule);
1098            } else {
1099                self.rows[index] = Row::Rule(new_rule);
1100            }
1101        } else {
1102            // Expand vector if needed
1103            if index >= self.rows.len() {
1104                self.rows.resize_with(index + 1, || Row::Empty);
1105            }
1106            if self.track_changes {
1107                self.set_partial_change();
1108            }
1109            self.rows[index] = row;
1110        }
1111        self.reindex();
1112        Ok(())
1113    }
1114
1115
1116    /// Clears a row at a specific index, removing the rule if present.
1117    ///
1118    /// This method removes the rule at the specified index but keeps the row
1119    /// structure intact. The row will become empty but still exist in the workflow.
1120    /// If the index is out of bounds, this method returns None.
1121    ///
1122    /// # Parameters
1123    ///
1124    /// * `index` - The zero-based row index to clear
1125    ///
1126    /// # Returns
1127    ///
1128    /// `Some(Rule)` if a rule was present and removed, `None` if the row was already empty or index was out of bounds
1129    pub fn clear_row(&mut self, index: usize) -> Row {
1130        // Check bounds
1131        if index >= self.rows.len() {
1132            return Row::Empty;
1133        }
1134        self.set_version_change();
1135        let row = self.rows[index].clone();
1136        self.rows[index] = Row::Empty;
1137        if let Row::Rule(rule) | Row::Touched(rule) = &row {
1138            self.lookup.remove(&rule.identifier());
1139        }
1140        row
1141    }
1142
1143    /// Removes a row at a specific index, removing the rule and shrinking the workflow.
1144    ///
1145    /// This method removes the rule at the specified index and removes the row
1146    /// entirely from the workflow, shifting subsequent rows to lower indices.
1147    /// If the index is out of bounds, this method returns None.
1148    ///
1149    /// # Parameters
1150    ///
1151    /// * `index` - The zero-based row index to remove
1152    ///
1153    /// # Returns
1154    ///
1155    /// `Some(Rule)` if a rule was present and removed, `None` if the index was out of bounds
1156    pub fn remove_row(&mut self, index: usize) -> Row {
1157        if index >= self.rows.len() {
1158            return Row::Empty;
1159        }
1160        self.set_version_change();
1161        let row = self.rows.remove(index);
1162        self.reindex();
1163        row
1164    }
1165
1166    /// Create an iterator over the rules with their indices.
1167    ///
1168    /// This iterator yields tuples of `(index, &Option<Rule>)`, providing
1169    /// both the position and content of each row including empty ones.
1170    pub fn iter_with_rows(&self) -> impl Iterator<Item = (usize, &Row)> {
1171        self.rows
1172            .iter()
1173            .enumerate()
1174            .map(|(index, row)| (index, row))
1175    }
1176
1177    /// Rebuilds the lookup mapping index.
1178    ///
1179    /// This helper function rebuilds the internal hash map that maps rule identifiers
1180    /// to their row indices. It's called automatically after operations that change
1181    /// the position of rules within the workflow.
1182    pub fn reindex(&mut self) {
1183        self.lookup.clear();
1184        for (index, row) in self.rows.iter().enumerate() {
1185            if let Row::Rule(rule) | Row::Touched(rule) = row {
1186                self.lookup.insert(rule.identifier().clone(), index);
1187            }
1188        }
1189    }
1190
1191    /// Parses AIM content and populates the workflow.
1192    ///
1193    /// This method parses AIM format content and creates rules based on the parsed data.
1194    /// It can optionally parse only up to a specific partial version. The method clears
1195    /// the current workflow contents before parsing.
1196    /// 
1197    /// # Special tokens and their meanings
1198    /// 
1199    /// - `[` .. `]` HEADER: Used to signify a version header.
1200    /// - `:` PARTIAL: Used inside a workflow header to signify a minor version number.
1201    /// - `@` DATETIME: Used inside a workflow header to signify the snapshot date and time.
1202    /// - `#` COMMENT: Used to signify a comment.
1203    /// - `$` CONSTANT: Used inside rule a definitions to signify a constant variable.
1204    /// - `(` .. `)` Delimits expression parenthesis
1205    /// - `{` .. `}` Delimits and expression collection
1206    /// - `"` | `'` Delimits strings
1207    /// - `^`, `!`, `&`, `%`, `*` Potentially available
1208    ///
1209    /// # Parameters
1210    ///
1211    /// * `input` - The AIM content to parse
1212    /// * `partial` - Optional partial version to parse up to
1213    ///
1214    /// # Returns
1215    ///
1216    /// `Ok(())` if parsing was successful, or an error if parsing failed
1217    pub fn parse(&mut self, input: &str, partial: Option<u32>) -> Result<()> {
1218        // Clear contents
1219        self.rows.clear();
1220        self.lookup.clear();
1221        self.clear_changes();
1222
1223        let lines: Vec<&str> = input.lines().collect();
1224        let mut is_version = false;
1225        let mut is_partial = false;
1226
1227        for line in lines {
1228            let mut opt_index: Option<u32> = None;
1229            let mut line = line.trim();
1230            // Append empty lines to maintain row spacing in first (non-partial) section
1231            if line.is_empty() {
1232                if is_version && self.version.partial() == 0 {
1233                    self.append_or_update(Row::Empty);
1234                }
1235                continue;
1236            }
1237
1238            // Try to parse comment (lines starting with #)
1239            if line.starts_with('#') {
1240                if is_version && self.version.partial() == 0 {
1241                    self.append_or_update(Row::Comment(Arc::from(line)));
1242                }
1243                continue;
1244            }
1245
1246            // Try to parse version headers (lines starting with [)
1247            if line.starts_with('[') {
1248                match parse_version(line) {
1249                    Ok((_, current)) => {
1250                        // When version/epoch is 0, assign the first version header encountered
1251                        if self.version.epoch() == 0 {
1252                            is_version = true;
1253                            self.version = current;
1254                        }
1255                        // When epoch is known, merge partials
1256                        else if self.version.epoch() == current.epoch() {
1257                            is_version = true;
1258                            // We've moved past the target partial, stop parsing
1259                            if is_partial {
1260                                break;
1261                            }
1262                            self.version.set_partial(current.partial());
1263                            self.version.set_date(current.date());
1264                            if let Some(opt_partial) = partial {
1265                                if opt_partial == current.partial() {
1266                                    is_partial = true;
1267                                }
1268                            }
1269                        } else if is_version {
1270                            // We've moved past the target version, stop parsing
1271                            break;
1272                        }
1273                    }
1274                    // Skip lines with syntax errors
1275                    Err(_e) => {}
1276                }
1277                continue;
1278            }
1279
1280            // If we haven't found the target version yet, skip this line
1281            if !is_version {
1282                continue;
1283            }
1284
1285            // Try to parse leading number (rule index used by partial saves)
1286            if line.chars().next().map_or(false, |c| c.is_ascii_digit()) {
1287                match parse_unsigned(line) {
1288                    Ok((remainder, index)) => {
1289                        opt_index = Some(index);
1290                        line = remainder;
1291                    }
1292                    Err(_) => {}
1293                }
1294            }
1295
1296            // Try to parse rule
1297            match parse_rule(line) {
1298                Ok(rule) => {
1299                    // If this is a node-typed rule, synthesize a lazy Node value now that
1300                    // we know the workflow locator. This keeps parsing simple while
1301                    // ensuring node rules always carry a concrete Node reference.
1302                    if rule.typedef().is_node() {
1303                        if let Some(node) = rule.get_node() {
1304                            // Derive a child reference from the workflow locator + rule identifier.
1305                            // Reference::new uses the identifier as a segment; append composes
1306                            // a hierarchical locator like `parent.child`.
1307                            let reference = self.locator.add_child(rule.identifier())?;
1308                            // Initialize the node as Unloaded
1309                            node.init(Arc::new(reference));
1310                        }
1311                    }
1312
1313                    match opt_index {
1314                        Some(index) => {
1315                            // Set rule at index
1316                            if self.contains(&rule.identifier()) {
1317                                let _ = self.update_rule(Arc::new(rule));
1318                            } else {
1319                                let _ = self.set_row(index as usize, Row::Rule(Arc::new(rule)));
1320                            }
1321                        }
1322                        None => {
1323                            // Append or overwrite existing rule
1324                            self.append_or_update(Row::Rule(Arc::new(rule)));
1325                        }
1326                    }
1327                }
1328                Err(_) => {
1329                    // Ignore syntax errors, but maintain row spacing in (non-partial) sections
1330                    if self.version.partial() == 0 {
1331                        self.append_or_update(Row::Empty);
1332                    }
1333                }
1334            }
1335        }
1336
1337        if !is_version && self.version.epoch() > 0 {
1338            return Err(anyhow!("Version {} not found", self.version.epoch()));
1339        }
1340
1341        Ok(())
1342    }
1343
1344    pub fn print(&self, writer: &mut Writer) {
1345        if writer.is_formulizer() {
1346            self.version.print(writer);
1347        }
1348        for row in self.iter_rows() {
1349            row.print(writer);
1350        }
1351    }
1352
1353    /// Return the formula-string representation (round-trippable by the parser).
1354    pub fn to_formula(&self) -> String {
1355        let mut writer = Writer::formulizer();
1356        self.print(&mut writer);
1357        writer.finish()
1358    }
1359}
1360
1361impl WriterLike for Workflow {
1362    fn write(&self, writer: &mut Writer) {
1363        self.print(writer);
1364    }
1365}
1366
1367impl fmt::Display for Workflow {
1368    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1369        write!(f, "{}", self.to_stringized())
1370    }
1371}