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 crate::operations::{arg_str, optional_u64, reject_unknown_arguments};
use blazingly_json::{Value, json};
use std::collections::{BTreeSet, VecDeque};
use weavatrix_graph::{Edge, EdgeKind};

const TRACE_PAGE: u64 = 100;

pub(super) fn trace(state: &RepositoryState, args: &Value) -> Result<Value, String> {
    reject_unknown_arguments(
        "n8n_trace",
        args,
        &["label", "depth", "max_nodes", "cursor"],
    )?;
    let label = arg_str(args, "label")?;
    let depth = usize::try_from(optional_u64(args, "depth")?.unwrap_or(8))
        .map_err(|_| "depth is too large")?;
    if !(1..=32).contains(&depth) {
        return Err("depth must be between 1 and 32".to_owned());
    }
    let max_nodes = usize::try_from(optional_u64(args, "max_nodes")?.unwrap_or(TRACE_PAGE))
        .map_err(|_| "max_nodes is too large")?;
    if max_nodes == 0 || max_nodes > 500 {
        return Err("max_nodes must be between 1 and 500".to_owned());
    }
    let offset = crate::operations::graph::page_offset(args)?;
    let start = state.resolve_node(label)?;
    let start_node = state.node(start)?;
    let start_id = start_node.id.clone();
    let mut cycle = false;
    let mut steps = Vec::new();
    walk_relation(
        state,
        start_id.as_str(),
        depth,
        "flows_to",
        &mut steps,
        &mut cycle,
    );
    walk_relation(
        state,
        start_id.as_str(),
        depth,
        "depends_on_output",
        &mut steps,
        &mut cycle,
    );
    walk_relation(
        state,
        start_id.as_str(),
        depth,
        "handles_error_with",
        &mut steps,
        &mut cycle,
    );
    walk_relation(
        state,
        start_id.as_str(),
        depth,
        "calls_workflow",
        &mut steps,
        &mut cycle,
    );
    let total = steps.len();
    let end = offset.saturating_add(max_nodes).min(total);
    let page = if offset > total {
        Vec::new()
    } else {
        steps[offset..end].to_vec()
    };
    Ok(json!({
        "label": label,
        "start": start_id,
        "steps": page,
        "cycle": cycle,
        "page": {
            "offset": offset,
            "returned": page.len(),
            "total": total,
            "has_more": end < total,
            "next_cursor": (end < total).then(|| format!("v1:{end}"))
        },
        "bounds": {"truncated": end < total, "runtime": false}
    }))
}

fn walk_relation(
    state: &RepositoryState,
    start: &str,
    depth: usize,
    relation: &str,
    steps: &mut Vec<Value>,
    cycle: &mut bool,
) {
    let seeds = expand_ports(state, start);
    let mut seen = BTreeSet::from([start.to_owned()]);
    let mut queue = VecDeque::new();
    for seed in seeds {
        seen.insert(seed.clone());
        queue.push_back((seed, 0_usize));
    }
    while let Some((id, hop)) = queue.pop_front() {
        if hop >= depth {
            continue;
        }
        for edge in state.graph().edges() {
            if edge.kind.as_str() != relation {
                continue;
            }
            let Some(next) = natural_next(edge, &id) else {
                continue;
            };
            if !seen.insert(next.to_owned()) {
                *cycle = true;
                continue;
            }
            steps.push(json!({
                "from": id,
                "to": next,
                "relation": edge.kind,
                "detail": edge.provenance.detail,
                "hop": hop + 1
            }));
            for extra in expand_ports(state, next) {
                if seen.insert(extra.clone()) {
                    queue.push_back((extra, hop + 1));
                }
            }
            queue.push_back((next.to_owned(), hop + 1));
        }
    }
}

fn natural_next<'a>(edge: &'a Edge, id: &str) -> Option<&'a str> {
    if edge.source.as_str() == id {
        Some(edge.target.as_str())
    } else {
        None
    }
}

fn expand_ports(state: &RepositoryState, id: &str) -> Vec<String> {
    let mut ids = vec![id.to_owned()];
    for edge in state.graph().edges() {
        if edge.kind != EdgeKind::Contains {
            continue;
        }
        if edge.source.as_str() == id {
            if state
                .graph()
                .node(edge.target.as_str())
                .is_some_and(|node| node.kind.as_str() == "n8n.port")
            {
                ids.push(edge.target.as_str().to_owned());
            }
        } else if edge.target.as_str() == id
            && state
                .graph()
                .node(edge.source.as_str())
                .is_some_and(|node| matches!(node.kind.as_str(), "n8n.node" | "n8n.workflow"))
        {
            ids.push(edge.source.as_str().to_owned());
        }
    }
    ids
}