weavatrix-rust 2.16.0

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

use crate::engine::RepositoryState;
use blazingly_json::{Value, json};
use std::collections::BTreeSet;

pub(super) struct WalkSpec<'a> {
    pub start: &'a str,
    pub depth: usize,
    pub max_nodes: usize,
    pub offset: usize,
    pub relations: &'a [&'a str],
    pub port_kind: &'a str,
    pub owner_kinds: &'a [&'a str],
    pub incoming: bool,
    pub revision: &'a str,
}

pub(super) fn trace(state: &RepositoryState, spec: &WalkSpec<'_>) -> Value {
    let index = graph::Adjacency::build(state, spec.port_kind, spec.owner_kinds);
    let mut cycle = false;
    let mut steps = Vec::new();
    let mut reasons = BTreeSet::new();
    for relation in spec.relations {
        graph::walk_relation(
            &index,
            spec.start,
            spec.depth,
            relation,
            spec.incoming,
            spec.max_nodes,
            &mut steps,
            &mut reasons,
        );
        cycle |= graph::relation_has_cycle(&index, &steps, relation);
    }
    let total = steps.len();
    let end = spec.offset.saturating_add(spec.max_nodes).min(total);
    let page = if spec.offset > total {
        Vec::new()
    } else {
        steps[spec.offset..end].to_vec()
    };
    if end < total {
        reasons.insert("page");
    }
    json!({
        "start": spec.start,
        "steps": page,
        "cycle": cycle,
        "page": {
            "offset": spec.offset,
            "returned": page.len(),
            "total": total,
            "has_more": end < total,
            "next_cursor": (end < total).then(|| format!("v1:{end}:{}", spec.revision))
        },
        "bounds": {
            "truncated": !reasons.is_empty(),
            "found": total,
            "shown": page.len(),
            "reasons": reasons.into_iter().collect::<Vec<_>>(),
            "depth": spec.depth,
            "max_nodes": spec.max_nodes,
            "revision": spec.revision,
            "runtime": false
        }
    })
}

pub(super) fn page_offset_for(args: &Value, revision: &str) -> Result<usize, String> {
    let Some(cursor) = args.get("cursor").and_then(Value::as_str) else {
        return Ok(0);
    };
    let Some(rest) = cursor.strip_prefix("v1:") else {
        return Err(
            "cursor format is invalid; expected v1:<offset> or v1:<offset>:<revision>".to_owned(),
        );
    };
    let mut parts = rest.splitn(2, ':');
    let offset = parts
        .next()
        .ok_or_else(|| "cursor offset is invalid".to_owned())?
        .parse::<usize>()
        .map_err(|_| "cursor offset is invalid".to_owned())?;
    if let Some(bound) = parts.next()
        && bound != revision
    {
        return Err("cursor belongs to a different revision".to_owned());
    }
    Ok(offset)
}

pub(super) fn incoming_arg(args: &Value) -> Result<bool, String> {
    match args.get("direction").and_then(Value::as_str) {
        None | Some("outgoing") => Ok(false),
        Some("incoming") => Ok(true),
        Some(other) => Err(format!(
            "direction must be outgoing or incoming, not {other}"
        )),
    }
}