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
157pub const DEFERRING_SEATS: usize = 2;
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
163#[serde(rename_all = "snake_case")]
164pub enum SeatChoice {
165 Yes,
166 No,
167 Abstain,
169 Defer,
171}
172
173impl From<SeatChoice> for CouncilVote {
174 fn from(choice: SeatChoice) -> Self {
175 match choice {
176 SeatChoice::Yes => Self::Yes,
177 SeatChoice::No => Self::No,
178 SeatChoice::Abstain => Self::Abstain,
179 SeatChoice::Defer => Self::Defer,
180 }
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
186#[error("`{0:?}` is not a seat's vote on a voted item")]
187pub struct NotASeatChoice(pub CouncilVote);
188
189impl TryFrom<CouncilVote> for SeatChoice {
190 type Error = NotASeatChoice;
191
192 fn try_from(vote: CouncilVote) -> Result<Self, Self::Error> {
193 match vote {
194 CouncilVote::Yes => Ok(Self::Yes),
195 CouncilVote::No => Ok(Self::No),
196 CouncilVote::Abstain => Ok(Self::Abstain),
197 CouncilVote::Defer => Ok(Self::Defer),
198 other @ (CouncilVote::Concur
199 | CouncilVote::Veto
200 | CouncilVote::Ranked
201 | CouncilVote::Recused) => Err(NotASeatChoice(other)),
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210pub struct SeatVote {
211 pub choice: SeatChoice,
212 pub statement: Statement,
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum Vote<'a> {
218 Seat(&'a SeatVote),
219 Steward(&'a StewardVote),
220}
221
222impl Vote<'_> {
223 pub fn cast(&self) -> CouncilVote {
225 match self {
226 Self::Seat(v) => v.choice.into(),
227 Self::Steward(v) => (*v).into(),
228 }
229 }
230
231 pub fn abstention(&self) -> Option<&Statement> {
233 match self {
234 Self::Seat(SeatVote {
235 choice: SeatChoice::Abstain,
236 statement,
237 }) => Some(statement),
238 Self::Steward(StewardVote::Abstain { rationale }) => {
239 Some(rationale)
240 }
241 Self::Seat(_) | Self::Steward(_) => None,
242 }
243 }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249pub struct SeatVotes {
250 pub artist: SeatVote,
251 pub philosopher: SeatVote,
252 pub lawyer: SeatVote,
253 pub engineer: SeatVote,
254}
255
256impl SeatVotes {
257 pub fn seat(&self, seat: CouncilSeat) -> &SeatVote {
259 match seat {
260 CouncilSeat::Artist => &self.artist,
261 CouncilSeat::Philosopher => &self.philosopher,
262 CouncilSeat::Lawyer => &self.lawyer,
263 CouncilSeat::Engineer => &self.engineer,
264 }
265 }
266
267 pub fn iter(&self) -> impl Iterator<Item = (CouncilSeat, &SeatVote)> {
269 CouncilSeat::ALL
270 .into_iter()
271 .map(|seat| (seat, self.seat(seat)))
272 }
273
274 pub fn yes_count(&self) -> usize {
276 self.iter()
277 .filter(|(_, v)| v.choice == SeatChoice::Yes)
278 .count()
279 }
280
281 pub fn no_count(&self) -> usize {
283 self.iter()
284 .filter(|(_, v)| v.choice == SeatChoice::No)
285 .count()
286 }
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292pub struct Votes {
293 #[serde(flatten)]
294 pub seats: SeatVotes,
295 pub steward: StewardVote,
296}
297
298impl Votes {
299 pub fn seat(&self, seat: CouncilSeat) -> &SeatVote {
301 self.seats.seat(seat)
302 }
303
304 pub fn iter(&self) -> impl Iterator<Item = (CouncilMember, Vote<'_>)> {
307 self.seats
308 .iter()
309 .map(|(seat, vote)| (seat.into(), Vote::Seat(vote)))
310 .chain([(CouncilMember::Steward, Vote::Steward(&self.steward))])
311 }
312
313 pub fn steward_abstention(&self) -> Option<Abstention> {
319 match &self.steward {
320 StewardVote::Abstain { rationale } => Some(Abstention {
321 member: CouncilMember::Steward,
322 rationale: rationale.to_string().into(),
323 }),
324 _ => None,
325 }
326 }
327
328 pub fn veto_rationale(&self) -> Option<&Statement> {
331 match &self.steward {
332 StewardVote::Veto { rationale } => Some(rationale),
333 _ => None,
334 }
335 }
336
337 pub fn outcome(&self, category: DecisionCategory) -> DecisionOutcome {
340 FinalVotes::from(self).outcome(category)
341 }
342
343 pub fn tally(&self) -> String {
345 FinalVotes::from(self).tally()
346 }
347}
348
349impl From<&Votes> for FinalVotes {
350 fn from(votes: &Votes) -> Self {
351 Self {
352 artist: votes.seats.artist.choice.into(),
353 philosopher: votes.seats.philosopher.choice.into(),
354 lawyer: votes.seats.lawyer.choice.into(),
355 engineer: votes.seats.engineer.choice.into(),
356 steward: (&votes.steward).into(),
357 }
358 }
359}
360
361impl FinalVotes {
362 fn all(&self) -> [CouncilVote; 5] {
364 [
365 self.artist,
366 self.philosopher,
367 self.lawyer,
368 self.engineer,
369 self.steward,
370 ]
371 }
372
373 fn seats(&self) -> [CouncilVote; 4] {
375 [self.artist, self.philosopher, self.lawyer, self.engineer]
376 }
377
378 pub fn yes_count(&self) -> usize {
380 self.all()
381 .iter()
382 .filter(|v| matches!(v, CouncilVote::Yes | CouncilVote::Concur))
383 .count()
384 }
385
386 pub fn no_count(&self) -> usize {
388 self.all()
389 .iter()
390 .filter(|v| matches!(v, CouncilVote::No))
391 .count()
392 }
393
394 pub fn steward_recused(&self) -> bool {
396 self.steward == CouncilVote::Recused
397 }
398
399 pub fn voting_count(&self) -> usize {
407 self.all()
408 .iter()
409 .filter(|v| {
410 !matches!(v, CouncilVote::Abstain | CouncilVote::Recused)
411 })
412 .count()
413 }
414
415 pub fn defer_count(&self) -> usize {
417 self.seats()
418 .iter()
419 .filter(|v| **v == CouncilVote::Defer)
420 .count()
421 }
422
423 pub fn tally(&self) -> String {
428 let deferring = self.defer_count();
429 let defer = match deferring >= DEFERRING_SEATS {
430 true => format!(", {deferring} defer"),
431 false => String::new(),
432 };
433 if self.steward_recused() {
434 format!(
435 "{}-{}{defer} (Steward recused)",
436 self.yes_count(),
437 self.no_count()
438 )
439 } else {
440 format!("{}-{}{defer}", self.yes_count(), self.no_count())
441 }
442 }
443
444 pub fn outcome(&self, category: DecisionCategory) -> DecisionOutcome {
455 let recused = self.steward_recused();
456 let passes = |pass: bool| match pass {
457 true => DecisionOutcome::Approved,
458 false => DecisionOutcome::Rejected,
459 };
460
461 if self.steward == CouncilVote::Veto {
465 return DecisionOutcome::Rejected;
466 }
467
468 if self.defer_count() >= DEFERRING_SEATS
476 && matches!(
477 category,
478 DecisionCategory::Routine
479 | DecisionCategory::Policy
480 | DecisionCategory::Constitutional
481 )
482 {
483 return DecisionOutcome::Deferred;
484 }
485
486 match category {
487 DecisionCategory::Routine => passes(self.yes_count() >= 3),
491 DecisionCategory::Policy if recused => {
495 passes(self.seats().iter().all(|v| *v == CouncilVote::Yes))
496 }
497 DecisionCategory::Policy => passes(
501 self.yes_count() >= 4
502 && matches!(
503 self.steward,
504 CouncilVote::Yes | CouncilVote::Concur
505 ),
506 ),
507 DecisionCategory::Constitutional => {
513 let voting = self.voting_count();
514 passes(voting >= 4 && self.yes_count() == voting)
515 }
516 DecisionCategory::Emergency => passes(matches!(
518 self.steward,
519 CouncilVote::Yes | CouncilVote::Concur
520 )),
521 DecisionCategory::Schedule => DecisionOutcome::Deferred,
522 }
523 }
524}
525
526#[cfg(test)]
527mod tests;