agileplus-trace-validator 0.3.0

Typed intent graphs for engineering work — convert prompts into validated DAGs of intents, features, tasks, tests, and edges, then store, query, and serve them from Rust, HTTP, or MCP.
Documentation
//! Edge constraint validation against the ontology's `EdgeConstraints`.

use crate::{ValidationError, ValidationResult};
use agileplus_domain::intent_graph::*;
use std::collections::HashMap;

/// Validate each edge against the ontology edge constraints.
pub fn validate_edge_constraints(graph: &IntentGraph) -> ValidationResult {
    let mut errors = Vec::new();
    let ontology = Ontology::default();
    let node_types: HashMap<&NodeId, NodeType> = graph
        .nodes
        .iter()
        .map(|n| (&n.id, n.node_type))
        .collect();

    for edge in &graph.edges {
        // Check source node exists
        let source_type = match node_types.get(&edge.source) {
            Some(t) => t,
            None => {
                errors.push(ValidationError::UnknownNode(format!(
                    "Edge {} references unknown source node: {}",
                    edge.id, edge.source
                )));
                continue;
            }
        };

        // Check target node exists
        let target_type = match node_types.get(&edge.target) {
            Some(t) => t,
            None => {
                errors.push(ValidationError::UnknownNode(format!(
                    "Edge {} references unknown target node: {}",
                    edge.id, edge.target
                )));
                continue;
            }
        };

        // Check self-loop
        if edge.source == edge.target {
            errors.push(ValidationError::SelfLoop(format!(
                "Edge {} is a self-loop on {}",
                edge.id, edge.source
            )));
            continue;
        }

        // Check against ontology constraints
        let allowed = get_allowed_pairs(&edge.relationship_type, &ontology);
        let source_name = source_type.to_string();
        let target_name = target_type.to_string();

        let valid = allowed.iter().any(|entry| {
            (entry.from == "*" || entry.from == source_name)
                && (entry.to == "*" || entry.to == target_name)
        });

        if !valid {
            errors.push(ValidationError::EdgeConstraintViolation(format!(
                "Edge {} ({:?}) from {} ({}) to {} ({}) violates ontology constraints",
                edge.id,
                edge.relationship_type,
                edge.source,
                source_name,
                edge.target,
                target_name
            )));
        }
    }

    if errors.is_empty() {
        Ok(())
    } else {
        Err(errors)
    }
}

/// Look up the allowed `(from, to)` entries for a given relationship type.
pub(crate) fn get_allowed_pairs<'a>(
    rt: &'a RelationshipType,
    ontology: &'a Ontology,
) -> &'a Vec<EdgeConstraintEntry> {
    match rt {
        RelationshipType::Implements => &ontology.edge_constraints.implements.allowed,
        RelationshipType::Tests => &ontology.edge_constraints.tests.allowed,
        RelationshipType::Covers => &ontology.edge_constraints.covers.allowed,
        RelationshipType::TracesTo => &ontology.edge_constraints.traces_to.allowed,
        RelationshipType::DerivesFrom => &ontology.edge_constraints.derives_from.allowed,
        RelationshipType::Resolves => &ontology.edge_constraints.resolves.allowed,
        RelationshipType::Blocks => &ontology.edge_constraints.blocks.allowed,
        RelationshipType::DependsOn => &ontology.edge_constraints.depends_on.allowed,
    }
}