vela-protocol 0.108.0

Core library for the Vela scientific knowledge protocol: replayable frontier state, signed canonical events, and proof packets.
Documentation
use serde_json::json;
use vela_protocol::bundle::{
    Assertion, Conditions, Confidence, ConfidenceKind, ConfidenceMethod, Evidence, Extraction,
    FindingBundle, Flags, Link, Provenance,
};
use vela_protocol::events::{self, FindingEventInput, NULL_HASH};
use vela_protocol::impact;
use vela_protocol::project;

fn finding(label: &str) -> FindingBundle {
    let assertion = Assertion {
        text: format!("Impact fixture finding {label}"),
        assertion_type: "mechanism".to_string(),
        entities: Vec::new(),
        relation: None,
        direction: None,
        causal_claim: None,
        causal_evidence_grade: None,
    };
    let provenance = Provenance {
        source_type: "published_paper".to_string(),
        doi: Some(format!("10.0000/impact.{label}")),
        pmid: None,
        pmc: None,
        openalex_id: None,
        url: Some(format!("https://example.org/impact/{label}")),
        title: format!("Impact fixture {label}"),
        authors: Vec::new(),
        year: Some(2026),
        journal: None,
        license: None,
        publisher: None,
        funders: Vec::new(),
        extraction: Extraction::default(),
        review: None,
        citation_count: None,
    };
    let mut bundle = FindingBundle::new(
        assertion,
        Evidence {
            evidence_type: "experimental".to_string(),
            model_system: "human".to_string(),
            species: Some("Homo sapiens".to_string()),
            method: "manual".to_string(),
            sample_size: None,
            effect_size: None,
            p_value: None,
            replicated: false,
            replication_count: None,
            evidence_spans: Vec::new(),
        },
        Conditions {
            text: "human BBB context".to_string(),
            species_verified: vec!["Homo sapiens".to_string()],
            species_unverified: Vec::new(),
            in_vitro: false,
            in_vivo: false,
            human_data: true,
            clinical_trial: false,
            concentration_range: None,
            duration: None,
            age_group: None,
            cell_type: None,
        },
        Confidence {
            kind: ConfidenceKind::FrontierEpistemic,
            score: 0.5,
            basis: "fixture".to_string(),
            method: ConfidenceMethod::ExpertJudgment,
            components: None,
            extraction_confidence: 1.0,
        },
        provenance,
        Flags::default(),
    );
    bundle.created = "2026-05-07T00:00:00Z".to_string();
    bundle
}

#[test]
fn impact_report_includes_direct_and_transitive_dependents_without_mutation() {
    let root = finding("root");
    let mut child = finding("child");
    let mut grandchild = finding("grandchild");
    child.links.push(Link {
        target: root.id.clone(),
        link_type: "depends".to_string(),
        note: "child depends on root".to_string(),
        inferred_by: "test".to_string(),
        created_at: "2026-05-07T00:00:00Z".to_string(),
        mechanism: None,
    });
    grandchild.links.push(Link {
        target: child.id.clone(),
        link_type: "supports".to_string(),
        note: "grandchild follows child".to_string(),
        inferred_by: "test".to_string(),
        created_at: "2026-05-07T00:00:00Z".to_string(),
        mechanism: None,
    });
    let mut frontier = project::assemble(
        "impact frontier",
        vec![root.clone(), child.clone(), grandchild.clone()],
        0,
        0,
        "test",
    );
    frontier.frontier_id = Some("vfr_impact_test".to_string());
    for finding in [&root, &child, &grandchild] {
        frontier.events.push(events::new_finding_event(FindingEventInput {
            kind: "finding.asserted",
            finding_id: &finding.id,
            actor_id: "reviewer:test",
            actor_type: "human",
            reason: "fixture genesis",
            before_hash: NULL_HASH,
            after_hash: &events::finding_hash(finding),
            payload: json!({"proposal_id": format!("vpr_{}", &finding.id[3..]), "finding": finding}),
            caveats: Vec::new(),
        }));
    }
    let before = serde_json::to_value(&frontier).expect("frontier json");

    let report = impact::analyze(&frontier, &root.id, Some(10)).expect("impact report");

    assert_eq!(report.schema, "vela.impact_report.v0.1");
    assert_eq!(report.target.id, root.id);
    assert_eq!(report.summary.direct_dependents, 1);
    assert_eq!(report.summary.total_downstream, 2);
    assert_eq!(report.downstream[0].finding_id, child.id);
    assert_eq!(report.downstream[0].depth, 1);
    assert_eq!(report.downstream[1].finding_id, grandchild.id);
    assert_eq!(report.downstream[1].depth, 2);
    assert_eq!(
        serde_json::to_value(&frontier).expect("frontier json"),
        before
    );
}

#[test]
fn impact_depth_one_excludes_transitive_dependents() {
    let root = finding("root_depth");
    let mut child = finding("child_depth");
    let mut grandchild = finding("grandchild_depth");
    child.links.push(Link {
        target: root.id.clone(),
        link_type: "depends".to_string(),
        note: "child depends on root".to_string(),
        inferred_by: "test".to_string(),
        created_at: "2026-05-07T00:00:00Z".to_string(),
        mechanism: None,
    });
    grandchild.links.push(Link {
        target: child.id.clone(),
        link_type: "depends".to_string(),
        note: "grandchild depends on child".to_string(),
        inferred_by: "test".to_string(),
        created_at: "2026-05-07T00:00:00Z".to_string(),
        mechanism: None,
    });
    let frontier = project::assemble(
        "impact depth frontier",
        vec![root.clone(), child.clone(), grandchild],
        0,
        0,
        "test",
    );

    let report = impact::analyze(&frontier, &root.id, Some(1)).expect("impact report");

    assert_eq!(report.summary.direct_dependents, 1);
    assert_eq!(report.summary.total_downstream, 1);
    assert_eq!(report.downstream.len(), 1);
    assert_eq!(report.downstream[0].finding_id, child.id);
}