weavatrix-rust 2.11.0

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
use crate::engine::RepositoryState;
use blazingly_json::{Value, json};
use std::collections::BTreeSet;
use std::fs;
use std::io::ErrorKind;

pub(super) fn load_optional(state: &RepositoryState) -> Result<Option<Value>, String> {
    let path = state.root().join(".weavatrix/architecture.json");
    let bytes = match fs::read(&path) {
        Ok(bytes) => bytes,
        Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None),
        Err(error) => return Err(format!("{}: {error}", path.display())),
    };
    let value: Value =
        blazingly_json::from_slice(&bytes).map_err(|error| format!("invalid contract: {error}"))?;
    if value.get("components").and_then(Value::as_array).is_none() {
        return Err("architecture contract has no components".to_owned());
    }
    Ok(Some(value))
}

pub(super) fn remediation() -> Value {
    json!({
        "offline_path": ".weavatrix/architecture.json",
        "next_tool": "verify_architecture"
    })
}

pub(super) fn not_configured(state: &RepositoryState, reason: &str) -> Value {
    json!({
        "state": "NOT_CONFIGURED",
        "reason": reason,
        "starter": starter(state),
        "remediation": remediation(),
        "write": "NONE"
    })
}

pub(super) fn starter(state: &RepositoryState) -> Value {
    // Components describe production modules. Documentation, CI descriptors
    // and tool configuration are inventory, not architecture, and a root-level
    // entry script is one `root` component rather than a component per file.
    let mut roots = BTreeSet::new();
    let mut root_files = BTreeSet::new();
    for node in state
        .graph()
        .nodes()
        .iter()
        .filter(|node| node.kind == weavatrix_graph::NodeKind::File)
        .filter(|node| !crate::operations::health::is_non_product(&node.label))
    {
        match node.label.split_once('/') {
            Some((first, _)) => {
                roots.insert(first.to_owned());
            }
            None => {
                root_files.insert(node.label.clone());
            }
        }
    }
    let mut components = roots
        .into_iter()
        .map(|root| json!({"id": root.replace('_', "-"), "name": root, "paths": [root]}))
        .collect::<Vec<_>>();
    if !root_files.is_empty() {
        components.push(json!({
            "id": "root",
            "name": "root",
            "paths": root_files.into_iter().collect::<Vec<_>>()
        }));
    }
    json!({
        "architectureContractV": 1,
        "name": "Derived no-regressions architecture",
        "style": "modular-components",
        "enforcement": "ratchet",
        "components": components,
        "dependencyRules": [],
        "budgets": {
            "runtimeCycles": 0,
            "maxFileLoc": 300,
            "maxFunctionLoc": 100
        },
        "exceptions": [],
        "ratchet": {"baseline": {"fingerprints": [], "metrics": {}}}
    })
}

pub(super) fn rules_for_file<'contract>(
    value: &'contract Value,
    file: &str,
) -> Vec<&'contract Value> {
    let Some(component) = component_for(value, file) else {
        return Vec::new();
    };
    value
        .get("dependencyRules")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
        .filter(|rule| {
            list_contains(rule.get("from"), component) || list_contains(rule.get("to"), component)
        })
        .collect()
}

pub(super) fn component_for<'contract>(
    value: &'contract Value,
    file: &str,
) -> Option<&'contract str> {
    value
        .get("components")?
        .as_array()?
        .iter()
        .filter_map(|component| {
            let id = component.get("id")?.as_str()?;
            let longest = component
                .get("paths")?
                .as_array()?
                .iter()
                .filter_map(Value::as_str)
                .filter(|path| file == *path || file.starts_with(&format!("{path}/")))
                .map(str::len)
                .max()?;
            Some((longest, id))
        })
        .max_by_key(|(length, _)| *length)
        .map(|(_, id)| id)
}

pub(super) fn list_contains(value: Option<&Value>, expected: &str) -> bool {
    value
        .and_then(Value::as_array)
        .is_some_and(|values| values.iter().any(|value| value.as_str() == Some(expected)))
}