Skip to main content

weavatrix_rust_refactor/
contract.rs

1//! The frozen tool contract, embedded rather than restated.
2//!
3//! `contract/refactor-tools.v1.json` was recorded from the shipping JavaScript implementation.
4//! It is compiled into the binary and is the only source of the tool catalog, so the Rust host
5//! cannot drift from the schemas agents already depend on: a change to a name, a schema or a
6//! status has to change that file, and changing it is a contract-version decision.
7
8use blazingly_json::Value;
9use std::collections::BTreeSet;
10use std::sync::OnceLock;
11
12const FROZEN: &str = include_str!("../contract/refactor-tools.v1.json");
13
14/// One tool exactly as the contract records it.
15#[derive(Debug, Clone)]
16pub struct ToolContract {
17    /// Tool name as agents call it.
18    pub name: String,
19    /// Description shown in the catalog.
20    pub description: String,
21    /// JSON Schema for the tool's arguments.
22    pub input_schema: Value,
23}
24
25/// A status an operation is allowed to answer with.
26///
27/// An operation that needs a state outside this set is not conformant; the contract must be
28/// versioned first. This is the rule that keeps two implementations answerable to one client.
29#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
30pub struct ResultState(pub String);
31
32struct Frozen {
33    tools: Vec<ToolContract>,
34    states: BTreeSet<ResultState>,
35    catalog: Value,
36}
37
38fn frozen() -> &'static Frozen {
39    static FROZEN_ONCE: OnceLock<Frozen> = OnceLock::new();
40    FROZEN_ONCE.get_or_init(|| {
41        let parsed: Value = blazingly_json::from_str(FROZEN)
42            .expect("the embedded contract is written by the build and must parse");
43        let entries = parsed
44            .get("tools")
45            .and_then(Value::as_array)
46            .expect("contract must carry a tools array")
47            .clone();
48        let tools = entries
49            .iter()
50            .map(|tool| ToolContract {
51                name: field(tool, "name"),
52                description: field(tool, "description"),
53                input_schema: tool
54                    .get("inputSchema")
55                    .cloned()
56                    .expect("every contract tool declares an inputSchema"),
57            })
58            .collect::<Vec<_>>();
59        let mut states = BTreeSet::new();
60        if let Some(groups) = parsed.get("resultStates").and_then(Value::as_object) {
61            for (_, group) in groups {
62                for state in group.as_array().into_iter().flatten() {
63                    if let Some(text) = state.as_str() {
64                        states.insert(ResultState(text.to_owned()));
65                    }
66                }
67            }
68        }
69        Frozen {
70            tools,
71            states,
72            catalog: Value::Array(entries),
73        }
74    })
75}
76
77fn field(value: &Value, key: &str) -> String {
78    value
79        .get(key)
80        .and_then(Value::as_str)
81        .unwrap_or_default()
82        .to_owned()
83}
84
85/// Every tool the contract freezes, in contract order.
86#[must_use]
87pub fn tools() -> &'static [ToolContract] {
88    &frozen().tools
89}
90
91/// The catalog exactly as the contract records it, ready for an MCP `tools/list`.
92#[must_use]
93pub fn catalog_value() -> &'static Value {
94    &frozen().catalog
95}
96
97/// Whether `name` is one of the frozen tools.
98#[must_use]
99pub fn declares(name: &str) -> bool {
100    frozen().tools.iter().any(|tool| tool.name == name)
101}
102
103/// Whether `state` is a status the contract permits an operation to answer with.
104#[must_use]
105pub fn permits_state(state: &str) -> bool {
106    frozen().states.contains(&ResultState(state.to_owned()))
107}
108
109#[cfg(test)]
110mod tests {
111    use super::{catalog_value, declares, permits_state, tools};
112
113    #[test]
114    fn the_contract_freezes_eleven_tools() {
115        assert_eq!(tools().len(), 11);
116        for name in [
117            "rename_symbol",
118            "rename_related_symbols",
119            "apply_edit_plan",
120            "rollback_last_apply",
121            "change_signature",
122            "edit_symbol",
123            "bulk_replace",
124            "organize_imports",
125            "move_file",
126            "move_symbol",
127            "delete_readiness",
128        ] {
129            assert!(declares(name), "{name} is missing from the frozen contract");
130        }
131    }
132
133    #[test]
134    fn every_tool_carries_a_description_and_a_schema() {
135        for tool in tools() {
136            assert!(
137                !tool.description.is_empty(),
138                "{} has no description",
139                tool.name
140            );
141            assert!(
142                tool.input_schema.get("type").is_some(),
143                "{} has no input schema",
144                tool.name
145            );
146        }
147    }
148
149    #[test]
150    fn load_bearing_states_are_permitted_and_invented_ones_are_not() {
151        for state in [
152            "PREVIEW_OK",
153            "APPLIED",
154            "STALE",
155            "REPO_BUSY",
156            "ROLLBACK_INCOMPLETE",
157            "INVALID_ARGS",
158            "PARTIAL",
159            "COMPLETE",
160            "EXACT_LSP",
161        ] {
162            assert!(permits_state(state), "{state} must be a contract state");
163        }
164        assert!(!permits_state("MOSTLY_FINE"));
165        assert!(!permits_state("OK_PROBABLY"));
166    }
167
168    #[test]
169    fn the_catalog_is_the_contract_itself() {
170        let catalog = catalog_value().as_array().expect("catalog is an array");
171        assert_eq!(catalog.len(), tools().len());
172    }
173}