Skip to main content

macp_core/policy/
rules.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4// ── Decision Policy Rules ───────────────────────────────────────────
5
6#[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/// Quorum rules used inside Decision mode's `voting.quorum`.
50#[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    /// RFC-MACP-0012: objections with severity "critical" trigger veto logic.
74    #[serde(default, alias = "critical_severity_vetoes")]
75    pub critical_severity_vetoes: bool,
76    #[serde(default = "default_veto_threshold")]
77    pub veto_threshold: u32,
78}
79
80impl Default for ObjectionHandlingRules {
81    fn default() -> Self {
82        Self {
83            critical_severity_vetoes: false,
84            veto_threshold: default_veto_threshold(),
85        }
86    }
87}
88
89fn default_veto_threshold() -> u32 {
90    1
91}
92
93#[derive(Clone, Debug, Serialize, Deserialize)]
94pub struct EvaluationRules {
95    #[serde(default)]
96    pub required_before_voting: bool,
97    #[serde(default)]
98    pub minimum_confidence: f64,
99}
100
101impl Default for EvaluationRules {
102    fn default() -> Self {
103        Self {
104            required_before_voting: false,
105            minimum_confidence: 0.0,
106        }
107    }
108}
109
110// `CommitmentRules` is shared with the modes (which read it directly to
111// authorize commitments), so it lives in `macp-core`. Re-exported here so the
112// per-mode rule structs below and `crate::rules::CommitmentRules` keep
113// resolving.
114pub use super::CommitmentRules;
115
116// ── Proposal Policy Rules (RFC-MACP-0012 Section 4.3) ──────────────
117
118#[derive(Clone, Debug, Serialize, Deserialize, Default)]
119pub struct ProposalPolicyRules {
120    #[serde(default)]
121    pub acceptance: ProposalAcceptanceRules,
122    #[serde(default)]
123    pub counter_proposal: CounterProposalRules,
124    #[serde(default)]
125    pub rejection: RejectionRules,
126    #[serde(default)]
127    pub commitment: CommitmentRules,
128}
129
130#[derive(Clone, Debug, Serialize, Deserialize)]
131pub struct ProposalAcceptanceRules {
132    #[serde(default = "default_acceptance_criterion")]
133    pub criterion: String,
134}
135
136impl Default for ProposalAcceptanceRules {
137    fn default() -> Self {
138        Self {
139            criterion: default_acceptance_criterion(),
140        }
141    }
142}
143
144fn default_acceptance_criterion() -> String {
145    "all_parties".into()
146}
147
148#[derive(Clone, Debug, Default, Serialize, Deserialize)]
149pub struct CounterProposalRules {
150    #[serde(default)]
151    pub max_rounds: usize,
152}
153
154#[derive(Clone, Debug, Default, Serialize, Deserialize)]
155pub struct RejectionRules {
156    #[serde(default)]
157    pub terminal_on_any_reject: bool,
158}
159
160// ── Task Policy Rules (RFC-MACP-0012 Section 4.4) ──────────────────
161
162#[derive(Clone, Debug, Serialize, Deserialize, Default)]
163pub struct TaskPolicyRules {
164    #[serde(default)]
165    pub assignment: TaskAssignmentRules,
166    #[serde(default)]
167    pub completion: TaskCompletionRules,
168    #[serde(default)]
169    pub commitment: CommitmentRules,
170}
171
172#[derive(Clone, Debug, Default, Serialize, Deserialize)]
173pub struct TaskAssignmentRules {
174    #[serde(default)]
175    pub allow_reassignment_on_reject: bool,
176}
177
178#[derive(Clone, Debug, Default, Serialize, Deserialize)]
179pub struct TaskCompletionRules {
180    #[serde(default)]
181    pub require_output: bool,
182}
183
184// ── Handoff Policy Rules (RFC-MACP-0012 Section 4.5) ───────────────
185
186#[derive(Clone, Debug, Serialize, Deserialize, Default)]
187pub struct HandoffPolicyRules {
188    #[serde(default)]
189    pub acceptance: HandoffAcceptanceRules,
190    #[serde(default)]
191    pub commitment: CommitmentRules,
192}
193
194#[derive(Clone, Debug, Default, Serialize, Deserialize)]
195pub struct HandoffAcceptanceRules {
196    #[serde(default)]
197    pub implicit_accept_timeout_ms: u64,
198}
199
200// ── Quorum Policy Rules (RFC-MACP-0012 Section 4.2) ────────────────
201
202#[derive(Clone, Debug, Serialize, Deserialize, Default)]
203pub struct QuorumPolicyRules {
204    #[serde(default)]
205    pub threshold: QuorumThreshold,
206    #[serde(default)]
207    pub abstention: AbstentionRules,
208    #[serde(default)]
209    pub commitment: CommitmentRules,
210}
211
212/// Threshold rules for Quorum mode (distinct from `QuorumRules` used in Decision mode's `voting.quorum`).
213#[derive(Clone, Debug, Serialize, Deserialize)]
214pub struct QuorumThreshold {
215    #[serde(default = "default_threshold_type", rename = "type")]
216    pub threshold_type: String,
217    #[serde(default)]
218    pub value: f64,
219}
220
221impl Default for QuorumThreshold {
222    fn default() -> Self {
223        Self {
224            threshold_type: default_threshold_type(),
225            value: 0.0,
226        }
227    }
228}
229
230fn default_threshold_type() -> String {
231    "n_of_m".into()
232}
233
234#[derive(Clone, Debug, Serialize, Deserialize)]
235pub struct AbstentionRules {
236    #[serde(default)]
237    pub counts_toward_quorum: bool,
238    #[serde(default = "default_interpretation")]
239    pub interpretation: String,
240}
241
242impl Default for AbstentionRules {
243    fn default() -> Self {
244        Self {
245            counts_toward_quorum: false,
246            interpretation: default_interpretation(),
247        }
248    }
249}
250
251fn default_interpretation() -> String {
252    "neutral".into()
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn decision_policy_rules_defaults() {
261        let rules = DecisionPolicyRules::default();
262        assert_eq!(rules.voting.algorithm, "none");
263        assert!((rules.voting.threshold - 0.5).abs() < f64::EPSILON);
264        assert_eq!(rules.voting.quorum.quorum_type, "count");
265        assert!(!rules.objection_handling.critical_severity_vetoes);
266        assert_eq!(rules.objection_handling.veto_threshold, 1);
267        assert!(!rules.evaluation.required_before_voting);
268        assert!((rules.evaluation.minimum_confidence).abs() < f64::EPSILON);
269        assert_eq!(rules.commitment.authority, "initiator_only");
270        assert!(rules.commitment.designated_roles.is_empty());
271        assert!(!rules.commitment.require_vote_quorum);
272    }
273
274    #[test]
275    fn decision_policy_rules_deserialization() {
276        let json = serde_json::json!({
277            "voting": {
278                "algorithm": "majority",
279                "threshold": 0.6,
280                "quorum": { "type": "percentage", "value": 75.0 },
281                "weights": { "agent://fraud": 2.0, "agent://growth": 1.0 }
282            },
283            "objection_handling": {
284                "critical_severity_vetoes": true,
285                "veto_threshold": 2
286            },
287            "evaluation": {
288                "required_before_voting": true,
289                "minimum_confidence": 0.8
290            },
291            "commitment": {
292                "authority": "designated_role",
293                "designated_roles": ["agent://lead"],
294                "require_vote_quorum": true
295            }
296        });
297
298        let rules: DecisionPolicyRules = serde_json::from_value(json).unwrap();
299        assert_eq!(rules.voting.algorithm, "majority");
300        assert!((rules.voting.threshold - 0.6).abs() < f64::EPSILON);
301        assert_eq!(rules.voting.quorum.quorum_type, "percentage");
302        assert!((rules.voting.quorum.value - 75.0).abs() < f64::EPSILON);
303        assert_eq!(*rules.voting.weights.get("agent://fraud").unwrap(), 2.0);
304        assert!(rules.objection_handling.critical_severity_vetoes);
305        assert_eq!(rules.objection_handling.veto_threshold, 2);
306        assert!(rules.evaluation.required_before_voting);
307        assert!((rules.evaluation.minimum_confidence - 0.8).abs() < f64::EPSILON);
308        assert_eq!(rules.commitment.authority, "designated_role");
309        assert_eq!(rules.commitment.designated_roles, vec!["agent://lead"]);
310        assert!(rules.commitment.require_vote_quorum);
311    }
312
313    #[test]
314    fn partial_deserialization_fills_defaults() {
315        let json = serde_json::json!({
316            "voting": { "algorithm": "unanimous" }
317        });
318        let rules: DecisionPolicyRules = serde_json::from_value(json).unwrap();
319        assert_eq!(rules.voting.algorithm, "unanimous");
320        assert!((rules.voting.threshold - 0.5).abs() < f64::EPSILON);
321        assert!(!rules.objection_handling.critical_severity_vetoes);
322        assert_eq!(rules.objection_handling.veto_threshold, 1);
323    }
324
325    #[test]
326    fn proposal_policy_rules_defaults() {
327        let rules = ProposalPolicyRules::default();
328        assert_eq!(rules.acceptance.criterion, "all_parties");
329        assert_eq!(rules.counter_proposal.max_rounds, 0);
330        assert!(!rules.rejection.terminal_on_any_reject);
331        assert_eq!(rules.commitment.authority, "initiator_only");
332    }
333
334    #[test]
335    fn proposal_policy_rules_deserialization() {
336        let json = serde_json::json!({
337            "acceptance": { "criterion": "counterparty" },
338            "counter_proposal": { "max_rounds": 3 },
339            "rejection": { "terminal_on_any_reject": true },
340            "commitment": { "authority": "any_participant" }
341        });
342        let rules: ProposalPolicyRules = serde_json::from_value(json).unwrap();
343        assert_eq!(rules.acceptance.criterion, "counterparty");
344        assert_eq!(rules.counter_proposal.max_rounds, 3);
345        assert!(rules.rejection.terminal_on_any_reject);
346        assert_eq!(rules.commitment.authority, "any_participant");
347    }
348
349    #[test]
350    fn task_policy_rules_defaults() {
351        let rules = TaskPolicyRules::default();
352        assert!(!rules.assignment.allow_reassignment_on_reject);
353        assert!(!rules.completion.require_output);
354        assert_eq!(rules.commitment.authority, "initiator_only");
355    }
356
357    #[test]
358    fn task_policy_rules_deserialization() {
359        let json = serde_json::json!({
360            "assignment": { "allow_reassignment_on_reject": true },
361            "completion": { "require_output": true },
362            "commitment": { "authority": "initiator_only" }
363        });
364        let rules: TaskPolicyRules = serde_json::from_value(json).unwrap();
365        assert!(rules.assignment.allow_reassignment_on_reject);
366        assert!(rules.completion.require_output);
367    }
368
369    #[test]
370    fn handoff_policy_rules_defaults() {
371        let rules = HandoffPolicyRules::default();
372        assert_eq!(rules.acceptance.implicit_accept_timeout_ms, 0);
373        assert_eq!(rules.commitment.authority, "initiator_only");
374    }
375
376    #[test]
377    fn handoff_policy_rules_deserialization() {
378        let json = serde_json::json!({
379            "acceptance": { "implicit_accept_timeout_ms": 5000 },
380            "commitment": { "authority": "any_participant" }
381        });
382        let rules: HandoffPolicyRules = serde_json::from_value(json).unwrap();
383        assert_eq!(rules.acceptance.implicit_accept_timeout_ms, 5000);
384        assert_eq!(rules.commitment.authority, "any_participant");
385    }
386
387    #[test]
388    fn quorum_policy_rules_defaults() {
389        let rules = QuorumPolicyRules::default();
390        assert_eq!(rules.threshold.threshold_type, "n_of_m");
391        assert!((rules.threshold.value).abs() < f64::EPSILON);
392        assert!(!rules.abstention.counts_toward_quorum);
393        assert_eq!(rules.abstention.interpretation, "neutral");
394        assert_eq!(rules.commitment.authority, "initiator_only");
395    }
396
397    #[test]
398    fn quorum_policy_rules_deserialization() {
399        let json = serde_json::json!({
400            "threshold": { "type": "percentage", "value": 75.0 },
401            "abstention": { "counts_toward_quorum": true, "interpretation": "implicit_reject" },
402            "commitment": { "authority": "initiator_only" }
403        });
404        let rules: QuorumPolicyRules = serde_json::from_value(json).unwrap();
405        assert_eq!(rules.threshold.threshold_type, "percentage");
406        assert!((rules.threshold.value - 75.0).abs() < f64::EPSILON);
407        assert!(rules.abstention.counts_toward_quorum);
408        assert_eq!(rules.abstention.interpretation, "implicit_reject");
409    }
410}