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> {
147 let steward = Some(self.final_votes.steward);
148 veto_rule(steward, self.veto_rationale.as_ref())?;
149 self.summary.as_ref().map_or(Ok(()), |s| s.check(steward))
150 }
151
152 pub fn with_summary(mut self, text: impl Into<String>) -> Self {
156 let recused = self.final_votes.steward == CouncilVote::Recused;
157 self.summary = Some(super::DecisionSummary::by_lawyer(text, recused));
158 self
159 }
160
161 pub fn check_data(data: &serde_json::Value) -> Result<(), RecordRuleError> {
167 #[derive(serde::Deserialize)]
168 struct Steward {
169 steward: CouncilVote,
170 }
171 #[derive(serde::Deserialize)]
172 struct Part {
173 final_votes: Option<Steward>,
174 veto_rationale: Option<Redactable<String>>,
175 summary: Option<super::DecisionSummary>,
176 }
177 let part: Part = serde_json::from_value(data.clone())
178 .map_err(|_| RecordRuleError::Unreadable)?;
179 let steward = part.final_votes.map(|v| v.steward);
180 veto_rule(steward, part.veto_rationale.as_ref())?;
181 part.summary.as_ref().map_or(Ok(()), |s| s.check(steward))
182 }
183}
184
185fn veto_rule(
187 steward: Option<CouncilVote>,
188 rationale: Option<&Redactable<String>>,
189) -> Result<(), RecordRuleError> {
190 let vetoed = steward == Some(CouncilVote::Veto);
191 let rationale = rationale.map(|r| match r {
192 Redactable::Value(text) => !text.trim().is_empty(),
193 Redactable::Redacted(_) => true,
195 });
196 match (vetoed, rationale) {
197 (true, Some(true)) | (false, None) => Ok(()),
198 (true, None | Some(false)) => {
199 Err(RecordRuleError::VetoWithoutRationale)
200 }
201 (false, Some(_)) => Err(RecordRuleError::RationaleWithoutVeto),
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
207pub enum RecordRuleError {
208 #[error(
209 "the Steward vetoed with no rationale; a veto is recorded with its \
210 rationale (Constitution Art. IV § 5)"
211 )]
212 VetoWithoutRationale,
213 #[error("the record carries a veto rationale but the Steward did not veto")]
214 RationaleWithoutVeto,
215 #[error(
216 "the record's `final_votes`, `veto_rationale` or `summary` does not read"
217 )]
218 Unreadable,
219 #[error("the summary is the {}'s; the Lawyer writes it", .0.name())]
221 SummaryWriter(CouncilSeat),
222 #[error("the summary is blank")]
224 BlankSummary,
225 #[error(
227 "the summary's acceptance is not {}",
228 if *recused {
229 "the Artist's, Philosopher's and Engineer's, as on an item the \
230 Steward is recused from"
231 } else {
232 "the Steward's"
233 }
234 )]
235 SummaryAcceptance { recused: bool },
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
240pub enum FinalRoundError {
241 #[error("round {0} is not a final vote")]
242 NotFinal(u32),
243 #[error("the {} has no response in the final round", .0.name())]
244 Missing(CouncilSeat),
245 #[error("the {} answered more than once in the final round", .0.name())]
246 Twice(CouncilSeat),
247 #[error("the {} cast no vote in the final round", .0.name())]
248 NoVote(CouncilSeat),
249 #[error("the {name}'s final vote, `{vote:?}`, is not a seat's vote", name = .0.name(), vote = .1)]
250 NotASeatChoice(CouncilSeat, CouncilVote),
251 #[error(
252 "the {}'s final-vote statement is blank or withheld; it is the \
253 record's words for the vote, and an abstention's rationale (Art. \
254 IV § 3)",
255 .0.name()
256 )]
257 NoStatement(CouncilSeat),
258}
259
260impl SeatVotes {
261 pub fn from_final_round(
264 round: &CouncilRound,
265 ) -> Result<Self, FinalRoundError> {
266 if round.round_type != RoundType::FinalVote {
267 return Err(FinalRoundError::NotFinal(round.number));
268 }
269 let seat = |seat: CouncilSeat| -> Result<SeatVote, FinalRoundError> {
270 let mut responses =
271 round.responses.iter().filter(|r| r.role == seat);
272 let response =
273 responses.next().ok_or(FinalRoundError::Missing(seat))?;
274 if responses.next().is_some() {
275 return Err(FinalRoundError::Twice(seat));
276 }
277 let vote = response.vote.ok_or(FinalRoundError::NoVote(seat))?;
278 let choice = SeatChoice::try_from(vote)
279 .map_err(|_| FinalRoundError::NotASeatChoice(seat, vote))?;
280 let statement = response
281 .position
282 .value()
283 .and_then(|p| Statement::try_from(p.as_str()).ok())
284 .ok_or(FinalRoundError::NoStatement(seat))?;
285 Ok(SeatVote { choice, statement })
286 };
287 Ok(Self {
288 artist: seat(CouncilSeat::Artist)?,
289 philosopher: seat(CouncilSeat::Philosopher)?,
290 lawyer: seat(CouncilSeat::Lawyer)?,
291 engineer: seat(CouncilSeat::Engineer)?,
292 })
293 }
294}
295
296#[cfg(test)]
297mod tests;