|
| 1 | +//! MBT driver: connects Quint Connect's generated/replayed traces to the |
| 2 | +//! real `protocol.rs` transition functions. |
| 3 | +//! |
| 4 | +//! [`MutationCursorDriver::step`] dispatches on the `MbtAction` variant Quint |
| 5 | +//! recorded — never on a before/after state diff — and every arm |
| 6 | +//! unconditionally calls the corresponding `protocol::*` function with the |
| 7 | +//! transported arguments, including on an `MbtAction` variant Quint produced |
| 8 | +//! from a guarded/no-op path (T02's `mbtStutterAs` instrumentation): the |
| 9 | +//! driver has no way to distinguish that case from a real transition, and |
| 10 | +//! must not try to, since replaying the guarded call and comparing the |
| 11 | +//! resulting no-op state against Quint is the point of the regressions in |
| 12 | +//! T05. |
| 13 | +
|
| 14 | +use std::collections::{BTreeMap, BTreeSet}; |
| 15 | + |
| 16 | +use quint_connect::{switch, Config, Driver, Result, State, Step}; |
| 17 | + |
| 18 | +use super::super::protocol; |
| 19 | +use super::super::types::{ |
| 20 | + boundary_worktree, ActorKind, AttemptId, AttemptState, AttemptStatus, Boundary, FailureKind, |
| 21 | + ProtocolState, ScopeId, ScopeState, ScopeStatus, TreeId, WorktreeId, WorktreeState, |
| 22 | +}; |
| 23 | +use super::model::{ |
| 24 | + ModelState, WireAttemptId, WireBoundary, WireScopeId, WireTreeId, WireWorktreeId, |
| 25 | +}; |
| 26 | + |
| 27 | +fn worktree(id: &str) -> WorktreeId { |
| 28 | + WorktreeId(id.to_string()) |
| 29 | +} |
| 30 | + |
| 31 | +fn scope(id: &str) -> ScopeId { |
| 32 | + ScopeId(id.to_string()) |
| 33 | +} |
| 34 | + |
| 35 | +fn tree(id: &str) -> TreeId { |
| 36 | + TreeId(id.to_string()) |
| 37 | +} |
| 38 | + |
| 39 | +fn attempt_id(id: &str) -> AttemptId { |
| 40 | + AttemptId(id.to_string()) |
| 41 | +} |
| 42 | + |
| 43 | +/// Replays a trace generated from/for `spec/mutation_cursor.qnt` through the |
| 44 | +/// real `protocol.rs` functions. |
| 45 | +/// |
| 46 | +/// Holds exactly the state the plan authorizes: the pure protocol state |
| 47 | +/// (refines `worktrees`/`scopes`/`externalTaint`/`processedEvents`/ |
| 48 | +/// `attempts`/`mutationEvents`) plus `worktree_trees`, the driver-only |
| 49 | +/// analogue of Quint's `worktreeTrees` var — the observed-tree input |
| 50 | +/// `prepare`/`recover` take explicitly, since the pure kernel performs no Git |
| 51 | +/// I/O. `MbtMutate` is the only action that touches `worktree_trees`; every |
| 52 | +/// other action calls a `protocol::*` function, never reimplementing its |
| 53 | +/// logic. |
| 54 | +pub(super) struct MutationCursorDriver { |
| 55 | + protocol: ProtocolState, |
| 56 | + worktree_trees: BTreeMap<WorktreeId, TreeId>, |
| 57 | +} |
| 58 | + |
| 59 | +impl MutationCursorDriver { |
| 60 | + /// Exactly `spec/mutation_cursor.qnt`'s `init`: both worktrees at |
| 61 | + /// `Tree0`/revision `0`/healthy/no-rebaseline, all four scopes |
| 62 | + /// `NeverSeen` with `scopeActor`'s fixed partition (`Scope0`/`Scope1` |
| 63 | + /// Claude Code and `Scope2` Codex on `WT0`, `Scope3` `OpenCode` on `WT1`), |
| 64 | + /// and all six attempts `Available` with the same placeholder |
| 65 | + /// `Flush(WT0)`/revision `0`/`Tree0`/`Tree0` baseline Quint's `init` |
| 66 | + /// assigns every `AttemptId`. |
| 67 | + fn init() -> Self { |
| 68 | + let wt0 = worktree("wt0"); |
| 69 | + let wt1 = worktree("wt1"); |
| 70 | + |
| 71 | + let mut worktrees = BTreeMap::new(); |
| 72 | + let mut worktree_trees = BTreeMap::new(); |
| 73 | + for id in [&wt0, &wt1] { |
| 74 | + worktrees.insert( |
| 75 | + id.clone(), |
| 76 | + WorktreeState { |
| 77 | + cursor_tree: tree("tree0"), |
| 78 | + revision: 0, |
| 79 | + tainted: false, |
| 80 | + failure_kind: FailureKind::Healthy, |
| 81 | + needs_rebaseline: false, |
| 82 | + }, |
| 83 | + ); |
| 84 | + worktree_trees.insert(id.clone(), tree("tree0")); |
| 85 | + } |
| 86 | + |
| 87 | + let scope_partition: [(&str, &WorktreeId, ActorKind); 4] = [ |
| 88 | + ("scope0", &wt0, ActorKind::ClaudeCode), |
| 89 | + ("scope1", &wt0, ActorKind::ClaudeCode), |
| 90 | + ("scope2", &wt0, ActorKind::Codex), |
| 91 | + ("scope3", &wt1, ActorKind::OpenCode), |
| 92 | + ]; |
| 93 | + let mut scopes = BTreeMap::new(); |
| 94 | + for (id, owning_worktree, actor_kind) in scope_partition { |
| 95 | + scopes.insert( |
| 96 | + scope(id), |
| 97 | + ScopeState { |
| 98 | + status: ScopeStatus::NeverSeen, |
| 99 | + actor_kind, |
| 100 | + worktree_id: owning_worktree.clone(), |
| 101 | + }, |
| 102 | + ); |
| 103 | + } |
| 104 | + |
| 105 | + let mut attempts = BTreeMap::new(); |
| 106 | + for id in [ |
| 107 | + "attempt0", "attempt1", "attempt2", "attempt3", "attempt4", "attempt5", |
| 108 | + ] { |
| 109 | + attempts.insert( |
| 110 | + attempt_id(id), |
| 111 | + AttemptState { |
| 112 | + status: AttemptStatus::Available, |
| 113 | + boundary: Boundary::Flush { |
| 114 | + worktree: wt0.clone(), |
| 115 | + }, |
| 116 | + expected_revision: 0, |
| 117 | + before_tree: tree("tree0"), |
| 118 | + after_tree: tree("tree0"), |
| 119 | + }, |
| 120 | + ); |
| 121 | + } |
| 122 | + |
| 123 | + Self { |
| 124 | + protocol: ProtocolState { |
| 125 | + worktrees, |
| 126 | + scopes, |
| 127 | + external_taint: BTreeSet::new(), |
| 128 | + processed_events: BTreeSet::new(), |
| 129 | + attempts, |
| 130 | + mutation_events: BTreeSet::new(), |
| 131 | + }, |
| 132 | + worktree_trees, |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + fn mbt_init(&mut self) { |
| 137 | + *self = Self::init(); |
| 138 | + } |
| 139 | + |
| 140 | + fn mbt_mutate(&mut self, worktree: WorktreeId, tree: TreeId) { |
| 141 | + self.worktree_trees.insert(worktree, tree); |
| 142 | + } |
| 143 | + |
| 144 | + fn observed_tree(&self, worktree: &WorktreeId) -> TreeId { |
| 145 | + self.worktree_trees |
| 146 | + .get(worktree) |
| 147 | + .cloned() |
| 148 | + .expect("every worktree tracked since init has an observed tree") |
| 149 | + } |
| 150 | + |
| 151 | + fn mbt_prepare(&mut self, attempt: AttemptId, boundary: Boundary) { |
| 152 | + let worktree = boundary_worktree(&boundary, &self.protocol.scopes) |
| 153 | + .expect("every boundary's scope is registered by init, matching Quint's static scopeWorktree partition"); |
| 154 | + let observed_tree = self.observed_tree(&worktree); |
| 155 | + self.protocol = protocol::prepare(&self.protocol, attempt, boundary, observed_tree); |
| 156 | + } |
| 157 | + |
| 158 | + fn mbt_commit(&mut self, attempt: &AttemptId) { |
| 159 | + self.protocol = protocol::commit(&self.protocol, attempt).state; |
| 160 | + } |
| 161 | + |
| 162 | + fn mbt_taint(&mut self, worktree: &WorktreeId) { |
| 163 | + self.protocol = protocol::taint(&self.protocol, worktree); |
| 164 | + } |
| 165 | + |
| 166 | + fn mbt_database_failure(&mut self, worktree: &WorktreeId) { |
| 167 | + self.protocol = protocol::database_failure(&self.protocol, worktree); |
| 168 | + } |
| 169 | + |
| 170 | + fn mbt_abandon(&mut self, scope: &ScopeId) { |
| 171 | + self.protocol = protocol::abandon(&self.protocol, scope); |
| 172 | + } |
| 173 | + |
| 174 | + fn mbt_recover(&mut self, worktree: &WorktreeId) { |
| 175 | + let observed_tree = self.observed_tree(worktree); |
| 176 | + self.protocol = protocol::recover(&self.protocol, worktree, observed_tree); |
| 177 | + } |
| 178 | + |
| 179 | + /// Refines the explicit top-level `stutter` action: no state change. |
| 180 | + #[allow(clippy::unused_self)] |
| 181 | + fn mbt_stutter(&self) {} |
| 182 | +} |
| 183 | + |
| 184 | +impl Default for MutationCursorDriver { |
| 185 | + fn default() -> Self { |
| 186 | + Self::init() |
| 187 | + } |
| 188 | +} |
| 189 | + |
| 190 | +impl Driver for MutationCursorDriver { |
| 191 | + type State = ModelState; |
| 192 | + |
| 193 | + fn config() -> Config { |
| 194 | + Config { |
| 195 | + state: &[], |
| 196 | + nondet: &["mbtAction"], |
| 197 | + } |
| 198 | + } |
| 199 | + |
| 200 | + fn step(&mut self, step: &Step) -> Result { |
| 201 | + switch!(step { |
| 202 | + MbtInit => self.mbt_init(), |
| 203 | + MbtMutate(worktree: WireWorktreeId, tree: WireTreeId) => |
| 204 | + self.mbt_mutate(worktree.into(), tree.into()), |
| 205 | + MbtPrepare(attempt: WireAttemptId, boundary: WireBoundary) => |
| 206 | + self.mbt_prepare(attempt.into(), boundary.into()), |
| 207 | + MbtCommit(attempt: WireAttemptId) => self.mbt_commit(&attempt.into()), |
| 208 | + MbtTaint(worktree: WireWorktreeId) => self.mbt_taint(&worktree.into()), |
| 209 | + MbtDatabaseFailure(worktree: WireWorktreeId) => |
| 210 | + self.mbt_database_failure(&worktree.into()), |
| 211 | + MbtAbandon(scope: WireScopeId) => self.mbt_abandon(&scope.into()), |
| 212 | + MbtRecover(worktree: WireWorktreeId) => self.mbt_recover(&worktree.into()), |
| 213 | + MbtStutter => self.mbt_stutter(), |
| 214 | + }) |
| 215 | + } |
| 216 | +} |
| 217 | + |
| 218 | +impl State<MutationCursorDriver> for ModelState { |
| 219 | + fn from_driver(driver: &MutationCursorDriver) -> Result<Self> { |
| 220 | + Ok(ModelState { |
| 221 | + worktrees: driver.protocol.worktrees.clone(), |
| 222 | + scopes: driver.protocol.scopes.clone(), |
| 223 | + worktree_trees: driver.worktree_trees.clone(), |
| 224 | + external_taint: driver.protocol.external_taint.clone(), |
| 225 | + processed_events: driver.protocol.processed_events.clone(), |
| 226 | + attempts: driver.protocol.attempts.clone(), |
| 227 | + mutation_events: driver.protocol.mutation_events.clone(), |
| 228 | + }) |
| 229 | + } |
| 230 | +} |
0 commit comments