Skip to main content

weavatrix_rust/operations/
mod.rs

1mod architecture;
2mod build;
3mod catalog;
4mod graph;
5mod health;
6mod history;
7mod memory;
8mod semantic;
9mod source;
10mod syntax;
11mod token_budget;
12mod transport_contracts;
13mod vector;
14mod workflow;
15
16pub use catalog::{ToolDefinition, ToolProfile, catalog, catalog_for_profile};
17
18use crate::engine::{RepositoryState, Weavatrix};
19use blazingly_json::{Value, json};
20
21/// Executes one bounded read-only repository tool.
22///
23/// # Errors
24///
25/// Returns invalid arguments, unavailable optional capabilities, or analysis
26/// failures without mutating repository source.
27#[allow(clippy::needless_pass_by_value)]
28pub fn call(weavatrix: &mut Weavatrix, name: &str, arguments: Value) -> Result<Value, String> {
29    let mut report = dispatch(weavatrix, name, &arguments)?;
30    // A budget an operation cannot apply is reported, not refused: the answer
31    // itself is never withheld.
32    token_budget::annotate_unapplied(name, &arguments, &mut report)?;
33    Ok(report)
34}
35
36fn dispatch(weavatrix: &mut Weavatrix, name: &str, arguments: &Value) -> Result<Value, String> {
37    if name == "trace_api_contract" {
38        return workflow::trace_api_cached(weavatrix, arguments);
39    }
40    let state = weavatrix.state();
41    match name {
42        "graph_stats" => Ok(graph::stats(state)),
43        "get_node" => graph::get_node(state, arguments),
44        "get_neighbors" => graph::neighbors(state, arguments),
45        "query_graph" => graph::query(state, arguments),
46        "god_nodes" => Ok(graph::hubs(state, arguments)),
47        "shortest_path" => graph::path(state, arguments),
48        "get_dependents" => graph::dependents(state, arguments),
49        "change_impact" => workflow::change_impact(state, arguments),
50        "map_stacktrace" => workflow::map_stacktrace(state, arguments),
51        "select_tests" => workflow::select_tests(state, arguments),
52        "git_history" => history::history(state, arguments),
53        "cross_repo_git" => history::cross_repo(state, arguments),
54        "verified_change" => workflow::verified_change(state, arguments),
55        "get_community" | "list_communities" => graph::communities(state, arguments),
56        "search_code" => source::search(state, arguments),
57        "read_source" => source::read_source(state, arguments),
58        "inspect_symbol" => source::inspect(state, arguments),
59        "context_bundle" => source::context(state, arguments),
60        "find_duplicates" => health::duplicates(state, arguments),
61        "find_dead_code" => health::dead_code(state, arguments),
62        "run_audit" => health::audit(state, arguments),
63        "coverage_map" => health::coverage(state, arguments),
64        "hot_path_review" => health::hot_paths(state, arguments),
65        "module_map" => graph::module_map(state, arguments),
66        "build_graph" => build::build_graph(state, arguments),
67        "list_endpoints" => graph::endpoints(state, arguments),
68        "trace_endpoint" => graph::trace_endpoint(state, arguments),
69        "graph_diff" => history::graph_diff(state, arguments),
70        "get_architecture_contract" => architecture::contract(state, arguments),
71        "prepare_change" => architecture::prepare(state, arguments),
72        "verify_architecture" => architecture::verify(state),
73        "explain_architecture_violation" => architecture::explain(state, arguments),
74        "propose_architecture_exception" => architecture::propose_exception(state, arguments),
75        "semantic_link" => semantic::semantic_link(state, arguments),
76        "vector_search" => vector::search(arguments),
77        "seo_link_suggestions" => semantic::seo_links(state, arguments),
78        "memory_context" => memory::context(state, arguments),
79        "rebuild_graph" => {
80            let before = graph::stats(state);
81            weavatrix.rebuild().map_err(|error| error.to_string())?;
82            Ok(json!({"before": before, "after": graph::stats(weavatrix.state())}))
83        }
84        "open_repo" => {
85            let path = arg_str(arguments, "path")?.to_owned();
86            let should_build = arg_bool(arguments, "build").unwrap_or(true);
87            let graph_built = weavatrix
88                .open_repository_with_build(&path, should_build)
89                .map_err(|error| error.to_string())?;
90            Ok(json!({
91                "repository": weavatrix.state().root(),
92                "built": graph_built,
93                "graph": graph::stats(weavatrix.state())
94            }))
95        }
96        "list_known_repos" => Ok(json!({
97            "repositories": weavatrix.known_roots().collect::<Vec<_>>()
98        })),
99        _ => Err(format!("unknown tool: {name}")),
100    }
101}
102
103fn arg_value<'value, T>(
104    args: &'value Value,
105    key: &str,
106    expected: &str,
107    extract: impl FnOnce(&'value Value) -> Option<T>,
108) -> Result<T, String> {
109    args.get(key)
110        .and_then(extract)
111        .ok_or_else(|| format!("{key} must be {expected}"))
112}
113
114pub(crate) fn arg_str<'value>(args: &'value Value, key: &str) -> Result<&'value str, String> {
115    arg_value(args, key, "a string", Value::as_str)
116}
117
118pub(crate) fn arg_u64(args: &Value, key: &str) -> Result<u64, String> {
119    arg_value(args, key, "a non-negative integer", Value::as_u64)
120}
121
122pub(crate) fn arg_bool(args: &Value, key: &str) -> Result<bool, String> {
123    arg_value(args, key, "a boolean", Value::as_bool)
124}
125
126pub(crate) fn optional_str<'value>(
127    args: &'value Value,
128    key: &str,
129) -> Result<Option<&'value str>, String> {
130    args.get(key)
131        .map(|value| {
132            value
133                .as_str()
134                .ok_or_else(|| format!("{key} must be a string"))
135        })
136        .transpose()
137}
138
139pub(crate) fn optional_u64(args: &Value, key: &str) -> Result<Option<u64>, String> {
140    args.get(key)
141        .map(|value| {
142            value
143                .as_u64()
144                .ok_or_else(|| format!("{key} must be a non-negative integer"))
145        })
146        .transpose()
147}
148
149pub(crate) fn optional_bool(args: &Value, key: &str) -> Result<Option<bool>, String> {
150    args.get(key)
151        .map(|value| {
152            value
153                .as_bool()
154                .ok_or_else(|| format!("{key} must be a boolean"))
155        })
156        .transpose()
157}
158
159pub(crate) fn require_graph_precision(args: &Value) -> Result<(), String> {
160    let Some(precision) = optional_str(args, "precision")? else {
161        return Ok(());
162    };
163    if precision == "graph" {
164        return Ok(());
165    }
166    Err(format!(
167        "precision '{precision}' is unsupported; this operation supports only 'graph' bounded static precision"
168    ))
169}
170
171#[cfg(any(feature = "semantic", feature = "vector"))]
172fn vector_values(value: &Value, array_error: &str) -> Result<Vec<f32>, String> {
173    value
174        .as_array()
175        .ok_or_else(|| array_error.to_owned())?
176        .iter()
177        .map(|value| {
178            let value = value
179                .as_f64()
180                .filter(|value| value.is_finite())
181                .ok_or_else(|| "vector value must be finite".to_owned())?;
182            if !(f64::from(f32::MIN)..=f64::from(f32::MAX)).contains(&value) {
183                return Err("vector value is outside finite f32 range".to_owned());
184            }
185            value
186                .to_string()
187                .parse::<f32>()
188                .map_err(|error| format!("invalid vector value: {error}"))
189        })
190        .collect()
191}
192
193/// The repository path a node's evidence comes from, if any.
194pub(crate) fn node_path(node: &weavatrix_graph::Node) -> Option<&str> {
195    node.span
196        .as_ref()
197        .map(|span| span.file.as_str())
198        .or_else(|| (node.kind == weavatrix_graph::NodeKind::File).then_some(node.label.as_str()))
199}
200
201/// Whether a node belongs in a production-first answer.
202///
203/// Every tool whose schema offers `include_classified` or `include_tests` must
204/// route through this, otherwise the parameter is advertised and ignored and
205/// the answer silently mixes test and generated evidence into production
206/// review.
207pub(crate) fn node_is_visible(state: &RepositoryState, slot: usize, args: &Value) -> bool {
208    let index = weavatrix_graph::NodeIndex::new(u32::try_from(slot).unwrap_or(u32::MAX));
209    let Some(node) = state.graph().node_at(index) else {
210        return true;
211    };
212    if node_path(node).is_some() {
213        return evidence_node_is_visible(node, args);
214    }
215    // Domain nodes such as endpoints, tables and topics carry no span: they are
216    // classified by the files that declare them, so a route declared only in a
217    // test is not part of a production-first answer.
218    let mut declared = false;
219    for edge in state.graph().incoming_at(index) {
220        let Some(source) = state.graph().node(edge.source.as_str()) else {
221            continue;
222        };
223        if node_path(source).is_none() {
224            continue;
225        }
226        declared = true;
227        if evidence_node_is_visible(source, args) {
228            return true;
229        }
230    }
231    // Repository and package nodes have no declaring file; keep them rather
232    // than hide evidence.
233    !declared
234}
235
236fn evidence_node_is_visible(node: &weavatrix_graph::Node, args: &Value) -> bool {
237    if matches!(
238        node.attributes.get("test_only"),
239        Some(weavatrix_graph::AttributeValue::Bool(true))
240    ) {
241        return args.get("include_tests").and_then(Value::as_bool) == Some(true);
242    }
243    node_path(node).is_none_or(|path| health::path_is_visible(path, args))
244}