normcore 0.1.1

Rust implementation baseline for NormCore normative admissibility evaluator
Documentation
use crate::normative::models::AxiomCheckResult;
use crate::normative::models::EvaluationStatus;
use crate::normative::models::GroundSet;
use crate::normative::models::License;
use crate::normative::models::Modality;
use crate::normative::models::Statement;

pub struct AxiomChecker;

impl AxiomChecker {
    pub fn check(
        &self,
        statement: &Statement,
        license: &License,
        ground_set: &GroundSet,
        _task_goal: &str,
    ) -> AxiomCheckResult {
        if statement.modality == Some(Modality::Refusal) {
            return AxiomCheckResult {
                status: EvaluationStatus::Acceptable,
                violated_axiom: None,
                explanation: "Explicit refusal is always admissible (A6)".to_string(),
            };
        }

        if statement.modality == Some(Modality::Assertive) && !license.permits(Modality::Assertive)
        {
            return AxiomCheckResult {
                status: EvaluationStatus::ViolatesNorm,
                violated_axiom: Some("A5".to_string()),
                explanation: "Assertive statement without sufficient grounding (categoricity ban)"
                    .to_string(),
            };
        }

        if statement.modality == Some(Modality::Conditional) {
            if license.permits(Modality::Assertive) {
                return AxiomCheckResult {
                    status: EvaluationStatus::ConditionallyAcceptable,
                    violated_axiom: None,
                    explanation:
                        "Conditional form chosen by agent (ASSERTIVE also permitted by grounding)"
                            .to_string(),
                };
            }
            if !statement.conditions.is_empty() {
                return AxiomCheckResult {
                    status: EvaluationStatus::ConditionallyAcceptable,
                    violated_axiom: None,
                    explanation: format!(
                        "Conditional statement with declared conditions: {:?}",
                        statement.conditions
                    ),
                };
            }
            return AxiomCheckResult {
                status: EvaluationStatus::Unsupported,
                violated_axiom: Some("A7".to_string()),
                explanation: "Conditional statement without declared conditions".to_string(),
            };
        }

        if self.is_normative(statement) && ground_set.is_empty() {
            return AxiomCheckResult {
                status: EvaluationStatus::Unsupported,
                violated_axiom: Some("A4".to_string()),
                explanation: "Normative claim without grounding".to_string(),
            };
        }

        if statement.modality == Some(Modality::Descriptive) {
            if ground_set.has_factual() {
                return AxiomCheckResult {
                    status: EvaluationStatus::Acceptable,
                    violated_axiom: None,
                    explanation: "Descriptive statement grounded in factual knowledge".to_string(),
                };
            }
            return AxiomCheckResult {
                status: EvaluationStatus::Unsupported,
                violated_axiom: Some("A4".to_string()),
                explanation: "Descriptive statement without factual grounding".to_string(),
            };
        }

        if let Some(modality) = &statement.modality {
            if license.permits(modality.clone()) {
                return AxiomCheckResult {
                    status: EvaluationStatus::Acceptable,
                    violated_axiom: None,
                    explanation: format!(
                        "Statement modality ({}) permitted by license",
                        modality.as_str()
                    ),
                };
            }
            return AxiomCheckResult {
                status: EvaluationStatus::Underdetermined,
                violated_axiom: None,
                explanation: format!(
                    "Cannot determine status (modality={}, license={:?})",
                    modality.as_str(),
                    license
                        .permitted_modalities
                        .iter()
                        .map(|m| m.as_str().to_string())
                        .collect::<Vec<_>>()
                ),
            };
        }

        AxiomCheckResult {
            status: EvaluationStatus::Underdetermined,
            violated_axiom: None,
            explanation: "Cannot determine status (modality=None)".to_string(),
        }
    }

    fn is_normative(&self, statement: &Statement) -> bool {
        matches!(
            statement.modality,
            Some(Modality::Assertive | Modality::Conditional)
        )
    }
}

#[cfg(test)]
mod tests {
    use super::AxiomChecker;
    use crate::normative::models::EvaluationStatus;
    use crate::normative::models::GroundSet;
    use crate::normative::models::License;
    use crate::normative::models::Modality;
    use crate::normative::models::Statement;
    use std::collections::BTreeSet;

    #[test]
    fn axiom_assertive_without_license_violates_a5() {
        let checker = AxiomChecker;
        let statement = Statement {
            id: "s1".to_string(),
            subject: "agent".to_string(),
            predicate: "participation".to_string(),
            raw_text: "text".to_string(),
            modality: Some(Modality::Assertive),
            conditions: vec![],
        };
        let mut permitted = BTreeSet::new();
        permitted.insert(Modality::Refusal);
        let license = License {
            permitted_modalities: permitted,
        };
        let result = checker.check(&statement, &license, &GroundSet { nodes: vec![] }, "goal");
        assert_eq!(result.status, EvaluationStatus::ViolatesNorm);
        assert_eq!(result.violated_axiom, Some("A5".to_string()));
    }

    #[test]
    fn refusal_is_always_acceptable() {
        let checker = AxiomChecker;
        let statement = Statement {
            id: "s1".to_string(),
            subject: "agent".to_string(),
            predicate: "participation".to_string(),
            raw_text: "cannot determine".to_string(),
            modality: Some(Modality::Refusal),
            conditions: vec![],
        };
        let license = License {
            permitted_modalities: BTreeSet::new(),
        };
        let result = checker.check(&statement, &license, &GroundSet { nodes: vec![] }, "goal");
        assert_eq!(result.status, EvaluationStatus::Acceptable);
        assert_eq!(result.violated_axiom, None);
    }

    #[test]
    fn normative_assertive_with_license_but_no_ground_is_unsupported() {
        let checker = AxiomChecker;
        let statement = Statement {
            id: "s1".to_string(),
            subject: "agent".to_string(),
            predicate: "participation".to_string(),
            raw_text: "You should proceed.".to_string(),
            modality: Some(Modality::Assertive),
            conditions: vec![],
        };
        let license = License {
            permitted_modalities: BTreeSet::from([Modality::Assertive]),
        };
        let result = checker.check(&statement, &license, &GroundSet { nodes: vec![] }, "goal");
        assert_eq!(result.status, EvaluationStatus::Unsupported);
        assert_eq!(result.violated_axiom, Some("A4".to_string()));
    }

    #[test]
    fn missing_modality_is_underdetermined_not_unsupported() {
        let checker = AxiomChecker;
        let statement = Statement {
            id: "s1".to_string(),
            subject: "agent".to_string(),
            predicate: "participation".to_string(),
            raw_text: "ambiguous".to_string(),
            modality: None,
            conditions: vec![],
        };
        let license = License {
            permitted_modalities: BTreeSet::new(),
        };
        let result = checker.check(&statement, &license, &GroundSet { nodes: vec![] }, "goal");
        assert_eq!(result.status, EvaluationStatus::Underdetermined);
        assert_eq!(result.violated_axiom, None);
    }
}