weavatrix-rust 2.16.1

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
mod view;
mod walk;

use crate::engine::RepositoryState;
use crate::operations::{arg_str, optional_str, optional_u64, reject_unknown_arguments};
use blazingly_json::{Value, json};
use weavatrix_graph::Node;

pub(super) fn inventory(state: &RepositoryState, args: &Value) -> Result<Value, String> {
    reject_unknown_arguments("n8n_inventory", args, &["path", "max_results"])?;
    let path = optional_str(args, "path")?;
    let max = usize::try_from(optional_u64(args, "max_results")?.unwrap_or(200))
        .map_err(|_| "max_results is too large")?;
    if max == 0 || max > 500 {
        return Err("max_results must be between 1 and 500".to_owned());
    }
    let all = state
        .graph()
        .nodes()
        .iter()
        .filter(|node| node.kind.as_str() == "n8n.workflow")
        .filter(|node| path.is_none_or(|filter| view::in_path(node, filter)))
        .collect::<Vec<_>>();
    let found = all.len();
    let workflows = all
        .into_iter()
        .take(max)
        .map(|node| view::workflow_row(state, node))
        .collect::<Vec<_>>();
    let decode_cut = state
        .snapshot()
        .diagnostics
        .iter()
        .any(|item| item.code == "n8n.truncated" || item.code == "n8n.limit");
    let page_cut = found > workflows.len();
    let mut reasons = Vec::new();
    if page_cut {
        reasons.push("max_results");
    }
    if decode_cut {
        reasons.push("decode");
    }
    Ok(json!({
        "workflows": workflows,
        "coverage": view::coverage_from(state, path),
        "bounds": {
            "truncated": page_cut || decode_cut,
            "found": found,
            "shown": workflows.len(),
            "reasons": reasons,
            "revision": state.snapshot().revision,
            "runtime": false
        },
        "diagnostics": state
            .snapshot()
            .diagnostics
            .iter()
            .filter(|item| item.code.starts_with("n8n."))
            .collect::<Vec<_>>()
    }))
}

pub(super) fn trace(state: &RepositoryState, args: &Value) -> Result<Value, String> {
    walk::trace(state, args)
}

pub(super) fn context(state: &RepositoryState, args: &Value) -> Result<Value, String> {
    reject_unknown_arguments("n8n_context", args, &["label", "task", "max_related"])?;
    let label = arg_str(args, "label")?;
    let task = optional_str(args, "task")?.unwrap_or("inspect");
    let max_related = usize::try_from(optional_u64(args, "max_related")?.unwrap_or(24))
        .map_err(|_| "max_related is too large")?;
    if max_related == 0 || max_related > 200 {
        return Err("max_related must be between 1 and 200".to_owned());
    }
    let index = state.resolve_node(label)?;
    let node = state.node(index)?;
    let domains = view::owned_domains(state, node.id.as_str())
        .into_iter()
        .filter(|item| !view::secret_domain(item))
        .collect::<Vec<_>>();
    let (dependencies, dep_cut) = take_related(
        domains.iter().filter(|item| {
            matches!(
                item["relation"].as_str(),
                Some("depends_on_output" | "reads_field" | "calls_workflow" | "configured_with")
            )
        }),
        max_related,
    );
    let (expressions, expr_cut) = take_related(
        domains.iter().filter(|item| {
            item["relation"].as_str() == Some("reads_field")
                || item["name"]
                    .as_str()
                    .is_some_and(|name| name.contains('.') || name.starts_with('$'))
        }),
        max_related,
    );
    let mut unknown = vec!["runtime response shape is not provided".to_owned()];
    for item in &domains {
        if item["name"]
            .as_str()
            .is_some_and(|name| name.starts_with("unresolved:") || name.contains(":dynamic"))
        {
            unknown.push(item["name"].as_str().unwrap_or("unresolved").to_owned());
        }
        if item["name"].as_str() == Some("semantics:unsupported")
            || item["name"].as_str() == Some("semantics:structure_only")
        {
            unknown.push(item["name"].as_str().unwrap_or("limited").to_owned());
        }
    }
    unknown.sort();
    unknown.dedup();
    let consumers = if impact_task(task) {
        consumers(state, node, max_related)
    } else {
        Vec::new()
    };
    let fragments = selected_fragments(state, node);
    let truncated = dep_cut || expr_cut || consumers.len() == max_related && impact_task(task);
    Ok(json!({
        "selected": {
            "id": node.id,
            "label": node.label,
            "kind": node.kind,
            "span": node.span
        },
        "task": task,
        "dependencies": dependencies,
        "expressions": expressions,
        "consumers": consumers,
        "fragments": fragments,
        "gaps": unknown,
        "coverage": view::coverage_from(state, node.span.as_ref().map(|span| span.file.as_str())),
        "bounds": {
            "truncated": truncated,
            "found": dependencies.len() + expressions.len() + consumers.len(),
            "shown": dependencies.len() + expressions.len() + consumers.len(),
            "reasons": if truncated { vec!["max_related"] } else { Vec::<&str>::new() },
            "revision": state.snapshot().revision,
            "complete": !truncated && unknown.len() == 1,
            "runtime": false
        }
    }))
}

fn take_related<'a>(items: impl Iterator<Item = &'a Value>, max: usize) -> (Vec<Value>, bool) {
    let all = items.cloned().collect::<Vec<_>>();
    let cut = all.len() > max;
    (all.into_iter().take(max).collect(), cut)
}

fn impact_task(task: &str) -> bool {
    let lower = task.to_ascii_lowercase();
    ["impact", "change", "delete", "consum", "rename", "remove"]
        .iter()
        .any(|marker| lower.contains(marker))
}

fn consumers(state: &RepositoryState, node: &Node, max: usize) -> Vec<Value> {
    state
        .graph()
        .edges()
        .iter()
        .filter(|edge| {
            edge.target.as_str() == node.id.as_str()
                && matches!(
                    edge.kind.as_str(),
                    "flows_to" | "depends_on_output" | "reads_field" | "calls_workflow"
                )
        })
        .filter_map(|edge| {
            let source = state.graph().node(edge.source.as_str())?;
            Some(json!({
                "id": source.id,
                "label": source.label,
                "kind": source.kind,
                "relation": edge.kind,
                "span": source.span
            }))
        })
        .take(max)
        .collect()
}

fn selected_fragments(state: &RepositoryState, node: &Node) -> Vec<Value> {
    if node.span.is_none() {
        return Vec::new();
    }
    let Ok(source) = crate::operations::source::read_source(
        state,
        &json!({
            "label": node.id.as_str(),
            "before": 1,
            "after": 12
        }),
    ) else {
        return Vec::new();
    };
    let lines = source["lines"]
        .as_array()
        .into_iter()
        .flatten()
        .filter_map(|line| {
            let text = line["text"].as_str()?;
            if looks_secret_line(text) {
                return Some(json!({
                    "line": line["line"],
                    "text": "[redacted]"
                }));
            }
            Some(line.clone())
        })
        .take(16)
        .collect::<Vec<_>>();
    if lines.is_empty() {
        Vec::new()
    } else {
        vec![json!({
            "path": source["path"],
            "start_line": source["start_line"],
            "lines": lines
        })]
    }
}

fn looks_secret_line(text: &str) -> bool {
    let lower = text.to_ascii_lowercase();
    lower.contains("password")
        || lower.contains("token")
        || lower.contains("secret")
        || lower.contains("api_key")
        || lower.contains("apikey")
        || lower.contains("authorization")
        || (lower.contains("://") && lower.contains('@'))
}