Skip to main content

brainwires_agents/
state_model.rs

1//! Three-State Model for comprehensive state tracking
2//!
3//! Based on SagaLLM's Three-State Architecture, this module maintains three
4//! separate state domains:
5//!
6//! 1. **Application State** - Domain logic (what resources exist, their current values)
7//! 2. **Operation State** - Execution logs, timing, agent actions
8//! 3. **Dependency State** - Constraint graphs, resource relationships
9//!
10//! This separation enables:
11//! - Better debugging through complete operation history
12//! - Deadlock detection via dependency graph analysis
13//! - Validation of operations against current state
14//! - Saga-style compensation using operation logs
15
16use std::collections::{HashMap, HashSet};
17use std::path::PathBuf;
18use std::sync::Arc;
19use std::time::{Instant, SystemTime};
20
21use petgraph::Direction;
22use petgraph::algo::is_cyclic_directed;
23use petgraph::graph::{DiGraph, NodeIndex};
24use petgraph::visit::EdgeRef;
25use serde::{Deserialize, Serialize};
26use tokio::sync::RwLock;
27
28/// Three-State Model for comprehensive state tracking
29pub struct ThreeStateModel {
30    /// Domain-level resource tracking.
31    pub application_state: Arc<ApplicationState>,
32    /// Execution logging and history.
33    pub operation_state: Arc<OperationState>,
34    /// Resource relationship graph.
35    pub dependency_state: Arc<DependencyState>,
36}
37
38impl ThreeStateModel {
39    /// Create a new three-state model
40    pub fn new() -> Self {
41        Self {
42            application_state: Arc::new(ApplicationState::new()),
43            operation_state: Arc::new(OperationState::new()),
44            dependency_state: Arc::new(DependencyState::new()),
45        }
46    }
47
48    /// Validate that a proposed operation is consistent with current state
49    pub async fn validate_operation(&self, op: &StateModelProposedOperation) -> ValidationResult {
50        let mut errors = Vec::new();
51        let mut warnings = Vec::new();
52
53        // Check application state - do required resources exist?
54        for resource in &op.resources_needed {
55            if !self.application_state.resource_exists(resource).await {
56                // Not necessarily an error - resource might be created
57                warnings.push(format!("Resource '{}' does not exist yet", resource));
58            }
59        }
60
61        // Check operation state - any conflicting running operations?
62        let active_ops = self.operation_state.get_active_operations().await;
63        for active_op in active_ops {
64            // Check if any resources overlap
65            let active_resources: HashSet<_> = active_op
66                .resources_needed
67                .iter()
68                .chain(active_op.resources_produced.iter())
69                .collect();
70
71            let proposed_resources: HashSet<_> = op
72                .resources_needed
73                .iter()
74                .chain(op.resources_produced.iter())
75                .collect();
76
77            let overlap: Vec<_> = active_resources.intersection(&proposed_resources).collect();
78
79            if !overlap.is_empty() {
80                errors.push(format!(
81                    "Conflict with running operation '{}': shared resources {:?}",
82                    active_op.id,
83                    overlap.iter().map(|s| s.as_str()).collect::<Vec<_>>()
84                ));
85            }
86        }
87
88        // Check dependency state - would this create a deadlock?
89        if self
90            .dependency_state
91            .would_deadlock(&op.agent_id, &op.resources_needed)
92            .await
93        {
94            errors.push("Operation would create a deadlock".to_string());
95        }
96
97        ValidationResult {
98            valid: errors.is_empty(),
99            errors,
100            warnings,
101        }
102    }
103
104    /// Record state change from an operation
105    pub async fn record_state_change(&self, change: StateChange) {
106        // Update application state
107        for app_change in &change.application_changes {
108            match app_change {
109                ApplicationChange::FileModified { path, new_hash } => {
110                    self.application_state
111                        .update_file(path.clone(), new_hash.clone())
112                        .await;
113                }
114                ApplicationChange::ArtifactInvalidated { artifact_id } => {
115                    self.application_state
116                        .invalidate_artifact(artifact_id)
117                        .await;
118                }
119                ApplicationChange::GitStateChanged { new_state } => {
120                    self.application_state
121                        .update_git_state(new_state.clone())
122                        .await;
123                }
124                ApplicationChange::ResourceCreated { resource_id } => {
125                    self.application_state
126                        .mark_resource_exists(resource_id)
127                        .await;
128                }
129                ApplicationChange::ResourceDeleted { resource_id } => {
130                    self.application_state
131                        .mark_resource_deleted(resource_id)
132                        .await;
133                }
134            }
135        }
136
137        // Update dependency state
138        for (from, to, edge) in &change.new_dependencies {
139            self.dependency_state
140                .add_dependency(from, to, edge.clone())
141                .await;
142        }
143    }
144
145    /// Get a snapshot of current state for validation
146    pub async fn snapshot(&self) -> StateSnapshot {
147        StateSnapshot {
148            files: self.application_state.get_all_files().await,
149            locks: self.dependency_state.get_current_holders().await,
150            git_state: self.application_state.get_git_state().await,
151            active_operations: self.operation_state.get_active_operation_ids().await,
152        }
153    }
154}
155
156impl Default for ThreeStateModel {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162// =============================================================================
163// Application State
164// =============================================================================
165
166/// Application State: Domain-level resource tracking
167pub struct ApplicationState {
168    /// Files and their current status
169    files: RwLock<HashMap<PathBuf, FileStatus>>,
170    /// Build artifacts and their validity
171    build_artifacts: RwLock<HashMap<String, ArtifactStatus>>,
172    /// Git repository state
173    git_state: RwLock<GitState>,
174    /// Generic resource existence tracking
175    resources: RwLock<HashSet<String>>,
176}
177
178impl ApplicationState {
179    /// Create a new empty application state.
180    pub fn new() -> Self {
181        Self {
182            files: RwLock::new(HashMap::new()),
183            build_artifacts: RwLock::new(HashMap::new()),
184            git_state: RwLock::new(GitState::default()),
185            resources: RwLock::new(HashSet::new()),
186        }
187    }
188
189    /// Check if a resource exists
190    pub async fn resource_exists(&self, resource_id: &str) -> bool {
191        // Check files first
192        let path = PathBuf::from(resource_id);
193        if self.files.read().await.contains_key(&path) {
194            return true;
195        }
196
197        // Check generic resources
198        self.resources.read().await.contains(resource_id)
199    }
200
201    /// Mark a resource as existing
202    pub async fn mark_resource_exists(&self, resource_id: &str) {
203        self.resources.write().await.insert(resource_id.to_string());
204    }
205
206    /// Mark a resource as deleted
207    pub async fn mark_resource_deleted(&self, resource_id: &str) {
208        self.resources.write().await.remove(resource_id);
209    }
210
211    /// Update file status
212    pub async fn update_file(&self, path: PathBuf, content_hash: String) {
213        let mut files = self.files.write().await;
214        let status = files.entry(path).or_insert_with(|| FileStatus {
215            exists: true,
216            content_hash: String::new(),
217            last_modified: SystemTime::now(),
218            locked_by: None,
219            dirty: false,
220        });
221        status.content_hash = content_hash;
222        status.last_modified = SystemTime::now();
223        status.dirty = true;
224        status.exists = true;
225    }
226
227    /// Get all file statuses
228    pub async fn get_all_files(&self) -> HashMap<PathBuf, FileStatus> {
229        self.files.read().await.clone()
230    }
231
232    /// Invalidate a build artifact
233    pub async fn invalidate_artifact(&self, artifact_id: &str) {
234        if let Some(artifact) = self.build_artifacts.write().await.get_mut(artifact_id) {
235            artifact.valid = false;
236        }
237    }
238
239    /// Update git state
240    pub async fn update_git_state(&self, state: GitState) {
241        *self.git_state.write().await = state;
242    }
243
244    /// Get current git state
245    pub async fn get_git_state(&self) -> GitState {
246        self.git_state.read().await.clone()
247    }
248
249    /// Mark file as locked by agent
250    pub async fn lock_file(&self, path: &PathBuf, agent_id: &str) {
251        if let Some(file) = self.files.write().await.get_mut(path) {
252            file.locked_by = Some(agent_id.to_string());
253        }
254    }
255
256    /// Release file lock
257    pub async fn unlock_file(&self, path: &PathBuf) {
258        if let Some(file) = self.files.write().await.get_mut(path) {
259            file.locked_by = None;
260        }
261    }
262
263    /// Mark all source files as clean after successful build
264    pub async fn mark_files_clean(&self) {
265        for file in self.files.write().await.values_mut() {
266            file.dirty = false;
267        }
268    }
269
270    /// Record a build artifact
271    pub async fn record_artifact(&self, artifact_id: String, source_hash: String) {
272        self.build_artifacts.write().await.insert(
273            artifact_id,
274            ArtifactStatus {
275                valid: true,
276                built_from_hash: source_hash,
277                build_time: Instant::now(),
278            },
279        );
280    }
281}
282
283impl Default for ApplicationState {
284    fn default() -> Self {
285        Self::new()
286    }
287}
288
289/// Status of a tracked file in application state.
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct FileStatus {
292    /// Whether the file exists on disk.
293    pub exists: bool,
294    /// Hash of the file contents.
295    pub content_hash: String,
296    /// Last modification time.
297    #[serde(skip, default = "default_system_time")]
298    pub last_modified: SystemTime,
299    /// Agent currently holding a lock on this file.
300    pub locked_by: Option<String>,
301    /// Whether the file has uncommitted changes.
302    pub dirty: bool,
303}
304
305fn default_system_time() -> SystemTime {
306    SystemTime::UNIX_EPOCH
307}
308
309/// Status of a build artifact.
310#[derive(Debug, Clone)]
311pub struct ArtifactStatus {
312    /// Whether the artifact is still valid.
313    pub valid: bool,
314    /// Hash of the source that produced this artifact.
315    pub built_from_hash: String,
316    /// When the artifact was built.
317    pub build_time: Instant,
318}
319
320/// Git repository state snapshot.
321#[derive(Debug, Clone, Default, Serialize, Deserialize)]
322pub struct GitState {
323    /// Current branch name.
324    pub current_branch: String,
325    /// HEAD commit hash.
326    pub head_commit: String,
327    /// Files staged for commit.
328    pub staged_files: Vec<String>,
329    /// Modified but unstaged files.
330    pub modified_files: Vec<String>,
331    /// Whether there are merge conflicts.
332    pub has_conflicts: bool,
333}
334
335// =============================================================================
336// Operation State
337// =============================================================================
338
339/// Operation State: Execution logging and history
340pub struct OperationState {
341    /// All operations by ID
342    operations: RwLock<HashMap<String, OperationLog>>,
343    /// Operations by agent
344    agent_operations: RwLock<HashMap<String, Vec<String>>>,
345    /// Current active operations
346    active_operations: RwLock<HashSet<String>>,
347    /// Operation ID counter
348    next_id: RwLock<u64>,
349}
350
351impl OperationState {
352    /// Create a new empty operation state.
353    pub fn new() -> Self {
354        Self {
355            operations: RwLock::new(HashMap::new()),
356            agent_operations: RwLock::new(HashMap::new()),
357            active_operations: RwLock::new(HashSet::new()),
358            next_id: RwLock::new(1),
359        }
360    }
361
362    /// Generate a new unique operation ID
363    pub async fn generate_id(&self) -> String {
364        let mut id = self.next_id.write().await;
365        let op_id = format!("op-{}", *id);
366        *id += 1;
367        op_id
368    }
369
370    /// Start tracking a new operation
371    pub async fn start_operation(&self, log: OperationLog) -> String {
372        let id = log.id.clone();
373
374        // Add to operations map
375        self.operations
376            .write()
377            .await
378            .insert(id.clone(), log.clone());
379
380        // Add to agent's operations
381        self.agent_operations
382            .write()
383            .await
384            .entry(log.agent_id.clone())
385            .or_default()
386            .push(id.clone());
387
388        // Mark as active
389        self.active_operations.write().await.insert(id.clone());
390
391        id
392    }
393
394    /// Complete an operation
395    pub async fn complete_operation(
396        &self,
397        operation_id: &str,
398        success: bool,
399        outputs: Option<serde_json::Value>,
400        error: Option<String>,
401    ) {
402        // Remove from active
403        self.active_operations.write().await.remove(operation_id);
404
405        // Update operation log
406        if let Some(op) = self.operations.write().await.get_mut(operation_id) {
407            op.completed_at = Some(Instant::now());
408            op.status = if success {
409                OperationLogStatus::Completed
410            } else {
411                OperationLogStatus::Failed
412            };
413            op.outputs = outputs;
414            op.error = error;
415        }
416    }
417
418    /// Mark an operation as compensated
419    pub async fn mark_compensated(&self, operation_id: &str) {
420        if let Some(op) = self.operations.write().await.get_mut(operation_id) {
421            op.status = OperationLogStatus::Compensated;
422        }
423    }
424
425    /// Get active operations
426    pub async fn get_active_operations(&self) -> Vec<OperationLog> {
427        let active_ids = self.active_operations.read().await.clone();
428        let operations = self.operations.read().await;
429
430        active_ids
431            .iter()
432            .filter_map(|id| operations.get(id).cloned())
433            .collect()
434    }
435
436    /// Get active operation IDs
437    pub async fn get_active_operation_ids(&self) -> Vec<String> {
438        self.active_operations
439            .read()
440            .await
441            .iter()
442            .cloned()
443            .collect()
444    }
445
446    /// Get operation by ID
447    pub async fn get_operation(&self, operation_id: &str) -> Option<OperationLog> {
448        self.operations.read().await.get(operation_id).cloned()
449    }
450
451    /// Get all operations for an agent
452    pub async fn get_agent_operations(&self, agent_id: &str) -> Vec<OperationLog> {
453        let op_ids = self
454            .agent_operations
455            .read()
456            .await
457            .get(agent_id)
458            .cloned()
459            .unwrap_or_default();
460
461        let operations = self.operations.read().await;
462        op_ids
463            .iter()
464            .filter_map(|id| operations.get(id).cloned())
465            .collect()
466    }
467
468    /// Add child operation to parent
469    pub async fn add_child_operation(&self, parent_id: &str, child_id: &str) {
470        let mut operations = self.operations.write().await;
471
472        if let Some(parent) = operations.get_mut(parent_id) {
473            parent.child_operations.push(child_id.to_string());
474        }
475
476        if let Some(child) = operations.get_mut(child_id) {
477            child.parent_operation = Some(parent_id.to_string());
478        }
479    }
480}
481
482impl Default for OperationState {
483    fn default() -> Self {
484        Self::new()
485    }
486}
487
488/// Log entry for a tracked operation.
489#[derive(Debug, Clone)]
490pub struct OperationLog {
491    /// Unique operation identifier.
492    pub id: String,
493    /// ID of the agent performing the operation.
494    pub agent_id: String,
495    /// Type of operation (e.g., "build", "test").
496    pub operation_type: String,
497    /// When the operation started.
498    pub started_at: Instant,
499    /// When the operation completed (if finished).
500    pub completed_at: Option<Instant>,
501    /// Current status of the operation.
502    pub status: OperationLogStatus,
503    /// Input parameters for the operation.
504    pub inputs: serde_json::Value,
505    /// Output data (if completed).
506    pub outputs: Option<serde_json::Value>,
507    /// Error message (if failed).
508    pub error: Option<String>,
509    /// IDs of child operations spawned by this one.
510    pub child_operations: Vec<String>,
511    /// ID of the parent operation (if this is a child).
512    pub parent_operation: Option<String>,
513    /// Resources required by this operation.
514    pub resources_needed: Vec<String>,
515    /// Resources produced by this operation.
516    pub resources_produced: Vec<String>,
517}
518
519impl OperationLog {
520    /// Create a new operation log entry.
521    pub fn new(
522        id: String,
523        agent_id: String,
524        operation_type: String,
525        inputs: serde_json::Value,
526    ) -> Self {
527        Self {
528            id,
529            agent_id,
530            operation_type,
531            started_at: Instant::now(),
532            completed_at: None,
533            status: OperationLogStatus::Running,
534            inputs,
535            outputs: None,
536            error: None,
537            child_operations: Vec::new(),
538            parent_operation: None,
539            resources_needed: Vec::new(),
540            resources_produced: Vec::new(),
541        }
542    }
543
544    /// Set the resources needed and produced by this operation.
545    pub fn with_resources(mut self, needed: Vec<String>, produced: Vec<String>) -> Self {
546        self.resources_needed = needed;
547        self.resources_produced = produced;
548        self
549    }
550
551    /// Get the duration of the operation (if completed).
552    pub fn duration(&self) -> Option<std::time::Duration> {
553        self.completed_at
554            .map(|end| end.duration_since(self.started_at))
555    }
556}
557
558/// Status of an operation in its lifecycle.
559#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
560pub enum OperationLogStatus {
561    /// Not yet started.
562    Pending,
563    /// Currently executing.
564    Running,
565    /// Finished successfully.
566    Completed,
567    /// Finished with an error.
568    Failed,
569    /// Rolled back via saga compensation.
570    Compensated,
571}
572
573// =============================================================================
574// Dependency State
575// =============================================================================
576
577/// Dependency State: Resource relationship graph
578pub struct DependencyState {
579    /// Resource dependency graph
580    graph: RwLock<DiGraph<ResourceNode, DependencyEdge>>,
581    /// Index: resource name -> node index
582    resource_index: RwLock<HashMap<String, NodeIndex>>,
583}
584
585impl DependencyState {
586    /// Create a new empty dependency state.
587    pub fn new() -> Self {
588        Self {
589            graph: RwLock::new(DiGraph::new()),
590            resource_index: RwLock::new(HashMap::new()),
591        }
592    }
593
594    /// Add or get a resource node
595    async fn ensure_node(&self, resource_id: &str, resource_type: ResourceNodeType) -> NodeIndex {
596        let mut index = self.resource_index.write().await;
597        let mut graph = self.graph.write().await;
598
599        if let Some(&node_idx) = index.get(resource_id) {
600            return node_idx;
601        }
602
603        let node = ResourceNode {
604            resource_id: resource_id.to_string(),
605            resource_type,
606            current_holder: None,
607        };
608
609        let node_idx = graph.add_node(node);
610        index.insert(resource_id.to_string(), node_idx);
611        node_idx
612    }
613
614    /// Add a dependency between resources
615    ///
616    /// For a "BlockedBy" relationship: `add_dependency("op-a", "op-b", BlockedBy)` means
617    /// "op-a is blocked by op-b", so op-b must execute before op-a.
618    /// The edge direction is from→to (to comes before from in execution order).
619    pub async fn add_dependency(&self, from: &str, to: &str, edge: DependencyEdge) {
620        let from_idx = self.ensure_node(from, ResourceNodeType::Generic).await;
621        let to_idx = self.ensure_node(to, ResourceNodeType::Generic).await;
622
623        // Edge from "to" to "from" because "from" depends on "to"
624        // In graph terms: to → from (to must be processed before from)
625        self.graph.write().await.add_edge(to_idx, from_idx, edge);
626    }
627
628    /// Remove a dependency
629    pub async fn remove_dependency(&self, from: &str, to: &str) {
630        let index = self.resource_index.read().await;
631        let mut graph = self.graph.write().await;
632
633        // Edge was added from to→from, so we need to find it that way
634        if let (Some(&from_idx), Some(&to_idx)) = (index.get(from), index.get(to))
635            && let Some(edge) = graph.find_edge(to_idx, from_idx)
636        {
637            graph.remove_edge(edge);
638        }
639    }
640
641    /// Check if acquiring resources would create a deadlock
642    pub async fn would_deadlock(&self, agent_id: &str, resources: &[String]) -> bool {
643        let mut graph = self.graph.write().await;
644        let mut index = self.resource_index.write().await;
645
646        // Create temporary nodes for the agent's wait-for relationship
647        let agent_node_id = format!("agent:{}", agent_id);
648
649        // Check if agent node exists, create if not
650        let agent_idx = if let Some(&idx) = index.get(&agent_node_id) {
651            idx
652        } else {
653            let node = ResourceNode {
654                resource_id: agent_node_id.clone(),
655                resource_type: ResourceNodeType::Agent(agent_id.to_string()),
656                current_holder: None,
657            };
658            let idx = graph.add_node(node);
659            index.insert(agent_node_id.clone(), idx);
660            idx
661        };
662
663        // Temporarily add edges from agent to requested resources
664        let mut temp_edges = Vec::new();
665        for resource in resources {
666            if let Some(&resource_idx) = index.get(resource) {
667                let edge = graph.add_edge(
668                    agent_idx,
669                    resource_idx,
670                    DependencyEdge {
671                        dependency_type: DependencyType::WaitsFor,
672                        strength: DependencyStrength::Hard,
673                    },
674                );
675                temp_edges.push(edge);
676            }
677        }
678
679        // Check for cycles
680        let has_cycle = is_cyclic_directed(&*graph);
681
682        // Remove temporary edges
683        for edge in temp_edges {
684            graph.remove_edge(edge);
685        }
686
687        has_cycle
688    }
689
690    /// Get all resources that must be released before agent can acquire resource
691    pub async fn get_blocking_resources(&self, resource_id: &str) -> Vec<String> {
692        let graph = self.graph.read().await;
693        let index = self.resource_index.read().await;
694
695        let mut blocking = Vec::new();
696
697        if let Some(&node_idx) = index.get(resource_id) {
698            // Find all incoming edges (resources that this resource depends on)
699            for edge_ref in graph.edges_directed(node_idx, Direction::Incoming) {
700                if let Some(source_node) = graph.node_weight(edge_ref.source())
701                    && source_node.current_holder.is_some()
702                {
703                    blocking.push(source_node.resource_id.clone());
704                }
705            }
706        }
707
708        blocking
709    }
710
711    /// Set the current holder of a resource
712    pub async fn set_holder(&self, resource_id: &str, agent_id: Option<&str>) {
713        let index = self.resource_index.read().await;
714        let mut graph = self.graph.write().await;
715
716        if let Some(&node_idx) = index.get(resource_id)
717            && let Some(node) = graph.node_weight_mut(node_idx)
718        {
719            node.current_holder = agent_id.map(String::from);
720        }
721    }
722
723    /// Get current resource holders
724    pub async fn get_current_holders(&self) -> HashMap<String, String> {
725        let graph = self.graph.read().await;
726
727        graph
728            .node_weights()
729            .filter_map(|node| {
730                node.current_holder
731                    .as_ref()
732                    .map(|holder| (node.resource_id.clone(), holder.clone()))
733            })
734            .collect()
735    }
736
737    /// Get resources held by an agent
738    pub async fn get_agent_resources(&self, agent_id: &str) -> Vec<String> {
739        let graph = self.graph.read().await;
740
741        graph
742            .node_weights()
743            .filter_map(|node| {
744                if node.current_holder.as_deref() == Some(agent_id) {
745                    Some(node.resource_id.clone())
746                } else {
747                    None
748                }
749            })
750            .collect()
751    }
752
753    /// Topological sort of operations respecting dependencies
754    pub async fn get_execution_order(&self, operation_ids: &[String]) -> Vec<String> {
755        let graph = self.graph.read().await;
756        let index = self.resource_index.read().await;
757
758        // Simple implementation: return in dependency order
759        // More sophisticated implementation would use Kahn's algorithm
760        let mut ordered = Vec::new();
761        let mut remaining: HashSet<_> = operation_ids.iter().cloned().collect();
762
763        while !remaining.is_empty() {
764            let mut made_progress = false;
765
766            for op_id in remaining.clone() {
767                if let Some(&node_idx) = index.get(&op_id) {
768                    // Check if all dependencies are satisfied
769                    let all_deps_satisfied = graph
770                        .edges_directed(node_idx, Direction::Incoming)
771                        .all(|edge| {
772                            graph
773                                .node_weight(edge.source())
774                                .map(|n| !remaining.contains(&n.resource_id))
775                                .unwrap_or(true)
776                        });
777
778                    if all_deps_satisfied {
779                        ordered.push(op_id.clone());
780                        remaining.remove(&op_id);
781                        made_progress = true;
782                    }
783                } else {
784                    // No dependencies, can be added
785                    ordered.push(op_id.clone());
786                    remaining.remove(&op_id);
787                    made_progress = true;
788                }
789            }
790
791            if !made_progress {
792                // Cycle detected or remaining items have unsatisfied deps
793                // Just add remaining in arbitrary order
794                ordered.extend(remaining.drain());
795                break;
796            }
797        }
798
799        ordered
800    }
801}
802
803impl Default for DependencyState {
804    fn default() -> Self {
805        Self::new()
806    }
807}
808
809/// A node in the resource dependency graph.
810#[derive(Debug, Clone)]
811pub struct ResourceNode {
812    /// Unique resource identifier.
813    pub resource_id: String,
814    /// Type of this resource.
815    pub resource_type: ResourceNodeType,
816    /// Agent currently holding this resource.
817    pub current_holder: Option<String>,
818}
819
820/// Type of resource node in the dependency graph.
821#[derive(Debug, Clone)]
822pub enum ResourceNodeType {
823    /// A file resource.
824    File(PathBuf),
825    /// Build system lock.
826    BuildLock,
827    /// Test system lock.
828    TestLock,
829    /// Git index lock.
830    GitIndex,
831    /// Git branch resource.
832    GitBranch(String),
833    /// An agent node (for wait-for graphs).
834    Agent(String),
835    /// Generic resource.
836    Generic,
837}
838
839/// An edge in the dependency graph between resources.
840#[derive(Debug, Clone)]
841pub struct DependencyEdge {
842    /// Type of dependency relationship.
843    pub dependency_type: DependencyType,
844    /// How strictly the dependency must be respected.
845    pub strength: DependencyStrength,
846}
847
848/// Type of dependency relationship between resources
849#[derive(Debug, Clone, PartialEq, Eq)]
850pub enum DependencyType {
851    /// A depends on B (A needs B to complete first)
852    BlockedBy,
853    /// A produces B (completing A makes B available)
854    Produces,
855    /// A and B conflict (cannot run concurrently)
856    ConflictsWith,
857    /// A reads B (A needs B in consistent state)
858    Reads,
859    /// A writes B (A will modify B)
860    Writes,
861    /// A waits for B (used in deadlock detection)
862    WaitsFor,
863}
864
865/// How strictly a dependency must be respected
866#[derive(Debug, Clone, PartialEq, Eq)]
867pub enum DependencyStrength {
868    /// Must be respected
869    Hard,
870    /// Preferred but can be violated
871    Soft,
872    /// Information only
873    Advisory,
874}
875
876// =============================================================================
877// Shared Types
878// =============================================================================
879
880/// Result of validating an operation
881#[derive(Debug, Clone)]
882pub struct ValidationResult {
883    /// Whether the operation passed validation.
884    pub valid: bool,
885    /// Validation errors (if any).
886    pub errors: Vec<String>,
887    /// Validation warnings (if any).
888    pub warnings: Vec<String>,
889}
890
891impl ValidationResult {
892    /// Create a passing validation result.
893    pub fn ok() -> Self {
894        Self {
895            valid: true,
896            errors: Vec::new(),
897            warnings: Vec::new(),
898        }
899    }
900
901    /// Create a failing validation result with a single error.
902    pub fn error(msg: impl Into<String>) -> Self {
903        Self {
904            valid: false,
905            errors: vec![msg.into()],
906            warnings: Vec::new(),
907        }
908    }
909}
910
911/// A proposed operation to validate (for the three-state model)
912#[derive(Debug, Clone)]
913pub struct StateModelProposedOperation {
914    /// ID of the agent proposing the operation.
915    pub agent_id: String,
916    /// Type of proposed operation.
917    pub operation_type: String,
918    /// Resources needed by the operation.
919    pub resources_needed: Vec<String>,
920    /// Resources that will be produced.
921    pub resources_produced: Vec<String>,
922}
923
924/// State change to record
925#[derive(Debug, Clone)]
926pub struct StateChange {
927    /// ID of the operation that caused this change.
928    pub operation_id: String,
929    /// Application-level changes.
930    pub application_changes: Vec<ApplicationChange>,
931    /// New dependency relationships (from, to, edge).
932    pub new_dependencies: Vec<(String, String, DependencyEdge)>,
933}
934
935/// Types of application state changes
936#[derive(Debug, Clone)]
937pub enum ApplicationChange {
938    /// A file was modified.
939    FileModified {
940        /// Path of the modified file.
941        path: PathBuf,
942        /// New content hash.
943        new_hash: String,
944    },
945    /// A build artifact was invalidated.
946    ArtifactInvalidated {
947        /// ID of the invalidated artifact.
948        artifact_id: String,
949    },
950    /// Git repository state changed.
951    GitStateChanged {
952        /// New git state.
953        new_state: GitState,
954    },
955    /// A new resource was created.
956    ResourceCreated {
957        /// ID of the created resource.
958        resource_id: String,
959    },
960    /// A resource was deleted.
961    ResourceDeleted {
962        /// ID of the deleted resource.
963        resource_id: String,
964    },
965}
966
967/// Snapshot of current state
968#[derive(Debug, Clone)]
969pub struct StateSnapshot {
970    /// All tracked files and their status.
971    pub files: HashMap<PathBuf, FileStatus>,
972    /// Current resource locks (resource -> holder agent).
973    pub locks: HashMap<String, String>,
974    /// Current git repository state.
975    pub git_state: GitState,
976    /// IDs of currently active operations.
977    pub active_operations: Vec<String>,
978}
979
980#[cfg(test)]
981mod tests {
982    use super::*;
983
984    #[tokio::test]
985    async fn test_three_state_model_creation() {
986        let model = ThreeStateModel::new();
987        let snapshot = model.snapshot().await;
988
989        assert!(snapshot.files.is_empty());
990        assert!(snapshot.locks.is_empty());
991        assert!(snapshot.active_operations.is_empty());
992    }
993
994    #[tokio::test]
995    async fn test_application_state_file_tracking() {
996        let app_state = ApplicationState::new();
997
998        let path = PathBuf::from("/test/file.rs");
999        app_state
1000            .update_file(path.clone(), "hash123".to_string())
1001            .await;
1002
1003        let files = app_state.get_all_files().await;
1004        assert!(files.contains_key(&path));
1005        assert_eq!(files[&path].content_hash, "hash123");
1006        assert!(files[&path].dirty);
1007    }
1008
1009    #[tokio::test]
1010    async fn test_operation_state_lifecycle() {
1011        let op_state = OperationState::new();
1012
1013        let log = OperationLog::new(
1014            "op-1".to_string(),
1015            "agent-1".to_string(),
1016            "build".to_string(),
1017            serde_json::json!({}),
1018        );
1019
1020        let id = op_state.start_operation(log).await;
1021        assert_eq!(id, "op-1");
1022
1023        let active = op_state.get_active_operations().await;
1024        assert_eq!(active.len(), 1);
1025
1026        op_state.complete_operation(&id, true, None, None).await;
1027
1028        let active = op_state.get_active_operations().await;
1029        assert!(active.is_empty());
1030
1031        let op = op_state.get_operation(&id).await.unwrap();
1032        assert_eq!(op.status, OperationLogStatus::Completed);
1033    }
1034
1035    #[tokio::test]
1036    async fn test_dependency_state_deadlock_detection() {
1037        let dep_state = DependencyState::new();
1038
1039        // Create a simple dependency: resource-a -> resource-b
1040        dep_state
1041            .add_dependency(
1042                "resource-a",
1043                "resource-b",
1044                DependencyEdge {
1045                    dependency_type: DependencyType::BlockedBy,
1046                    strength: DependencyStrength::Hard,
1047                },
1048            )
1049            .await;
1050
1051        // Set holder for resource-a
1052        dep_state.set_holder("resource-a", Some("agent-1")).await;
1053
1054        // Agent-1 trying to acquire resource-b shouldn't deadlock
1055        let would_deadlock = dep_state
1056            .would_deadlock("agent-1", &["resource-b".to_string()])
1057            .await;
1058
1059        // This shouldn't cause a deadlock in this simple case
1060        // (more complex scenarios would involve actual wait-for graphs)
1061        assert!(!would_deadlock);
1062    }
1063
1064    #[tokio::test]
1065    async fn test_validate_operation_conflict_detection() {
1066        let model = ThreeStateModel::new();
1067
1068        // Start a running operation
1069        let log = OperationLog::new(
1070            "op-1".to_string(),
1071            "agent-1".to_string(),
1072            "build".to_string(),
1073            serde_json::json!({}),
1074        )
1075        .with_resources(vec!["resource-a".to_string()], vec![]);
1076
1077        model.operation_state.start_operation(log).await;
1078
1079        // Try to validate an operation that conflicts
1080        let proposed = StateModelProposedOperation {
1081            agent_id: "agent-2".to_string(),
1082            operation_type: "build".to_string(),
1083            resources_needed: vec!["resource-a".to_string()],
1084            resources_produced: vec![],
1085        };
1086
1087        let result = model.validate_operation(&proposed).await;
1088        assert!(!result.valid);
1089        assert!(!result.errors.is_empty());
1090    }
1091
1092    #[tokio::test]
1093    async fn test_state_change_recording() {
1094        let model = ThreeStateModel::new();
1095
1096        let change = StateChange {
1097            operation_id: "op-1".to_string(),
1098            application_changes: vec![
1099                ApplicationChange::FileModified {
1100                    path: PathBuf::from("/test/file.rs"),
1101                    new_hash: "newhash".to_string(),
1102                },
1103                ApplicationChange::ResourceCreated {
1104                    resource_id: "build-artifact".to_string(),
1105                },
1106            ],
1107            new_dependencies: vec![],
1108        };
1109
1110        model.record_state_change(change).await;
1111
1112        let snapshot = model.snapshot().await;
1113        assert!(snapshot.files.contains_key(&PathBuf::from("/test/file.rs")));
1114        assert!(
1115            model
1116                .application_state
1117                .resource_exists("build-artifact")
1118                .await
1119        );
1120    }
1121
1122    #[tokio::test]
1123    async fn test_execution_order() {
1124        let dep_state = DependencyState::new();
1125
1126        // A depends on B
1127        dep_state
1128            .add_dependency(
1129                "op-a",
1130                "op-b",
1131                DependencyEdge {
1132                    dependency_type: DependencyType::BlockedBy,
1133                    strength: DependencyStrength::Hard,
1134                },
1135            )
1136            .await;
1137
1138        let order = dep_state
1139            .get_execution_order(&["op-a".to_string(), "op-b".to_string()])
1140            .await;
1141
1142        // op-b should come before op-a since op-a depends on op-b
1143        let pos_a = order.iter().position(|x| x == "op-a").unwrap();
1144        let pos_b = order.iter().position(|x| x == "op-b").unwrap();
1145        assert!(pos_b < pos_a);
1146    }
1147}