weavatrix-rust 2.16.0

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
use super::code;
use super::detect::{self, MAX_EDGES, MAX_FILE_BYTES, MAX_NODES, Mode};
use super::flow;
use super::model::{AppRecord, Coverage, DomainBatch, DomainRecord, NodeRecord, app_key, node_key};
use super::variables;
use crate::language::yaml_doc::{self, Node};
use crate::model::Diagnostic;
use weavatrix_graph::NodeKind;

pub(super) fn decode(path: &str, raw: &str, documents: &[Node]) -> Option<DomainBatch> {
    let exports = documents
        .iter()
        .filter(|document| detect::is_export(document))
        .collect::<Vec<_>>();
    if exports.is_empty() {
        return None;
    }
    if u64::try_from(raw.len()).unwrap_or(u64::MAX) > MAX_FILE_BYTES {
        return Some(limit(
            path,
            format!("Dify YAML exceeds {MAX_FILE_BYTES} bytes"),
        ));
    }
    let mut batch = DomainBatch {
        apps: Vec::new(),
        diagnostics: Vec::new(),
        truncated: false,
    };
    for (index, document) in exports.into_iter().enumerate() {
        match app(path, raw, document, index) {
            Ok(app) => batch.apps.push(app),
            Err(diagnostic) => {
                batch.truncated = true;
                batch.diagnostics.push(diagnostic);
            }
        }
    }
    Some(batch)
}

fn app(path: &str, raw: &str, root: &Node, index: usize) -> Result<AppRecord, Diagnostic> {
    let envelope = root
        .get("app")
        .ok_or_else(|| diagnostic(path, raw, root, "missing app envelope"))?;
    let name = envelope
        .get("name")
        .and_then(Node::as_str)
        .unwrap_or("untitled")
        .to_owned();
    let mode = Mode::parse(envelope.get("mode").and_then(Node::as_str).unwrap_or(""));
    let dsl_version = root
        .get("version")
        .and_then(Node::as_str)
        .unwrap_or("")
        .to_owned();
    let key = app_key(path, &name, index);
    let span = yaml_doc::span_for(path, raw, root.range().0, root.range().1);
    let mut record = AppRecord {
        key: key.clone(),
        name,
        mode: mode.as_str().to_owned(),
        dsl_version: dsl_version.clone(),
        supported: mode.supported() && detect::known_dsl(&dsl_version),
        span: span.clone(),
        nodes: Vec::new(),
        variables: Vec::new(),
        links: Vec::new(),
        domains: Vec::new(),
        coverage: Coverage::default(),
    };
    if !mode.supported() {
        record.domains.push(domain(
            &key,
            format!("mode:{}", mode.as_str()),
            "semantics:structure_only",
            span.clone(),
        ));
    }
    if !detect::known_dsl(&dsl_version) {
        record.domains.push(domain(
            &key,
            format!("dsl:{dsl_version}"),
            "semantics:unsupported",
            span.clone(),
        ));
    }
    let Some(graph) = root
        .get("workflow")
        .and_then(|workflow| workflow.get("graph"))
    else {
        return Ok(record);
    };
    let nodes = graph.get("nodes").and_then(Node::items).unwrap_or(&[]);
    if nodes.len() > MAX_NODES {
        return Err(diagnostic(
            path,
            raw,
            root,
            format!("Dify node count exceeds {MAX_NODES}"),
        ));
    }
    for node in nodes {
        record.nodes.push(node_record(&key, path, raw, node));
    }
    let edges = graph.get("edges").and_then(Node::items).unwrap_or(&[]);
    if edges.len() > MAX_EDGES {
        return Err(diagnostic(
            path,
            raw,
            root,
            format!("Dify edge count exceeds {MAX_EDGES}"),
        ));
    }
    flow::collect(&mut record, edges, path, raw);
    variables::collect(&mut record, root, path, raw);
    code::collect(&mut record, nodes, path, raw);
    super::plugins::collect(&mut record, root, nodes, path, raw);
    record.coverage = coverage_of(&record);
    Ok(record)
}

fn node_record(app: &str, path: &str, raw: &str, node: &Node) -> NodeRecord {
    let id = node
        .get("id")
        .and_then(Node::as_str)
        .unwrap_or("")
        .to_owned();
    let data = node.get("data");
    let type_name = data
        .and_then(|data| data.get("type"))
        .and_then(Node::as_str)
        .unwrap_or("custom")
        .to_owned();
    let title = data
        .and_then(|data| data.get("title"))
        .and_then(Node::as_str)
        .unwrap_or("")
        .to_owned();
    let parent = node
        .get("parentId")
        .and_then(Node::as_str)
        .map(str::to_owned)
        .or_else(|| {
            data.and_then(|data| data.get("iteration_id"))
                .and_then(Node::as_str)
                .map(str::to_owned)
        });
    let (start, end) = node.range();
    NodeRecord {
        key: node_key(app, &id),
        id,
        title,
        type_name,
        parent,
        span: yaml_doc::span_for(path, raw, start, end),
    }
}

fn coverage_of(record: &AppRecord) -> Coverage {
    let mut coverage = record.coverage.clone();
    coverage.structure_seen = u32::try_from(record.nodes.len()).unwrap_or(u32::MAX);
    coverage.structure_accepted = coverage.structure_seen;
    coverage.semantics_seen = coverage.structure_seen;
    coverage.semantics_supported = u32::try_from(
        record
            .nodes
            .iter()
            .filter(|node| detect::known_node(&node.type_name))
            .count(),
    )
    .unwrap_or(u32::MAX);
    coverage.locations_seen = coverage.structure_seen;
    coverage.locations_exact = coverage.structure_seen;
    coverage
}

fn domain(
    owner: &str,
    name: impl Into<String>,
    relation: &str,
    span: weavatrix_graph::SourceSpan,
) -> DomainRecord {
    let relation = match relation {
        "uses_model" => super::model::uses_model(),
        "invokes_tool" => super::model::invokes_tool(),
        "queries_knowledge" => super::model::queries_knowledge(),
        "imports_plugin" => super::model::imports_plugin(),
        _ => weavatrix_graph::EdgeKind::Configures,
    };
    DomainRecord {
        owner: owner.to_owned(),
        name: name.into(),
        kind: NodeKind::Unknown,
        relation,
        span,
    }
}

fn span(path: &str, raw: &str, node: &Node) -> weavatrix_graph::SourceSpan {
    let (start, end) = node.range();
    yaml_doc::span_for(path, raw, start, end)
}

fn diagnostic(path: &str, raw: &str, node: &Node, message: impl Into<String>) -> Diagnostic {
    Diagnostic {
        code: "dify.limit".into(),
        message: message.into(),
        span: Some(span(path, raw, node)),
    }
}

fn limit(path: &str, message: String) -> DomainBatch {
    DomainBatch {
        apps: Vec::new(),
        diagnostics: vec![Diagnostic {
            code: "dify.limit".into(),
            message,
            span: Some(yaml_doc::span_for(path, "", 0, 0)),
        }],
        truncated: true,
    }
}