Skip to main content

weavatrix_rust/operations/
mod.rs

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