Skip to main content

organism_intent/
lib.rs

1//! Intent system.
2//!
3//! Translates human goals into structured, machine-executable specifications.
4//!
5//! - [`IntentPacket`] — typed contract between humans and the runtime
6//! - [`admission`] — feasibility gate before any planning begins
7//! - [`decomposition`] — breaks intents into governed intent trees
8
9pub mod admission;
10pub mod decomposition;
11pub mod resolution;
12
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use uuid::Uuid;
16
17// ── Intent Packet ──────────────────────────────────────────────────
18
19/// The contract between humans and the runtime.
20///
21/// Authority is *not* granted by the existence of an IntentPacket — it is
22/// recomputed at the Converge commit boundary. The `authority` field is a
23/// declaration of what the system is *permitted* to attempt, not proof that
24/// it is allowed to commit.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct IntentPacket {
27    pub id: Uuid,
28    pub outcome: String,
29    pub context: serde_json::Value,
30    pub constraints: Vec<String>,
31    pub authority: Vec<String>,
32    pub forbidden: Vec<ForbiddenAction>,
33    pub reversibility: Reversibility,
34    pub expires: DateTime<Utc>,
35    pub expiry_action: ExpiryAction,
36}
37
38impl IntentPacket {
39    pub fn new(outcome: impl Into<String>, expires: DateTime<Utc>) -> Self {
40        Self {
41            id: Uuid::new_v4(),
42            outcome: outcome.into(),
43            context: serde_json::Value::Null,
44            constraints: Vec::new(),
45            authority: Vec::new(),
46            forbidden: Vec::new(),
47            reversibility: Reversibility::Reversible,
48            expires,
49            expiry_action: ExpiryAction::Halt,
50        }
51    }
52
53    pub fn is_expired(&self, now: DateTime<Utc>) -> bool {
54        now >= self.expires
55    }
56
57    pub fn with_context(mut self, ctx: serde_json::Value) -> Self {
58        self.context = ctx;
59        self
60    }
61
62    pub fn with_authority(mut self, authority: Vec<String>) -> Self {
63        self.authority = authority;
64        self
65    }
66
67    pub fn with_reversibility(mut self, r: Reversibility) -> Self {
68        self.reversibility = r;
69        self
70    }
71
72    pub fn with_expiry_action(mut self, action: ExpiryAction) -> Self {
73        self.expiry_action = action;
74        self
75    }
76}
77
78// ── Reversibility ──────────────────────────────────────────────────
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum Reversibility {
83    Reversible,
84    Partial,
85    Irreversible,
86}
87
88// ── Forbidden Actions ──────────────────────────────────────────────
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ForbiddenAction {
92    pub action: String,
93    pub reason: String,
94}
95
96// ── Expiry ─────────────────────────────────────────────────────────
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum ExpiryAction {
101    Halt,
102    Escalate,
103    CompleteAndHalt,
104}
105
106// ── Admission Control ──────────────────────────────────────────────
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct AdmissionResult {
110    pub feasible: bool,
111    pub dimensions: Vec<FeasibilityAssessment>,
112    pub rejection_reason: Option<String>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct FeasibilityAssessment {
117    pub dimension: FeasibilityDimension,
118    pub kind: FeasibilityKind,
119    pub reason: String,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum FeasibilityDimension {
125    Capability,
126    Context,
127    Resources,
128    Authority,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub enum FeasibilityKind {
134    Feasible,
135    FeasibleWithConstraints,
136    Uncertain,
137    Infeasible,
138}
139
140pub trait AdmissionController: Send + Sync {
141    fn evaluate(&self, intent: &IntentPacket) -> AdmissionResult;
142}
143
144// ── Intent Decomposition ───────────────────────────────────────────
145
146/// A node in the intent decomposition tree. Authority can only narrow
147/// during decomposition, never expand.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct IntentNode {
150    pub id: Uuid,
151    pub intent: IntentPacket,
152    pub children: Vec<IntentNode>,
153}
154
155impl IntentNode {
156    pub fn leaf(intent: IntentPacket) -> Self {
157        Self {
158            id: Uuid::new_v4(),
159            intent,
160            children: Vec::new(),
161        }
162    }
163
164    pub fn is_leaf(&self) -> bool {
165        self.children.is_empty()
166    }
167}
168
169// ── Errors ─────────────────────────────────────────────────────────
170
171#[derive(Debug, thiserror::Error)]
172pub enum IntentError {
173    #[error("intent expired at {0}")]
174    Expired(DateTime<Utc>),
175    #[error("intent forbidden by rule: {0}")]
176    Forbidden(String),
177    #[error("intent infeasible: {0}")]
178    Infeasible(String),
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use chrono::Duration;
185
186    fn future() -> DateTime<Utc> {
187        Utc::now() + Duration::hours(1)
188    }
189
190    fn past() -> DateTime<Utc> {
191        Utc::now() - Duration::seconds(10)
192    }
193
194    #[test]
195    fn new_sets_defaults() {
196        let intent = IntentPacket::new("ship q3", future());
197        assert_eq!(intent.outcome, "ship q3");
198        assert_eq!(intent.context, serde_json::Value::Null);
199        assert!(intent.constraints.is_empty());
200        assert!(intent.authority.is_empty());
201        assert!(intent.forbidden.is_empty());
202        assert_eq!(intent.reversibility, Reversibility::Reversible);
203        assert_eq!(intent.expiry_action, ExpiryAction::Halt);
204    }
205
206    #[test]
207    fn new_generates_unique_ids() {
208        let a = IntentPacket::new("a", future());
209        let b = IntentPacket::new("b", future());
210        assert_ne!(a.id, b.id);
211    }
212
213    #[test]
214    fn is_expired_past() {
215        let intent = IntentPacket::new("late", past());
216        assert!(intent.is_expired(Utc::now()));
217    }
218
219    #[test]
220    fn is_expired_future() {
221        let intent = IntentPacket::new("on time", future());
222        assert!(!intent.is_expired(Utc::now()));
223    }
224
225    #[test]
226    fn is_expired_exact_boundary() {
227        let now = Utc::now();
228        let intent = IntentPacket::new("boundary", now);
229        assert!(intent.is_expired(now));
230    }
231
232    #[test]
233    fn with_context() {
234        let intent =
235            IntentPacket::new("ctx", future()).with_context(serde_json::json!({"key": "value"}));
236        assert_eq!(intent.context["key"], "value");
237    }
238
239    #[test]
240    fn with_authority() {
241        let intent = IntentPacket::new("auth", future())
242            .with_authority(vec!["admin".into(), "finance".into()]);
243        assert_eq!(intent.authority.len(), 2);
244        assert_eq!(intent.authority[0], "admin");
245    }
246
247    #[test]
248    fn with_reversibility() {
249        let intent =
250            IntentPacket::new("rev", future()).with_reversibility(Reversibility::Irreversible);
251        assert_eq!(intent.reversibility, Reversibility::Irreversible);
252    }
253
254    #[test]
255    fn with_expiry_action() {
256        let intent = IntentPacket::new("exp", future()).with_expiry_action(ExpiryAction::Escalate);
257        assert_eq!(intent.expiry_action, ExpiryAction::Escalate);
258    }
259
260    #[test]
261    fn builder_chain() {
262        let intent = IntentPacket::new("full", future())
263            .with_context(serde_json::json!(null))
264            .with_authority(vec![])
265            .with_reversibility(Reversibility::Partial)
266            .with_expiry_action(ExpiryAction::CompleteAndHalt);
267        assert_eq!(intent.reversibility, Reversibility::Partial);
268        assert_eq!(intent.expiry_action, ExpiryAction::CompleteAndHalt);
269    }
270
271    #[test]
272    fn serde_roundtrip() {
273        let intent = IntentPacket::new("roundtrip", future())
274            .with_context(serde_json::json!({"n": 42}))
275            .with_authority(vec!["ops".into()])
276            .with_reversibility(Reversibility::Partial)
277            .with_expiry_action(ExpiryAction::Escalate);
278
279        let json = serde_json::to_string(&intent).unwrap();
280        let back: IntentPacket = serde_json::from_str(&json).unwrap();
281        assert_eq!(back.id, intent.id);
282        assert_eq!(back.outcome, "roundtrip");
283        assert_eq!(back.context["n"], 42);
284        assert_eq!(back.authority, vec!["ops"]);
285        assert_eq!(back.reversibility, Reversibility::Partial);
286        assert_eq!(back.expiry_action, ExpiryAction::Escalate);
287    }
288
289    #[test]
290    fn serde_with_forbidden() {
291        let mut intent = IntentPacket::new("forbidden", future());
292        intent.forbidden.push(ForbiddenAction {
293            action: "delete_prod".into(),
294            reason: "destructive".into(),
295        });
296
297        let json = serde_json::to_string(&intent).unwrap();
298        let back: IntentPacket = serde_json::from_str(&json).unwrap();
299        assert_eq!(back.forbidden.len(), 1);
300        assert_eq!(back.forbidden[0].action, "delete_prod");
301    }
302
303    #[test]
304    fn reversibility_all_variants_serde() {
305        for v in [
306            Reversibility::Reversible,
307            Reversibility::Partial,
308            Reversibility::Irreversible,
309        ] {
310            let json = serde_json::to_string(&v).unwrap();
311            let back: Reversibility = serde_json::from_str(&json).unwrap();
312            assert_eq!(v, back);
313        }
314    }
315
316    #[test]
317    fn reversibility_snake_case() {
318        assert_eq!(
319            serde_json::to_string(&Reversibility::Reversible).unwrap(),
320            "\"reversible\""
321        );
322        assert_eq!(
323            serde_json::to_string(&Reversibility::Partial).unwrap(),
324            "\"partial\""
325        );
326        assert_eq!(
327            serde_json::to_string(&Reversibility::Irreversible).unwrap(),
328            "\"irreversible\""
329        );
330    }
331
332    #[test]
333    fn expiry_action_all_variants_serde() {
334        for v in [
335            ExpiryAction::Halt,
336            ExpiryAction::Escalate,
337            ExpiryAction::CompleteAndHalt,
338        ] {
339            let json = serde_json::to_string(&v).unwrap();
340            let back: ExpiryAction = serde_json::from_str(&json).unwrap();
341            assert_eq!(v, back);
342        }
343    }
344
345    #[test]
346    fn expiry_action_snake_case() {
347        assert_eq!(
348            serde_json::to_string(&ExpiryAction::Halt).unwrap(),
349            "\"halt\""
350        );
351        assert_eq!(
352            serde_json::to_string(&ExpiryAction::Escalate).unwrap(),
353            "\"escalate\""
354        );
355        assert_eq!(
356            serde_json::to_string(&ExpiryAction::CompleteAndHalt).unwrap(),
357            "\"complete_and_halt\""
358        );
359    }
360
361    #[test]
362    fn feasibility_dimension_all_variants_serde() {
363        for v in [
364            FeasibilityDimension::Capability,
365            FeasibilityDimension::Context,
366            FeasibilityDimension::Resources,
367            FeasibilityDimension::Authority,
368        ] {
369            let json = serde_json::to_string(&v).unwrap();
370            let back: FeasibilityDimension = serde_json::from_str(&json).unwrap();
371            assert_eq!(v, back);
372        }
373    }
374
375    #[test]
376    fn feasibility_kind_all_variants_serde() {
377        for v in [
378            FeasibilityKind::Feasible,
379            FeasibilityKind::FeasibleWithConstraints,
380            FeasibilityKind::Uncertain,
381            FeasibilityKind::Infeasible,
382        ] {
383            let json = serde_json::to_string(&v).unwrap();
384            let back: FeasibilityKind = serde_json::from_str(&json).unwrap();
385            assert_eq!(v, back);
386        }
387    }
388
389    #[test]
390    fn feasibility_kind_snake_case() {
391        assert_eq!(
392            serde_json::to_string(&FeasibilityKind::FeasibleWithConstraints).unwrap(),
393            "\"feasible_with_constraints\""
394        );
395    }
396
397    #[test]
398    fn admission_result_serde_roundtrip() {
399        let result = AdmissionResult {
400            feasible: false,
401            dimensions: vec![FeasibilityAssessment {
402                dimension: FeasibilityDimension::Authority,
403                kind: FeasibilityKind::Infeasible,
404                reason: "no authority".into(),
405            }],
406            rejection_reason: Some("not authorized".into()),
407        };
408        let json = serde_json::to_string(&result).unwrap();
409        let back: AdmissionResult = serde_json::from_str(&json).unwrap();
410        assert!(!back.feasible);
411        assert_eq!(back.dimensions.len(), 1);
412        assert_eq!(back.rejection_reason.as_deref(), Some("not authorized"));
413    }
414
415    #[test]
416    fn forbidden_action_serde_roundtrip() {
417        let fa = ForbiddenAction {
418            action: "fire_all".into(),
419            reason: "HR policy".into(),
420        };
421        let json = serde_json::to_string(&fa).unwrap();
422        let back: ForbiddenAction = serde_json::from_str(&json).unwrap();
423        assert_eq!(back.action, "fire_all");
424        assert_eq!(back.reason, "HR policy");
425    }
426
427    #[test]
428    fn intent_node_leaf_is_leaf() {
429        let node = IntentNode::leaf(IntentPacket::new("leaf", future()));
430        assert!(node.is_leaf());
431    }
432
433    #[test]
434    fn intent_node_with_children_not_leaf() {
435        let child = IntentNode::leaf(IntentPacket::new("child", future()));
436        let parent = IntentNode {
437            id: Uuid::new_v4(),
438            intent: IntentPacket::new("parent", future()),
439            children: vec![child],
440        };
441        assert!(!parent.is_leaf());
442    }
443
444    #[test]
445    fn intent_error_display() {
446        let err = IntentError::Forbidden("no access".into());
447        assert_eq!(err.to_string(), "intent forbidden by rule: no access");
448
449        let err = IntentError::Infeasible("not enough resources".into());
450        assert_eq!(err.to_string(), "intent infeasible: not enough resources");
451    }
452
453    #[test]
454    fn intent_error_expired_display() {
455        let t = Utc::now();
456        let err = IntentError::Expired(t);
457        assert!(err.to_string().starts_with("intent expired at "));
458    }
459
460    #[test]
461    fn intent_packet_accepts_string_and_str() {
462        let from_str = IntentPacket::new("literal", future());
463        let from_string = IntentPacket::new(String::from("owned"), future());
464        assert_eq!(from_str.outcome, "literal");
465        assert_eq!(from_string.outcome, "owned");
466    }
467
468    #[test]
469    fn intent_packet_empty_outcome() {
470        let intent = IntentPacket::new("", future());
471        assert_eq!(intent.outcome, "");
472    }
473
474    #[test]
475    fn intent_packet_with_constraints() {
476        let mut intent = IntentPacket::new("constrained", future());
477        intent.constraints = vec!["budget < 10k".into(), "no external vendors".into()];
478        let json = serde_json::to_string(&intent).unwrap();
479        let back: IntentPacket = serde_json::from_str(&json).unwrap();
480        assert_eq!(back.constraints.len(), 2);
481    }
482}