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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
//! State validation for poka-yoke error prevention
//!
//! This module provides validation logic for lifecycle state to prevent using
//! corrupted or invalid state data.
use super::error::{LifecycleError, Result};
use super::state::LifecycleState;
/// Validation error for lifecycle state
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StateValidationError {
/// Phase history shows invalid transition
InvalidTransition { from: String, to: String },
/// Cache key references non-existent phase
InvalidCacheKey { phase: String },
/// State shows phase completed but required prerequisite not completed
MissingPrerequisite { phase: String, prerequisite: String },
/// Phase history is inconsistent (e.g., deploy before build)
InconsistentHistory { message: String },
}
impl std::fmt::Display for StateValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidTransition { from, to } => {
write!(f, "Invalid transition from '{}' to '{}'", from, to)
}
Self::InvalidCacheKey { phase } => {
write!(f, "Cache key references non-existent phase: '{}'", phase)
}
Self::MissingPrerequisite {
phase,
prerequisite,
} => {
write!(
f,
"Phase '{}' completed but prerequisite '{}' not completed",
phase, prerequisite
)
}
Self::InconsistentHistory { message } => {
write!(f, "Inconsistent phase history: {}", message)
}
}
}
}
impl std::error::Error for StateValidationError {}
/// Validated lifecycle state wrapper
///
/// **Poka-yoke**: Only `ValidatedLifecycleState` can be used in operations that
/// require valid state. This prevents using corrupted or invalid state.
#[derive(Debug, Clone)]
pub struct ValidatedLifecycleState {
state: LifecycleState,
}
impl ValidatedLifecycleState {
/// Create validated state from raw state
///
/// Validates the state before wrapping it. Returns error if validation fails.
pub fn new(state: LifecycleState) -> Result<Self> {
Self::validate(&state)?;
Ok(Self { state })
}
/// Get the underlying state
pub fn state(&self) -> &LifecycleState {
&self.state
}
/// Modify state with automatic re-validation
///
/// **Poka-yoke**: Modification happens inside closure, then state is re-validated.
/// If validation fails, state modification is NOT applied (transaction-like behavior).
///
/// # Example
///
/// ```rust
/// # use crate::lifecycle::state_validation::ValidatedLifecycleState;
/// # use crate::lifecycle::state::LifecycleState;
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut state = LifecycleState::default();
/// state.record_run("init".to_string(), 0, 100, true);
/// let mut validated = ValidatedLifecycleState::new(state)?;
///
/// // Safe modification with automatic re-validation
/// validated.modify(|state| {
/// state.record_run("setup".to_string(), 100, 200, true);
/// })?;
/// # Ok(())
/// # }
/// ```
pub fn modify<F>(&mut self, f: F) -> Result<()>
where
F: FnOnce(&mut LifecycleState),
{
// Clone state for transactional modification
let mut new_state = self.state.clone();
// Apply modification
f(&mut new_state);
// Validate modified state
Self::validate(&new_state)?;
// Only commit if validation passes
self.state = new_state;
Ok(())
}
/// Get mutable access to state (DEPRECATED - use modify() instead)
///
/// **Warning**: This method is deprecated. Use `modify()` for safe mutation.
/// Direct mutation can break validation invariants.
///
/// **Poka-yoke**: Prefer `modify()` which re-validates after changes.
#[deprecated(
since = "0.1.0",
note = "Use modify() instead for automatic re-validation"
)]
pub fn state_mut(&mut self) -> &mut LifecycleState {
&mut self.state
}
/// Validate state invariants
///
/// Checks:
/// - Phase history consistency
/// - Cache keys reference valid phases
/// - Basic invariants (deploy requires test, but hooks can run phases out of order)
///
/// **Note**: Prerequisites are not strictly enforced because hooks can run phases
/// in any order. This validation only checks for truly invalid states.
pub fn validate(state: &LifecycleState) -> Result<()> {
let completed_phases: std::collections::HashSet<&str> = state
.phase_history
.iter()
.filter(|r| r.success)
.map(|r| r.phase.as_str())
.collect();
// Only check critical prerequisites that should never be violated:
// - deploy should not run without test (critical safety check)
// Note: Hooks can run phases out of order, so we don't check all prerequisites
if completed_phases.contains("deploy") && !completed_phases.contains("test") {
return Err(LifecycleError::Other(format!(
"{}",
StateValidationError::MissingPrerequisite {
phase: "deploy".to_string(),
prerequisite: "test".to_string(),
}
)));
}
// Check cache keys reference valid phases
for cache_key in &state.cache_keys {
if !completed_phases.contains(cache_key.phase.as_str()) {
return Err(LifecycleError::Other(format!(
"{}",
StateValidationError::InvalidCacheKey {
phase: cache_key.phase.clone(),
}
)));
}
}
// Note: We don't check for inconsistent history (e.g., deploy before build)
// because hooks can run phases in any order. The type-level state machine
// prevents invalid transitions at compile time for direct phase execution,
// but hooks can violate the normal order.
Ok(())
}
}
impl AsRef<LifecycleState> for ValidatedLifecycleState {
fn as_ref(&self) -> &LifecycleState {
&self.state
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_state_passes_validation() {
let mut state = LifecycleState::default();
state.record_run("init".to_string(), 0, 100, true);
state.record_run("setup".to_string(), 100, 200, true);
state.record_run("build".to_string(), 200, 300, true);
assert!(ValidatedLifecycleState::validate(&state).is_ok());
}
#[test]
fn test_missing_prerequisite_fails_validation() {
let mut state = LifecycleState::default();
// Deploy without test should fail (critical safety check)
state.record_run("deploy".to_string(), 0, 100, true);
assert!(ValidatedLifecycleState::validate(&state).is_err());
}
#[test]
fn test_invalid_cache_key_fails_validation() {
let mut state = LifecycleState::default();
state.record_run("init".to_string(), 0, 100, true);
state.add_cache_key("nonexistent".to_string(), "key".to_string());
assert!(ValidatedLifecycleState::validate(&state).is_err());
}
#[test]
fn test_inconsistent_history_allowed_for_hooks() {
// Hooks can run phases out of order, so this should be allowed
let mut state = LifecycleState::default();
state.record_run("build".to_string(), 0, 100, true);
state.record_run("init".to_string(), 100, 200, true); // init after build (via hook)
// This should pass because hooks can run phases in any order
assert!(ValidatedLifecycleState::validate(&state).is_ok());
}
#[test]
fn test_validated_state_wrapper() {
let mut state = LifecycleState::default();
state.record_run("init".to_string(), 0, 100, true);
let validated = ValidatedLifecycleState::new(state).unwrap();
assert!(validated.state().has_completed_phase("init"));
}
}