weavatrix-graph 0.6.3

Deterministic, evidence-carrying graph core for Weavatrix repository intelligence
Documentation
use std::collections::BTreeMap;
use std::error::Error as _;
use std::str::FromStr as _;
use weavatrix_graph::{
    AttributeValue, Confidence, Edge, EdgeKind, EvidenceKind, FiniteF64, Graph, GraphBuilder,
    GraphError, LegacyGraph, LegacyLink, LegacyNode, LegacyPoint, LegacyRange, Node, NodeId,
    NodeKind, Provenance, SourcePosition, SourceSpan,
};

fn node(id: &str) -> Node {
    Node::new(id, id, NodeKind::File).unwrap()
}

fn provenance() -> Provenance {
    Provenance::new("coverage.contract", EvidenceKind::Parsed, Confidence::High).unwrap()
}

#[test]
fn attribute_values_cover_all_public_conversions_and_float_contracts() {
    assert_eq!(
        FiniteF64::new(1.25).unwrap().get().to_bits(),
        1.25_f64.to_bits()
    );
    assert!(FiniteF64::new(f64::NAN).unwrap_err().contains("finite"));
    assert!(FiniteF64::new(f64::INFINITY).is_err());

    let mut object = BTreeMap::new();
    object.insert("nested".to_owned(), AttributeValue::from("value"));
    assert_eq!(AttributeValue::from(9_u64), AttributeValue::Unsigned(9));
    assert_eq!(AttributeValue::from(4_u32), AttributeValue::Unsigned(4));
    let values = vec![
        AttributeValue::Null,
        true.into(),
        (-7_i64).into(),
        AttributeValue::Unsigned(u64::MAX),
        3_i32.into(),
        "u32 conversion checked directly above".into(),
        AttributeValue::try_from(2.5).unwrap(),
        "text".into(),
        String::from("owned").into(),
        vec![AttributeValue::from(false)].into(),
        object.into(),
    ];

    let encoded = serde_json::to_string(&values).unwrap();
    let decoded: Vec<AttributeValue> = serde_json::from_str(&encoded).unwrap();
    assert_eq!(decoded, values);
    assert!(serde_json::from_str::<FiniteF64>("null").is_err());
}

#[test]
fn graph_helpers_and_validation_errors_are_contractually_visible() {
    let empty = Graph::try_from_parts([], []).unwrap();
    assert!(empty.is_empty());
    assert_eq!(empty.into_parts(), (Vec::new(), Vec::new()));

    let invalid_language = node("bad-language").with_language(" rust ");
    assert!(matches!(
        GraphBuilder::new().add_node(invalid_language),
        Err(GraphError::InvalidKind {
            category: "language",
            ..
        })
    ));

    let empty_file_span = SourceSpan::new("", SourcePosition::new(1, 1), SourcePosition::new(1, 2));
    assert!(matches!(
        GraphBuilder::new().add_node(node("empty-file").with_span(empty_file_span)),
        Err(GraphError::InvalidSpan { reason, .. }) if reason == "file must not be empty"
    ));

    let zero_position_span = SourceSpan::new(
        "src/lib.rs",
        SourcePosition::new(0, 1),
        SourcePosition::new(1, 1),
    );
    let bad_edge = Edge::new(
        NodeId::new("source").unwrap(),
        NodeId::new("target").unwrap(),
        EdgeKind::Calls,
        provenance().with_span(zero_position_span),
    );
    assert!(matches!(
        GraphBuilder::new().add_edge(bad_edge),
        Err(GraphError::InvalidSpan { reason, .. }) if reason == "positions are one-based"
    ));
}

#[test]
fn graph_error_display_messages_cover_every_variant() {
    let errors = [
        GraphError::EmptyNodeId,
        GraphError::InvalidKind {
            category: "node",
            value: " bad ".into(),
        },
        GraphError::ConflictingNode { id: "same".into() },
        GraphError::MissingEdgeSource {
            id: "source".into(),
        },
        GraphError::MissingEdgeTarget {
            id: "target".into(),
        },
        GraphError::EmptyExtractor,
        GraphError::InvalidSpan {
            file: "src/lib.rs".into(),
            reason: "positions are one-based",
        },
        GraphError::IndexCapacityExceeded {
            category: "nodes",
            count: usize::MAX,
        },
        GraphError::InvalidTopologyEndpoint {
            edge: 4,
            node: 9,
            node_count: 3,
        },
        GraphError::ArithmeticOverflow {
            operation: "test operation",
        },
        GraphError::InvalidNodeIndex {
            node: 4,
            node_count: 2,
        },
        GraphError::InvalidProbability {
            numerator: 2,
            denominator: 1,
        },
        GraphError::InvalidAlgorithmParameter {
            algorithm: "PageRank",
            parameter: "damping",
            value: "NaN".into(),
        },
        GraphError::NegativeCycle {
            algorithm: "Bellman-Ford",
        },
        GraphError::CyclicGraph {
            algorithm: "DAG longest path",
        },
        GraphError::MissingKeyedNode { endpoint: "source" },
    ];

    let messages = errors
        .iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join("\n");
    assert!(messages.contains("node id must not be empty"));
    assert!(messages.contains("invalid node kind"));
    assert!(messages.contains("conflicting definitions"));
    assert!(messages.contains("edge source does not exist"));
    assert!(messages.contains("edge target does not exist"));
    assert!(messages.contains("extractor must not be empty"));
    assert!(messages.contains("invalid source span"));
    assert!(messages.contains("exceeds u32 index capacity"));
    assert!(messages.contains("references node index"));
    assert!(messages.contains("arithmetic overflow"));
    assert!(messages.contains("outside matrix node count"));
    assert!(messages.contains("probability 2/1"));
    assert!(messages.contains("invalid damping for PageRank"));
    assert!(messages.contains("reachable negative cycle"));
    assert!(messages.contains("requires an acyclic graph"));
    assert!(messages.contains("keyed graph source node does not exist"));
    assert!(GraphError::EmptyNodeId.source().is_none());
}

