agora_agentkit/govlog/council/
decided.rs1use super::{
12 AgendaRanking, CouncilDecisionRecord, CouncilRound, CouncilSeat,
13 CouncilVote, DecisionCategory, FinalVotes, Redactable, SeatChoice,
14 SeatVote, SeatVotes, Statement, Votes,
15};
16use crate::enums::{DecisionOutcome, RoundType};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Decided {
27 Voted(Votes),
29 Ranked(AgendaRanking),
34 Tabled,
36 RecusedDeferral { deferring: u32 },
40}
41
42impl Decided {
43 pub fn final_votes(&self) -> FinalVotes {
45 match self {
46 Self::Voted(votes) => votes.into(),
47 Self::Ranked(ranking) => {
48 let ranked = |seat: CouncilSeat| match ranking
49 .ballots
50 .iter()
51 .any(|b| b.seat == seat)
52 {
53 true => CouncilVote::Ranked,
54 false => CouncilVote::Abstain,
55 };
56 FinalVotes {
57 artist: ranked(CouncilSeat::Artist),
58 philosopher: ranked(CouncilSeat::Philosopher),
59 lawyer: ranked(CouncilSeat::Lawyer),
60 engineer: ranked(CouncilSeat::Engineer),
61 steward: CouncilVote::Abstain,
62 }
63 }
64 Self::Tabled => FinalVotes {
65 artist: CouncilVote::Defer,
66 philosopher: CouncilVote::Defer,
67 lawyer: CouncilVote::Defer,
68 engineer: CouncilVote::Defer,
69 steward: CouncilVote::Defer,
70 },
71 Self::RecusedDeferral { .. } => FinalVotes {
72 artist: CouncilVote::Defer,
73 philosopher: CouncilVote::Defer,
74 lawyer: CouncilVote::Defer,
75 engineer: CouncilVote::Defer,
76 steward: CouncilVote::Recused,
77 },
78 }
79 }
80
81 pub fn outcome(&self, category: DecisionCategory) -> DecisionOutcome {
83 match self {
84 Self::Voted(votes) => votes.outcome(category),
85 Self::Ranked(_) => DecisionOutcome::Approved,
86 Self::Tabled | Self::RecusedDeferral { .. } => {
87 DecisionOutcome::Deferred
88 }
89 }
90 }
91
92 pub fn tally(&self) -> String {
94 match self {
95 Self::Voted(votes) => votes.tally(),
96 Self::Ranked(ranking) => format!(
97 "ranked {}/{}; the Steward executes the order and does not vote",
98 ranking.ballots.len(),
99 CouncilSeat::ALL.len()
100 ),
101 Self::Tabled => "Deferred".to_string(),
102 Self::RecusedDeferral { deferring } => format!(
103 "Deferred: {deferring} seat(s) still deferring at the round cap, \
104 Steward recused; it returns to a later sitting and may not be \
105 deferred again"
106 ),
107 }
108 }
109}
110
111impl CouncilDecisionRecord {
112 pub fn decided(mut self, decided: Decided) -> Self {
116 self.final_votes = decided.final_votes();
117 self.outcome = decided.outcome(self.category);
118 self.vote_tally = decided.tally().into();
119 (self.veto_rationale, self.abstentions, self.agenda_ranking) =
120 match decided {
121 Decided::Voted(votes) => (
122 votes.veto_rationale().map(|r| r.to_string().into()),
123 votes.steward_abstention().into_iter().collect(),
124 None,
125 ),
126 Decided::Ranked(ranking) => (None, Vec::new(), Some(ranking)),
127 Decided::Tabled | Decided::RecusedDeferral { .. } => {
128 (None, Vec::new(), None)
129 }
130 };
131 self
132 }
133
134 pub fn check(&self) -> Result<(), RecordRuleError> {
143 veto_rule(Some(self.final_votes.steward), self.veto_rationale.as_ref())
144 }
145
146 pub fn check_data(data: &serde_json::Value) -> Result<(), RecordRuleError> {
151 #[derive(serde::Deserialize)]
152 struct Steward {
153 steward: CouncilVote,
154 }
155 #[derive(serde::Deserialize)]
156 struct Part {
157 final_votes: Option<Steward>,
158 veto_rationale: Option<Redactable<String>>,
159 }
160 let part: Part = serde_json::from_value(data.clone())
161 .map_err(|_| RecordRuleError::Unreadable)?;
162 veto_rule(
163 part.final_votes.map(|v| v.steward),
164 part.veto_rationale.as_ref(),
165 )
166 }
167}
168
169fn veto_rule(
171 steward: Option<CouncilVote>,
172 rationale: Option<&Redactable<String>>,
173) -> Result<(), RecordRuleError> {
174 let vetoed = steward == Some(CouncilVote::Veto);
175 let rationale = rationale.map(|r| match r {
176 Redactable::Value(text) => !text.trim().is_empty(),
177 Redactable::Redacted(_) => true,
179 });
180 match (vetoed, rationale) {
181 (true, Some(true)) | (false, None) => Ok(()),
182 (true, None | Some(false)) => {
183 Err(RecordRuleError::VetoWithoutRationale)
184 }
185 (false, Some(_)) => Err(RecordRuleError::RationaleWithoutVeto),
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
191pub enum RecordRuleError {
192 #[error(
193 "the Steward vetoed with no rationale; a veto is recorded with its \
194 rationale (Constitution Art. IV § 5)"
195 )]
196 VetoWithoutRationale,
197 #[error("the record carries a veto rationale but the Steward did not veto")]
198 RationaleWithoutVeto,
199 #[error("the record's `final_votes` or `veto_rationale` does not read")]
200 Unreadable,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
205pub enum FinalRoundError {
206 #[error("round {0} is not a final vote")]
207 NotFinal(u32),
208 #[error("the {} has no response in the final round", .0.name())]
209 Missing(CouncilSeat),
210 #[error("the {} answered more than once in the final round", .0.name())]
211 Twice(CouncilSeat),
212 #[error("the {} cast no vote in the final round", .0.name())]
213 NoVote(CouncilSeat),
214 #[error("the {name}'s final vote, `{vote:?}`, is not a seat's vote", name = .0.name(), vote = .1)]
215 NotASeatChoice(CouncilSeat, CouncilVote),
216 #[error(
217 "the {}'s final-vote statement is blank or withheld; it is the \
218 record's words for the vote, and an abstention's rationale (Art. \
219 IV § 3)",
220 .0.name()
221 )]
222 NoStatement(CouncilSeat),
223}
224
225impl SeatVotes {
226 pub fn from_final_round(
229 round: &CouncilRound,
230 ) -> Result<Self, FinalRoundError> {
231 if round.round_type != RoundType::FinalVote {
232 return Err(FinalRoundError::NotFinal(round.number));
233 }
234 let seat = |seat: CouncilSeat| -> Result<SeatVote, FinalRoundError> {
235 let mut responses =
236 round.responses.iter().filter(|r| r.role == seat);
237 let response =
238 responses.next().ok_or(FinalRoundError::Missing(seat))?;
239 if responses.next().is_some() {
240 return Err(FinalRoundError::Twice(seat));
241 }
242 let vote = response.vote.ok_or(FinalRoundError::NoVote(seat))?;
243 let choice = SeatChoice::try_from(vote)
244 .map_err(|_| FinalRoundError::NotASeatChoice(seat, vote))?;
245 let statement = response
246 .position
247 .value()
248 .and_then(|p| Statement::try_from(p.as_str()).ok())
249 .ok_or(FinalRoundError::NoStatement(seat))?;
250 Ok(SeatVote { choice, statement })
251 };
252 Ok(Self {
253 artist: seat(CouncilSeat::Artist)?,
254 philosopher: seat(CouncilSeat::Philosopher)?,
255 lawyer: seat(CouncilSeat::Lawyer)?,
256 engineer: seat(CouncilSeat::Engineer)?,
257 })
258 }
259}
260
261#[cfg(test)]
262mod tests;