Skip to main content

organism_adversarial/
lib.rs

1//! Adversarial vocabulary and agents.
2//!
3//! Types for institutionalized disagreement: challenges, findings, signals.
4//! Adversarial agents are Suggestors — they participate in the convergence
5//! loop alongside planners, simulators, and policy gates.
6//!
7//! The debate cycle is natural convergence: planning proposes → adversaries
8//! challenge (via `ContextKey::Constraints`) → planning revises → repeat.
9//! Converge's fixed-point detection handles termination.
10
11pub mod agents;
12pub mod types;
13
14use serde::{Deserialize, Serialize};
15use uuid::Uuid;
16
17pub use agents::{
18    AssumptionBreakerAgent, ConstraintCheck, ConstraintCheckerAgent, EconomicSkepticAgent,
19    OperationalSkepticAgent, OrgConstraint,
20};
21pub use types::{AdversarialVerdict, AgentId, Complexity};
22
23// ── Challenge ──────────────────────────────────────────────────────
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Challenge {
27    pub id: Uuid,
28    pub kind: SkepticismKind,
29    pub target_plan: Uuid,
30    pub description: String,
31    pub severity: Severity,
32    pub evidence: Vec<String>,
33    pub suggestion: Option<String>,
34}
35
36impl Challenge {
37    pub fn new(
38        kind: SkepticismKind,
39        target_plan: Uuid,
40        description: impl Into<String>,
41        severity: Severity,
42    ) -> Self {
43        Self {
44            id: Uuid::new_v4(),
45            kind,
46            target_plan,
47            description: description.into(),
48            severity,
49            evidence: Vec::new(),
50            suggestion: None,
51        }
52    }
53
54    pub fn is_blocking(&self) -> bool {
55        self.severity == Severity::Blocker
56    }
57}
58
59// ── Skepticism Taxonomy ────────────────────────────────────────────
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63pub enum SkepticismKind {
64    AssumptionBreaking,
65    ConstraintChecking,
66    CausalSkepticism,
67    EconomicSkepticism,
68    OperationalSkepticism,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum Severity {
74    Advisory,
75    Warning,
76    Blocker,
77}
78
79// ── Finding (simplified challenge for reporting) ───────────────────
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct Finding {
83    pub agent: String,
84    pub severity: Severity,
85    pub message: String,
86}
87
88// ── Adversarial Signal (training data for learning system) ─────────
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct AdversarialSignal {
92    pub kind: SkepticismKind,
93    pub failed_assumption: String,
94    pub context: serde_json::Value,
95    pub revision_summary: Option<String>,
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use proptest::prelude::*;
102
103    fn plan_id() -> Uuid {
104        Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap()
105    }
106
107    #[test]
108    fn challenge_new_sets_defaults() {
109        let c = Challenge::new(
110            SkepticismKind::CausalSkepticism,
111            plan_id(),
112            "bad assumption",
113            Severity::Warning,
114        );
115        assert_eq!(c.kind, SkepticismKind::CausalSkepticism);
116        assert_eq!(c.target_plan, plan_id());
117        assert_eq!(c.description, "bad assumption");
118        assert_eq!(c.severity, Severity::Warning);
119        assert!(c.evidence.is_empty());
120        assert!(c.suggestion.is_none());
121    }
122
123    #[test]
124    fn challenge_new_generates_unique_ids() {
125        let a = Challenge::new(
126            SkepticismKind::AssumptionBreaking,
127            plan_id(),
128            "",
129            Severity::Advisory,
130        );
131        let b = Challenge::new(
132            SkepticismKind::AssumptionBreaking,
133            plan_id(),
134            "",
135            Severity::Advisory,
136        );
137        assert_ne!(a.id, b.id);
138    }
139
140    #[test]
141    fn is_blocking_only_for_blocker() {
142        let blocker = Challenge::new(
143            SkepticismKind::ConstraintChecking,
144            plan_id(),
145            "stop",
146            Severity::Blocker,
147        );
148        let warning = Challenge::new(
149            SkepticismKind::ConstraintChecking,
150            plan_id(),
151            "maybe",
152            Severity::Warning,
153        );
154        let advisory = Challenge::new(
155            SkepticismKind::ConstraintChecking,
156            plan_id(),
157            "fyi",
158            Severity::Advisory,
159        );
160        assert!(blocker.is_blocking());
161        assert!(!warning.is_blocking());
162        assert!(!advisory.is_blocking());
163    }
164
165    #[test]
166    fn challenge_new_accepts_string_and_str() {
167        let from_str = Challenge::new(
168            SkepticismKind::EconomicSkepticism,
169            plan_id(),
170            "lit",
171            Severity::Advisory,
172        );
173        let from_string = Challenge::new(
174            SkepticismKind::EconomicSkepticism,
175            plan_id(),
176            String::from("owned"),
177            Severity::Advisory,
178        );
179        assert_eq!(from_str.description, "lit");
180        assert_eq!(from_string.description, "owned");
181    }
182
183    #[test]
184    fn challenge_new_empty_description() {
185        let c = Challenge::new(
186            SkepticismKind::OperationalSkepticism,
187            plan_id(),
188            "",
189            Severity::Advisory,
190        );
191        assert_eq!(c.description, "");
192    }
193
194    #[test]
195    fn skepticism_kind_all_variants_distinct() {
196        let variants = [
197            SkepticismKind::AssumptionBreaking,
198            SkepticismKind::ConstraintChecking,
199            SkepticismKind::CausalSkepticism,
200            SkepticismKind::EconomicSkepticism,
201            SkepticismKind::OperationalSkepticism,
202        ];
203        for (i, a) in variants.iter().enumerate() {
204            for (j, b) in variants.iter().enumerate() {
205                assert_eq!(i == j, a == b);
206            }
207        }
208    }
209
210    #[test]
211    fn severity_all_variants_distinct() {
212        let variants = [Severity::Advisory, Severity::Warning, Severity::Blocker];
213        for (i, a) in variants.iter().enumerate() {
214            for (j, b) in variants.iter().enumerate() {
215                assert_eq!(i == j, a == b);
216            }
217        }
218    }
219
220    #[test]
221    fn challenge_serde_roundtrip() {
222        let mut c = Challenge::new(
223            SkepticismKind::EconomicSkepticism,
224            plan_id(),
225            "too expensive",
226            Severity::Blocker,
227        );
228        c.evidence = vec!["cost +40%".into()];
229        c.suggestion = Some("reduce scope".into());
230
231        let json = serde_json::to_string(&c).unwrap();
232        let back: Challenge = serde_json::from_str(&json).unwrap();
233        assert_eq!(back.id, c.id);
234        assert_eq!(back.kind, c.kind);
235        assert_eq!(back.description, c.description);
236        assert_eq!(back.severity, c.severity);
237        assert_eq!(back.evidence, c.evidence);
238        assert_eq!(back.suggestion, c.suggestion);
239    }
240
241    #[test]
242    fn finding_serde_roundtrip() {
243        let f = Finding {
244            agent: "economic-skeptic".into(),
245            severity: Severity::Warning,
246            message: "budget overrun".into(),
247        };
248        let json = serde_json::to_string(&f).unwrap();
249        let back: Finding = serde_json::from_str(&json).unwrap();
250        assert_eq!(back.agent, f.agent);
251        assert_eq!(back.message, f.message);
252    }
253
254    #[test]
255    fn adversarial_signal_serde_roundtrip() {
256        let s = AdversarialSignal {
257            kind: SkepticismKind::CausalSkepticism,
258            failed_assumption: "X causes Y".into(),
259            context: serde_json::json!({"key": "value"}),
260            revision_summary: Some("added control".into()),
261        };
262        let json = serde_json::to_string(&s).unwrap();
263        let back: AdversarialSignal = serde_json::from_str(&json).unwrap();
264        assert_eq!(back.kind, s.kind);
265        assert_eq!(back.failed_assumption, s.failed_assumption);
266        assert_eq!(back.context, s.context);
267        assert_eq!(back.revision_summary, s.revision_summary);
268    }
269
270    #[test]
271    fn adversarial_signal_none_revision() {
272        let s = AdversarialSignal {
273            kind: SkepticismKind::AssumptionBreaking,
274            failed_assumption: "assumption".into(),
275            context: serde_json::json!(null),
276            revision_summary: None,
277        };
278        let json = serde_json::to_string(&s).unwrap();
279        let back: AdversarialSignal = serde_json::from_str(&json).unwrap();
280        assert!(back.revision_summary.is_none());
281    }
282
283    #[test]
284    fn skepticism_kind_serde_snake_case() {
285        let json = serde_json::to_string(&SkepticismKind::AssumptionBreaking).unwrap();
286        assert_eq!(json, "\"assumption_breaking\"");
287        let json = serde_json::to_string(&SkepticismKind::CausalSkepticism).unwrap();
288        assert_eq!(json, "\"causal_skepticism\"");
289    }
290
291    #[test]
292    fn severity_serde_snake_case() {
293        let json = serde_json::to_string(&Severity::Blocker).unwrap();
294        assert_eq!(json, "\"blocker\"");
295        let json = serde_json::to_string(&Severity::Advisory).unwrap();
296        assert_eq!(json, "\"advisory\"");
297    }
298
299    proptest! {
300        #[test]
301        fn challenge_never_panics_on_arbitrary_description(desc in ".*") {
302            let c = Challenge::new(
303                SkepticismKind::OperationalSkepticism,
304                plan_id(),
305                desc.clone(),
306                Severity::Advisory,
307            );
308            prop_assert_eq!(c.description, desc);
309        }
310
311        #[test]
312        fn challenge_blocking_iff_blocker(sev in prop_oneof![
313            Just(Severity::Advisory),
314            Just(Severity::Warning),
315            Just(Severity::Blocker),
316        ]) {
317            let c = Challenge::new(SkepticismKind::AssumptionBreaking, plan_id(), "x", sev);
318            prop_assert_eq!(c.is_blocking(), sev == Severity::Blocker);
319        }
320    }
321}