weavatrix-rust 2.1.1

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
use super::{
    audit::severity_at_least,
    manifests::{Declaration, duplicates, normalize, parse},
    paths::is_non_product,
    project_identity,
};
use crate::engine::RepositoryState;
use blazingly_json::{Value, json};
use import_evidence::{ImportEvidence, installed_peer_obligations};
use lexicon::{builtin, development_scope, ecosystem, languages, matches_declaration};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use weavatrix_graph::NodeKind;

#[path = "dependency_imports.rs"]
mod import_evidence;
#[path = "dependency_lexicon.rs"]
mod lexicon;

const MANIFESTS: &[&str] = &[
    "Cargo.toml",
    "package.json",
    "go.mod",
    "requirements.txt",
    "pyproject.toml",
];

pub(super) fn report(state: &RepositoryState, max: usize, filter: (bool, u8)) -> Value {
    let (enabled, min_severity) = filter;
    let declarations = declarations(state);
    let imports = ImportEvidence::collect(state);
    let direct_consumers = declarations
        .iter()
        .filter(|declaration| declaration_has_import(declaration, &imports))
        .collect::<Vec<_>>();
    let peer_obligations = installed_peer_obligations(state.root(), &direct_consumers);
    let peer_required = peer_obligations
        .iter()
        .map(|obligation| {
            (
                obligation.manifest.clone(),
                normalize("npm", &obligation.package),
            )
        })
        .collect::<BTreeSet<_>>();
    let duplicate_groups = duplicates(&declarations);
    let mut findings = missing_declaration_findings(state, &declarations, &imports);
    findings.extend(unused_declaration_findings(
        &declarations,
        &imports,
        &peer_required,
    ));
    findings.extend(duplicate_findings(&duplicate_groups));
    filter_findings(&mut findings, enabled, min_severity);
    findings.sort_by(|left, right| left["id"].as_str().cmp(&right["id"].as_str()));
    json!({
        "status": if findings.is_empty() {"PASS"} else {"REVIEW"},
        "execution": {"status": "COMPLETE"},
        "declared": declarations.len(),
        "external_imports": imports.external.values().map(BTreeSet::len).sum::<usize>(),
        "peer_obligations": peer_obligations.iter().map(|obligation| json!({
            "manifest": obligation.manifest,
            "consumer": obligation.consumer,
            "package": obligation.package,
            "evidence": obligation.evidence,
            "required": true
        })).collect::<Vec<_>>(),
        "duplicate_declarations": duplicate_groups.len(),
        "findings_total": findings.len(),
        "findings": findings.into_iter().take(max).collect::<Vec<_>>(),
        "manifest_evidence": manifest_evidence(&declarations)
    })
}

fn missing_declaration_findings(
    state: &RepositoryState,
    declarations: &[Declaration],
    imports: &ImportEvidence,
) -> Vec<Value> {
    let mut findings = Vec::new();
    for (language, packages) in &imports.external {
        let ecosystem = ecosystem(language);
        for package in packages {
            if builtin(language, package)
                || project_identity::contains(state, ecosystem, package)
                || declarations
                    .iter()
                    .any(|item| matches_declaration(item, ecosystem, package))
            {
                continue;
            }
            findings.push(json!({
                "id": format!("dependency.missing:{language}:{package}"),
                "rule": "dependency.missing_declaration",
                "category": "dependencies",
                "severity": "medium",
                "language": language,
                "package": package,
                "message": "external import has no matching supported manifest declaration"
            }));
        }
    }
    findings
}

fn unused_declaration_findings(
    declarations: &[Declaration],
    imports: &ImportEvidence,
    peer_required: &BTreeSet<(String, String)>,
) -> Vec<Value> {
    let mut findings = Vec::new();
    for declaration in declarations {
        let used = declaration_has_import(declaration, imports)
            || (declaration.ecosystem == "npm"
                && peer_required.contains(&(
                    declaration.manifest.clone(),
                    normalize("npm", &declaration.name),
                )));
        if !used && !development_scope(&declaration.scope) {
            findings.push(json!({
                "id": format!(
                    "dependency.unused:{}:{}:{}",
                    declaration.ecosystem, declaration.manifest, declaration.name
                ),
                "rule": "dependency.unused_declaration",
                "category": "dependencies",
                "severity": "low",
                "manifest": declaration.manifest,
                "package": declaration.name,
                "message": "manifest declaration has no matching static import evidence",
                "caveat": "plugins, build scripts, reflection and generated imports may be invisible"
            }));
        }
    }
    findings
}

