meta-ast 0.7.0

Polyglot static-analysis engine: extract symbols and cross-language dependency graphs from 9 supported source languages, with optional MetaCall deployment manifest generation.
Documentation
//! Edge types for the dependency graph.
//!
//! Defines the semantic relationships between nodes in the code graph.
//! Edges are directed and carry metadata about the relationship type
//! and confidence level.
//!
//! ## Design: uni-directional edges
//!
//! All edges are directed. There is no bidirectional or monitor/link
//! pattern - an edge from A to B means A depends on B, not that B will
//! be notified of A's failure. Shared-fate semantics (failure propagation)
//! are a separate, optional annotation that the deploy layer may add
//! during cut-edge RPC conversion. This follows the principle from
//! *A Unified Semantics for Future Erlang* §2.2/§6.2: bidirectional links
//! are replaced by uni-directional links plus monitors, and supervision
//! trees can be built from uni-directional links alone.

/// Semantic kind of a directed edge in the code graph.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
#[non_exhaustive]
pub enum EdgeKind {
    /// Ownership edge: File owns/contains a symbol.
    Ownership,

    /// Import edge: File imports/depends on another file.
    Import,

    /// Reference edge: Symbol references/uses another symbol.
    Reference,

    /// Flow edge: DataNode def-use or dataflow relationship.
    Flow,
}

/// Confidence ladder for scope-resolved edges.
///
/// Own file and direct same-language imports score 1.0. Transitive
/// same-language imports decay to 0.8. Cross-language imports score 0.6.
pub const CONFIDENCE_OWN_OR_DIRECT: f32 = 1.0;
/// Transitive same-language import.
pub const CONFIDENCE_TRANSITIVE: f32 = 0.8;
/// Cross-language import.
pub const CONFIDENCE_CROSS_LANGUAGE: f32 = 0.6;

/// Confidence ladder for MetaCall client-call edges.
///
/// Unique load-confirmed calls score 1.0. Multiple load-confirmed calls
/// score 0.8. Unique global matches score 0.6. Multiple global matches
/// score 0.5. Computed names cap at 0.4.
pub const CONFIDENCE_CLIENT_UNIQUE_LOAD: f32 = 1.0;
/// Multiple load-confirmed candidates.
pub const CONFIDENCE_CLIENT_MULTI_LOAD: f32 = 0.8;
/// Unique global fallback match.
pub const CONFIDENCE_CLIENT_UNIQUE_GLOBAL: f32 = 0.6;
/// Multiple global fallback matches.
pub const CONFIDENCE_CLIENT_MULTI_GLOBAL: f32 = 0.5;
/// Computed function, tag, or script name.
pub const CONFIDENCE_COMPUTED: f32 = 0.4;

/// Dataflow def-use edge confidence.
pub const CONFIDENCE_DEF_USE: f32 = 0.9;

/// Coarse class of a confidence value. Exact ladder matching, no interpolation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ConfidenceTier {
    /// Own file, or a direct same-language import, or a unique load-confirmed
    /// call.
    OwnOrDirect,
    /// Definition-use edge.
    DefUse,
    /// Transitive same-language import, or multiple load-confirmed calls.
    Transitive,
    /// Cross-language import, or a unique global call match.
    CrossLanguage,
    /// Multiple global call candidates.
    ClientMultiGlobal,
    /// Computed call name.
    Computed,
    /// Not one of the ladder values.
    Unknown,
}

/// Classify a confidence value by exact equality with the ladder constants.
///
/// A value the ladder does not define is [`ConfidenceTier::Unknown`], so a
/// consumer decides how to present it instead of letting it fall between two
/// tiers.
pub fn confidence_tier(confidence: f32) -> ConfidenceTier {
    if confidence == CONFIDENCE_OWN_OR_DIRECT || confidence == CONFIDENCE_CLIENT_UNIQUE_LOAD {
        ConfidenceTier::OwnOrDirect
    } else if confidence == CONFIDENCE_DEF_USE {
        ConfidenceTier::DefUse
    } else if confidence == CONFIDENCE_TRANSITIVE || confidence == CONFIDENCE_CLIENT_MULTI_LOAD {
        ConfidenceTier::Transitive
    } else if confidence == CONFIDENCE_CROSS_LANGUAGE
        || confidence == CONFIDENCE_CLIENT_UNIQUE_GLOBAL
    {
        ConfidenceTier::CrossLanguage
    } else if confidence == CONFIDENCE_CLIENT_MULTI_GLOBAL {
        ConfidenceTier::ClientMultiGlobal
    } else if confidence == CONFIDENCE_COMPUTED {
        ConfidenceTier::Computed
    } else {
        ConfidenceTier::Unknown
    }
}

impl EdgeKind {
    /// Returns true if this edge kind participates in SCC computation.
    pub fn participates_in_scc(self) -> bool {
        matches!(self, EdgeKind::Import | EdgeKind::Reference)
    }

    /// Returns true if this edge represents a cross-file dependency.
    pub fn is_cross_file(self) -> bool {
        matches!(self, EdgeKind::Import)
    }

    /// Returns the human-readable name of this edge kind.
    pub const fn as_str(self) -> &'static str {
        match self {
            EdgeKind::Ownership => "ownership",
            EdgeKind::Import => "import",
            EdgeKind::Reference => "reference",
            EdgeKind::Flow => "flow",
        }
    }
}

impl std::fmt::Display for EdgeKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

