normcore 0.1.1

Rust implementation baseline for NormCore normative admissibility evaluator
Documentation
use crate::normative::models::GroundSet;
use crate::normative::models::KnowledgeNode;
use crate::normative::models::Modality;
use crate::normative::models::Scope;
use crate::normative::models::Statement;

pub struct GroundSetMatcher;

impl GroundSetMatcher {
    pub fn match_nodes(
        &self,
        statement: &Statement,
        knowledge_nodes: &[KnowledgeNode],
    ) -> GroundSet {
        let mut relevant = Vec::new();
        for k in knowledge_nodes {
            if self.is_relevant(statement, k) {
                relevant.push(k.clone());
            }
        }
        GroundSet { nodes: relevant }
    }

    fn is_relevant(&self, statement: &Statement, node: &KnowledgeNode) -> bool {
        match statement.modality {
            Some(Modality::Descriptive) => node.scope == Scope::Factual,
            Some(Modality::Assertive) | Some(Modality::Conditional) => {
                node.scope == Scope::Factual || node.scope == Scope::Contextual
            }
            Some(Modality::Refusal) => false,
            None => false,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::GroundSetMatcher;
    use crate::normative::models::KnowledgeNode;
    use crate::normative::models::Modality;
    use crate::normative::models::Scope;
    use crate::normative::models::Source;
    use crate::normative::models::Statement;
    use crate::normative::models::Status;

    fn node(id: &str, scope: Scope) -> KnowledgeNode {
        KnowledgeNode::new(
            id.to_string(),
            Source::Observed,
            Status::Confirmed,
            1.0,
            scope,
            "strong".to_string(),
            None,
        )
        .expect("must create node")
    }

    fn statement(modality: Modality) -> Statement {
        Statement {
            id: "s1".to_string(),
            subject: "agent".to_string(),
            predicate: "participation".to_string(),
            raw_text: "text".to_string(),
            modality: Some(modality),
            conditions: vec![],
        }
    }

    #[test]
    fn descriptive_keeps_only_factual_nodes() {
        let matcher = GroundSetMatcher;
        let stmt = statement(Modality::Descriptive);
        let grounds = matcher.match_nodes(
            &stmt,
            &[node("f", Scope::Factual), node("c", Scope::Contextual)],
        );
        assert_eq!(grounds.nodes.len(), 1);
        assert_eq!(grounds.nodes[0].id, "f");
    }

    #[test]
    fn assertive_keeps_factual_and_contextual_nodes() {
        let matcher = GroundSetMatcher;
        let stmt = statement(Modality::Assertive);
        let grounds = matcher.match_nodes(
            &stmt,
            &[node("f", Scope::Factual), node("c", Scope::Contextual)],
        );
        assert_eq!(grounds.nodes.len(), 2);
    }
}