1use 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#[derive(Debug, Clone)]
12pub struct EngineConfig {
13 pub validate_on_operation: bool,
15 pub max_batch_size: usize,
17 pub enable_transactions: bool,
19 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
34pub struct Engine {
36 config: EngineConfig,
37 validator: ValidationPipeline,
38 transactions: TransactionManager,
39 snapshots: SnapshotManager,
40}
41
42impl Engine {
43 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 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 #[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 #[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 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 pub fn validate(&self, doc: &Document) -> ValidationResult {
117 self.validator.validate_document(doc)
118 }
119
120 pub fn begin_transaction(&mut self) -> TransactionId {
122 self.transactions.begin()
123 }
124
125 pub fn begin_named_transaction(&mut self, name: impl Into<String>) -> TransactionId {
127 self.transactions.begin_named(name)
128 }
129
130 pub fn add_to_transaction(&mut self, txn_id: &TransactionId, op: Operation) -> Result<()> {
132 self.transactions.add_operation(txn_id, op)
133 }
134
135 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 pub fn rollback_transaction(&mut self, txn_id: &TransactionId) -> Result<()> {
147 self.transactions.rollback(txn_id)
148 }
149
150 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 pub fn restore_snapshot(&self, name: &str) -> Result<Document> {
163 self.snapshots.restore(name)
164 }
165
166 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 pub fn delete_snapshot(&mut self, name: &str) -> bool {
177 self.snapshots.delete(name)
178 }
179
180 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 Ok(OperationResult::failure(
233 "Use create_snapshot method for snapshots",
234 ))
235 }
236
237 Operation::RestoreSnapshot { .. } => {
238 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, §ion_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 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 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 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 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 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 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 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 let block = doc.get_block_mut(source).unwrap();
513 block.add_edge(edge.clone());
514
515 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 if !doc.blocks.contains_key(section_id) {
553 return Err(Error::BlockNotFound(section_id.to_string()));
554 }
555
556 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 let removed = clear_section_content(doc, section_id)
569 .map_err(|e| Error::InvalidBlockId(format!("Failed to clear section: {}", e)))?;
570
571 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 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 let block = Block::new(Content::text("Original"), None);
627 let id = doc.add_block(block, &root).unwrap();
628
629 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); }
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}