1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Clone, Debug, Serialize, Deserialize, Default)]
7pub struct DecisionPolicyRules {
8 #[serde(default)]
9 pub voting: VotingRules,
10 #[serde(default)]
11 pub objection_handling: ObjectionHandlingRules,
12 #[serde(default)]
13 pub evaluation: EvaluationRules,
14 #[serde(default)]
15 pub commitment: CommitmentRules,
16}
17
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub struct VotingRules {
20 #[serde(default = "default_algorithm")]
21 pub algorithm: String,
22 #[serde(default = "default_threshold")]
23 pub threshold: f64,
24 #[serde(default)]
25 pub quorum: QuorumRules,
26 #[serde(default)]
27 pub weights: HashMap<String, f64>,
28}
29
30impl Default for VotingRules {
31 fn default() -> Self {
32 Self {
33 algorithm: default_algorithm(),
34 threshold: default_threshold(),
35 quorum: QuorumRules::default(),
36 weights: HashMap::new(),
37 }
38 }
39}
40
41fn default_algorithm() -> String {
42 "none".into()
43}
44
45fn default_threshold() -> f64 {
46 0.5
47}
48
49#[derive(Clone, Debug, Serialize, Deserialize)]
51pub struct QuorumRules {
52 #[serde(default = "default_quorum_type", rename = "type")]
53 pub quorum_type: String,
54 #[serde(default)]
55 pub value: f64,
56}
57
58impl Default for QuorumRules {
59 fn default() -> Self {
60 Self {
61 quorum_type: default_quorum_type(),
62 value: 0.0,
63 }
64 }
65}
66
67fn default_quorum_type() -> String {
68 "count".into()
69}
70
71#[derive(Clone, Debug, Serialize, Deserialize)]
72pub struct ObjectionHandlingRules {
73 #[serde(default, alias = "critical_severity_vetoes")]
75 pub critical_severity_vetoes: bool,
76 #[serde(default = "default_veto_threshold")]
77 pub veto_threshold: u32,
78 #[serde(default)]
85 pub critical_objection_action: CriticalObjectionAction,
86}
87
88impl Default for ObjectionHandlingRules {
89 fn default() -> Self {
90 Self {
91 critical_severity_vetoes: false,
92 veto_threshold: default_veto_threshold(),
93 critical_objection_action: CriticalObjectionAction::default(),
94 }
95 }
96}
97
98fn default_veto_threshold() -> u32 {
99 1
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
104#[serde(rename_all = "snake_case")]
105pub enum CriticalObjectionAction {
106 #[default]
109 Deny,
110 FinalizeDecline,
113 Hold,
118}
119
120#[derive(Clone, Debug, Serialize, Deserialize)]
121pub struct EvaluationRules {
122 #[serde(default)]
123 pub required_before_voting: bool,
124 #[serde(default)]
125 pub minimum_confidence: f64,
126}
127
128impl Default for EvaluationRules {
129 fn default() -> Self {
130 Self {
131 required_before_voting: false,
132 minimum_confidence: 0.0,
133 }
134 }
135}
136
137pub use super::CommitmentRules;
142
143#[derive(Clone, Debug, Serialize, Deserialize, Default)]
146pub struct ProposalPolicyRules {
147 #[serde(default)]
148 pub acceptance: ProposalAcceptanceRules,
149 #[serde(default)]
150 pub counter_proposal: CounterProposalRules,
151 #[serde(default)]
152 pub rejection: RejectionRules,
153 #[serde(default)]
154 pub commitment: CommitmentRules,
155}
156
157#[derive(Clone, Debug, Serialize, Deserialize)]
158pub struct ProposalAcceptanceRules {
159 #[serde(default = "default_acceptance_criterion")]
160 pub criterion: String,
161}
162
163impl Default for ProposalAcceptanceRules {
164 fn default() -> Self {
165 Self {
166 criterion: default_acceptance_criterion(),
167 }
168 }
169}
170
171fn default_acceptance_criterion() -> String {
172 "all_parties".into()
173}
174
175#[derive(Clone, Debug, Default, Serialize, Deserialize)]
176pub struct CounterProposalRules {
177 #[serde(default)]
178 pub max_rounds: usize,
179}
180
181#[derive(Clone, Debug, Default, Serialize, Deserialize)]
182pub struct RejectionRules {
183 #[serde(default)]
184 pub terminal_on_any_reject: bool,
185}
186
187#[derive(Clone, Debug, Serialize, Deserialize, Default)]
190pub struct TaskPolicyRules {
191 #[serde(default)]
192 pub assignment: TaskAssignmentRules,
193 #[serde(default)]
194 pub completion: TaskCompletionRules,
195 #[serde(default)]
196 pub commitment: CommitmentRules,
197}
198
199#[derive(Clone, Debug, Default, Serialize, Deserialize)]
200pub struct TaskAssignmentRules {
201 #[serde(default)]
202 pub allow_reassignment_on_reject: bool,
203}
204
205#[derive(Clone, Debug, Default, Serialize, Deserialize)]
206pub struct TaskCompletionRules {
207 #[serde(default)]
208 pub require_output: bool,
209}
210
211#[derive(Clone, Debug, Serialize, Deserialize, Default)]
214pub struct HandoffPolicyRules {
215 #[serde(default)]
216 pub acceptance: HandoffAcceptanceRules,
217 #[serde(default)]
218 pub commitment: CommitmentRules,
219}
220
221#[derive(Clone, Debug, Default, Serialize, Deserialize)]
222pub struct HandoffAcceptanceRules {
223 #[serde(default)]
224 pub implicit_accept_timeout_ms: u64,
225}
226
227#[derive(Clone, Debug, Serialize, Deserialize, Default)]
230pub struct QuorumPolicyRules {
231 #[serde(default)]
232 pub threshold: QuorumThreshold,
233 #[serde(default)]
234 pub abstention: AbstentionRules,
235 #[serde(default)]
236 pub commitment: CommitmentRules,
237}
238
239#[derive(Clone, Debug, Serialize, Deserialize)]
241pub struct QuorumThreshold {
242 #[serde(default = "default_threshold_type", rename = "type")]
243 pub threshold_type: String,
244 #[serde(default)]
245 pub value: f64,
246}
247
248impl Default for QuorumThreshold {
249 fn default() -> Self {
250 Self {
251 threshold_type: default_threshold_type(),
252 value: 0.0,
253 }
254 }
255}
256
257fn default_threshold_type() -> String {
258 "n_of_m".into()
259}
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
289pub enum EffectiveThreshold {
290 Inert,
298 Approvals(u32),
301 Unsatisfiable,
317}
318
319impl QuorumThreshold {
320 pub fn effective(&self, total_participants: usize) -> EffectiveThreshold {
339 if self.value.partial_cmp(&0.0) != Some(std::cmp::Ordering::Greater) {
342 return EffectiveThreshold::Inert;
343 }
344 let required: f64 = match self.threshold_type.as_str() {
345 "percentage" => {
346 if total_participants == 0 {
347 return EffectiveThreshold::Unsatisfiable;
351 }
352 if self.value.fract() == 0.0 {
374 let scaled = (self.value as u128)
375 .saturating_mul(total_participants as u128)
376 .div_ceil(100);
377 let required = scaled.clamp(1, u32::MAX as u128) as u32;
378 return EffectiveThreshold::Approvals(required);
379 }
380 self.value * total_participants as f64 / 100.0
385 }
386 "n_of_m" | "count" => self.value,
392 _ => return EffectiveThreshold::Unsatisfiable,
393 };
394 EffectiveThreshold::Approvals((required.ceil() as u32).max(1))
397 }
398}
399
400#[derive(Clone, Debug, Serialize, Deserialize)]
401pub struct AbstentionRules {
402 #[serde(default)]
403 pub counts_toward_quorum: bool,
404 #[serde(default = "default_interpretation")]
405 pub interpretation: String,
406}
407
408impl Default for AbstentionRules {
409 fn default() -> Self {
410 Self {
411 counts_toward_quorum: false,
412 interpretation: default_interpretation(),
413 }
414 }
415}
416
417fn default_interpretation() -> String {
418 "neutral".into()
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn decision_policy_rules_defaults() {
427 let rules = DecisionPolicyRules::default();
428 assert_eq!(rules.voting.algorithm, "none");
429 assert!((rules.voting.threshold - 0.5).abs() < f64::EPSILON);
430 assert_eq!(rules.voting.quorum.quorum_type, "count");
431 assert!(!rules.objection_handling.critical_severity_vetoes);
432 assert_eq!(rules.objection_handling.veto_threshold, 1);
433 assert_eq!(
434 rules.objection_handling.critical_objection_action,
435 CriticalObjectionAction::Deny
436 );
437 assert!(!rules.commitment.allow_decline_over_approval);
438 assert!(!rules.evaluation.required_before_voting);
439 assert!((rules.evaluation.minimum_confidence).abs() < f64::EPSILON);
440 assert_eq!(rules.commitment.authority, "initiator_only");
441 assert!(rules.commitment.designated_roles.is_empty());
442 assert!(!rules.commitment.require_vote_quorum);
443 }
444
445 #[test]
446 fn decision_policy_rules_deserialization() {
447 let json = serde_json::json!({
448 "voting": {
449 "algorithm": "majority",
450 "threshold": 0.6,
451 "quorum": { "type": "percentage", "value": 75.0 },
452 "weights": { "agent://fraud": 2.0, "agent://growth": 1.0 }
453 },
454 "objection_handling": {
455 "critical_severity_vetoes": true,
456 "veto_threshold": 2
457 },
458 "evaluation": {
459 "required_before_voting": true,
460 "minimum_confidence": 0.8
461 },
462 "commitment": {
463 "authority": "designated_role",
464 "designated_roles": ["agent://lead"],
465 "require_vote_quorum": true
466 }
467 });
468
469 let rules: DecisionPolicyRules = serde_json::from_value(json).unwrap();
470 assert_eq!(rules.voting.algorithm, "majority");
471 assert!((rules.voting.threshold - 0.6).abs() < f64::EPSILON);
472 assert_eq!(rules.voting.quorum.quorum_type, "percentage");
473 assert!((rules.voting.quorum.value - 75.0).abs() < f64::EPSILON);
474 assert_eq!(*rules.voting.weights.get("agent://fraud").unwrap(), 2.0);
475 assert!(rules.objection_handling.critical_severity_vetoes);
476 assert_eq!(rules.objection_handling.veto_threshold, 2);
477 assert!(rules.evaluation.required_before_voting);
478 assert!((rules.evaluation.minimum_confidence - 0.8).abs() < f64::EPSILON);
479 assert_eq!(rules.commitment.authority, "designated_role");
480 assert_eq!(rules.commitment.designated_roles, vec!["agent://lead"]);
481 assert!(rules.commitment.require_vote_quorum);
482 }
483
484 #[test]
485 fn partial_deserialization_fills_defaults() {
486 let json = serde_json::json!({
487 "voting": { "algorithm": "unanimous" }
488 });
489 let rules: DecisionPolicyRules = serde_json::from_value(json).unwrap();
490 assert_eq!(rules.voting.algorithm, "unanimous");
491 assert!((rules.voting.threshold - 0.5).abs() < f64::EPSILON);
492 assert!(!rules.objection_handling.critical_severity_vetoes);
493 assert_eq!(rules.objection_handling.veto_threshold, 1);
494 }
495
496 #[test]
497 fn proposal_policy_rules_defaults() {
498 let rules = ProposalPolicyRules::default();
499 assert_eq!(rules.acceptance.criterion, "all_parties");
500 assert_eq!(rules.counter_proposal.max_rounds, 0);
501 assert!(!rules.rejection.terminal_on_any_reject);
502 assert_eq!(rules.commitment.authority, "initiator_only");
503 }
504
505 #[test]
506 fn proposal_policy_rules_deserialization() {
507 let json = serde_json::json!({
508 "acceptance": { "criterion": "counterparty" },
509 "counter_proposal": { "max_rounds": 3 },
510 "rejection": { "terminal_on_any_reject": true },
511 "commitment": { "authority": "any_participant" }
512 });
513 let rules: ProposalPolicyRules = serde_json::from_value(json).unwrap();
514 assert_eq!(rules.acceptance.criterion, "counterparty");
515 assert_eq!(rules.counter_proposal.max_rounds, 3);
516 assert!(rules.rejection.terminal_on_any_reject);
517 assert_eq!(rules.commitment.authority, "any_participant");
518 }
519
520 #[test]
521 fn task_policy_rules_defaults() {
522 let rules = TaskPolicyRules::default();
523 assert!(!rules.assignment.allow_reassignment_on_reject);
524 assert!(!rules.completion.require_output);
525 assert_eq!(rules.commitment.authority, "initiator_only");
526 }
527
528 #[test]
529 fn task_policy_rules_deserialization() {
530 let json = serde_json::json!({
531 "assignment": { "allow_reassignment_on_reject": true },
532 "completion": { "require_output": true },
533 "commitment": { "authority": "initiator_only" }
534 });
535 let rules: TaskPolicyRules = serde_json::from_value(json).unwrap();
536 assert!(rules.assignment.allow_reassignment_on_reject);
537 assert!(rules.completion.require_output);
538 }
539
540 #[test]
541 fn handoff_policy_rules_defaults() {
542 let rules = HandoffPolicyRules::default();
543 assert_eq!(rules.acceptance.implicit_accept_timeout_ms, 0);
544 assert_eq!(rules.commitment.authority, "initiator_only");
545 }
546
547 #[test]
548 fn handoff_policy_rules_deserialization() {
549 let json = serde_json::json!({
550 "acceptance": { "implicit_accept_timeout_ms": 5000 },
551 "commitment": { "authority": "any_participant" }
552 });
553 let rules: HandoffPolicyRules = serde_json::from_value(json).unwrap();
554 assert_eq!(rules.acceptance.implicit_accept_timeout_ms, 5000);
555 assert_eq!(rules.commitment.authority, "any_participant");
556 }
557
558 #[test]
559 fn quorum_policy_rules_defaults() {
560 let rules = QuorumPolicyRules::default();
561 assert_eq!(rules.threshold.threshold_type, "n_of_m");
562 assert!((rules.threshold.value).abs() < f64::EPSILON);
563 assert!(!rules.abstention.counts_toward_quorum);
564 assert_eq!(rules.abstention.interpretation, "neutral");
565 assert_eq!(rules.commitment.authority, "initiator_only");
566 }
567
568 #[test]
569 fn effective_threshold_ceils_and_floors_at_one() {
570 let t = |kind: &str, value: f64| QuorumThreshold {
571 threshold_type: kind.into(),
572 value,
573 };
574 assert_eq!(
576 t("n_of_m", 0.5).effective(3),
577 EffectiveThreshold::Approvals(1)
578 );
579 assert_eq!(
580 t("count", 2.4).effective(3),
581 EffectiveThreshold::Approvals(3)
582 );
583 assert_eq!(
585 t("percentage", 50.0).effective(3),
586 EffectiveThreshold::Approvals(2)
587 );
588 assert_eq!(
590 t("percentage", 0.5).effective(3),
591 EffectiveThreshold::Approvals(1)
592 );
593 assert_eq!(t("n_of_m", 0.0).effective(3), EffectiveThreshold::Inert);
595 assert_eq!(t("n_of_m", -1.0).effective(3), EffectiveThreshold::Inert);
596 assert_eq!(
597 t("n_of_m", f64::NAN).effective(3),
598 EffectiveThreshold::Inert
599 );
600 assert_eq!(
603 t("weighted", 2.0).effective(3),
604 EffectiveThreshold::Unsatisfiable
605 );
606 assert_eq!(
607 t("two_thirds", 2.0).effective(3),
608 EffectiveThreshold::Unsatisfiable
609 );
610 assert_eq!(
611 t("percentage", 50.0).effective(0),
612 EffectiveThreshold::Unsatisfiable
613 );
614 assert_eq!(
616 t("n_of_m", 1e30).effective(3),
617 EffectiveThreshold::Approvals(u32::MAX)
618 );
619 assert_eq!(
620 t("percentage", 1e30).effective(3),
621 EffectiveThreshold::Approvals(u32::MAX)
622 );
623 }
624
625 #[test]
631 fn percentage_threshold_uses_exact_integer_ceiling_division() {
632 let pct = |value: f64| QuorumThreshold {
633 threshold_type: "percentage".into(),
634 value,
635 };
636 assert_eq!(pct(28.0).effective(25), EffectiveThreshold::Approvals(7));
639 assert_eq!(pct(14.0).effective(50), EffectiveThreshold::Approvals(7));
640 assert_eq!(pct(7.0).effective(100), EffectiveThreshold::Approvals(7));
641 assert_eq!(pct(68.0).effective(75), EffectiveThreshold::Approvals(51));
642
643 assert_eq!(pct(67.0).effective(3), EffectiveThreshold::Approvals(3));
646 assert_eq!(pct(51.0).effective(200), EffectiveThreshold::Approvals(102));
647 assert_eq!(pct(50.0).effective(3), EffectiveThreshold::Approvals(2));
648
649 assert_eq!(pct(50.0).effective(4), EffectiveThreshold::Approvals(2));
651 assert_eq!(pct(100.0).effective(7), EffectiveThreshold::Approvals(7));
652 assert_eq!(pct(25.0).effective(8), EffectiveThreshold::Approvals(2));
653
654 for value in 1..=100u128 {
660 for participants in 1..=500usize {
661 let product = value * participants as u128;
662 let expected = product / 100 + u128::from(!product.is_multiple_of(100));
663 assert_eq!(
664 pct(value as f64).effective(participants),
665 EffectiveThreshold::Approvals(expected as u32),
666 "value {value} over {participants} participants"
667 );
668 }
669 }
670 }
671
672 #[test]
673 fn quorum_policy_rules_deserialization() {
674 let json = serde_json::json!({
675 "threshold": { "type": "percentage", "value": 75.0 },
676 "abstention": { "counts_toward_quorum": true, "interpretation": "implicit_reject" },
677 "commitment": { "authority": "initiator_only" }
678 });
679 let rules: QuorumPolicyRules = serde_json::from_value(json).unwrap();
680 assert_eq!(rules.threshold.threshold_type, "percentage");
681 assert!((rules.threshold.value - 75.0).abs() < f64::EPSILON);
682 assert!(rules.abstention.counts_toward_quorum);
683 assert_eq!(rules.abstention.interpretation, "implicit_reject");
684 }
685}