fn duplicate_findings(duplicate_groups: &BTreeMap<String, Vec<&Declaration>>) -> Vec<Value> {
    duplicate_groups
        .values()
        .map(|items| {
            let first = items[0];
            json!({
                "id": format!(
                    "dependency.duplicate:{}:{}:{}",
                    first.ecosystem, first.manifest, first.name
                ),
                "rule": "dependency.duplicate_declaration",
                "category": "dependencies",
                "severity": "medium",
                "manifest": first.manifest,
                "package": first.name,
                "scopes": items.iter().map(|item| item.scope.as_str()).collect::<Vec<_>>()
            })
        })
        .collect()
}

fn manifest_evidence(declarations: &[Declaration]) -> Value {
    json!({
        "present": !declarations.is_empty(),
        "reason": if declarations.is_empty() {
            json!("no Cargo.toml, package.json, go.mod, requirements.txt, or pyproject.toml dependency declarations were found")
        } else {
            Value::Null
        },
        "formats": {
            "cargo": {"present": true, "scope": "Cargo.toml dependency sections"},
            "npm": {"present": true, "scope": "package.json dependency sections"},
            "go": {"present": true, "scope": "go.mod require directives"},
            "python": {"present": true, "scope": "requirements.txt and pyproject.toml dependency declarations"},
            "maven_gradle": {
                "present": false,
                "reason": "Maven and Gradle manifests are not inputs to this dependency audit contract"
            }
        }
    })
}

fn declaration_has_import(declaration: &Declaration, imports: &ImportEvidence) -> bool {
    languages(declaration.ecosystem).iter().any(|language| {
        imports.all.get(*language).is_some_and(|packages| {
            packages
                .iter()
                .any(|package| matches_declaration(declaration, declaration.ecosystem, package))
        })
    })
}

fn filter_findings(findings: &mut Vec<Value>, enabled: bool, min_severity: u8) {
    findings.retain(|finding| {
        enabled
            && finding["severity"]
                .as_str()
                .is_some_and(|severity| severity_at_least(severity, min_severity))
    });
}

fn declarations(state: &RepositoryState) -> Vec<Declaration> {
    let mut result = Vec::new();
    for relative in manifest_paths(state) {
        if manifest_is_non_product(&relative) {
            continue;
        }
        let absolute = state.root().join(Path::new(&relative));
        let Ok(metadata) = fs::metadata(&absolute) else {
            continue;
        };
        if metadata.len() > 2_000_000 {
            continue;
        }
        let Ok(text) = fs::read_to_string(&absolute) else {
            continue;
        };
        result.extend(parse(&absolute, &relative, &text));
    }
    result.sort_by(|left, right| {
        (&left.manifest, left.ecosystem, &left.name, &left.scope).cmp(&(
            &right.manifest,
            right.ecosystem,
            &right.name,
            &right.scope,
        ))
    });
    result
}

fn manifest_is_non_product(relative: &str) -> bool {
    Path::new(relative)
        .parent()
        .is_some_and(|parent| is_non_product(&parent.to_string_lossy()))
}

fn manifest_paths(state: &RepositoryState) -> BTreeSet<String> {
    let mut directories = BTreeSet::from([PathBuf::new()]);
    for node in state.graph().nodes() {
        if node.kind != NodeKind::File {
            continue;
        }
        let mut directory = Path::new(&node.label).parent();
        while let Some(value) = directory {
            directories.insert(value.to_path_buf());
            directory = value.parent();
        }
    }
    directories
        .into_iter()
        .flat_map(|directory| {
            MANIFESTS
                .iter()
                .map(move |name| directory.join(name).to_string_lossy().replace('\\', "/"))
        })
        .collect()
}