1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum CollaborationTopology {
16 Huddle,
17 DiscussionGroup,
18 Panel,
19 SelfOrganizing,
20}
21
22impl CollaborationTopology {
23 #[must_use]
24 pub const fn label(self) -> &'static str {
25 match self {
26 Self::Huddle => "huddle",
27 Self::DiscussionGroup => "discussion_group",
28 Self::Panel => "panel",
29 Self::SelfOrganizing => "self_organizing",
30 }
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum TeamFormationMode {
38 Curated,
39 CapabilityMatched,
40 SelfSelected,
41 OpenCall,
42}
43
44impl TeamFormationMode {
45 #[must_use]
46 pub const fn label(self) -> &'static str {
47 match self {
48 Self::Curated => "curated",
49 Self::CapabilityMatched => "capability_matched",
50 Self::SelfSelected => "self_selected",
51 Self::OpenCall => "open_call",
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum CollaborationDiscipline {
60 Enforced,
61 Moderated,
62 Loose,
63}
64
65impl CollaborationDiscipline {
66 #[must_use]
67 pub const fn label(self) -> &'static str {
68 match self {
69 Self::Enforced => "enforced",
70 Self::Moderated => "moderated",
71 Self::Loose => "loose",
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum CollaborationRole {
80 Lead,
81 Domain,
82 Critic,
83 Synthesizer,
84 Judge,
85 ReportWriter,
86 Moderator,
87 Generalist,
88 Observer,
89}
90
91impl CollaborationRole {
92 #[must_use]
93 pub const fn label(self) -> &'static str {
94 match self {
95 Self::Lead => "lead",
96 Self::Domain => "domain",
97 Self::Critic => "critic",
98 Self::Synthesizer => "synthesizer",
99 Self::Judge => "judge",
100 Self::ReportWriter => "report_writer",
101 Self::Moderator => "moderator",
102 Self::Generalist => "generalist",
103 Self::Observer => "observer",
104 }
105 }
106
107 #[must_use]
108 pub const fn contributes_in_rounds(self) -> bool {
109 matches!(
110 self,
111 Self::Lead | Self::Domain | Self::Critic | Self::Synthesizer | Self::Generalist
112 )
113 }
114
115 #[must_use]
116 pub const fn votes_on_done_gate(self) -> bool {
117 matches!(
118 self,
119 Self::Lead | Self::Domain | Self::Critic | Self::Judge | Self::Generalist
120 )
121 }
122
123 #[must_use]
124 pub const fn can_write_report(self) -> bool {
125 matches!(
126 self,
127 Self::ReportWriter | Self::Synthesizer | Self::Lead | Self::Generalist
128 )
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum TurnCadence {
136 RoundRobin,
137 LeadThenRoundRobin,
138 ModeratorThenRoundRobin,
139 SynthesisOnly,
140 FigureItOut,
141}
142
143impl TurnCadence {
144 #[must_use]
145 pub const fn label(self) -> &'static str {
146 match self {
147 Self::RoundRobin => "round_robin",
148 Self::LeadThenRoundRobin => "lead_then_round_robin",
149 Self::ModeratorThenRoundRobin => "moderator_then_round_robin",
150 Self::SynthesisOnly => "synthesis_only",
151 Self::FigureItOut => "figure_it_out",
152 }
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159pub enum ConsensusRule {
160 Majority,
161 Supermajority,
162 Unanimous,
163 LeadDecides,
164 AdvisoryOnly,
165}
166
167impl ConsensusRule {
168 #[must_use]
169 pub fn passes(self, yes_votes: usize, total_voters: usize) -> bool {
170 match self {
171 Self::Majority => yes_votes * 2 > total_voters,
172 Self::Supermajority => yes_votes * 3 >= total_voters * 2,
173 Self::Unanimous => yes_votes == total_voters,
174 Self::LeadDecides => yes_votes >= 1,
175 Self::AdvisoryOnly => true,
176 }
177 }
178
179 #[must_use]
180 pub const fn label(self) -> &'static str {
181 match self {
182 Self::Majority => "majority",
183 Self::Supermajority => "supermajority",
184 Self::Unanimous => "unanimous",
185 Self::LeadDecides => "lead_decides",
186 Self::AdvisoryOnly => "advisory_only",
187 }
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct CollaborationMember {
195 pub id: String,
196 pub display_name: String,
197 pub role: CollaborationRole,
198 pub persona: Option<String>,
199}
200
201impl CollaborationMember {
202 #[must_use]
203 pub fn new(
204 id: impl Into<String>,
205 display_name: impl Into<String>,
206 role: CollaborationRole,
207 ) -> Self {
208 Self {
209 id: id.into(),
210 display_name: display_name.into(),
211 role,
212 persona: None,
213 }
214 }
215
216 #[must_use]
217 pub fn with_persona(mut self, persona: impl Into<String>) -> Self {
218 self.persona = Some(persona.into());
219 self
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct TeamFormation {
227 pub mode: TeamFormationMode,
228 pub members: Vec<CollaborationMember>,
229}
230
231impl TeamFormation {
232 #[must_use]
233 pub fn new(mode: TeamFormationMode, members: Vec<CollaborationMember>) -> Self {
234 Self { mode, members }
235 }
236
237 #[must_use]
238 pub fn curated(members: Vec<CollaborationMember>) -> Self {
239 Self::new(TeamFormationMode::Curated, members)
240 }
241
242 #[must_use]
243 pub fn member_count(&self) -> usize {
244 self.members.len()
245 }
246
247 #[must_use]
248 pub fn roles(&self) -> Vec<CollaborationRole> {
249 self.members.iter().map(|member| member.role).collect()
250 }
251
252 #[must_use]
253 pub fn contributors(&self) -> Vec<&CollaborationMember> {
254 self.members
255 .iter()
256 .filter(|member| member.role.contributes_in_rounds())
257 .collect()
258 }
259
260 #[must_use]
261 pub fn voters(&self) -> Vec<&CollaborationMember> {
262 self.members
263 .iter()
264 .filter(|member| member.role.votes_on_done_gate())
265 .collect()
266 }
267
268 #[must_use]
269 pub fn report_owner(&self) -> Option<&CollaborationMember> {
270 self.members
271 .iter()
272 .find(|member| member.role.can_write_report())
273 }
274}
275
276#[allow(clippy::struct_excessive_bools)]
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279#[serde(rename_all = "camelCase")]
280pub struct CollaborationCharter {
281 pub topology: CollaborationTopology,
282 pub formation_mode: TeamFormationMode,
283 pub discipline: CollaborationDiscipline,
284 pub turn_cadence: TurnCadence,
285 pub consensus_rule: ConsensusRule,
286 pub minimum_members: usize,
287 pub require_explicit_turns: bool,
288 pub require_round_synthesis: bool,
289 pub require_dissent_map: bool,
290 pub require_done_gate: bool,
291 pub require_report_owner: bool,
292 pub expected_roles: Vec<CollaborationRole>,
293}
294
295impl CollaborationCharter {
296 #[must_use]
297 pub fn huddle() -> Self {
298 Self {
299 topology: CollaborationTopology::Huddle,
300 formation_mode: TeamFormationMode::CapabilityMatched,
301 discipline: CollaborationDiscipline::Enforced,
302 turn_cadence: TurnCadence::RoundRobin,
303 consensus_rule: ConsensusRule::Majority,
304 minimum_members: 3,
305 require_explicit_turns: true,
306 require_round_synthesis: true,
307 require_dissent_map: true,
308 require_done_gate: true,
309 require_report_owner: true,
310 expected_roles: vec![
311 CollaborationRole::Lead,
312 CollaborationRole::Domain,
313 CollaborationRole::Critic,
314 CollaborationRole::Synthesizer,
315 ],
316 }
317 }
318
319 #[must_use]
320 pub fn discussion_group() -> Self {
321 Self {
322 topology: CollaborationTopology::DiscussionGroup,
323 formation_mode: TeamFormationMode::CapabilityMatched,
324 discipline: CollaborationDiscipline::Moderated,
325 turn_cadence: TurnCadence::ModeratorThenRoundRobin,
326 consensus_rule: ConsensusRule::AdvisoryOnly,
327 minimum_members: 3,
328 require_explicit_turns: true,
329 require_round_synthesis: true,
330 require_dissent_map: false,
331 require_done_gate: false,
332 require_report_owner: true,
333 expected_roles: vec![
334 CollaborationRole::Moderator,
335 CollaborationRole::Domain,
336 CollaborationRole::Generalist,
337 ],
338 }
339 }
340
341 #[must_use]
342 pub fn panel() -> Self {
343 Self {
344 topology: CollaborationTopology::Panel,
345 formation_mode: TeamFormationMode::Curated,
346 discipline: CollaborationDiscipline::Enforced,
347 turn_cadence: TurnCadence::LeadThenRoundRobin,
348 consensus_rule: ConsensusRule::Majority,
349 minimum_members: 3,
350 require_explicit_turns: true,
351 require_round_synthesis: true,
352 require_dissent_map: true,
353 require_done_gate: true,
354 require_report_owner: true,
355 expected_roles: vec![
356 CollaborationRole::Lead,
357 CollaborationRole::Domain,
358 CollaborationRole::Critic,
359 ],
360 }
361 }
362
363 #[must_use]
365 pub fn self_organizing() -> Self {
366 Self {
367 topology: CollaborationTopology::SelfOrganizing,
368 formation_mode: TeamFormationMode::OpenCall,
369 discipline: CollaborationDiscipline::Loose,
370 turn_cadence: TurnCadence::FigureItOut,
371 consensus_rule: ConsensusRule::AdvisoryOnly,
372 minimum_members: 1,
373 require_explicit_turns: false,
374 require_round_synthesis: true,
375 require_dissent_map: false,
376 require_done_gate: true,
377 require_report_owner: true,
378 expected_roles: vec![CollaborationRole::Generalist],
379 }
380 }
381
382 #[must_use]
383 pub fn with_consensus_rule(mut self, rule: ConsensusRule) -> Self {
384 self.consensus_rule = rule;
385 self
386 }
387
388 #[must_use]
389 pub fn with_turn_cadence(mut self, cadence: TurnCadence) -> Self {
390 self.turn_cadence = cadence;
391 self
392 }
393
394 #[must_use]
395 pub fn with_discipline(mut self, discipline: CollaborationDiscipline) -> Self {
396 self.discipline = discipline;
397 self
398 }
399
400 #[must_use]
401 pub fn with_topology(mut self, topology: CollaborationTopology) -> Self {
402 self.topology = topology;
403 self
404 }
405
406 #[must_use]
407 pub fn with_minimum_members(mut self, n: usize) -> Self {
408 self.minimum_members = n;
409 self
410 }
411
412 #[must_use]
413 pub fn with_formation_mode(mut self, mode: TeamFormationMode) -> Self {
414 self.formation_mode = mode;
415 self
416 }
417
418 #[must_use]
419 pub fn with_expected_roles(mut self, roles: Vec<CollaborationRole>) -> Self {
420 self.expected_roles = roles;
421 self
422 }
423
424 pub fn validate(&self, team: &TeamFormation) -> Result<(), CollaborationValidationError> {
425 if team.members.len() < self.minimum_members {
426 return Err(CollaborationValidationError::TooFewMembers {
427 required: self.minimum_members,
428 actual: team.members.len(),
429 });
430 }
431
432 if team.mode != self.formation_mode && self.discipline == CollaborationDiscipline::Enforced
433 {
434 return Err(CollaborationValidationError::FormationModeMismatch {
435 expected: self.formation_mode,
436 actual: team.mode,
437 });
438 }
439
440 for expected in &self.expected_roles {
441 if !team.members.iter().any(|member| member.role == *expected) {
442 return Err(CollaborationValidationError::MissingRole { role: *expected });
443 }
444 }
445
446 if self.require_done_gate && team.voters().is_empty() {
447 return Err(CollaborationValidationError::NoVoters);
448 }
449
450 if self.require_report_owner && team.report_owner().is_none() {
451 return Err(CollaborationValidationError::MissingReportOwner);
452 }
453
454 Ok(())
455 }
456}
457
458#[derive(Debug, thiserror::Error, PartialEq, Eq)]
459pub enum CollaborationValidationError {
460 #[error("team requires at least {required} members, found {actual}")]
461 TooFewMembers { required: usize, actual: usize },
462 #[error("expected formation mode {expected:?}, found {actual:?}")]
463 FormationModeMismatch {
464 expected: TeamFormationMode,
465 actual: TeamFormationMode,
466 },
467 #[error("team is missing required role {role:?}")]
468 MissingRole { role: CollaborationRole },
469 #[error("collaboration requires at least one voter")]
470 NoVoters,
471 #[error("collaboration requires a report owner")]
472 MissingReportOwner,
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 fn sample_panel_team() -> TeamFormation {
480 TeamFormation::curated(vec![
481 CollaborationMember::new("lead", "Lead", CollaborationRole::Lead),
482 CollaborationMember::new("domain", "Domain", CollaborationRole::Domain),
483 CollaborationMember::new("critic", "Critic", CollaborationRole::Critic),
484 CollaborationMember::new("judge", "Judge", CollaborationRole::Judge),
485 CollaborationMember::new("writer", "Writer", CollaborationRole::ReportWriter),
486 ])
487 }
488
489 #[test]
490 fn panel_requires_expected_roles() {
491 let team = sample_panel_team();
492 assert!(CollaborationCharter::panel().validate(&team).is_ok());
493 }
494
495 #[test]
496 fn self_organizing_accepts_loose_team() {
497 let team = TeamFormation::new(
498 TeamFormationMode::OpenCall,
499 vec![CollaborationMember::new(
500 "generalist",
501 "Generalist",
502 CollaborationRole::Generalist,
503 )],
504 );
505 assert!(
506 CollaborationCharter::self_organizing()
507 .validate(&team)
508 .is_ok()
509 );
510 }
511
512 #[test]
513 fn enforced_panel_rejects_missing_critic() {
514 let team = TeamFormation::curated(vec![
515 CollaborationMember::new("lead", "Lead", CollaborationRole::Lead),
516 CollaborationMember::new("domain", "Domain", CollaborationRole::Domain),
517 CollaborationMember::new("judge", "Judge", CollaborationRole::Judge),
518 ]);
519 assert_eq!(
520 CollaborationCharter::panel().validate(&team),
521 Err(CollaborationValidationError::MissingRole {
522 role: CollaborationRole::Critic,
523 })
524 );
525 }
526
527 #[test]
530 fn huddle_rejects_empty_team() {
531 let team = TeamFormation::new(TeamFormationMode::CapabilityMatched, vec![]);
532 assert_eq!(
533 CollaborationCharter::huddle().validate(&team),
534 Err(CollaborationValidationError::TooFewMembers {
535 required: 3,
536 actual: 0,
537 })
538 );
539 }
540
541 #[test]
542 fn huddle_rejects_undersized_team() {
543 let team = TeamFormation::new(
544 TeamFormationMode::CapabilityMatched,
545 vec![
546 CollaborationMember::new("lead", "Lead", CollaborationRole::Lead),
547 CollaborationMember::new("domain", "Domain", CollaborationRole::Domain),
548 ],
549 );
550 assert_eq!(
551 CollaborationCharter::huddle().validate(&team),
552 Err(CollaborationValidationError::TooFewMembers {
553 required: 3,
554 actual: 2,
555 })
556 );
557 }
558
559 #[test]
560 fn enforced_charter_rejects_formation_mode_mismatch() {
561 let team = TeamFormation::new(
562 TeamFormationMode::OpenCall,
563 vec![
564 CollaborationMember::new("lead", "Lead", CollaborationRole::Lead),
565 CollaborationMember::new("domain", "Domain", CollaborationRole::Domain),
566 CollaborationMember::new("critic", "Critic", CollaborationRole::Critic),
567 ],
568 );
569 assert_eq!(
570 CollaborationCharter::panel().validate(&team),
571 Err(CollaborationValidationError::FormationModeMismatch {
572 expected: TeamFormationMode::Curated,
573 actual: TeamFormationMode::OpenCall,
574 })
575 );
576 }
577
578 #[test]
579 fn moderated_charter_ignores_formation_mode_mismatch() {
580 let team = TeamFormation::new(
581 TeamFormationMode::SelfSelected,
582 vec![
583 CollaborationMember::new("mod", "Mod", CollaborationRole::Moderator),
584 CollaborationMember::new("domain", "Domain", CollaborationRole::Domain),
585 CollaborationMember::new("gen", "Gen", CollaborationRole::Generalist),
586 ],
587 );
588 assert!(
589 CollaborationCharter::discussion_group()
590 .validate(&team)
591 .is_ok()
592 );
593 }
594
595 #[test]
596 fn done_gate_requires_at_least_one_voter() {
597 let team = TeamFormation::new(
598 TeamFormationMode::OpenCall,
599 vec![CollaborationMember::new(
600 "writer",
601 "Writer",
602 CollaborationRole::ReportWriter,
603 )],
604 );
605 let charter =
606 CollaborationCharter::self_organizing().with_consensus_rule(ConsensusRule::Majority);
607
608 let result = charter.validate(&team);
609 assert_eq!(
610 result,
611 Err(CollaborationValidationError::MissingRole {
612 role: CollaborationRole::Generalist,
613 })
614 );
615 }
616
617 #[test]
618 fn team_of_only_observers_has_no_voters() {
619 let mut charter = CollaborationCharter::self_organizing();
620 charter.expected_roles = vec![];
621 charter.require_report_owner = false;
622
623 let team = TeamFormation::new(
624 TeamFormationMode::OpenCall,
625 vec![
626 CollaborationMember::new("obs1", "Observer1", CollaborationRole::Observer),
627 CollaborationMember::new("obs2", "Observer2", CollaborationRole::Observer),
628 ],
629 );
630 assert_eq!(
631 charter.validate(&team),
632 Err(CollaborationValidationError::NoVoters)
633 );
634 }
635
636 #[test]
637 fn missing_report_owner_rejected() {
638 let mut charter = CollaborationCharter::self_organizing();
639 charter.expected_roles = vec![];
640 charter.require_done_gate = false;
641
642 let team = TeamFormation::new(
643 TeamFormationMode::OpenCall,
644 vec![CollaborationMember::new(
645 "critic",
646 "Critic",
647 CollaborationRole::Critic,
648 )],
649 );
650 assert_eq!(
651 charter.validate(&team),
652 Err(CollaborationValidationError::MissingReportOwner)
653 );
654 }
655
656 #[test]
659 fn majority_needs_strict_majority() {
660 assert!(!ConsensusRule::Majority.passes(2, 4));
661 assert!(ConsensusRule::Majority.passes(3, 4));
662 assert!(!ConsensusRule::Majority.passes(0, 1));
663 assert!(ConsensusRule::Majority.passes(1, 1));
664 }
665
666 #[test]
667 fn supermajority_threshold() {
668 assert!(!ConsensusRule::Supermajority.passes(1, 3));
669 assert!(ConsensusRule::Supermajority.passes(2, 3));
670 assert!(ConsensusRule::Supermajority.passes(4, 6));
671 assert!(!ConsensusRule::Supermajority.passes(3, 6));
672 }
673
674 #[test]
675 fn unanimous_requires_all() {
676 assert!(ConsensusRule::Unanimous.passes(5, 5));
677 assert!(!ConsensusRule::Unanimous.passes(4, 5));
678 assert!(!ConsensusRule::Unanimous.passes(0, 1));
679 }
680
681 #[test]
682 fn lead_decides_needs_one_yes() {
683 assert!(ConsensusRule::LeadDecides.passes(1, 100));
684 assert!(!ConsensusRule::LeadDecides.passes(0, 100));
685 }
686
687 #[test]
688 fn advisory_always_passes() {
689 assert!(ConsensusRule::AdvisoryOnly.passes(0, 0));
690 assert!(ConsensusRule::AdvisoryOnly.passes(0, 100));
691 }
692
693 #[test]
694 fn consensus_with_zero_voters() {
695 assert!(!ConsensusRule::Majority.passes(0, 0));
696 assert!(ConsensusRule::Unanimous.passes(0, 0));
697 assert!(!ConsensusRule::LeadDecides.passes(0, 0));
698 }
699
700 #[test]
703 fn observer_cannot_contribute_vote_or_write() {
704 assert!(!CollaborationRole::Observer.contributes_in_rounds());
705 assert!(!CollaborationRole::Observer.votes_on_done_gate());
706 assert!(!CollaborationRole::Observer.can_write_report());
707 }
708
709 #[test]
710 fn report_writer_can_write_but_not_vote() {
711 assert!(!CollaborationRole::ReportWriter.contributes_in_rounds());
712 assert!(!CollaborationRole::ReportWriter.votes_on_done_gate());
713 assert!(CollaborationRole::ReportWriter.can_write_report());
714 }
715
716 #[test]
717 fn judge_can_vote_but_not_contribute_or_write() {
718 assert!(!CollaborationRole::Judge.contributes_in_rounds());
719 assert!(CollaborationRole::Judge.votes_on_done_gate());
720 assert!(!CollaborationRole::Judge.can_write_report());
721 }
722
723 #[test]
724 fn moderator_has_no_capabilities() {
725 assert!(!CollaborationRole::Moderator.contributes_in_rounds());
726 assert!(!CollaborationRole::Moderator.votes_on_done_gate());
727 assert!(!CollaborationRole::Moderator.can_write_report());
728 }
729
730 #[test]
731 fn generalist_can_do_everything() {
732 assert!(CollaborationRole::Generalist.contributes_in_rounds());
733 assert!(CollaborationRole::Generalist.votes_on_done_gate());
734 assert!(CollaborationRole::Generalist.can_write_report());
735 }
736
737 #[test]
740 fn contributors_excludes_non_contributing_roles() {
741 let team = sample_panel_team();
742 let contributors = team.contributors();
743 assert!(contributors.iter().all(|m| m.role.contributes_in_rounds()));
744 assert!(!contributors.iter().any(
745 |m| m.role == CollaborationRole::Judge || m.role == CollaborationRole::ReportWriter
746 ));
747 }
748
749 #[test]
750 fn voters_excludes_non_voting_roles() {
751 let team = sample_panel_team();
752 let voters = team.voters();
753 assert!(voters.iter().all(|m| m.role.votes_on_done_gate()));
754 }
755
756 #[test]
757 fn report_owner_picks_first_capable() {
758 let team = TeamFormation::curated(vec![
759 CollaborationMember::new("observer", "Observer", CollaborationRole::Observer),
760 CollaborationMember::new("critic", "Critic", CollaborationRole::Critic),
761 CollaborationMember::new("writer", "Writer", CollaborationRole::ReportWriter),
762 ]);
763 assert_eq!(team.report_owner().unwrap().id, "writer");
764 }
765
766 #[test]
767 fn report_owner_none_when_no_capable() {
768 let team = TeamFormation::curated(vec![
769 CollaborationMember::new("observer", "Observer", CollaborationRole::Observer),
770 CollaborationMember::new("critic", "Critic", CollaborationRole::Critic),
771 ]);
772 assert!(team.report_owner().is_none());
773 }
774
775 #[test]
776 fn member_with_persona() {
777 let member = CollaborationMember::new("critic", "Red Team", CollaborationRole::Critic)
778 .with_persona("Aggressive skeptic who challenges every assumption");
779 assert_eq!(
780 member.persona.as_deref(),
781 Some("Aggressive skeptic who challenges every assumption")
782 );
783 }
784
785 #[test]
788 fn all_presets_require_round_synthesis() {
789 assert!(CollaborationCharter::huddle().require_round_synthesis);
790 assert!(CollaborationCharter::discussion_group().require_round_synthesis);
791 assert!(CollaborationCharter::panel().require_round_synthesis);
792 assert!(CollaborationCharter::self_organizing().require_round_synthesis);
793 }
794
795 #[test]
796 fn self_organizing_is_the_only_single_member_preset() {
797 assert_eq!(CollaborationCharter::self_organizing().minimum_members, 1);
798 assert!(CollaborationCharter::huddle().minimum_members >= 3);
799 assert!(CollaborationCharter::discussion_group().minimum_members >= 3);
800 assert!(CollaborationCharter::panel().minimum_members >= 3);
801 }
802
803 mod proptests {
806 use super::*;
807 use proptest::prelude::*;
808
809 fn arb_consensus_rule() -> impl Strategy<Value = ConsensusRule> {
810 prop_oneof![
811 Just(ConsensusRule::Majority),
812 Just(ConsensusRule::Supermajority),
813 Just(ConsensusRule::Unanimous),
814 Just(ConsensusRule::LeadDecides),
815 Just(ConsensusRule::AdvisoryOnly),
816 ]
817 }
818
819 proptest! {
820 #[test]
821 fn consensus_yes_votes_never_exceed_total(
822 rule in arb_consensus_rule(),
823 total in 0_usize..100,
824 yes in 0_usize..100,
825 ) {
826 if yes <= total {
827 let _ = rule.passes(yes, total);
828 }
829 }
830
831 #[test]
832 fn unanimous_passes_iff_all_vote_yes(
833 total in 0_usize..50,
834 yes in 0_usize..50,
835 ) {
836 prop_assume!(yes <= total);
837 let result = ConsensusRule::Unanimous.passes(yes, total);
838 prop_assert_eq!(result, yes == total);
839 }
840
841 #[test]
842 fn advisory_always_passes_regardless_of_votes(
843 total in 0_usize..100,
844 yes in 0_usize..100,
845 ) {
846 prop_assert!(ConsensusRule::AdvisoryOnly.passes(yes, total));
847 }
848
849 #[test]
850 fn majority_monotonic_in_yes_votes(
851 total in 1_usize..50,
852 yes1_frac in 0.0..=1.0_f64,
853 yes2_frac in 0.0..=1.0_f64,
854 ) {
855 let (lo, hi) = if yes1_frac <= yes2_frac {
856 (yes1_frac, yes2_frac)
857 } else {
858 (yes2_frac, yes1_frac)
859 };
860 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss)]
861 let yes1 = (lo * total as f64) as usize;
862 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss)]
863 let yes2 = (hi * total as f64) as usize;
864 if ConsensusRule::Majority.passes(yes1, total) {
865 prop_assert!(ConsensusRule::Majority.passes(yes2, total));
866 }
867 }
868
869 #[test]
870 fn supermajority_is_stricter_than_majority(
871 total in 1_usize..50,
872 yes in 0_usize..50,
873 ) {
874 prop_assume!(yes <= total);
875 if ConsensusRule::Supermajority.passes(yes, total) {
876 prop_assert!(ConsensusRule::Majority.passes(yes, total));
877 }
878 }
879 }
880 }
881}