weavatrix-rust 2.16.0

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
use super::view::{self, owned_domains};
use crate::engine::RepositoryState;
use blazingly_json::{Value, json};
use weavatrix_graph::Node;

pub(super) fn schema_change(
    state: &RepositoryState,
    name: &str,
    before: &Node,
    after: &Node,
    transforms: &[&Node],
) -> Value {
    let left = owned_domains(state, before.id.as_str());
    let right = owned_domains(state, after.id.as_str());
    let package = field(&right, "package:").or_else(|| field(&left, "package:"));
    let catalog = field(&right, "catalog:").or_else(|| field(&left, "catalog:"));
    let added_required = added_csv(
        &field(&left, "required:").unwrap_or_default(),
        &field(&right, "required:").unwrap_or_default(),
    );
    let transform = scoped_transform(
        state,
        name,
        transforms,
        "exposure:",
        package.as_deref(),
        catalog.as_deref(),
    );
    let linked = transform.or_else(|| {
        scoped_transform(
            state,
            name,
            transforms,
            "upstream:",
            package.as_deref(),
            catalog.as_deref(),
        )
    });
    let filled = transform
        .as_ref()
        .map(|item| injected(state, item))
        .unwrap_or_default();
    let remaining = added_required
        .iter()
        .filter(|item| !filled.iter().any(|inject| inject == *item))
        .cloned()
        .collect::<Vec<_>>();
    let desc_changed = field(&left, "description:") != field(&right, "description:");
    let (compatibility, checked, premises) = decide(&left, &right, &remaining, desc_changed);
    json!({
        "tool": name,
        "change": "schema",
        "compatibility": compatibility,
        "direction": "before-request-accepted-by-after",
        "added_required": added_required,
        "adapter_fills": filled,
        "breaking_for_exposure": remaining,
        "description_changed": desc_changed,
        "checked": checked,
        "premises": premises,
        "transform": linked.map(|node| node.label.clone()),
        "witness": after.span
    })
}

fn decide(
    left: &[Value],
    right: &[Value],
    remaining: &[String],
    desc_changed: bool,
) -> (&'static str, Vec<&'static str>, Vec<&'static str>) {
    let checked = vec![
        "required",
        "property_types",
        "enum",
        "bounds",
        "additionalProperties",
        "supported_subset",
    ];
    if flagged(left) || flagged(right) {
        return (
            "undetermined",
            checked,
            vec!["unsupported or unresolved schema construct is present"],
        );
    }
    if breaking(left, right) || !remaining.is_empty() {
        return (
            "proven-incompatible",
            checked,
            vec!["a supported-subset change rejects a previously valid request"],
        );
    }
    if desc_changed {
        return (
            "description-only",
            checked,
            vec!["description is not part of request acceptance"],
        );
    }
    (
        "proven-compatible",
        checked,
        vec!["supported subset is backward-compatible for existing requests"],
    )
}

fn flagged(domains: &[Value]) -> bool {
    domains.iter().any(|item| {
        item["name"].as_str().is_some_and(|name| {
            name == "schema:unresolved_ref" || name.starts_with("schema:unsupported:")
        })
    })
}

fn breaking(left: &[Value], right: &[Value]) -> bool {
    type_changed(left, right)
        || property_removed(left, right)
        || enum_restricted(left, right)
        || bounds_tightened(left, right)
        || additional_restricted(left, right)
}

fn type_changed(left: &[Value], right: &[Value]) -> bool {
    prefixed(left, "type:").into_iter().any(|(name, before)| {
        prefixed(right, "type:")
            .into_iter()
            .any(|(other, after)| other == name && after != before)
    })
}

fn property_removed(left: &[Value], right: &[Value]) -> bool {
    let before = field(left, "properties:").unwrap_or_default();
    let after = field(right, "properties:").unwrap_or_default();
    before
        .split(',')
        .filter(|item| !item.is_empty())
        .any(|name| !after.split(',').any(|item| item == name))
}

fn enum_restricted(left: &[Value], right: &[Value]) -> bool {
    prefixed(left, "enum:").into_iter().any(|(name, before)| {
        prefixed(right, "enum:").into_iter().any(|(other, after)| {
            other == name
                && before
                    .split(',')
                    .filter(|item| !item.is_empty())
                    .any(|value| !after.split(',').any(|item| item == value))
        })
    })
}

