Skip to main content

mj_client/
review.rs

1//! Review data shared by Mjolnir's control surfaces.
2
3use mj_core::review::driver::{Resolution, RoleState, RoleStatus, TurnReviewPhase};
4use mj_core::review::lanes::ReviewTier;
5use mj_core::review::verdict::ReviewVerdict;
6
7/// What the host tells a surface about one running review.
8#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct RuntimeReviewView {
11    pub session_id: String,
12    pub tier: ReviewTier,
13    pub phase: TurnReviewPhase,
14    pub roles: Vec<RoleStatus>,
15    /// What the review is doing, in one line.
16    pub status: String,
17    /// Present once the review has reached a verdict the user must answer.
18    pub verdict: Option<VerdictView>,
19}
20
21impl RuntimeReviewView {
22    /// Whether progress indicators should move. A verdict and a failed
23    /// handoff wait for the user even though they retain an activity label.
24    #[must_use]
25    pub fn is_working(&self) -> bool {
26        matches!(
27            self.phase,
28            TurnReviewPhase::CapturingDelta
29                | TurnReviewPhase::LaunchingReviewer
30                | TurnReviewPhase::Running { .. }
31                | TurnReviewPhase::Forwarding { error: None, .. }
32        )
33    }
34
35    /// A compact activity label for session lists and headers. Read typed
36    /// state rather than matching the driver's human-facing progress text.
37    #[must_use]
38    pub fn activity_label(&self) -> Option<&'static str> {
39        match &self.phase {
40            TurnReviewPhase::Resolved(_) => None,
41            TurnReviewPhase::Forwarding { error: None, .. } => Some("Sending findings"),
42            TurnReviewPhase::Forwarding { error: Some(_), .. } => Some("Forward failed"),
43            TurnReviewPhase::Verdict(verdict) => Some(match verdict {
44                ReviewVerdict::Findings { .. } => "Findings",
45                ReviewVerdict::Failed { .. } => "Review failed",
46                ReviewVerdict::Clean => "Review complete",
47            }),
48            TurnReviewPhase::Running { roles }
49                if roles.iter().any(|role| {
50                    role.role == mj_core::review::driver::VALIDATOR_ROLE
51                        && matches!(role.state, RoleState::Pending | RoleState::Running)
52                }) =>
53            {
54                Some("Validating")
55            }
56            _ => Some("Reviewing"),
57        }
58    }
59}
60
61/// A verdict as a surface renders it.
62#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
63#[serde(deny_unknown_fields)]
64pub struct VerdictView {
65    pub kind: VerdictKind,
66    /// The findings, or the failure's reason. Empty for a clean verdict, which
67    /// resolves itself and is never on screen.
68    pub text: String,
69    /// Which resolutions this verdict accepts right now. A surface shows the
70    /// rest disabled rather than hiding them, so the buttons do not move.
71    pub allowed: Vec<Resolution>,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum VerdictKind {
77    Clean,
78    Findings,
79    Failed,
80}
81
82/// The relay session id one reviewing role journals under. The default role
83/// keeps the plan reviewer's id, which is the one the worker uses.
84#[must_use]
85pub fn role_session_id(primary_session_id: &str, role: &str) -> String {
86    if role == mj_core::review::driver::REVIEWER_ROLE {
87        format!("{primary_session_id}-reviewer")
88    } else {
89        format!("{primary_session_id}-review-{role}")
90    }
91}