/// Data stored for each edge in the graph.
#[derive(Debug, Clone, Copy, serde::Serialize)]
pub struct EdgeData {
    /// Semantic kind of the relationship.
    pub kind: EdgeKind,

    /// Confidence level for the edge resolution.
    /// Used for cross-language and best-effort resolution.
    pub confidence: f32,

    /// Flow kind for dataflow edges (None for non-Flow edges).
    pub flow_kind: Option<crate::model::FlowKind>,
}

impl EdgeData {
    /// Creates a new edge with full confidence (1.0) and no flow kind.
    pub fn new(kind: EdgeKind) -> Self {
        Self {
            kind,
            confidence: 1.0,
            flow_kind: None,
        }
    }

    /// Creates a new edge with specified confidence and no flow kind.
    pub fn with_confidence(kind: EdgeKind, confidence: f32) -> Self {
        Self {
            kind,
            confidence: confidence.clamp(0.0, 1.0),
            flow_kind: None,
        }
    }

    /// Creates a Flow edge with a flow kind and confidence.
    pub fn flow(flow_kind: crate::model::FlowKind, confidence: f32) -> Self {
        Self {
            kind: EdgeKind::Flow,
            confidence: confidence.clamp(0.0, 1.0),
            flow_kind: Some(flow_kind),
        }
    }

    pub fn participates_in_scc(&self) -> bool {
        self.kind.participates_in_scc()
    }

    /// Merges a repeated `(source, target, kind)` edge into this one.
    ///
    /// The stronger confidence wins and the first flow kind is kept. This is
    /// the only copy of the rule; every writer calls it.
    pub(crate) fn merge_repeated(
        &mut self,
        confidence: f32,
        flow_kind: Option<crate::model::FlowKind>,
    ) {
        self.confidence = self.confidence.max(confidence.clamp(0.0, 1.0));
        if self.flow_kind.is_none() {
            self.flow_kind = flow_kind;
        }
    }
}

impl Default for EdgeData {
    fn default() -> Self {
        Self::new(EdgeKind::Reference)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn edge_kind_scc_participation() {
        assert!(!EdgeKind::Ownership.participates_in_scc());
        assert!(EdgeKind::Import.participates_in_scc());
        assert!(EdgeKind::Reference.participates_in_scc());
    }

    #[test]
    fn edge_kind_as_str() {
        assert_eq!(EdgeKind::Ownership.as_str(), "ownership");
        assert_eq!(EdgeKind::Import.as_str(), "import");
        assert_eq!(EdgeKind::Reference.as_str(), "reference");
        assert_eq!(EdgeKind::Flow.as_str(), "flow");
    }

    #[test]
    fn flow_edge_excluded_from_scc() {
        assert!(!EdgeKind::Flow.participates_in_scc());
    }

    #[test]
    fn flow_edge_not_cross_file() {
        assert!(!EdgeKind::Flow.is_cross_file());
    }

    #[test]
    fn edge_kind_display() {
        assert_eq!(format!("{}", EdgeKind::Import), "import");
    }

    #[test]
    fn edge_data_new_defaults_to_full_confidence() {
        let edge = EdgeData::new(EdgeKind::Import);
        assert_eq!(edge.kind, EdgeKind::Import);
        assert_eq!(edge.confidence, 1.0);
        assert!(edge.participates_in_scc());
    }

    #[test]
    fn edge_data_with_confidence_clamps() {
        let low = EdgeData::with_confidence(EdgeKind::Reference, -0.5);
        assert_eq!(low.confidence, 0.0);

        let high = EdgeData::with_confidence(EdgeKind::Reference, 1.5);
        assert_eq!(high.confidence, 1.0);

        let mid = EdgeData::with_confidence(EdgeKind::Reference, 0.75);
        assert_eq!(mid.confidence, 0.75);
    }

    #[test]
    fn edge_data_default() {
        let edge: EdgeData = Default::default();
        assert_eq!(edge.confidence, 1.0);
        assert!(edge.participates_in_scc());
    }

    #[test]
    fn every_ladder_value_has_exactly_one_tier() {
        for (confidence, tier) in [
            (CONFIDENCE_OWN_OR_DIRECT, ConfidenceTier::OwnOrDirect),
            (CONFIDENCE_CLIENT_UNIQUE_LOAD, ConfidenceTier::OwnOrDirect),
            (CONFIDENCE_DEF_USE, ConfidenceTier::DefUse),
            (CONFIDENCE_TRANSITIVE, ConfidenceTier::Transitive),
            (CONFIDENCE_CLIENT_MULTI_LOAD, ConfidenceTier::Transitive),
            (CONFIDENCE_CROSS_LANGUAGE, ConfidenceTier::CrossLanguage),
            (
                CONFIDENCE_CLIENT_UNIQUE_GLOBAL,
                ConfidenceTier::CrossLanguage,
            ),
            (
                CONFIDENCE_CLIENT_MULTI_GLOBAL,
                ConfidenceTier::ClientMultiGlobal,
            ),
            (CONFIDENCE_COMPUTED, ConfidenceTier::Computed),
        ] {
            assert_eq!(confidence_tier(confidence), tier, "at {confidence}");
        }
    }

    #[test]
    fn a_value_between_ladder_steps_is_unknown() {
        for confidence in [0.7, 0.0, 1.5, -1.0] {
            assert_eq!(
                confidence_tier(confidence),
                ConfidenceTier::Unknown,
                "at {confidence}"
            );
        }
    }
}