agora_agentkit/govlog/council/
votes.rs1use std::fmt;
25use std::ops::Deref;
26
27use serde::{Deserialize, Serialize};
28
29use super::{
30 Abstention, CouncilMember, CouncilSeat, CouncilVote, DecisionCategory,
31 FinalVotes,
32};
33use crate::enums::DecisionOutcome;
34
35#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
44#[serde(try_from = "String", into = "String")]
45pub struct Statement(String);
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
49#[error("words for the record may not be blank")]
50pub struct BlankStatement;
51
52impl TryFrom<String> for Statement {
53 type Error = BlankStatement;
54
55 fn try_from(text: String) -> Result<Self, Self::Error> {
56 if text.trim().is_empty() {
57 Err(BlankStatement)
58 } else {
59 Ok(Self(text))
60 }
61 }
62}
63
64impl TryFrom<&str> for Statement {
65 type Error = BlankStatement;
66
67 fn try_from(text: &str) -> Result<Self, Self::Error> {
68 text.to_string().try_into()
69 }
70}
71
72impl From<Statement> for String {
73 fn from(statement: Statement) -> Self {
74 statement.0
75 }
76}
77
78impl Deref for Statement {
79 type Target = str;
80
81 fn deref(&self) -> &str {
82 &self.0
83 }
84}
85
86impl AsRef<str> for Statement {
87 fn as_ref(&self) -> &str {
88 &self.0
89 }
90}
91
92impl fmt::Display for Statement {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 f.write_str(&self.0)
95 }
96}
97
98impl fmt::Debug for Statement {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 fmt::Debug::fmt(&self.0, f)
101 }
102}
103
104#[cfg(feature = "schemars")]
105impl schemars::JsonSchema for Statement {
106 fn inline_schema() -> bool {
107 true
108 }
109
110 fn schema_name() -> std::borrow::Cow<'static, str> {
111 std::borrow::Cow::Borrowed("Statement")
112 }
113
114 fn json_schema(
115 generator: &mut schemars::SchemaGenerator,
116 ) -> schemars::Schema {
117 String::json_schema(generator)
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(tag = "vote", rename_all = "snake_case")]
126pub enum StewardVote {
127 Yes,
128 No,
129 Concur,
131 Abstain {
132 rationale: Statement,
133 },
134 Veto {
136 rationale: Statement,
137 },
138 Recused,
142}
143
144impl From<&StewardVote> for CouncilVote {
145 fn from(vote: &StewardVote) -> Self {
146 match vote {
147 StewardVote::Yes => Self::Yes,
148 StewardVote::No => Self::No,
149 StewardVote::Concur => Self::Concur,
150 StewardVote::Abstain { .. } => Self::Abstain,
151 StewardVote::Veto { .. } => Self::Veto,
152 StewardVote::Recused => Self::Recused,
153 }
154 }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
159#[serde(rename_all = "snake_case")]
160pub enum SeatChoice {
161 Yes,
162 No,
163 Abstain,
165 Defer,
167}
168
169impl From<SeatChoice> for CouncilVote {
170 fn from(choice: SeatChoice) -> Self {
171 match choice {
172 SeatChoice::Yes => Self::Yes,
173 SeatChoice::No => Self::No,
174 SeatChoice::Abstain => Self::Abstain,
175 SeatChoice::Defer => Self::Defer,
176 }
177 }
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
182#[error("`{0:?}` is not a seat's vote on a voted item")]
183pub struct NotASeatChoice(pub CouncilVote);
184
185impl TryFrom<CouncilVote> for SeatChoice {
186 type Error = NotASeatChoice;
187
188 fn try_from(vote: CouncilVote) -> Result<Self, Self::Error> {
189 match vote {
190 CouncilVote::Yes => Ok(Self::Yes),
191 CouncilVote::No => Ok(Self::No),
192 CouncilVote::Abstain => Ok(Self::Abstain),
193 CouncilVote::Defer => Ok(Self::Defer),
194 other @ (CouncilVote::Concur
195 | CouncilVote::Veto
196 | CouncilVote::Ranked
197 | CouncilVote::Recused) => Err(NotASeatChoice(other)),
198 }
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206pub struct SeatVote {
207 pub choice: SeatChoice,
208 pub statement: Statement,
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum Vote<'a> {
214 Seat(&'a SeatVote),
215 Steward(&'a StewardVote),
216}
217
218impl Vote<'_> {
219 pub fn cast(&self) -> CouncilVote {
221 match self {
222 Self::Seat(v) => v.choice.into(),
223 Self::Steward(v) => (*v).into(),
224 }
225 }
226
227 pub fn abstention(&self) -> Option<&Statement> {
229 match self {
230 Self::Seat(SeatVote {
231 choice: SeatChoice::Abstain,
232 statement,
233 }) => Some(statement),
234 Self::Steward(StewardVote::Abstain { rationale }) => {
235 Some(rationale)
236 }
237 Self::Seat(_) | Self::Steward(_) => None,
238 }
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
245pub struct SeatVotes {
246 pub artist: SeatVote,
247 pub philosopher: SeatVote,
248 pub lawyer: SeatVote,
249 pub engineer: SeatVote,
250}
251
252impl SeatVotes {
253 pub fn seat(&self, seat: CouncilSeat) -> &SeatVote {
255 match seat {
256 CouncilSeat::Artist => &self.artist,
257 CouncilSeat::Philosopher => &self.philosopher,
258 CouncilSeat::Lawyer => &self.lawyer,
259 CouncilSeat::Engineer => &self.engineer,
260 }
261 }
262
263 pub fn iter(&self) -> impl Iterator<Item = (CouncilSeat, &SeatVote)> {
265 CouncilSeat::ALL
266 .into_iter()
267 .map(|seat| (seat, self.seat(seat)))
268 }
269
270 pub fn yes_count(&self) -> usize {
272 self.iter()
273 .filter(|(_, v)| v.choice == SeatChoice::Yes)
274 .count()
275 }
276
277 pub fn no_count(&self) -> usize {
279 self.iter()
280 .filter(|(_, v)| v.choice == SeatChoice::No)
281 .count()
282 }
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288pub struct Votes {
289 #[serde(flatten)]
290 pub seats: SeatVotes,
291 pub steward: StewardVote,
292}
293
294impl Votes {
295 pub fn seat(&self, seat: CouncilSeat) -> &SeatVote {
297 self.seats.seat(seat)
298 }
299
300 pub fn iter(&self) -> impl Iterator<Item = (CouncilMember, Vote<'_>)> {
303 self.seats
304 .iter()
305 .map(|(seat, vote)| (seat.into(), Vote::Seat(vote)))
306 .chain([(CouncilMember::Steward, Vote::Steward(&self.steward))])
307 }
308
309 pub fn steward_abstention(&self) -> Option<Abstention> {
315 match &self.steward {
316 StewardVote::Abstain { rationale } => Some(Abstention {
317 member: CouncilMember::Steward,
318 rationale: rationale.to_string().into(),
319 }),
320 _ => None,
321 }
322 }
323
324 pub fn veto_rationale(&self) -> Option<&Statement> {
327 match &self.steward {
328 StewardVote::Veto { rationale } => Some(rationale),
329 _ => None,
330 }
331 }
332
333 pub fn outcome(&self, category: DecisionCategory) -> DecisionOutcome {
336 FinalVotes::from(self).outcome(category)
337 }
338
339 pub fn tally(&self) -> String {
341 FinalVotes::from(self).tally()
342 }
343}
344
345impl From<&Votes> for FinalVotes {
346 fn from(votes: &Votes) -> Self {
347 Self {
348 artist: votes.seats.artist.choice.into(),
349 philosopher: votes.seats.philosopher.choice.into(),
350 lawyer: votes.seats.lawyer.choice.into(),
351 engineer: votes.seats.engineer.choice.into(),
352 steward: (&votes.steward).into(),
353 }
354 }
355}
356
357impl FinalVotes {
358 fn all(&self) -> [CouncilVote; 5] {
360 [
361 self.artist,
362 self.philosopher,
363 self.lawyer,
364 self.engineer,
365 self.steward,
366 ]
367 }
368
369 fn seats(&self) -> [CouncilVote; 4] {
371 [self.artist, self.philosopher, self.lawyer, self.engineer]
372 }
373
374 pub fn yes_count(&self) -> usize {
376 self.all()
377 .iter()
378 .filter(|v| matches!(v, CouncilVote::Yes | CouncilVote::Concur))
379 .count()
380 }
381
382 pub fn no_count(&self) -> usize {
384 self.all()
385 .iter()
386 .filter(|v| matches!(v, CouncilVote::No))
387 .count()
388 }
389
390 pub fn steward_recused(&self) -> bool {
392 self.steward == CouncilVote::Recused
393 }
394
395 pub fn voting_count(&self) -> usize {
403 self.all()
404 .iter()
405 .filter(|v| {
406 !matches!(v, CouncilVote::Abstain | CouncilVote::Recused)
407 })
408 .count()
409 }
410
411 pub fn tally(&self) -> String {
414 if self.steward_recused() {
415 format!(
416 "{}-{} (Steward recused)",
417 self.yes_count(),
418 self.no_count()
419 )
420 } else {
421 format!("{}-{}", self.yes_count(), self.no_count())
422 }
423 }
424
425 pub fn outcome(&self, category: DecisionCategory) -> DecisionOutcome {
434 let recused = self.steward_recused();
435 let passes = |pass: bool| match pass {
436 true => DecisionOutcome::Approved,
437 false => DecisionOutcome::Rejected,
438 };
439
440 if self.steward == CouncilVote::Veto {
444 return DecisionOutcome::Rejected;
445 }
446
447 match category {
448 DecisionCategory::Routine => passes(self.yes_count() >= 3),
452 DecisionCategory::Policy if recused => {
456 passes(self.seats().iter().all(|v| *v == CouncilVote::Yes))
457 }
458 DecisionCategory::Policy => passes(
462 self.yes_count() >= 4
463 && matches!(
464 self.steward,
465 CouncilVote::Yes | CouncilVote::Concur
466 ),
467 ),
468 DecisionCategory::Constitutional => {
474 let voting = self.voting_count();
475 passes(voting >= 4 && self.yes_count() == voting)
476 }
477 DecisionCategory::Emergency => passes(matches!(
479 self.steward,
480 CouncilVote::Yes | CouncilVote::Concur
481 )),
482 DecisionCategory::Schedule => DecisionOutcome::Deferred,
483 }
484 }
485}
486
487#[cfg(test)]
488mod tests;