cvkg_cli/patch_engine.rs
1//! Patch Engine
2//! Responsible for generating patches from compiled artifacts
3
4use serde::{Deserialize, Serialize};
5
6/// Compiled artifact from the build process
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct CompiledArtifact {
9 /// The root node ID of the view
10 pub root_id: u64,
11 /// The serialized view
12 pub view: SerializedView,
13}
14
15/// Serialized view representation
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SerializedView {
18 /// The view type (e.g., "Text", "Button")
19 pub view_type: String,
20 /// The view properties
21 pub props: serde_json::Value,
22 /// The child views
23 pub children: Vec<SerializedView>,
24}
25
26/// Runtime patch types
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub enum RuntimePatch {
29 /// Replace a view at the specified node ID
30 ReplaceView {
31 /// The node ID to replace
32 node_id: u64,
33 /// The new view to insert
34 new_view: SerializedView,
35 },
36 /// Update state at the specified node ID
37 UpdateState {
38 /// The node ID to update
39 node_id: u64,
40 /// The field to update
41 field: String,
42 /// The new value
43 value: serde_json::Value,
44 },
45 /// Batch multiple patches together
46 Batch(Vec<RuntimePatch>),
47}
48
49/// Patch Engine implementation
50/// PatchEngine — Responsible for generating atomic updates between build artifacts.
51///
52/// The PatchEngine diffs serialized view trees from the Muspelheim build pipeline
53/// to produce minimal patches for runtime hot-reloading.
54pub struct PatchEngine {
55 previous_view: Option<SerializedView>,
56}
57
58impl PatchEngine {
59 /// Create a new PatchEngine
60 pub fn new() -> Self {
61 Self {
62 previous_view: None,
63 }
64 }
65
66 /// Generate a patch from a compiled artifact
67 pub fn generate_patch(&mut self, artifact: CompiledArtifact) -> RuntimePatch {
68 let mut patches = Vec::new();
69
70 if let Some(prev) = &self.previous_view {
71 self.diff_recursive(artifact.root_id, prev, &artifact.view, &mut patches);
72 } else {
73 // First run, replace everything
74 patches.push(RuntimePatch::ReplaceView {
75 node_id: artifact.root_id,
76 new_view: artifact.view.clone(),
77 });
78 }
79
80 self.previous_view = Some(artifact.view);
81
82 if patches.len() == 1 {
83 patches.remove(0)
84 } else {
85 RuntimePatch::Batch(patches)
86 }
87 }
88
89 fn diff_recursive(
90 &self,
91 node_id: u64,
92 old: &SerializedView,
93 new: &SerializedView,
94 patches: &mut Vec<RuntimePatch>,
95 ) {
96 // If types are different, we must replace the whole subtree
97 if old.view_type != new.view_type {
98 patches.push(RuntimePatch::ReplaceView {
99 node_id,
100 new_view: new.clone(),
101 });
102 return;
103 }
104
105 // If props changed, we might generate UpdateState or just ReplaceView
106 // For simplicity in this "real" version, we'll replace the node if anything changed
107 if old.props != new.props || old.children.len() != new.children.len() {
108 patches.push(RuntimePatch::ReplaceView {
109 node_id,
110 new_view: new.clone(),
111 });
112 return;
113 }
114
115 // Recursively diff children if they exist
116 // Note: Without stable IDs for children in SerializedView, we use index-based matching
117 for (i, (old_child, new_child)) in old.children.iter().zip(new.children.iter()).enumerate()
118 {
119 // We need a way to address child nodes.
120 // In CVKG, we assume a deterministic ID generation based on path for dev-server patches.
121 let child_id = node_id * 100 + (i as u64 + 1);
122 self.diff_recursive(child_id, old_child, new_child, patches);
123 }
124 }
125}