weavatrix-graph 0.6.2

Deterministic, evidence-carrying graph core for Weavatrix repository intelligence
Documentation
use serde_json::{Value, json};
use weavatrix_graph::{
    AttributeValue, Confidence, Edge, EdgeKind, EvidenceKind, FiniteF64, Graph, LegacyGraph, Node,
    NodeId, NodeKind, Provenance,
};

#[test]
fn known_and_custom_kinds_have_a_stable_string_contract() {
    let known = serde_json::to_string(&NodeKind::KubernetesResource).unwrap();
    assert_eq!(known, "\"kubernetes_resource\"");
    assert_eq!(
        serde_json::from_str::<NodeKind>(&known).unwrap(),
        NodeKind::KubernetesResource
    );

    let custom = NodeKind::custom("terraform_resource").unwrap();
    let encoded = serde_json::to_string(&custom).unwrap();
    assert_eq!(encoded, "\"terraform_resource\"");
    assert_eq!(serde_json::from_str::<NodeKind>(&encoded).unwrap(), custom);

    assert!(EdgeKind::custom(" leading-space").is_err());
    assert!(EvidenceKind::custom("trailing-space ").is_err());

    assert_eq!(
        serde_json::from_str::<EdgeKind>("\"re_exports\"").unwrap(),
        EdgeKind::ReExports
    );
    assert_eq!(
        serde_json::from_str::<EvidenceKind>("\"EXACT_LSP\"").unwrap(),
        EvidenceKind::ExactLsp
    );
}

#[test]
fn empty_node_ids_are_rejected_by_construction_and_deserialization() {
    assert!(NodeId::new("").is_err());
    assert!(serde_json::from_str::<NodeId>("\"\"").is_err());
}

#[test]
fn graph_json_round_trip_preserves_the_canonical_graph() {
    let repository = Node::new("repo:demo", "demo", NodeKind::Repository).unwrap();
    let file = Node::new("file:src/lib.rs", "src/lib.rs", NodeKind::File)
        .unwrap()
        .with_language("rust");
    let graph = Graph::try_from_parts(
        [file.clone(), repository.clone()],
        [Edge::new(
            repository.id,
            file.id,
            EdgeKind::Contains,
            Provenance::new("test", EvidenceKind::Parsed, Confidence::Exact).unwrap(),
        )],
    )
    .unwrap();

    let encoded = serde_json::to_string(&graph).unwrap();
    let wire: Value = serde_json::from_str(&encoded).unwrap();
    let keys = wire.as_object().unwrap().keys().collect::<Vec<_>>();
    assert_eq!(keys, ["edges", "nodes"]);
    let decoded: Graph = serde_json::from_str(&encoded).unwrap();
    assert_eq!(decoded, graph);
}

#[test]
fn graph_json_round_trip_preserves_structured_attributes() {
    let source = Node::new("src", "src", NodeKind::File)
        .unwrap()
        .with_attribute("physical_loc", 42_i64)
        .with_attribute("generated", false);
    let target = Node::new("target", "target", NodeKind::Function).unwrap();
    let edge = Edge::new(
        source.id.clone(),
        target.id.clone(),
        EdgeKind::References,
        Provenance::new("test", EvidenceKind::Extracted, Confidence::High).unwrap(),
    )
    .with_attribute("compileOnly", true)
    .with_attribute(
        "source_range",
        AttributeValue::Object(
            [("line".to_owned(), AttributeValue::Integer(7))]
                .into_iter()
                .collect(),
        ),
    );
    let graph = Graph::try_from_parts([source, target], [edge]).unwrap();

    let decoded: Graph = serde_json::from_str(&serde_json::to_string(&graph).unwrap()).unwrap();

    assert_eq!(
        decoded.nodes()[0].attributes.get("physical_loc"),
        Some(&AttributeValue::Integer(42))
    );
    assert_eq!(
        decoded.edges()[0].attributes.get("compileOnly"),
        Some(&AttributeValue::Bool(true))
    );

    let numeric: AttributeValue = serde_json::from_str("1.25").unwrap();
    assert_eq!(
        numeric,
        AttributeValue::Float(FiniteF64::new(1.25).unwrap())
    );
    let unsigned: AttributeValue = serde_json::from_str("18446744073709551615").unwrap();
    assert_eq!(unsigned, AttributeValue::Unsigned(u64::MAX));
}

#[test]
fn legacy_nodes_links_convert_without_losing_weavatrix_edge_details() {
    let legacy: LegacyGraph = serde_json::from_value(json!({
        "nodes": [
            { "id": "src/lib.rs", "label": "lib.rs", "source_file": "src/lib.rs" },
            {
                "id": "src/lib.rs#entry@3",
                "label": "entry()",
                "source_file": "src/lib.rs",
                "source_range": {
                    "start": { "line": 2, "character": 0 },
                    "end": { "line": 4, "character": 1 }
                },
                "callable": true,
                "complexity": { "cyclomatic": 1 }
            }
        ],
        "links": [
            {
                "source": "src/lib.rs",
                "target": "src/lib.rs#entry@3",
                "relation": "contains",
                "confidence": "EXTRACTED"
            },
            {
                "source": "src/lib.rs#entry@3",
                "target": "src/lib.rs",
                "relation": "references",
                "confidence": "INFERRED",
                "provenance": "RESOLVED",
                "line": 3,
                "compileOnly": true,
                "specifier": "crate::helper"
            }
        ],
        "edgeTypesV": 2,
        "edgeProvenanceV": 1
    }))
    .unwrap();

    let graph = legacy.into_graph("legacy-test").unwrap();

    assert_eq!(graph.node_count(), 2);
    assert_eq!(graph.edge_count(), 2);
    let reference = graph
        .edges()
        .iter()
        .find(|edge| edge.kind == EdgeKind::References)
        .unwrap();
    assert_eq!(
        reference.provenance.evidence,
        EvidenceKind::ResolvedCanonical
    );
    assert_eq!(reference.provenance.confidence, Confidence::Low);
    assert_eq!(
        reference.attributes.get("compileOnly"),
        Some(&AttributeValue::Bool(true))
    );
    assert_eq!(
        reference.attributes.get("specifier"),
        Some(&AttributeValue::String("crate::helper".into()))
    );
}

#[test]
fn deserialization_cannot_bypass_dangling_edge_validation() {
    let value = json!({
        "nodes": [{
            "id": "source",
            "label": "source",
            "kind": "file"
        }],
        "edges": [{
            "source": "source",
            "target": "missing",
            "kind": "references",
            "provenance": {
                "extractor": "test",
                "evidence": "parsed",
                "confidence": "high"
            }
        }]
    });

    let error = serde_json::from_value::<Graph>(value).unwrap_err();
    assert!(error.to_string().contains("edge target does not exist"));
}

#[test]
fn deserialization_cannot_bypass_conflicting_node_validation() {
    let value: Value = json!({
        "nodes": [
            { "id": "same", "label": "first", "kind": "file" },
            { "id": "same", "label": "second", "kind": "function" }
        ],
        "edges": []
    });

    let error = serde_json::from_value::<Graph>(value).unwrap_err();
    assert!(error.to_string().contains("conflicting definitions"));
}