Skip to main content

cortex_core/
claim_language.rs

1//! Shared AXIOM/Cortex claim language.
2//!
3//! AXIOM work records are useful because they separate criteria, hypotheses,
4//! capabilities, evidence, and residual risk. Cortex treats those records as
5//! structured candidate evidence, never as product authority. This module
6//! provides the common vocabulary both sides can serialize without allowing
7//! AXIOM formatting to upgrade truth or execution authority.
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12use crate::claims::{ClaimCeiling, ClaimProofState};
13
14/// AXIOM element kind mapped into Cortex candidate evidence.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
16#[serde(rename_all = "snake_case")]
17pub enum AxiomElementKind {
18    /// Completion criterion, e.g. `CHK[1]`.
19    CompletionCriterion,
20    /// Anti-criterion or forbidden outcome.
21    AntiCriterion,
22    /// Hypothesis, e.g. `HYP[1]`.
23    Hypothesis,
24    /// Capability invocation, e.g. `CAP[1]`.
25    Capability,
26    /// Evidence entry.
27    Evidence,
28    /// Decision record.
29    Decision,
30    /// Residual risk.
31    ResidualRisk,
32}
33
34/// Cortex-side role assigned to an AXIOM element.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
36#[serde(rename_all = "snake_case")]
37pub enum CortexClaimRole {
38    /// Candidate claim only; requires normal Cortex admission/promotion gates.
39    CandidateClaim,
40    /// Proof/evidence reference supporting another candidate.
41    EvidenceReference,
42    /// Capability provenance reference.
43    CapabilityReference,
44    /// Constraint that narrows what an agent may claim or do next.
45    Constraint,
46    /// Residual risk retained for review.
47    ResidualRisk,
48}
49
50/// AXIOM evidence type.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
52#[serde(rename_all = "snake_case")]
53pub enum AxiomEvidenceKind {
54    /// Directly inspected, executed, read, measured, or verified by the agent.
55    Observed,
56    /// Derived from named evidence.
57    Inferred,
58    /// Asserted by a source, vendor, user, or model but not independently verified.
59    Claimed,
60    /// Hypothetical, dry-run, estimated, or modelled.
61    Simulated,
62    /// Missing or unclassified evidence kind.
63    Unknown,
64}
65
66impl AxiomEvidenceKind {
67    /// Maximum Cortex claim ceiling supported by this AXIOM evidence class.
68    ///
69    /// Even AXIOM `observed` evidence is still agent-procedure evidence. It may
70    /// support local candidate reasoning, but never signed or authority-grade
71    /// Cortex claims by itself.
72    #[must_use]
73    pub const fn claim_ceiling(self) -> ClaimCeiling {
74        match self {
75            Self::Observed | Self::Inferred => ClaimCeiling::LocalUnsigned,
76            Self::Claimed | Self::Simulated | Self::Unknown => ClaimCeiling::DevOnly,
77        }
78    }
79}
80
81/// AXIOM status after CHECK.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
83#[serde(rename_all = "snake_case")]
84pub enum AxiomStatus {
85    /// Criterion passed or hypothesis supported.
86    Supported,
87    /// Criterion/hypothesis is partially supported.
88    Partial,
89    /// Criterion failed or hypothesis was rejected.
90    Rejected,
91    /// State is unknown or unresolved.
92    Unknown,
93    /// Capability was invoked.
94    Invoked,
95    /// Capability was explicitly not used.
96    NotInvoked,
97}
98
99impl AxiomStatus {
100    /// Proof state implied by the AXIOM status and source-anchor presence.
101    #[must_use]
102    pub const fn proof_state(self, has_evidence_refs: bool) -> ClaimProofState {
103        match self {
104            Self::Supported | Self::Invoked if has_evidence_refs => ClaimProofState::Partial,
105            Self::Supported | Self::Invoked | Self::Partial => ClaimProofState::Unknown,
106            Self::Rejected => ClaimProofState::Broken,
107            Self::Unknown | Self::NotInvoked => ClaimProofState::Unknown,
108        }
109    }
110}
111
112/// A candidate AXIOM element before Cortex maps it into shared claim language.
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
114pub struct AxiomClaimInput {
115    /// Stable AXIOM label such as `CHK[1]`, `HYP[2]`, or `CAP[1]`.
116    pub axiom_label: String,
117    /// AXIOM element kind.
118    pub axiom_kind: AxiomElementKind,
119    /// Human-readable statement.
120    pub statement: String,
121    /// CHECK-time status.
122    pub status: AxiomStatus,
123    /// Evidence kind declared by AXIOM.
124    pub evidence_kind: AxiomEvidenceKind,
125    /// Concrete source anchors or evidence refs.
126    pub evidence_refs: Vec<String>,
127    /// Capability/tool invocation refs, when applicable.
128    pub capability_refs: Vec<String>,
129    /// Residual risks attached to the element.
130    pub residual_risks: Vec<String>,
131    /// Highest Cortex claim level requested by the producer.
132    pub requested_ceiling: ClaimCeiling,
133}
134
135impl AxiomClaimInput {
136    /// Construct an AXIOM claim input.
137    #[must_use]
138    pub fn new(
139        axiom_label: impl Into<String>,
140        axiom_kind: AxiomElementKind,
141        statement: impl Into<String>,
142        status: AxiomStatus,
143        evidence_kind: AxiomEvidenceKind,
144    ) -> Self {
145        Self {
146            axiom_label: axiom_label.into(),
147            axiom_kind,
148            statement: statement.into(),
149            status,
150            evidence_kind,
151            evidence_refs: Vec::new(),
152            capability_refs: Vec::new(),
153            residual_risks: Vec::new(),
154            requested_ceiling: ClaimCeiling::LocalUnsigned,
155        }
156    }
157
158    /// Attach source/evidence refs.
159    #[must_use]
160    pub fn with_evidence_refs<I, S>(mut self, refs: I) -> Self
161    where
162        I: IntoIterator<Item = S>,
163        S: Into<String>,
164    {
165        self.evidence_refs = refs.into_iter().map(Into::into).collect();
166        self
167    }
168
169    /// Attach capability refs.
170    #[must_use]
171    pub fn with_capability_refs<I, S>(mut self, refs: I) -> Self
172    where
173        I: IntoIterator<Item = S>,
174        S: Into<String>,
175    {
176        self.capability_refs = refs.into_iter().map(Into::into).collect();
177        self
178    }
179
180    /// Attach residual risks.
181    #[must_use]
182    pub fn with_residual_risks<I, S>(mut self, risks: I) -> Self
183    where
184        I: IntoIterator<Item = S>,
185        S: Into<String>,
186    {
187        self.residual_risks = risks.into_iter().map(Into::into).collect();
188        self
189    }
190
191    /// Set the requested ceiling.
192    #[must_use]
193    pub const fn with_requested_ceiling(mut self, ceiling: ClaimCeiling) -> Self {
194        self.requested_ceiling = ceiling;
195        self
196    }
197}
198
199/// Constraint severity exported from Cortex to AXIOM.
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
201#[serde(rename_all = "snake_case")]
202pub enum AxiomConstraintSeverity {
203    /// Informational; narrows interpretation but does not block work.
204    Advisory,
205    /// Limits claim scope.
206    Limit,
207    /// Hard prohibition.
208    Hard,
209}
210
211/// Constraint category exported from Cortex to AXIOM.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
213#[serde(rename_all = "snake_case")]
214pub enum AxiomConstraintKind {
215    /// Content may be admitted only as a Cortex candidate.
216    CandidateOnly,
217    /// AXIOM must not emit promotion-shaped output.
218    ForbidPromotionShapedOutput,
219    /// A concrete source anchor is required.
220    RequireSourceAnchor,
221    /// Capability/tool provenance is required.
222    RequireToolProvenance,
223    /// Truth ceiling caps claim language.
224    TruthCeiling,
225    /// Proof state is unknown, partial, or broken.
226    ProofStateLimit,
227    /// Open conflict or contradiction exists.
228    ConflictPresent,
229    /// Redaction policy limits what AXIOM may see or repeat.
230    RedactionBoundary,
231    /// Low trust prevents stronger claims.
232    LowTrust,
233    /// Exported constraints do not grant execution authority.
234    NoExecutionAuthority,
235}
236
237/// Cortex constraint exported to AXIOM.
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
239pub struct AxiomConstraint {
240    /// Constraint kind.
241    pub kind: AxiomConstraintKind,
242    /// Constraint severity.
243    pub severity: AxiomConstraintSeverity,
244    /// Operator-facing message.
245    pub message: String,
246    /// Related references.
247    pub refs: Vec<String>,
248}
249
250impl AxiomConstraint {
251    /// Construct a constraint.
252    #[must_use]
253    pub fn new(
254        kind: AxiomConstraintKind,
255        severity: AxiomConstraintSeverity,
256        message: impl Into<String>,
257    ) -> Self {
258        Self {
259            kind,
260            severity,
261            message: message.into(),
262            refs: Vec::new(),
263        }
264    }
265
266    /// Attach refs to the constraint.
267    #[must_use]
268    pub fn with_refs<I, S>(mut self, refs: I) -> Self
269    where
270        I: IntoIterator<Item = S>,
271        S: Into<String>,
272    {
273        self.refs = refs.into_iter().map(Into::into).collect();
274        self
275    }
276}
277
278/// Cortex representation of one AXIOM element.
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
280pub struct CortexClaimMapping {
281    /// Original AXIOM label.
282    pub axiom_label: String,
283    /// Original AXIOM element kind.
284    pub axiom_kind: AxiomElementKind,
285    /// Cortex role assigned to the element.
286    pub cortex_role: CortexClaimRole,
287    /// Statement retained as candidate evidence.
288    pub statement: String,
289    /// Declared AXIOM evidence kind.
290    pub evidence_kind: AxiomEvidenceKind,
291    /// AXIOM status.
292    pub status: AxiomStatus,
293    /// Evidence refs retained for Cortex admission/review.
294    pub evidence_refs: Vec<String>,
295    /// Capability refs retained for Cortex admission/review.
296    pub capability_refs: Vec<String>,
297    /// Residual risks retained for review.
298    pub residual_risks: Vec<String>,
299    /// Proof state implied by AXIOM status and refs.
300    pub proof_state: ClaimProofState,
301    /// Effective claim ceiling after AXIOM evidence and proof-state caps.
302    pub claim_ceiling: ClaimCeiling,
303    /// Constraints generated by the mapping.
304    pub constraints: Vec<AxiomConstraint>,
305}
306
307impl CortexClaimMapping {
308    /// Map one AXIOM element into Cortex shared claim language.
309    #[must_use]
310    pub fn from_axiom(input: AxiomClaimInput) -> Self {
311        let cortex_role = cortex_role_for(input.axiom_kind);
312        let proof_state = input.status.proof_state(!input.evidence_refs.is_empty());
313        let claim_ceiling = ClaimCeiling::weakest([
314            input.requested_ceiling,
315            input.evidence_kind.claim_ceiling(),
316            proof_state.claim_ceiling(),
317        ])
318        .expect("fixed-size ceiling set is non-empty");
319
320        let mut constraints = Vec::new();
321        constraints.push(AxiomConstraint::new(
322            AxiomConstraintKind::CandidateOnly,
323            AxiomConstraintSeverity::Hard,
324            "AXIOM output is candidate evidence only; Cortex promotion gates still apply",
325        ));
326
327        if input.evidence_refs.is_empty()
328            && matches!(
329                input.axiom_kind,
330                AxiomElementKind::CompletionCriterion
331                    | AxiomElementKind::Hypothesis
332                    | AxiomElementKind::Evidence
333                    | AxiomElementKind::Decision
334            )
335        {
336            constraints.push(AxiomConstraint::new(
337                AxiomConstraintKind::RequireSourceAnchor,
338                AxiomConstraintSeverity::Hard,
339                "substantive AXIOM claims require Cortex-readable source anchors",
340            ));
341        }
342
343        if input.axiom_kind == AxiomElementKind::Capability && input.capability_refs.is_empty() {
344            constraints.push(AxiomConstraint::new(
345                AxiomConstraintKind::RequireToolProvenance,
346                AxiomConstraintSeverity::Hard,
347                "AXIOM capability claims require invocation provenance",
348            ));
349        }
350
351        if proof_state != ClaimProofState::FullChainVerified {
352            constraints.push(AxiomConstraint::new(
353                AxiomConstraintKind::ProofStateLimit,
354                AxiomConstraintSeverity::Limit,
355                format!("proof state {proof_state:?} limits Cortex authority"),
356            ));
357        }
358
359        if claim_ceiling < input.requested_ceiling {
360            let requested_ceiling = input.requested_ceiling;
361            constraints.push(AxiomConstraint::new(
362                AxiomConstraintKind::TruthCeiling,
363                AxiomConstraintSeverity::Limit,
364                format!("requested ceiling {requested_ceiling:?} downgraded to {claim_ceiling:?}"),
365            ));
366        }
367
368        Self {
369            axiom_label: input.axiom_label,
370            axiom_kind: input.axiom_kind,
371            cortex_role,
372            statement: input.statement,
373            evidence_kind: input.evidence_kind,
374            status: input.status,
375            evidence_refs: input.evidence_refs,
376            capability_refs: input.capability_refs,
377            residual_risks: input.residual_risks,
378            proof_state,
379            claim_ceiling,
380            constraints,
381        }
382    }
383}
384
385/// Map a batch of AXIOM elements into Cortex shared claim language.
386#[must_use]
387pub fn map_axiom_claims<I>(inputs: I) -> Vec<CortexClaimMapping>
388where
389    I: IntoIterator<Item = AxiomClaimInput>,
390{
391    inputs
392        .into_iter()
393        .map(CortexClaimMapping::from_axiom)
394        .collect()
395}
396
397fn cortex_role_for(kind: AxiomElementKind) -> CortexClaimRole {
398    match kind {
399        AxiomElementKind::CompletionCriterion
400        | AxiomElementKind::AntiCriterion
401        | AxiomElementKind::Hypothesis
402        | AxiomElementKind::Decision => CortexClaimRole::CandidateClaim,
403        AxiomElementKind::Capability => CortexClaimRole::CapabilityReference,
404        AxiomElementKind::Evidence => CortexClaimRole::EvidenceReference,
405        AxiomElementKind::ResidualRisk => CortexClaimRole::ResidualRisk,
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    #[test]
414    fn axiom_observed_claim_maps_to_candidate_not_authority() {
415        let mapping = CortexClaimMapping::from_axiom(
416            AxiomClaimInput::new(
417                "CHK[1]",
418                AxiomElementKind::CompletionCriterion,
419                "workspace tests passed",
420                AxiomStatus::Supported,
421                AxiomEvidenceKind::Observed,
422            )
423            .with_evidence_refs(["cmd:cargo test --workspace"])
424            .with_requested_ceiling(ClaimCeiling::AuthorityGrade),
425        );
426
427        assert_eq!(mapping.cortex_role, CortexClaimRole::CandidateClaim);
428        assert_eq!(mapping.proof_state, ClaimProofState::Partial);
429        assert_eq!(mapping.claim_ceiling, ClaimCeiling::LocalUnsigned);
430        assert!(mapping
431            .constraints
432            .iter()
433            .any(|constraint| constraint.kind == AxiomConstraintKind::CandidateOnly));
434    }
435
436    #[test]
437    fn claimed_or_simulated_axiom_evidence_fails_down_to_dev_only() {
438        let mapping = CortexClaimMapping::from_axiom(
439            AxiomClaimInput::new(
440                "HYP[2]",
441                AxiomElementKind::Hypothesis,
442                "the system is production ready",
443                AxiomStatus::Supported,
444                AxiomEvidenceKind::Claimed,
445            )
446            .with_evidence_refs(["agent:final-answer"])
447            .with_requested_ceiling(ClaimCeiling::AuthorityGrade),
448        );
449
450        assert_eq!(mapping.claim_ceiling, ClaimCeiling::DevOnly);
451        assert!(mapping
452            .constraints
453            .iter()
454            .any(|constraint| constraint.kind == AxiomConstraintKind::TruthCeiling));
455    }
456
457    #[test]
458    fn capability_claim_without_provenance_requires_tool_reference() {
459        let mapping = CortexClaimMapping::from_axiom(AxiomClaimInput::new(
460            "CAP[1]",
461            AxiomElementKind::Capability,
462            "tests invoked",
463            AxiomStatus::Invoked,
464            AxiomEvidenceKind::Observed,
465        ));
466
467        assert_eq!(mapping.cortex_role, CortexClaimRole::CapabilityReference);
468        assert!(mapping.constraints.iter().any(|constraint| {
469            constraint.kind == AxiomConstraintKind::RequireToolProvenance
470                && constraint.severity == AxiomConstraintSeverity::Hard
471        }));
472    }
473
474    #[test]
475    fn shared_claim_language_wire_shape_is_stable() {
476        let mapping = CortexClaimMapping::from_axiom(
477            AxiomClaimInput::new(
478                "EVIDENCE[1]",
479                AxiomElementKind::Evidence,
480                "read docs/LANES.md",
481                AxiomStatus::Supported,
482                AxiomEvidenceKind::Observed,
483            )
484            .with_evidence_refs(["file:docs/LANES.md"]),
485        );
486
487        let value = serde_json::to_value(&mapping).expect("serialize mapping");
488        assert_eq!(value["axiom_label"], serde_json::json!("EVIDENCE[1]"));
489        assert_eq!(value["axiom_kind"], serde_json::json!("evidence"));
490        assert_eq!(
491            value["cortex_role"],
492            serde_json::json!("evidence_reference")
493        );
494        assert_eq!(value["claim_ceiling"], serde_json::json!("local_unsigned"));
495    }
496}