Skip to main content

synapse_application/
catalog.rs

1use std::{collections::BTreeMap, sync::OnceLock};
2
3use serde::de::DeserializeOwned;
4use serde_json::{Map, Value, json};
5use soma_ops::{DiagnosticCode, OperationName, OperationSpec};
6
7use crate::{
8    CompatibilityError, DiagnosticProjection, LegacyOperationBinding, LegacyTool,
9    binding::LegacyBindingKey,
10    schema::{
11        CanonicalBundle, DiagnosticBundle, LegacyBundle, OperationSchemaContract, ParameterBundle,
12        ResultBundle, build_parameter_schemas, build_result_schemas,
13    },
14};
15
16const LEGACY: &str = include_str!(concat!(
17    env!("CARGO_MANIFEST_DIR"),
18    "/../../../docs/unify/03-contracts/examples/synapse-operations.json"
19));
20const CANONICAL: &str = include_str!(concat!(
21    env!("CARGO_MANIFEST_DIR"),
22    "/../../../docs/unify/03-contracts/examples/synapse-canonical-operations.json"
23));
24const PARAMETERS: &str = include_str!(concat!(
25    env!("CARGO_MANIFEST_DIR"),
26    "/../../../docs/unify/03-contracts/examples/synapse-operation-parameters.json"
27));
28const RESULTS: &str = include_str!(concat!(
29    env!("CARGO_MANIFEST_DIR"),
30    "/../../../docs/unify/03-contracts/examples/synapse-operation-results.json"
31));
32const DIAGNOSTICS: &str = include_str!(concat!(
33    env!("CARGO_MANIFEST_DIR"),
34    "/../../../docs/unify/03-contracts/examples/operation-diagnostic-projections.json"
35));
36const EXPECTED_OPERATIONS: usize = 59;
37const EXPECTED_DIAGNOSTICS: usize = 33;
38
39/// Embedded canonical Synapse catalog and product compatibility registry.
40pub struct SynapseCatalog {
41    operations: BTreeMap<OperationName, OperationSpec>,
42    bindings: Vec<LegacyOperationBinding>,
43    binding_index: BTreeMap<LegacyBindingKey, usize>,
44    parameter_schemas: BTreeMap<OperationName, OperationSchemaContract>,
45    result_schemas: BTreeMap<OperationName, OperationSchemaContract>,
46    diagnostics: BTreeMap<DiagnosticCode, DiagnosticProjection>,
47}
48
49impl SynapseCatalog {
50    /// Returns the process-wide checked-in catalog.
51    #[must_use]
52    pub fn embedded() -> &'static Self {
53        static CATALOG: OnceLock<SynapseCatalog> = OnceLock::new();
54        CATALOG.get_or_init(|| {
55            Self::try_from_embedded().expect("checked-in Synapse contracts must remain valid")
56        })
57    }
58
59    /// Parses and cross-validates every checked-in compatibility artifact.
60    pub fn try_from_embedded() -> Result<Self, CompatibilityError> {
61        let canonical: CanonicalBundle = parse("synapse-canonical-operations.json", CANONICAL)?;
62        let legacy: LegacyBundle = parse("synapse-operations.json", LEGACY)?;
63        let parameters: ParameterBundle = parse("synapse-operation-parameters.json", PARAMETERS)?;
64        let results: ResultBundle = parse("synapse-operation-results.json", RESULTS)?;
65        let diagnostics: DiagnosticBundle =
66            parse("operation-diagnostic-projections.json", DIAGNOSTICS)?;
67
68        if canonical.operations.len() != EXPECTED_OPERATIONS
69            || legacy.operations.len() != EXPECTED_OPERATIONS
70            || parameters.schemas.len() != EXPECTED_OPERATIONS
71            || results.schemas.len() != EXPECTED_OPERATIONS
72        {
73            return contract_error("catalog", "expected 59 records in every operation artifact");
74        }
75        if diagnostics.mappings.len() != EXPECTED_DIAGNOSTICS {
76            return contract_error("catalog", "expected 33 diagnostic projections");
77        }
78        for digest in [
79            &parameters.classification_sha256,
80            &results.classification_sha256,
81            &diagnostics.classification_sha256,
82        ] {
83            if digest != &canonical.classification_sha256 {
84                return contract_error("catalog", "classification digest mismatch");
85            }
86        }
87
88        let mut operations = BTreeMap::new();
89        for operation in canonical.operations {
90            operation
91                .validate()
92                .map_err(|error| CompatibilityError::EmbeddedContract {
93                    artifact: "synapse-canonical-operations.json",
94                    message: format!("{}: {error}", operation.name()),
95                })?;
96            if operations
97                .insert(operation.name().clone(), operation)
98                .is_some()
99            {
100                return contract_error("catalog", "duplicate canonical operation");
101            }
102        }
103
104        let mut bindings = Vec::with_capacity(EXPECTED_OPERATIONS);
105        let mut binding_index = BTreeMap::new();
106        for binding in legacy.operations {
107            let operation = operations.get(binding.canonical_name()).ok_or_else(|| {
108                CompatibilityError::EmbeddedContract {
109                    artifact: "synapse-operations.json",
110                    message: format!(
111                        "binding {} targets missing {}",
112                        binding.legacy_name(),
113                        binding.canonical_name()
114                    ),
115                }
116            })?;
117            validate_binding_parity(&binding, operation)?;
118            let key = LegacyBindingKey::new(binding.tool(), binding.action(), binding.subaction());
119            let index = bindings.len();
120            if binding_index.insert(key, index).is_some() {
121                return contract_error("synapse-operations.json", "duplicate legacy routing key");
122            }
123            bindings.push(binding);
124        }
125
126        let parameter_schemas = build_parameter_schemas(parameters, &operations)?;
127        let result_schemas = build_result_schemas(results, &operations)?;
128        let mut projected = BTreeMap::new();
129        for projection in diagnostics.mappings {
130            if projected
131                .insert(projection.code().clone(), projection)
132                .is_some()
133            {
134                return contract_error(
135                    "operation-diagnostic-projections.json",
136                    "duplicate diagnostic code",
137                );
138            }
139        }
140        for operation in operations.values() {
141            for code in operation.diagnostic_codes() {
142                if !projected.contains_key(code) {
143                    return contract_error(
144                        "operation-diagnostic-projections.json",
145                        &format!("missing projection for {code}"),
146                    );
147                }
148            }
149        }
150
151        Ok(Self {
152            operations,
153            bindings,
154            binding_index,
155            parameter_schemas,
156            result_schemas,
157            diagnostics: projected,
158        })
159    }
160
161    /// Returns the number of canonical operations.
162    #[must_use]
163    pub fn operation_count(&self) -> usize {
164        self.operations.len()
165    }
166
167    /// Returns the number of legacy bindings.
168    #[must_use]
169    pub fn binding_count(&self) -> usize {
170        self.bindings.len()
171    }
172
173    /// Returns the number of stable diagnostic projections.
174    #[must_use]
175    pub fn diagnostic_count(&self) -> usize {
176        self.diagnostics.len()
177    }
178
179    /// Iterates over canonical operation specifications.
180    pub fn operations(&self) -> impl Iterator<Item = &OperationSpec> {
181        self.operations.values()
182    }
183
184    /// Iterates over all product-owned legacy bindings.
185    pub fn bindings(&self) -> impl Iterator<Item = &LegacyOperationBinding> {
186        self.bindings.iter()
187    }
188
189    /// Returns a canonical operation specification.
190    #[must_use]
191    pub fn operation(&self, name: &OperationName) -> Option<&OperationSpec> {
192        self.operations.get(name)
193    }
194
195    /// Resolves a Flux or Scout route to its product-owned binding.
196    #[must_use]
197    pub fn binding(
198        &self,
199        tool: LegacyTool,
200        action: &str,
201        subaction: Option<&str>,
202    ) -> Option<&LegacyOperationBinding> {
203        let direct = LegacyBindingKey::new(tool, action, subaction);
204        let shared = LegacyBindingKey::new(LegacyTool::Both, action, subaction);
205        self.binding_index
206            .get(&direct)
207            .or_else(|| self.binding_index.get(&shared))
208            .map(|index| &self.bindings[*index])
209    }
210
211    /// Returns a canonical parameter schema.
212    #[must_use]
213    pub fn parameter_schema(&self, operation: &OperationName) -> Option<&OperationSchemaContract> {
214        self.parameter_schemas.get(operation)
215    }
216
217    /// Returns a canonical result schema.
218    #[must_use]
219    pub fn result_schema(&self, operation: &OperationName) -> Option<&OperationSchemaContract> {
220        self.result_schemas.get(operation)
221    }
222
223    /// Returns a global diagnostic surface projection.
224    #[must_use]
225    pub fn diagnostic_projection(&self, code: &DiagnosticCode) -> Option<&DiagnosticProjection> {
226        self.diagnostics.get(code)
227    }
228
229    /// Returns bindings available through one legacy MCP tool.
230    pub fn bindings_for_tool(
231        &self,
232        tool: LegacyTool,
233    ) -> impl Iterator<Item = &LegacyOperationBinding> {
234        self.bindings
235            .iter()
236            .filter(move |binding| binding.tool() == tool || binding.tool() == LegacyTool::Both)
237    }
238
239    /// Generates the closed legacy MCP input schema for Flux or Scout.
240    #[must_use]
241    pub fn legacy_tool_schema(&self, tool: LegacyTool) -> Value {
242        let branches = self
243            .bindings_for_tool(tool)
244            .filter_map(|binding| {
245                let contract = self.parameter_schemas.get(binding.canonical_name())?;
246                let mut branch = contract.schema().clone();
247                let object = branch.as_object_mut()?;
248                object.remove("$schema");
249                object.remove("$id");
250                object.remove("title");
251                let properties = object
252                    .entry("properties")
253                    .or_insert_with(|| Value::Object(Map::new()))
254                    .as_object_mut()?;
255                properties.insert("action".into(), json!({"const": binding.action()}));
256                if let Some(subaction) = binding.subaction() {
257                    properties.insert("subaction".into(), json!({"const": subaction}));
258                }
259                let presentation = json!({"type":"string","enum":["markdown","json"]});
260                properties.insert("response_format".into(), presentation.clone());
261                properties.insert("format".into(), presentation);
262                let required = object
263                    .entry("required")
264                    .or_insert_with(|| Value::Array(Vec::new()))
265                    .as_array_mut()?;
266                required.push(Value::String("action".into()));
267                if binding.subaction().is_some() {
268                    required.push(Value::String("subaction".into()));
269                }
270                Some(branch)
271            })
272            .collect::<Vec<_>>();
273        json!({
274            "$schema": "https://json-schema.org/draft/2020-12/schema",
275            "type": "object",
276            "oneOf": branches
277        })
278    }
279}
280
281fn validate_binding_parity(
282    binding: &LegacyOperationBinding,
283    operation: &OperationSpec,
284) -> Result<(), CompatibilityError> {
285    let required = operation.required().iter().collect::<Vec<_>>();
286    let mut legacy_required = binding
287        .required_params()
288        .iter()
289        .map(String::as_str)
290        .collect::<Vec<_>>();
291    legacy_required.sort_unstable();
292    if required != legacy_required {
293        return contract_error(
294            "synapse-operations.json",
295            &format!("required field drift for {}", operation.name()),
296        );
297    }
298    let mut canonical_any = operation
299        .required_any()
300        .iter()
301        .map(|group| group.iter().map(str::to_owned).collect::<Vec<_>>())
302        .collect::<Vec<_>>();
303    canonical_any.sort();
304    let mut legacy_any = binding.required_any().to_vec();
305    for group in &mut legacy_any {
306        group.sort();
307    }
308    legacy_any.sort();
309    if canonical_any != legacy_any {
310        return contract_error(
311            "synapse-operations.json",
312            &format!("alternative field drift for {}", operation.name()),
313        );
314    }
315    Ok(())
316}
317
318fn parse<T: DeserializeOwned>(
319    artifact: &'static str,
320    input: &str,
321) -> Result<T, CompatibilityError> {
322    serde_json::from_str(input).map_err(|error| CompatibilityError::EmbeddedContract {
323        artifact,
324        message: error.to_string(),
325    })
326}
327
328pub(crate) fn contract_error<T>(
329    artifact: &'static str,
330    message: &str,
331) -> Result<T, CompatibilityError> {
332    Err(CompatibilityError::EmbeddedContract {
333        artifact,
334        message: message.to_owned(),
335    })
336}
337
338#[cfg(test)]
339#[path = "catalog_tests.rs"]
340mod tests;