Skip to main content

mempill_types/
proposal.rs

1//! Proposal types: stochastic proposer output and adjudication request/response.
2//!
3//! These types cross the stochastic/deterministic boundary. Proposals from extractors
4//! and oracles are always advisory — the deterministic engine core decides all
5//! dispositions and no stochastic output can commit directly.
6
7use crate::belief::Belief;
8use crate::claim::{Cardinality, Confidence, Criticality};
9use crate::identity::SubjectLineRef;
10use crate::provenance::ProvenanceLabel;
11use crate::time::ValidTime;
12use crate::claim::Fact;
13use crate::claim::Claim;
14
15/// Stochastic proposer output — never a commit.
16///
17/// The engine receives proposals from `ExtractorPort` and decides all dispositions
18/// deterministically. Proposals carry no authority to commit; they flow through the
19/// reconciler and adjudication gate before any write is made.
20#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
21pub struct ClaimProposal {
22    /// The (subject, predicate, value) triple being proposed.
23    pub fact: Fact,
24    /// Advisory valid-time window suggested by the extractor.
25    pub suggested_valid_time: Option<ValidTime>,
26    /// Advisory cardinality hint suggested by the extractor.
27    pub suggested_cardinality: Cardinality,
28    /// Confidence scores from the extraction model.
29    pub confidence: Confidence,
30    /// ADVISORY ONLY — engine enforces ModelDerived default and provenance immutability.
31    /// If None, gateway assigns ModelDerived (the mandatory default).
32    pub suggested_provenance: Option<ProvenanceLabel>,
33}
34
35/// Adjudication request sent to the `OraclePort` by the adjudication gate.
36#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
37pub struct AdjudicationRequest {
38    /// The subject-line (agent_id, subject, predicate) under adjudication.
39    pub subject_line: SubjectLineRef,
40    /// The currently live belief (the incumbent to be potentially superseded).
41    pub incumbent: Belief,
42    /// The incoming claim challenging the incumbent.
43    pub challenger: Claim,
44    /// Criticality class of the highest-importance claim involved.
45    pub criticality: Criticality,
46    /// Why adjudication was triggered.
47    pub reason: OverturnReason,
48}
49
50/// Why an adjudication was triggered.
51#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52#[non_exhaustive]
53pub enum OverturnReason {
54    /// An external claim directly contradicts the incumbent.
55    ExternalContradiction,
56    /// A validity-bound assertion marks the incumbent as no longer true.
57    ValidityBound,
58    /// A parent claim of the incumbent was superseded.
59    DependsOnSuperseded,
60    /// Derivation depth exceeds the configured threshold.
61    HighDerivationDepth,
62}
63
64/// Response delivered asynchronously back into the engine from the oracle.
65#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
66pub struct AdjudicationResponse {
67    /// Correlates this response back to the originating `AdjudicationRequest`.
68    pub handle_id: uuid::Uuid,
69    /// The oracle's verdict on the challenger vs incumbent dispute.
70    pub verdict: AdjudicationVerdict,
71    /// Provenance label that the oracle used to reach this verdict.
72    pub evidence_provenance: ProvenanceLabel,
73}
74
75/// The oracle's verdict on a challenged belief.
76///
77/// Delivered asynchronously via [`AdjudicationResponse`] after the oracle resolves the dispute.
78#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
79#[non_exhaustive]
80pub enum AdjudicationVerdict {
81    /// Challenger confirmed; incumbent bounded.
82    Affirm,
83    /// Incumbent affirmed; challenger goes Superseded.
84    Deny,
85    /// Ambiguous; surfaces Contested.
86    Unknown,
87}
88
89/// The resolved outcome of an adjudication, delivered asynchronously from the oracle loop.
90/// Carries the identity of the adjudication request (`handle_id`), the final disposition
91/// applied to the challenger claim, and the claim reference the outcome targets.
92#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
93pub struct AdjudicationOutcome {
94    /// Correlates this outcome back to the originating [`AdjudicationRequest`].
95    pub handle_id: uuid::Uuid,
96    /// The deterministic disposition the engine will apply to the challenger claim.
97    pub disposition: crate::disposition::Disposition,
98    /// The claim this outcome acts upon.
99    pub claim_ref: crate::identity::ClaimRef,
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::provenance::{ExternalKind, ProvenanceLabel};
106
107    #[test]
108    fn claim_proposal_carries_suggested_provenance() {
109        let p = ClaimProposal {
110            fact: Fact { subject: "s".into(), predicate: "p".into(), value: serde_json::json!(1) },
111            suggested_valid_time: None,
112            suggested_cardinality: Cardinality::Unknown,
113            confidence: Confidence { value_confidence: 0.8, valid_time_confidence: 0.0 },
114            suggested_provenance: Some(ProvenanceLabel::External(ExternalKind::UserAsserted)),
115        };
116        assert!(p.suggested_provenance.is_some());
117    }
118
119    #[test]
120    fn overture_reason_round_trip_serde() {
121        let reasons = [
122            OverturnReason::ExternalContradiction,
123            OverturnReason::ValidityBound,
124            OverturnReason::DependsOnSuperseded,
125            OverturnReason::HighDerivationDepth,
126        ];
127        for r in &reasons {
128            let json = serde_json::to_string(r).unwrap();
129            let back: OverturnReason = serde_json::from_str(&json).unwrap();
130            assert_eq!(r, &back);
131        }
132    }
133
134    #[test]
135    fn adjudication_verdict_round_trip_serde() {
136        let verdicts = [
137            AdjudicationVerdict::Affirm,
138            AdjudicationVerdict::Deny,
139            AdjudicationVerdict::Unknown,
140        ];
141        for v in &verdicts {
142            let json = serde_json::to_string(v).unwrap();
143            let back: AdjudicationVerdict = serde_json::from_str(&json).unwrap();
144            assert_eq!(v, &back);
145        }
146    }
147
148    #[test]
149    fn adjudication_outcome_round_trip_serde() {
150        use crate::disposition::Disposition;
151        use crate::identity::ClaimRef;
152
153        let outcome = AdjudicationOutcome {
154            handle_id: uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(),
155            disposition: Disposition::Superseded,
156            claim_ref: ClaimRef::new_random(),
157        };
158        let json = serde_json::to_string(&outcome).unwrap();
159        let back: AdjudicationOutcome = serde_json::from_str(&json).unwrap();
160        assert_eq!(back.handle_id, outcome.handle_id);
161        assert_eq!(back.disposition, outcome.disposition);
162        assert_eq!(back.claim_ref, outcome.claim_ref);
163    }
164
165    #[test]
166    fn adjudication_response_has_oracle_present_field_via_handle_id() {
167        // A24: oracle_present is on the internal Proposal (engine/gate.rs), not on
168        // AdjudicationResponse. The response carries the verdict; oracle_present is
169        // a gate input, not part of the async response payload.
170        let resp = AdjudicationResponse {
171            handle_id: uuid::Uuid::new_v4(),
172            verdict: AdjudicationVerdict::Affirm,
173            evidence_provenance: ProvenanceLabel::External(ExternalKind::ExternalFirstHand),
174        };
175        assert_eq!(resp.verdict, AdjudicationVerdict::Affirm);
176    }
177}