1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//! Common types for parallel execution.
use std::collections::{HashMap, HashSet};
/// Result of a workspace execution (VCS-agnostic)
#[derive(Debug, Clone)]
pub struct WorkspaceResult {
/// OpenSpec change ID
pub change_id: String,
/// Workspace name
pub workspace_name: String,
/// Final revision if successful
pub final_revision: Option<String>,
/// Error message if failed
pub error: Option<String>,
/// Rejection reason when acceptance was blocked and rejection flow completed
pub rejected: Option<String>,
}
/// Successful bookkeeping outcome of a background merge operation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MergeTaskOutcome {
/// The archived change was merged into the base branch.
Merged,
/// The merge did not complete and must remain pending for retry or operator action.
Deferred {
/// Human-readable reason for defer.
reason: String,
/// Whether the deferral can be retried automatically when the base-mutating lane frees.
auto_resumable: bool,
},
}
impl MergeTaskOutcome {
pub fn deferred(reason: impl Into<String>, auto_resumable: bool) -> Self {
Self::Deferred {
reason: reason.into(),
auto_resumable,
}
}
}
/// Result of a background merge operation triggered after workspace completion.
#[derive(Debug, Clone)]
pub struct MergeResult {
/// Change associated with this merge attempt.
pub change_id: String,
/// Workspace name that produced this merge attempt.
pub workspace_name: String,
/// Outcome of the merge attempt.
pub outcome: std::result::Result<MergeTaskOutcome, String>,
}
/// Tracks failed changes and their dependencies to enable automatic skipping.
///
/// When a change fails, any changes that depend on it should be skipped
/// since they are unlikely to succeed without the dependency.
#[derive(Debug, Default)]
pub struct FailedChangeTracker {
/// Set of failed change IDs
failed_changes: HashSet<String>,
/// Dependencies between changes (change_id -> list of dependencies)
dependencies: HashMap<String, Vec<String>>,
}
impl FailedChangeTracker {
/// Create a new empty tracker
pub fn new() -> Self {
Self::default()
}
/// Set the dependencies for all changes.
///
/// The dependencies map should contain change_id -> [dependency_ids].
pub fn set_dependencies(&mut self, dependencies: HashMap<String, Vec<String>>) {
self.dependencies = dependencies;
}
/// Mark a change as failed
pub fn mark_failed(&mut self, change_id: &str) {
self.failed_changes.insert(change_id.to_string());
}
/// Check if a change should be skipped due to a failed dependency.
///
/// Returns `Some(failed_dep_id)` if the change depends on a failed change,
/// otherwise returns `None`.
pub fn should_skip(&self, change_id: &str) -> Option<String> {
if let Some(deps) = self.dependencies.get(change_id) {
for dep in deps {
if self.failed_changes.contains(dep) {
return Some(dep.clone());
}
}
}
None
}
/// Get all failed changes
#[allow(dead_code)] // Public API for external callers
pub fn failed_changes(&self) -> &HashSet<String> {
&self.failed_changes
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_failed_tracker_new() {
let tracker = FailedChangeTracker::new();
assert!(tracker.failed_changes.is_empty());
assert!(tracker.dependencies.is_empty());
}
#[test]
fn test_mark_failed() {
let mut tracker = FailedChangeTracker::new();
tracker.mark_failed("change-a");
assert!(tracker.failed_changes.contains("change-a"));
}
#[test]
fn test_should_skip_no_dependencies() {
let tracker = FailedChangeTracker::new();
assert!(tracker.should_skip("change-a").is_none());
}
#[test]
fn test_should_skip_with_failed_dependency() {
let mut tracker = FailedChangeTracker::new();
// Set up: change-b depends on change-a
let mut deps = HashMap::new();
deps.insert("change-b".to_string(), vec!["change-a".to_string()]);
tracker.set_dependencies(deps);
// change-a fails
tracker.mark_failed("change-a");
// change-b should be skipped
let result = tracker.should_skip("change-b");
assert_eq!(result, Some("change-a".to_string()));
}
#[test]
fn test_should_skip_no_failed_dependency() {
let mut tracker = FailedChangeTracker::new();
// Set up: change-b depends on change-a
let mut deps = HashMap::new();
deps.insert("change-b".to_string(), vec!["change-a".to_string()]);
tracker.set_dependencies(deps);
// change-a did NOT fail
// change-b should NOT be skipped
assert!(tracker.should_skip("change-b").is_none());
}
#[test]
fn test_should_skip_with_multiple_dependencies() {
let mut tracker = FailedChangeTracker::new();
// Set up: change-c depends on change-a and change-b
let mut deps = HashMap::new();
deps.insert(
"change-c".to_string(),
vec!["change-a".to_string(), "change-b".to_string()],
);
tracker.set_dependencies(deps);
// Only change-b fails
tracker.mark_failed("change-b");
// change-c should be skipped (returns first failed dep found)
let result = tracker.should_skip("change-c");
assert_eq!(result, Some("change-b".to_string()));
}
}