weavatrix-rust 2.1.1

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
//! JSON rendering for build topology answers.

use super::manifests::CargoManifest;
use super::{ManifestIndex, Member, Workspace};
use crate::operations::optional_u64;
use blazingly_json::{Value, json};

const MAX_TARGETS: usize = 50;
const MAX_RUNNERS: usize = 200;

pub(super) fn report(
    args: &Value,
    workspaces: Vec<Workspace>,
    index: &ManifestIndex,
) -> Result<Value, String> {
    let max_members = usize::try_from(optional_u64(args, "max_members")?.unwrap_or(500))
        .unwrap_or(500)
        .clamp(1, 2_000);
    let workspaces_total = workspaces.len();
    let members_total = workspaces
        .iter()
        .map(|workspace| workspace.members.len())
        .sum::<usize>();
    let mut remaining = max_members;
    let rendered = workspaces
        .into_iter()
        .map(|workspace| {
            let take = remaining.min(workspace.members.len());
            remaining -= take;
            json!({
                "ecosystem": workspace.ecosystem,
                "aggregator": workspace.aggregator,
                "members_total": workspace.members.len(),
                "members": workspace.members.iter().take(take).map(member).collect::<Vec<_>>()
            })
        })
        .collect::<Vec<_>>();
    Ok(json!({
        "status": "COMPLETE",
        "workspaces": rendered,
        "workspaces_total": workspaces_total,
        "members_total": members_total,
        "members_truncated": members_total > max_members,
        "runners": runner_configs(index),
        "model": "manifest and lockfile evidence only; no build tool was executed",
        "semantic_precision": "BOUNDED_STATIC"
    }))
}

fn member(member: &Member) -> Value {
    json!({
        "name": member.name,
        "path": member.dir,
        "manifest": member.manifest,
        "targets": member.targets,
        "internal_dependencies": member.internal
    })
}

pub(super) fn script_targets(scripts: &[(String, String)]) -> Vec<Value> {
    scripts
        .iter()
        .take(MAX_TARGETS)
        .map(|(name, command)| json!({"kind": "script", "name": name, "command": command}))
        .collect()
}

pub(super) fn cargo_targets(
    parsed: &CargoManifest,
    index: &ManifestIndex,
    dir: &str,
) -> Vec<Value> {
    let mut targets = parsed
        .targets
        .iter()
        .take(MAX_TARGETS)
        .map(|(kind, name)| json!({"kind": kind, "name": name}))
        .collect::<Vec<_>>();
    let source = |suffix: &str| {
        if dir.is_empty() {
            suffix.to_owned()
        } else {
            format!("{dir}/{suffix}")
        }
    };
    if index.contains_file(&source("src/main.rs")) {
        targets.push(json!({
            "kind": "bin", "name": parsed.name, "path": "src/main.rs", "implicit": true
        }));
    }
    if index.contains_file(&source("src/lib.rs")) {
        targets.push(json!({
            "kind": "lib", "name": parsed.name, "path": "src/lib.rs", "implicit": true
        }));
    }
    targets
}

pub(super) fn pending_dependency(name: &str, scope: &str) -> Value {
    json!({"name": name, "scope": scope})
}

pub(super) fn path_dependency(name: &str, member_dir: &str, scope: &str) -> Value {
    json!({"name": name, "member": member_dir, "scope": scope})
}

pub(super) fn stamp_member(dependency: &mut Value, member_dir: &str) {
    if let Some(object) = dependency.as_object_mut() {
        object.insert("member".to_owned(), json!(member_dir));
    }
}

fn runner_configs(index: &ManifestIndex) -> Vec<Value> {
    index
        .labels()
        .iter()
        .filter_map(|path| runner_kind(path).map(|kind| json!({"path": path, "kind": kind})))
        .take(MAX_RUNNERS)
        .collect()
}

fn runner_kind(path: &str) -> Option<&'static str> {
    let normalized = path.to_ascii_lowercase();
    let file = normalized.rsplit('/').next().unwrap_or(normalized.as_str());
    let extension = std::path::Path::new(file)
        .extension()
        .and_then(|value| value.to_str())
        .unwrap_or_default();
    if normalized.contains(".github/workflows/") && matches!(extension, "yml" | "yaml") {
        return Some("github-actions");
    }
    let prefixed = [
        ("jest.config.", "jest"),
        ("vitest.config.", "vitest"),
        ("playwright.config.", "playwright"),
        ("cypress.config.", "cypress"),
        ("karma.conf", "karma"),
        (".mocharc", "mocha"),
        ("webpack.config.", "webpack"),
        ("vite.config.", "vite"),
        ("rollup.config.", "rollup"),
        ("babel.config.", "babel"),
        (".babelrc", "babel"),
    ];
    for (prefix, kind) in prefixed {
        if file.starts_with(prefix) {
            return Some(kind);
        }
    }
    if file.starts_with("tsconfig") && extension == "json" {
        return Some("typescript");
    }
    match file {
        "turbo.json" => Some("turbo"),
        "nx.json" => Some("nx"),
        "lerna.json" => Some("lerna"),
        "pnpm-workspace.yaml" => Some("pnpm-workspace"),
        "go.work" => Some("go-work"),
        "makefile" | "gnumakefile" => Some("make"),
        "justfile" => Some("just"),
        "taskfile.yml" | "taskfile.yaml" => Some("task"),
        "pom.xml" => Some("maven"),
        "build.gradle" | "build.gradle.kts" | "settings.gradle" | "settings.gradle.kts" => {
            Some("gradle")
        }
        _ => None,
    }
}