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
//! File- and pod-level deployment metrics.
//!
//! Metrics are derived from data already collected during extraction
//! (`FileExtraction`) and graph construction, so no extra passes are
//! needed. The default metric is AST node count (a proxy for
//! computational surface area); the schema is open for future
//! telemetry-based metrics.

use std::collections::HashMap;
use std::path::PathBuf;

use crate::deploy::pod::PodPartition;
use crate::graph::{CodeGraph, NodeData};
use crate::model::{FileExtraction, FileId};

/// Per-file deployment metrics, derived from extraction results.
#[derive(Debug, Clone, serde::Serialize)]
pub struct FileMetrics {
    pub ast_node_count: usize,
    pub symbol_count: usize,
    pub import_count: usize,
    pub reference_count: usize,
}

/// Aggregated per-pod deployment metrics.
#[derive(Debug, Clone, serde::Serialize)]
pub struct PodMetrics {
    pub total_ast_nodes: usize,
    pub file_count: usize,
    pub symbol_count: usize,
}

/// Compute per-file metrics from extraction results, keyed by file path.
pub fn compute_file_metrics<F>(extractions: &[F]) -> HashMap<PathBuf, FileMetrics>
where
    F: std::borrow::Borrow<FileExtraction>,
{
    let mut metrics = HashMap::new();
    for file in extractions {
        let file = file.borrow();
        metrics.insert(
            file.path.clone(),
            FileMetrics {
                ast_node_count: file.ast_node_count,
                symbol_count: file.symbols.len(),
                import_count: file.imports.len(),
                reference_count: file.references.len(),
            },
        );
    }
    metrics
}

/// Compute per-pod metrics from pod partition and extraction data.
pub fn compute_pod_metrics(
    partition: &PodPartition,
    file_metrics: &HashMap<PathBuf, FileMetrics>,
    graph: &CodeGraph,
) -> Vec<PodMetrics> {
    // Build a reverse mapping from FileId -> Path using the graph's file nodes.
    let mut fid_to_path: HashMap<FileId, PathBuf> = HashMap::new();
    for (&fid, &idx) in &graph.file_to_index {
        if let Some(NodeData::File(f)) = graph.graph().node_weight(idx) {
            fid_to_path.insert(fid, f.path.clone());
        }
    }

    let mut pod_metrics = Vec::with_capacity(partition.pods.len());
    for pod in &partition.pods {
        let mut total_ast_nodes = 0usize;
        let file_count = pod.files.len();
        let mut symbol_count = 0usize;

        for &fid in &pod.files {
            match fid_to_path
                .get(&fid)
                .and_then(|path| file_metrics.get(path))
            {
                Some(metrics) => {
                    total_ast_nodes = total_ast_nodes.saturating_add(metrics.ast_node_count);
                    symbol_count = symbol_count.saturating_add(metrics.symbol_count);
                }
                None => {
                    tracing::warn!(pod = pod.id, file = fid.to_raw(), "pod file has no metrics")
                }
            }
        }

        pod_metrics.push(PodMetrics {
            total_ast_nodes,
            file_count,
            symbol_count,
        });
    }
    pod_metrics
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::deploy::pod::{Pod, PodPartition};
    use crate::graph::node::{FileNode, NodeData};
    use crate::language::LangId;
    use crate::model::ids::{FileId, SnapshotId};
    use std::collections::HashMap;

    /// The aggregate must saturate instead of overflowing.
    #[test]
    fn pod_metrics_total_saturates() {
        let mut graph = CodeGraph::new(SnapshotId::new(1).unwrap());
        let a = FileId::new(1).unwrap();
        let b = FileId::new(2).unwrap();
        for (fid, path) in [(a, "a.py"), (b, "b.py")] {
            let idx = graph.add_node(NodeData::File(FileNode::new(
                fid,
                PathBuf::from(path),
                LangId::Python,
                SnapshotId::new(1).unwrap(),
            )));
            graph.file_to_index.insert(fid, idx);
        }

        let partition = PodPartition {
            pods: vec![Pod {
                id: 0,
                files: vec![a, b],
                language: LangId::Python,
            }],
            inter_pod_edges: Vec::new(),
            file_languages: HashMap::from([(a, LangId::Python), (b, LangId::Python)]),
        };
        let file_metrics = HashMap::from([
            (
                PathBuf::from("a.py"),
                FileMetrics {
                    ast_node_count: usize::MAX,
                    symbol_count: 0,
                    import_count: 0,
                    reference_count: 0,
                },
            ),
            (
                PathBuf::from("b.py"),
                FileMetrics {
                    ast_node_count: usize::MAX,
                    symbol_count: 0,
                    import_count: 0,
                    reference_count: 0,
                },
            ),
        ]);

        let metrics = compute_pod_metrics(&partition, &file_metrics, &graph);
        assert_eq!(metrics.len(), 1);
        assert_eq!(
            metrics[0].total_ast_nodes,
            usize::MAX,
            "the aggregate must saturate, not overflow"
        );
    }
}