Skip to main content

ucm_engine/
engine.rs

1//! Main transformation engine.
2
3use crate::operation::{EditOperator, MoveTarget, Operation, OperationResult, PruneCondition};
4use crate::snapshot::SnapshotManager;
5use crate::transaction::{TransactionId, TransactionManager};
6use crate::validate::{ValidationPipeline, ValidationResult};
7use tracing::{debug, info, instrument, warn};
8use ucm_core::{Block, Content, Document, Edge, Error, Result};
9
10/// Configuration for the engine
11#[derive(Debug, Clone)]
12pub struct EngineConfig {
13    /// Whether to validate after each operation
14    pub validate_on_operation: bool,
15    /// Maximum operations per batch
16    pub max_batch_size: usize,
17    /// Enable transaction support
18    pub enable_transactions: bool,
19    /// Enable snapshots
20    pub enable_snapshots: bool,
21}
22
23impl Default for EngineConfig {
24    fn default() -> Self {
25        Self {
26            validate_on_operation: true,
27            max_batch_size: 10000,
28            enable_transactions: true,
29            enable_snapshots: true,
30        }
31    }
32}
33
34/// The main transformation engine
35pub struct Engine {
36    config: EngineConfig,
37    validator: ValidationPipeline,
38    transactions: TransactionManager,
39    snapshots: SnapshotManager,
40}
41
42impl Engine {
43    /// Create a new engine with default configuration
44    pub fn new() -> Self {
45        Self {
46            config: EngineConfig::default(),
47            validator: ValidationPipeline::new(),
48            transactions: TransactionManager::new(),
49            snapshots: SnapshotManager::new(),
50        }
51    }
52
53    /// Create an engine with custom configuration
54    pub fn with_config(config: EngineConfig) -> Self {
55        Self {
56            config,
57            validator: ValidationPipeline::new(),
58            transactions: TransactionManager::new(),
59            snapshots: SnapshotManager::new(),
60        }
61    }
62
63    /// Execute a single operation on a document
64    #[instrument(skip(self, doc), fields(op = %op.description()))]
65    pub fn execute(&self, doc: &mut Document, op: Operation) -> Result<OperationResult> {
66        debug!("Executing operation: {}", op.description());
67
68        let result = self.execute_internal(doc, op)?;
69
70        if self.config.validate_on_operation && !result.success {
71            warn!("Operation failed: {:?}", result.error);
72        }
73
74        Ok(result)
75    }
76
77    /// Execute multiple operations atomically
78    #[instrument(skip(self, doc, ops), fields(op_count = ops.len()))]
79    pub fn execute_batch(
80        &self,
81        doc: &mut Document,
82        ops: Vec<Operation>,
83    ) -> Result<Vec<OperationResult>> {
84        if ops.len() > self.config.max_batch_size {
85            return Err(Error::ResourceLimit(format!(
86                "Batch size {} exceeds maximum {}",
87                ops.len(),
88                self.config.max_batch_size
89            )));
90        }
91
92        info!("Executing batch of {} operations", ops.len());
93
94        let mut results = Vec::with_capacity(ops.len());
95        for op in ops {
96            match self.execute_internal(doc, op) {
97                Ok(result) => {
98                    if !result.success {
99                        // On failure, return results so far
100                        results.push(result);
101                        break;
102                    }
103                    results.push(result);
104                }
105                Err(e) => {
106                    results.push(OperationResult::failure(e.to_string()));
107                    break;
108                }
109            }
110        }
111
112        Ok(results)
113    }
114
115    /// Validate a document
116    pub fn validate(&self, doc: &Document) -> ValidationResult {
117        self.validator.validate_document(doc)
118    }
119
120    /// Begin a transaction
121    pub fn begin_transaction(&mut self) -> TransactionId {
122        self.transactions.begin()
123    }
124
125    /// Begin a named transaction
126    pub fn begin_named_transaction(&mut self, name: impl Into<String>) -> TransactionId {
127        self.transactions.begin_named(name)
128    }
129
130    /// Add operation to a transaction
131    pub fn add_to_transaction(&mut self, txn_id: &TransactionId, op: Operation) -> Result<()> {
132        self.transactions.add_operation(txn_id, op)
133    }
134
135    /// Commit a transaction
136    pub fn commit_transaction(
137        &mut self,
138        txn_id: &TransactionId,
139        doc: &mut Document,
140    ) -> Result<Vec<OperationResult>> {
141        let ops = self.transactions.commit(txn_id)?;
142        self.execute_batch(doc, ops)
143    }
144
145    /// Rollback a transaction
146    pub fn rollback_transaction(&mut self, txn_id: &TransactionId) -> Result<()> {
147        self.transactions.rollback(txn_id)
148    }
149
150    /// Create a snapshot
151    pub fn create_snapshot(
152        &mut self,
153        name: impl Into<String>,
154        doc: &Document,
155        description: Option<String>,
156    ) -> Result<()> {
157        self.snapshots.create(name, doc, description)?;
158        Ok(())
159    }
160
161    /// Restore from a snapshot
162    pub fn restore_snapshot(&self, name: &str) -> Result<Document> {
163        self.snapshots.restore(name)
164    }
165
166    /// List snapshots
167    pub fn list_snapshots(&self) -> Vec<String> {
168        self.snapshots
169            .list()
170            .iter()
171            .map(|s| s.id.0.clone())
172            .collect()
173    }
174
175    /// Delete a snapshot
176    pub fn delete_snapshot(&mut self, name: &str) -> bool {
177        self.snapshots.delete(name)
178    }
179
180    // Internal operation execution
181    fn execute_internal(&self, doc: &mut Document, op: Operation) -> Result<OperationResult> {
182        match op {
183            Operation::Edit {
184                block_id,
185                path,
186                value,
187                operator,
188            } => self.execute_edit(doc, &block_id, &path, value, operator),
189
190            Operation::Move {
191                block_id,
192                new_parent,
193                index,
194            } => self.execute_move(doc, &block_id, &new_parent, index),
195
196            Operation::MoveToTarget { block_id, target } => {
197                self.execute_move_to_target(doc, &block_id, target)
198            }
199
200            Operation::Append {
201                parent_id,
202                content,
203                label,
204                tags,
205                semantic_role,
206                index,
207            } => self.execute_append(doc, &parent_id, content, label, tags, semantic_role, index),
208
209            Operation::Delete {
210                block_id,
211                cascade,
212                preserve_children,
213            } => self.execute_delete(doc, &block_id, cascade, preserve_children),
214
215            Operation::Prune { condition } => self.execute_prune(doc, condition),
216
217            Operation::Link {
218                source,
219                edge_type,
220                target,
221                metadata,
222            } => self.execute_link(doc, &source, edge_type, &target, metadata),
223
224            Operation::Unlink {
225                source,
226                edge_type,
227                target,
228            } => self.execute_unlink(doc, &source, edge_type, &target),
229
230            Operation::CreateSnapshot { .. } => {
231                // Snapshots are handled separately
232                Ok(OperationResult::failure(
233                    "Use create_snapshot method for snapshots",
234                ))
235            }
236
237            Operation::RestoreSnapshot { .. } => {
238                // Snapshots are handled separately
239                Ok(OperationResult::failure(
240                    "Use restore_snapshot method for snapshots",
241                ))
242            }
243
244            Operation::WriteSection {
245                section_id,
246                markdown,
247                base_heading_level,
248            } => self.execute_write_section(doc, &section_id, &markdown, base_heading_level),
249        }
250    }
251
252    fn execute_edit(
253        &self,
254        doc: &mut Document,
255        block_id: &ucm_core::BlockId,
256        path: &str,
257        value: serde_json::Value,
258        operator: EditOperator,
259    ) -> Result<OperationResult> {
260        let block = doc
261            .get_block_mut(block_id)
262            .ok_or_else(|| Error::BlockNotFound(block_id.to_string()))?;
263
264        // Parse path and apply edit
265        // This is simplified - a full implementation would parse JSON paths
266        if path == "content.text" || path == "text" {
267            if let Content::Text(ref mut text) = block.content {
268                match operator {
269                    EditOperator::Set => {
270                        text.text = value.as_str().unwrap_or_default().to_string();
271                    }
272                    EditOperator::Append => {
273                        text.text.push_str(value.as_str().unwrap_or_default());
274                    }
275                    EditOperator::Remove => {
276                        let to_remove = value.as_str().unwrap_or_default();
277                        text.text = text.text.replace(to_remove, "");
278                    }
279                    _ => {}
280                }
281                block.version.increment();
282                return Ok(OperationResult::success(vec![*block_id]));
283            }
284        }
285
286        // Handle metadata paths
287        if path.starts_with("metadata.") {
288            let meta_path = path.strip_prefix("metadata.").unwrap();
289            match meta_path {
290                "label" => {
291                    block.metadata.label = value.as_str().map(String::from);
292                }
293                "tags" => {
294                    if let Some(arr) = value.as_array() {
295                        match operator {
296                            EditOperator::Set => {
297                                block.metadata.tags = arr
298                                    .iter()
299                                    .filter_map(|v| v.as_str().map(String::from))
300                                    .collect();
301                            }
302                            EditOperator::Append => {
303                                for v in arr {
304                                    if let Some(s) = v.as_str() {
305                                        block.metadata.tags.push(s.to_string());
306                                    }
307                                }
308                            }
309                            EditOperator::Remove => {
310                                let to_remove: Vec<String> = arr
311                                    .iter()
312                                    .filter_map(|v| v.as_str().map(String::from))
313                                    .collect();
314                                block.metadata.tags.retain(|t| !to_remove.contains(t));
315                            }
316                            _ => {}
317                        }
318                    } else if let Some(s) = value.as_str() {
319                        match operator {
320                            EditOperator::Append => block.metadata.tags.push(s.to_string()),
321                            EditOperator::Remove => block.metadata.tags.retain(|t| t != s),
322                            _ => {}
323                        }
324                    }
325                }
326                "summary" => {
327                    block.metadata.summary = value.as_str().map(String::from);
328                }
329                _ => {
330                    // Custom metadata
331                    block.metadata.custom.insert(meta_path.to_string(), value);
332                }
333            }
334            block.version.increment();
335            return Ok(OperationResult::success(vec![*block_id]));
336        }
337
338        Ok(OperationResult::failure(format!(
339            "Unsupported path: {}",
340            path
341        )))
342    }
343
344    fn execute_move(
345        &self,
346        doc: &mut Document,
347        block_id: &ucm_core::BlockId,
348        new_parent: &ucm_core::BlockId,
349        index: Option<usize>,
350    ) -> Result<OperationResult> {
351        match index {
352            Some(idx) => doc.move_block_at(block_id, new_parent, idx)?,
353            None => doc.move_block(block_id, new_parent)?,
354        }
355        Ok(OperationResult::success(vec![*block_id]))
356    }
357
358    fn execute_move_to_target(
359        &self,
360        doc: &mut Document,
361        block_id: &ucm_core::BlockId,
362        target: MoveTarget,
363    ) -> Result<OperationResult> {
364        match target {
365            MoveTarget::ToParent { parent_id, index } => {
366                self.execute_move(doc, block_id, &parent_id, index)
367            }
368            MoveTarget::Before { sibling_id } => {
369                // Find sibling's parent and index
370                let parent_id = doc
371                    .parent(&sibling_id)
372                    .cloned()
373                    .ok_or_else(|| Error::BlockNotFound(sibling_id.to_string()))?;
374                let siblings = doc.children(&parent_id);
375                let sibling_index = siblings
376                    .iter()
377                    .position(|id| id == &sibling_id)
378                    .ok_or_else(|| Error::Internal("Sibling not found in parent".into()))?;
379                doc.move_block_at(block_id, &parent_id, sibling_index)?;
380                Ok(OperationResult::success(vec![*block_id]))
381            }
382            MoveTarget::After { sibling_id } => {
383                // Find sibling's parent and index
384                let parent_id = doc
385                    .parent(&sibling_id)
386                    .cloned()
387                    .ok_or_else(|| Error::BlockNotFound(sibling_id.to_string()))?;
388                let siblings = doc.children(&parent_id);
389                let sibling_index = siblings
390                    .iter()
391                    .position(|id| id == &sibling_id)
392                    .ok_or_else(|| Error::Internal("Sibling not found in parent".into()))?;
393                doc.move_block_at(block_id, &parent_id, sibling_index + 1)?;
394                Ok(OperationResult::success(vec![*block_id]))
395            }
396        }
397    }
398
399    #[allow(clippy::too_many_arguments)]
400    fn execute_append(
401        &self,
402        doc: &mut Document,
403        parent_id: &ucm_core::BlockId,
404        content: Content,
405        label: Option<String>,
406        tags: Vec<String>,
407        semantic_role: Option<String>,
408        index: Option<usize>,
409    ) -> Result<OperationResult> {
410        let mut block = Block::new(content, semantic_role.as_deref());
411
412        if let Some(l) = label {
413            block.metadata.label = Some(l);
414        }
415        block.metadata.tags = tags;
416
417        let id = match index {
418            Some(idx) => doc.add_block_at(block, parent_id, idx)?,
419            None => doc.add_block(block, parent_id)?,
420        };
421
422        Ok(OperationResult::success(vec![id]))
423    }
424
425    fn execute_delete(
426        &self,
427        doc: &mut Document,
428        block_id: &ucm_core::BlockId,
429        cascade: bool,
430        preserve_children: bool,
431    ) -> Result<OperationResult> {
432        if preserve_children {
433            // Reparent children to grandparent
434            if let Some(parent) = doc.parent(block_id).cloned() {
435                let children: Vec<_> = doc.children(block_id).to_vec();
436                for child in children {
437                    doc.move_block(&child, &parent)?;
438                }
439            }
440        }
441
442        let deleted = if cascade {
443            doc.delete_cascade(block_id)?
444        } else {
445            vec![doc.delete_block(block_id)?]
446        };
447
448        let ids: Vec<_> = deleted.iter().map(|b| b.id).collect();
449        Ok(OperationResult::success(ids))
450    }
451
452    fn execute_prune(
453        &self,
454        doc: &mut Document,
455        condition: Option<PruneCondition>,
456    ) -> Result<OperationResult> {
457        let pruned = match condition {
458            None | Some(PruneCondition::Unreachable) => doc.prune_unreachable(),
459            Some(PruneCondition::TagContains(tag)) => {
460                let to_prune: Vec<_> = doc
461                    .blocks
462                    .values()
463                    .filter(|b| b.has_tag(&tag))
464                    .map(|b| b.id)
465                    .collect();
466
467                let mut pruned = Vec::new();
468                for id in to_prune {
469                    if let Ok(block) = doc.delete_block(&id) {
470                        pruned.push(block);
471                    }
472                }
473                pruned
474            }
475            Some(PruneCondition::Custom(_)) => {
476                // Custom conditions require UCL expression evaluation
477                return Ok(OperationResult::failure(
478                    "Custom prune conditions not yet supported",
479                ));
480            }
481        };
482
483        let ids: Vec<_> = pruned.iter().map(|b| b.id).collect();
484        Ok(OperationResult::success(ids))
485    }
486
487    fn execute_link(
488        &self,
489        doc: &mut Document,
490        source: &ucm_core::BlockId,
491        edge_type: ucm_core::EdgeType,
492        target: &ucm_core::BlockId,
493        metadata: Option<serde_json::Value>,
494    ) -> Result<OperationResult> {
495        if !doc.blocks.contains_key(source) {
496            return Err(Error::BlockNotFound(source.to_string()));
497        }
498        if !doc.blocks.contains_key(target) {
499            return Err(Error::BlockNotFound(target.to_string()));
500        }
501
502        let mut edge = Edge::new(edge_type, *target);
503        if let Some(meta) = metadata {
504            if let Some(obj) = meta.as_object() {
505                for (k, v) in obj {
506                    edge.metadata.custom.insert(k.clone(), v.clone());
507                }
508            }
509        }
510
511        // Add edge to block
512        let block = doc.get_block_mut(source).unwrap();
513        block.add_edge(edge.clone());
514
515        // Update edge index
516        doc.edge_index.add_edge(source, &edge);
517
518        Ok(OperationResult::success(vec![*source]))
519    }
520
521    fn execute_unlink(
522        &self,
523        doc: &mut Document,
524        source: &ucm_core::BlockId,
525        edge_type: ucm_core::EdgeType,
526        target: &ucm_core::BlockId,
527    ) -> Result<OperationResult> {
528        let block = doc
529            .get_block_mut(source)
530            .ok_or_else(|| Error::BlockNotFound(source.to_string()))?;
531
532        let removed = block.remove_edge(target, &edge_type);
533
534        if removed {
535            doc.edge_index.remove_edge(source, target, &edge_type);
536            Ok(OperationResult::success(vec![*source]))
537        } else {
538            Ok(OperationResult::failure("Edge not found"))
539        }
540    }
541
542    fn execute_write_section(
543        &self,
544        doc: &mut Document,
545        section_id: &ucm_core::BlockId,
546        markdown: &str,
547        base_heading_level: Option<usize>,
548    ) -> Result<OperationResult> {
549        use crate::section::{clear_section_content, integrate_section_blocks};
550
551        // Verify section exists
552        if !doc.blocks.contains_key(section_id) {
553            return Err(Error::BlockNotFound(section_id.to_string()));
554        }
555
556        // Parse markdown into temporary document
557        let temp_doc = match ucp_translator_markdown::parse_markdown(markdown) {
558            Ok(d) => d,
559            Err(e) => {
560                return Ok(OperationResult::failure(format!(
561                    "Failed to parse markdown: {}",
562                    e
563                )));
564            }
565        };
566
567        // Clear existing section content
568        let removed = clear_section_content(doc, section_id)
569            .map_err(|e| Error::InvalidBlockId(format!("Failed to clear section: {}", e)))?;
570
571        // Integrate new blocks from parsed markdown
572        let added = integrate_section_blocks(doc, section_id, &temp_doc, base_heading_level)
573            .map_err(|e| Error::InvalidBlockId(format!("Failed to integrate blocks: {}", e)))?;
574
575        // Collect all affected block IDs
576        let mut affected = vec![*section_id];
577        affected.extend(removed);
578        affected.extend(added);
579
580        Ok(OperationResult::success(affected))
581    }
582}
583
584impl Default for Engine {
585    fn default() -> Self {
586        Self::new()
587    }
588}
589
590#[cfg(test)]
591mod tests {
592    use super::*;
593    use ucm_core::DocumentId;
594
595    #[test]
596    fn test_engine_append() {
597        let engine = Engine::new();
598        let mut doc = Document::new(DocumentId::new("test"));
599        let root = doc.root;
600
601        let result = engine
602            .execute(
603                &mut doc,
604                Operation::Append {
605                    parent_id: root,
606                    content: Content::text("Hello, world!"),
607                    label: Some("Greeting".into()),
608                    tags: vec!["test".into()],
609                    semantic_role: Some("intro".into()),
610                    index: None,
611                },
612            )
613            .unwrap();
614
615        assert!(result.success);
616        assert_eq!(doc.block_count(), 2);
617    }
618
619    #[test]
620    fn test_engine_edit() {
621        let engine = Engine::new();
622        let mut doc = Document::new(DocumentId::new("test"));
623        let root = doc.root;
624
625        // Add a block
626        let block = Block::new(Content::text("Original"), None);
627        let id = doc.add_block(block, &root).unwrap();
628
629        // Edit it
630        let result = engine
631            .execute(
632                &mut doc,
633                Operation::Edit {
634                    block_id: id,
635                    path: "content.text".into(),
636                    value: serde_json::json!("Modified"),
637                    operator: EditOperator::Set,
638                },
639            )
640            .unwrap();
641
642        assert!(result.success);
643
644        let block = doc.get_block(&id).unwrap();
645        if let Content::Text(text) = &block.content {
646            assert_eq!(text.text, "Modified");
647        }
648    }
649
650    #[test]
651    fn test_engine_transaction() {
652        let mut engine = Engine::new();
653        let mut doc = Document::new(DocumentId::new("test"));
654        let root = doc.root;
655
656        let txn_id = engine.begin_transaction();
657
658        engine
659            .add_to_transaction(
660                &txn_id,
661                Operation::Append {
662                    parent_id: root,
663                    content: Content::text("Block 1"),
664                    label: None,
665                    tags: vec![],
666                    semantic_role: None,
667                    index: None,
668                },
669            )
670            .unwrap();
671
672        engine
673            .add_to_transaction(
674                &txn_id,
675                Operation::Append {
676                    parent_id: root,
677                    content: Content::text("Block 2"),
678                    label: None,
679                    tags: vec![],
680                    semantic_role: None,
681                    index: None,
682                },
683            )
684            .unwrap();
685
686        let results = engine.commit_transaction(&txn_id, &mut doc).unwrap();
687
688        assert_eq!(results.len(), 2);
689        assert!(results.iter().all(|r| r.success));
690        assert_eq!(doc.block_count(), 3); // root + 2 new blocks
691    }
692
693    #[test]
694    fn test_move_before_target() {
695        let engine = Engine::new();
696        let mut doc = Document::new(DocumentId::new("test"));
697        let root = doc.root;
698
699        let block_a = doc
700            .add_block(Block::new(Content::text("A"), None), &root)
701            .unwrap();
702        let block_b = doc
703            .add_block(Block::new(Content::text("B"), None), &root)
704            .unwrap();
705        let block_c = doc
706            .add_block(Block::new(Content::text("C"), None), &root)
707            .unwrap();
708
709        let result = engine
710            .execute(
711                &mut doc,
712                Operation::MoveToTarget {
713                    block_id: block_c,
714                    target: MoveTarget::Before {
715                        sibling_id: block_a,
716                    },
717                },
718            )
719            .unwrap();
720
721        assert!(result.success);
722        let children = doc.children(&root);
723        assert_eq!(children[0], block_c);
724        assert_eq!(children[1], block_a);
725        assert_eq!(children[2], block_b);
726    }
727
728    #[test]
729    fn test_move_after_target() {
730        let engine = Engine::new();
731        let mut doc = Document::new(DocumentId::new("test"));
732        let root = doc.root;
733
734        let block_a = doc
735            .add_block(Block::new(Content::text("A"), None), &root)
736            .unwrap();
737        let block_b = doc
738            .add_block(Block::new(Content::text("B"), None), &root)
739            .unwrap();
740        let block_c = doc
741            .add_block(Block::new(Content::text("C"), None), &root)
742            .unwrap();
743
744        let result = engine
745            .execute(
746                &mut doc,
747                Operation::MoveToTarget {
748                    block_id: block_a,
749                    target: MoveTarget::After {
750                        sibling_id: block_c,
751                    },
752                },
753            )
754            .unwrap();
755
756        assert!(result.success);
757        let children = doc.children(&root);
758        assert_eq!(children[0], block_b);
759        assert_eq!(children[1], block_c);
760        assert_eq!(children[2], block_a);
761    }
762}