fn bounds_tightened(left: &[Value], right: &[Value]) -> bool {
    prefixed(left, "bound:").into_iter().any(|(name, before)| {
        prefixed(right, "bound:")
            .into_iter()
            .any(|(other, after)| other == name && tighter(&before, &after))
    }) || prefixed(right, "bound:").into_iter().any(|(name, _)| {
        !prefixed(left, "bound:")
            .iter()
            .any(|(other, _)| other == &name)
    })
}

fn additional_restricted(left: &[Value], right: &[Value]) -> bool {
    let before = field(left, "additionalProperties:").unwrap_or_else(|| "absent".into());
    let after = field(right, "additionalProperties:").unwrap_or_else(|| "absent".into());
    matches!(
        (before.as_str(), after.as_str()),
        ("true" | "absent", "false")
    )
}

fn tighter(before: &str, after: &str) -> bool {
    matches!(
        (int_part(before, "min:"), int_part(after, "min:")),
        (Some(old), Some(new)) if new > old
    ) || matches!(
        (int_part(before, "max:"), int_part(after, "max:")),
        (Some(old), Some(new)) if new < old
    )
}

fn int_part(spec: &str, key: &str) -> Option<i64> {
    spec.split(',')
        .find_map(|part| part.strip_prefix(key))
        .and_then(|part| part.parse().ok())
}

fn scoped_transform<'a>(
    state: &'a RepositoryState,
    name: &str,
    transforms: &'a [&Node],
    prefix: &str,
    package: Option<&str>,
    catalog: Option<&str>,
) -> Option<&'a Node> {
    let expected = format!("{prefix}{name}");
    transforms.iter().copied().find(|node| {
        let domains = owned_domains(state, node.id.as_str());
        same_scope(&domains, package, catalog)
            && domains
                .iter()
                .any(|item| item["name"].as_str() == Some(expected.as_str()))
    })
}

fn same_scope(domains: &[Value], package: Option<&str>, catalog: Option<&str>) -> bool {
    package.is_none_or(|expected| field(domains, "package:").as_deref() == Some(expected))
        && catalog.is_none_or(|expected| field(domains, "catalog:").as_deref() == Some(expected))
}

fn injected(state: &RepositoryState, transform: &Node) -> Vec<String> {
    owned_domains(state, transform.id.as_str())
        .iter()
        .filter_map(|item| {
            item["name"]
                .as_str()
                .and_then(|name| name.strip_prefix("inject:"))
                .map(ToOwned::to_owned)
        })
        .collect()
}

fn prefixed(domains: &[Value], prefix: &str) -> Vec<(String, String)> {
    domains
        .iter()
        .filter_map(|item| {
            item["name"]
                .as_str()
                .and_then(|name| name.strip_prefix(prefix))
                .and_then(|rest| rest.split_once('='))
                .map(|(name, value)| (name.to_owned(), value.to_owned()))
        })
        .collect()
}

pub(super) fn field(domains: &[Value], prefix: &str) -> Option<String> {
    domains.iter().find_map(|item| {
        item["name"]
            .as_str()
            .and_then(|name| name.strip_prefix(prefix))
            .map(ToOwned::to_owned)
    })
}

fn added_csv(before: &str, after: &str) -> Vec<String> {
    after
        .split(',')
        .filter(|item| !item.is_empty() && !before.split(',').any(|old| old == *item))
        .map(ToOwned::to_owned)
        .collect()
}

pub(super) fn selected_consumers(
    state: &RepositoryState,
    names: &[&str],
    max: usize,
) -> Vec<Value> {
    state
        .graph()
        .nodes()
        .iter()
        .filter(|node| node.kind.as_str() == "agent.skill")
        .filter(|node| {
            owned_domains(state, node.id.as_str()).iter().any(|item| {
                item["name"].as_str().is_some_and(|label| {
                    names
                        .iter()
                        .any(|name| label == format!("declared_allowed_tools:{name}"))
                })
            })
        })
        .take(max)
        .map(view::selected)
        .collect()
}