#[test]
fn node_ids_kinds_and_provenance_round_trip_through_public_traits() {
    let id = NodeId::from_str("node:1").unwrap();
    assert_eq!(id.to_string(), "node:1");
    let decoded: NodeId = serde_json::from_str("\"node:2\"").unwrap();
    assert_eq!(decoded.as_str(), "node:2");

    let custom = NodeKind::custom("domain_specific").unwrap();
    assert_eq!(custom.to_string(), "domain_specific");
    assert!(NodeKind::custom(" domain_specific").is_err());
    assert!(serde_json::from_str::<NodeKind>("\" \"").is_err());

    let span = SourceSpan::new(
        "src/lib.rs",
        SourcePosition::new(1, 1),
        SourcePosition::new(1, 5),
    );
    let provenance = provenance()
        .with_span(span.clone())
        .with_detail("checked in contract test");
    assert_eq!(provenance.span, Some(span));
    assert_eq!(
        provenance.detail.as_deref(),
        Some("checked in contract test")
    );
}

#[test]
fn legacy_conversion_covers_inference_attributes_and_fallbacks() {
    let graph = LegacyGraph {
        nodes: vec![
            LegacyNode {
                id: "src/lib.rs".into(),
                label: None,
                kind: None,
                node_type: None,
                language: Some("rust".into()),
                source_file: Some("src/lib.rs".into()),
                source_range: Some(LegacyRange {
                    start: LegacyPoint {
                        line: 0,
                        character: 0,
                    },
                    end: LegacyPoint {
                        line: 0,
                        character: 10,
                    },
                }),
                selection_start: Some(LegacyPoint {
                    line: 0,
                    character: 1,
                }),
                selection_end: Some(LegacyPoint {
                    line: 0,
                    character: 4,
                }),
                attributes: BTreeMap::from([("owned".into(), AttributeValue::from(true))]),
            },
            LegacyNode {
                id: "src/lib.rs#run".into(),
                label: None,
                kind: None,
                node_type: None,
                language: Some("rust".into()),
                source_file: None,
                source_range: None,
                selection_start: None,
                selection_end: None,
                attributes: BTreeMap::new(),
            },
            LegacyNode {
                id: "src/lib.rs#custom".into(),
                label: None,
                kind: Some("unknown-kind".into()),
                node_type: None,
                language: Some("rust".into()),
                source_file: None,
                source_range: None,
                selection_start: None,
                selection_end: None,
                attributes: BTreeMap::new(),
            },
        ],
        links: vec![LegacyLink {
            source: "src/lib.rs#run".into(),
            target: "src/lib.rs".into(),
            relation: None,
            kind: None,
            edge_type: Some("calls".into()),
            confidence: Some("conflict".into()),
            provenance: Some("not-real-evidence".into()),
            line: Some(3),
            character: None,
            compile_only: Some(true),
            type_only: Some(false),
            specifier: Some("crate::lib".into()),
            usage: Some("runtime".into()),
            attributes: BTreeMap::new(),
        }],
        metadata: BTreeMap::from([("schemaVersion".into(), AttributeValue::from("legacy"))]),
    };

    let converted = Graph::try_from(graph).unwrap();
    assert_eq!(converted.node_count(), 3);
    assert_eq!(converted.edge_count(), 1);
    assert_eq!(converted.node("src/lib.rs").unwrap().label, "lib.rs");
    assert_eq!(
        converted.node("src/lib.rs#run").unwrap().kind,
        NodeKind::Function
    );
    assert_eq!(
        converted.node("src/lib.rs#custom").unwrap().kind.as_str(),
        "unknown-kind"
    );

    let edge = &converted.edges()[0];
    assert_eq!(edge.kind, EdgeKind::Calls);
    assert_eq!(edge.provenance.evidence.as_str(), "not-real-evidence");
    assert_eq!(edge.provenance.confidence, Confidence::Low);
    assert!(edge.attributes.contains_key("compileOnly"));
    assert!(edge.attributes.contains_key("typeOnly"));
    assert!(edge.attributes.contains_key("specifier"));
    assert!(edge.attributes.contains_key("usage"));
}