Skip to main content

allow_core/
ledger_posture.rs

1use crate::CargoAllowError;
2use std::fmt;
3use std::str::FromStr;
4
5/// Canonical presence movement for ledger entries and findings in a diff context.
6///
7/// Internal model uses `Introduced`, `Retained`, and `Removed`. Artifact
8/// projections for PR summaries use [`PresenceMovement::movement_projection`].
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub enum PresenceMovement {
11    Introduced,
12    Retained,
13    Removed,
14}
15
16impl PresenceMovement {
17    pub const ALL: &[Self] = &[Self::Introduced, Self::Retained, Self::Removed];
18
19    /// Stable snake_case field name for machine-readable ledger-state records.
20    pub const fn field_name(self) -> &'static str {
21        match self {
22            Self::Introduced => "introduced",
23            Self::Retained => "retained",
24            Self::Removed => "removed",
25        }
26    }
27
28    /// PR-summary movement projection (PR 2). Not used as internal storage.
29    pub const fn movement_projection(self) -> &'static str {
30        match self {
31            Self::Introduced => "new",
32            Self::Retained => "inherited",
33            Self::Removed => "resolved",
34        }
35    }
36
37    /// Current finding-change artifact label (`finding_changes[].change`).
38    pub const fn finding_change_label(self) -> &'static str {
39        match self {
40            Self::Introduced => "new",
41            Self::Retained => "retained",
42            Self::Removed => "removed",
43        }
44    }
45
46    pub const fn display_label(self) -> &'static str {
47        match self {
48            Self::Introduced => "introduced",
49            Self::Retained => "retained",
50            Self::Removed => "removed",
51        }
52    }
53
54    pub const fn as_str(self) -> &'static str {
55        self.field_name()
56    }
57
58    pub fn parse_field_name(value: &str) -> Result<Self, CargoAllowError> {
59        Self::from_str(value)
60    }
61
62    pub fn parse_finding_change_label(value: &str) -> Result<Self, CargoAllowError> {
63        match value.trim() {
64            "new" => Ok(Self::Introduced),
65            "removed" => Ok(Self::Removed),
66            "retained" => Ok(Self::Retained),
67            other => Err(CargoAllowError::new(format!(
68                "unsupported finding posture change `{other}`"
69            ))),
70        }
71    }
72
73    pub fn parse_movement_projection(value: &str) -> Result<Self, CargoAllowError> {
74        match value.trim() {
75            "new" => Ok(Self::Introduced),
76            "inherited" => Ok(Self::Retained),
77            "resolved" => Ok(Self::Removed),
78            other => Err(CargoAllowError::new(format!(
79                "unsupported movement projection `{other}`"
80            ))),
81        }
82    }
83}
84
85impl fmt::Display for PresenceMovement {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "{}", self.display_label())
88    }
89}
90
91impl FromStr for PresenceMovement {
92    type Err = CargoAllowError;
93
94    fn from_str(value: &str) -> Result<Self, Self::Err> {
95        match value.trim() {
96            "introduced" => Ok(Self::Introduced),
97            "retained" => Ok(Self::Retained),
98            "removed" => Ok(Self::Removed),
99            other => Err(CargoAllowError::new(format!(
100                "unsupported presence movement `{other}`"
101            ))),
102        }
103    }
104}
105
106/// Canonical posture quality delta for a retained ledger entry or finding.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
108pub enum PostureDelta {
109    Improved,
110    Worsened,
111    ReviewRequired,
112    Unchanged,
113}
114
115impl PostureDelta {
116    pub const ALL: &[Self] = &[
117        Self::Improved,
118        Self::Worsened,
119        Self::ReviewRequired,
120        Self::Unchanged,
121    ];
122
123    pub const fn field_name(self) -> &'static str {
124        match self {
125            Self::Improved => "improved",
126            Self::Worsened => "worsened",
127            Self::ReviewRequired => "review_required",
128            Self::Unchanged => "unchanged",
129        }
130    }
131
132    pub const fn display_label(self) -> &'static str {
133        self.field_name()
134    }
135
136    pub const fn as_str(self) -> &'static str {
137        self.field_name()
138    }
139
140    pub fn parse_field_name(value: &str) -> Option<Self> {
141        Self::ALL
142            .iter()
143            .copied()
144            .find(|delta| delta.field_name() == value.trim())
145    }
146}
147
148impl fmt::Display for PostureDelta {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(f, "{}", self.display_label())
151    }
152}
153
154impl FromStr for PostureDelta {
155    type Err = CargoAllowError;
156
157    fn from_str(value: &str) -> Result<Self, Self::Err> {
158        Self::parse_field_name(value)
159            .ok_or_else(|| CargoAllowError::new(format!("unsupported posture delta `{value}`")))
160    }
161}
162
163/// Aggregate PR diff net posture for summary surfaces (`diff.net_posture`).
164///
165/// Distinct from per-row [`PostureDelta`]: aggregate summaries use `worse` and
166/// hyphenated `review-required` spellings required by existing artifacts.
167#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
168pub enum NetPosture {
169    Worse,
170    ReviewRequired,
171    Improved,
172    Unchanged,
173}
174
175impl NetPosture {
176    pub const ALL: &[Self] = &[
177        Self::Worse,
178        Self::ReviewRequired,
179        Self::Improved,
180        Self::Unchanged,
181    ];
182
183    pub const fn net_posture_label(self) -> &'static str {
184        match self {
185            Self::Worse => "worse",
186            Self::ReviewRequired => "review-required",
187            Self::Improved => "improved",
188            Self::Unchanged => "unchanged",
189        }
190    }
191
192    pub const fn as_str(self) -> &'static str {
193        self.net_posture_label()
194    }
195
196    pub const fn reviewer_action(self) -> &'static str {
197        match self {
198            Self::Worse => {
199                "block until failing source exception changes are fixed, narrowed, or receipted."
200            }
201            Self::ReviewRequired => "review the source exception posture change before merging.",
202            Self::Improved => "verify the cleanup was intentional and keep the narrower posture.",
203            Self::Unchanged => "no source exception posture change detected.",
204        }
205    }
206
207    pub fn parse_net_posture_label(value: &str) -> Option<Self> {
208        Self::ALL
209            .iter()
210            .copied()
211            .find(|posture| posture.net_posture_label() == value.trim())
212    }
213
214    pub const fn posture_delta(self) -> Option<PostureDelta> {
215        match self {
216            Self::Worse => Some(PostureDelta::Worsened),
217            Self::ReviewRequired => Some(PostureDelta::ReviewRequired),
218            Self::Improved => Some(PostureDelta::Improved),
219            Self::Unchanged => Some(PostureDelta::Unchanged),
220        }
221    }
222}
223
224impl fmt::Display for NetPosture {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        write!(f, "{}", self.net_posture_label())
227    }
228}
229
230impl FromStr for NetPosture {
231    type Err = CargoAllowError;
232
233    fn from_str(value: &str) -> Result<Self, Self::Err> {
234        Self::parse_net_posture_label(value)
235            .ok_or_else(|| CargoAllowError::new(format!("unsupported net posture `{value}`")))
236    }
237}
238
239/// Orthogonal movement and posture delta for a ledger-state projection.
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub struct LedgerPosture {
242    pub movement: PresenceMovement,
243    pub delta: PostureDelta,
244}
245
246impl LedgerPosture {
247    pub const fn new(movement: PresenceMovement, delta: PostureDelta) -> Self {
248        Self { movement, delta }
249    }
250
251    /// PR-summary movement projection honoring `touched_in_diff` attribution.
252    pub fn movement_projection(self, touched_in_diff: bool) -> &'static str {
253        match self.movement {
254            PresenceMovement::Introduced => "new",
255            PresenceMovement::Removed => "resolved",
256            PresenceMovement::Retained
257                if self.delta == PostureDelta::Unchanged && !touched_in_diff =>
258            {
259                "inherited"
260            }
261            PresenceMovement::Retained => "retained",
262        }
263    }
264}
265
266#[cfg(test)]
267#[path = "ledger_posture_tests.rs"]
268mod tests;