1use mj_core::review::driver::{Resolution, RoleState, RoleStatus, TurnReviewPhase};
4use mj_core::review::lanes::ReviewTier;
5use mj_core::review::verdict::ReviewVerdict;
6
7#[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 pub status: String,
17 pub verdict: Option<VerdictView>,
19 #[serde(default, skip_serializing_if = "Vec::is_empty")]
22 pub questions: Vec<ReviewerQuestion>,
23}
24
25#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct ReviewerQuestion {
29 pub role: String,
30 pub request: mj_core::elicitation::ElicitationRequest,
31}
32
33const REVIEWER_QUESTION_PREFIX: &str = "review.";
37
38impl ReviewerQuestion {
39 #[must_use]
42 pub fn session_request(&self) -> mj_core::elicitation::ElicitationRequest {
43 let mut request = self.request.clone();
44 request.id = format!(
45 "{REVIEWER_QUESTION_PREFIX}{}.{}",
46 self.role, self.request.id
47 );
48 request
49 }
50}
51
52#[must_use]
55pub fn parse_reviewer_question_id(id: &str) -> Option<(&str, &str)> {
56 id.strip_prefix(REVIEWER_QUESTION_PREFIX)?
57 .split_once('.')
58 .filter(|(role, inner)| !role.is_empty() && !inner.is_empty())
59}
60
61impl RuntimeReviewView {
62 #[must_use]
65 pub fn is_working(&self) -> bool {
66 self.questions.is_empty()
68 && matches!(
69 self.phase,
70 TurnReviewPhase::CapturingDelta
71 | TurnReviewPhase::LaunchingReviewer
72 | TurnReviewPhase::Running { .. }
73 | TurnReviewPhase::Forwarding { error: None, .. }
74 )
75 }
76
77 #[must_use]
80 pub fn activity_label(&self) -> Option<&'static str> {
81 match &self.phase {
82 TurnReviewPhase::Resolved(_) => None,
83 _ if !self.questions.is_empty() => Some("Question"),
84 TurnReviewPhase::Forwarding { error: None, .. } => Some("Sending findings"),
85 TurnReviewPhase::Forwarding { error: Some(_), .. } => Some("Forward failed"),
86 TurnReviewPhase::Verdict(verdict) => Some(match verdict {
87 ReviewVerdict::Findings { .. } => "Findings",
88 ReviewVerdict::Failed { .. } => "Review failed",
89 ReviewVerdict::Clean => "Review complete",
90 }),
91 TurnReviewPhase::Running { roles }
92 if roles.iter().any(|role| {
93 role.role == mj_core::review::driver::VALIDATOR_ROLE
94 && matches!(role.state, RoleState::Pending | RoleState::Running)
95 }) =>
96 {
97 Some("Validating")
98 }
99 _ => Some("Reviewing"),
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
106#[serde(deny_unknown_fields)]
107pub struct VerdictView {
108 pub kind: VerdictKind,
109 pub text: String,
112 pub allowed: Vec<Resolution>,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
118#[serde(rename_all = "snake_case")]
119pub enum VerdictKind {
120 Clean,
121 Findings,
122 Failed,
123}
124
125#[must_use]
128pub fn role_session_id(primary_session_id: &str, role: &str) -> String {
129 if role == mj_core::review::driver::REVIEWER_ROLE {
130 format!("{primary_session_id}-reviewer")
131 } else {
132 format!("{primary_session_id}-review-{role}")
133 }
134}