Skip to main content

golem_schema/schema/tool/compatibility/
mod.rs

1// Copyright 2024-2026 Golem Cloud
2//
3// Licensed under the Golem Source License v1.1 (the "License");
4// you may not use this file except in compliance with the License.
5
6//! Pure compiler for the directional value projections at a monomorphic
7//! middleware edge. Applying these plans, including disposal and stream relay,
8//! is a runtime responsibility.
9
10use super::canonical::CanonicalSurfaceRef;
11use super::{CommandBody, ErrorCase, FlagShape, OptionShape, Tool, validation::validate_tool};
12use crate::schema::graph::{SchemaGraph, SchemaTypeDef};
13use crate::schema::metadata::TypeId;
14use crate::schema::schema_type::SchemaType;
15use crate::schema::schema_value::SchemaValue;
16use crate::schema::schema_value::{ResultValuePayload, UnionValuePayload, VariantValuePayload};
17use crate::schema::validation::subtyping::{is_assignable, is_equivalent_cross_graph};
18use crate::schema::validation::validate_value;
19use serde::{Deserialize, Serialize};
20use std::collections::{BTreeMap, HashMap, HashSet};
21
22const MAX_PROJECTION_NODES: usize = 4096;
23const MAX_PROJECTION_DEPTH: usize = 128;
24
25#[derive(
26    Clone,
27    Copy,
28    Debug,
29    Default,
30    PartialEq,
31    Eq,
32    Serialize,
33    Deserialize,
34    golem_schema_derive::IntoSchema,
35    golem_schema_derive::FromSchema,
36)]
37#[cfg_attr(
38    feature = "full",
39    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
40)]
41#[serde(rename_all = "kebab-case")]
42pub enum ToolCompatibilityMode {
43    StrictEquality,
44    #[default]
45    StructuralSubtype,
46    Nominal,
47}
48
49#[derive(
50    Clone,
51    Debug,
52    PartialEq,
53    Serialize,
54    Deserialize,
55    golem_schema_derive::IntoSchema,
56    golem_schema_derive::FromSchema,
57)]
58#[cfg_attr(
59    feature = "full",
60    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
61)]
62#[cfg_attr(feature = "full", desert(evolution()))]
63pub struct CompiledToolCompatibility {
64    pub mode: ToolCompatibilityMode,
65    pub commands: Vec<CompiledCommandCompatibility>,
66    pub warnings: Vec<ToolCompatibilityWarning>,
67}
68
69#[derive(
70    Clone,
71    Debug,
72    PartialEq,
73    Eq,
74    Serialize,
75    Deserialize,
76    golem_schema_derive::IntoSchema,
77    golem_schema_derive::FromSchema,
78)]
79#[cfg_attr(
80    feature = "full",
81    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
82)]
83#[cfg_attr(feature = "full", desert(evolution()))]
84pub struct ToolCompatibilityWarning {
85    pub path: String,
86    pub name: String,
87}
88
89#[derive(
90    Clone,
91    Debug,
92    PartialEq,
93    Serialize,
94    Deserialize,
95    golem_schema_derive::IntoSchema,
96    golem_schema_derive::FromSchema,
97)]
98#[cfg_attr(
99    feature = "full",
100    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
101)]
102#[cfg_attr(feature = "full", desert(evolution()))]
103pub struct CompiledCommandCompatibility {
104    pub expected_path: Vec<String>,
105    pub inner_path: Vec<String>,
106    pub input: ProjectionPlan,
107    pub result: Option<ProjectionPlan>,
108    pub errors: Vec<ErrorProjection>,
109    pub forward_unknown_errors: bool,
110}
111
112#[derive(
113    Clone,
114    Debug,
115    PartialEq,
116    Serialize,
117    Deserialize,
118    golem_schema_derive::IntoSchema,
119    golem_schema_derive::FromSchema,
120)]
121#[cfg_attr(
122    feature = "full",
123    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
124)]
125#[cfg_attr(feature = "full", desert(evolution()))]
126pub struct ProjectionPlan {
127    pub source_schema: SchemaGraph,
128    pub target_schema: SchemaGraph,
129    pub nodes: Vec<ProjectionNode>,
130    pub root: usize,
131}
132
133#[derive(
134    Clone,
135    Debug,
136    PartialEq,
137    Serialize,
138    Deserialize,
139    golem_schema_derive::IntoSchema,
140    golem_schema_derive::FromSchema,
141)]
142#[cfg_attr(
143    feature = "full",
144    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
145)]
146#[cfg_attr(feature = "full", desert(evolution()))]
147#[serde(tag = "kind", rename_all = "kebab-case")]
148pub enum ProjectionNode {
149    Identity,
150    DynamicChecked,
151    Record {
152        fields: Vec<RecordFieldProjection>,
153        discard: Vec<usize>,
154    },
155    Tuple {
156        elements: Vec<usize>,
157    },
158    List {
159        item: usize,
160    },
161    FixedList {
162        item: usize,
163        length: u32,
164    },
165    Map {
166        key: usize,
167        value: usize,
168    },
169    Option {
170        some: usize,
171    },
172    Result {
173        ok: Option<usize>,
174        err: Option<usize>,
175    },
176    Variant {
177        cases: Vec<CaseProjection>,
178    },
179    Enum {
180        /// Target case index for each source case index.
181        cases: Vec<usize>,
182    },
183    Flags {
184        /// Target flag index for each source flag index.
185        flags: Vec<usize>,
186    },
187    Union {
188        branches: Vec<CaseProjection>,
189    },
190    Stream {
191        item: Option<usize>,
192    },
193    Recursive {
194        node: usize,
195    },
196}
197
198#[derive(
199    Clone,
200    Debug,
201    PartialEq,
202    Serialize,
203    Deserialize,
204    golem_schema_derive::IntoSchema,
205    golem_schema_derive::FromSchema,
206)]
207#[cfg_attr(
208    feature = "full",
209    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
210)]
211#[cfg_attr(feature = "full", desert(evolution()))]
212pub struct RecordFieldProjection {
213    pub target_name: String,
214    pub source_index: Option<usize>,
215    pub plan: Option<usize>,
216    pub default: Option<SchemaValue>,
217}
218
219#[derive(
220    Clone,
221    Debug,
222    PartialEq,
223    Serialize,
224    Deserialize,
225    golem_schema_derive::IntoSchema,
226    golem_schema_derive::FromSchema,
227)]
228#[cfg_attr(
229    feature = "full",
230    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
231)]
232#[cfg_attr(feature = "full", desert(evolution()))]
233pub struct CaseProjection {
234    pub name: String,
235    pub source_index: usize,
236    pub target_index: usize,
237    pub payload: Option<usize>,
238}
239
240#[derive(
241    Clone,
242    Debug,
243    PartialEq,
244    Serialize,
245    Deserialize,
246    golem_schema_derive::IntoSchema,
247    golem_schema_derive::FromSchema,
248)]
249#[cfg_attr(
250    feature = "full",
251    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
252)]
253#[cfg_attr(feature = "full", desert(evolution()))]
254pub struct ErrorProjection {
255    pub name: String,
256    pub inner_index: usize,
257    pub expected_index: Option<usize>,
258    pub payload: Option<ProjectionPlan>,
259}
260
261#[derive(
262    Clone,
263    Debug,
264    PartialEq,
265    Eq,
266    Serialize,
267    Deserialize,
268    golem_schema_derive::IntoSchema,
269    golem_schema_derive::FromSchema,
270)]
271#[cfg_attr(
272    feature = "full",
273    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
274)]
275#[cfg_attr(feature = "full", desert(evolution()))]
276pub struct ToolCompatibilityError {
277    pub path: String,
278    pub message: String,
279}
280
281/// Runtime-independent hooks needed while applying a projection. Streams are
282/// affine, so the evaluator transfers each retained stream to `project_stream`
283/// and every stream below a discarded value to `discard_stream`. On error,
284/// `project_stream` must dispose the stream it consumed.
285pub trait ProjectionStreamHandler {
286    fn project_stream(
287        &mut self,
288        stream: crate::schema::SchemaValueStream,
289        item_plan: Option<ProjectionPlan>,
290    ) -> Result<crate::schema::SchemaValueStream, String>;
291
292    fn discard_stream(&mut self, stream: crate::schema::SchemaValueStream);
293}
294
295/// Applies a compiled directional projection, validating both boundary values.
296pub fn apply_projection(
297    plan: &ProjectionPlan,
298    value: SchemaValue,
299    streams: &mut impl ProjectionStreamHandler,
300) -> Result<SchemaValue, ToolCompatibilityError> {
301    if let Err(errors) = validate_value(&plan.source_schema, &plan.source_schema.root, &value) {
302        let error = err(
303            "source",
304            format!("value does not match source schema: {errors:?}"),
305        );
306        Evaluator { plan, streams }.discard_value(value);
307        return Err(error);
308    }
309    let mut evaluator = Evaluator { plan, streams };
310    let result = evaluator.apply(
311        plan.root,
312        &plan.source_schema.root,
313        &plan.target_schema.root,
314        value,
315    )?;
316    if let Err(errors) = validate_value(&plan.target_schema, &plan.target_schema.root, &result) {
317        let error = err(
318            "target",
319            format!("projected value does not match target schema: {errors:?}"),
320        );
321        evaluator.discard_value(result);
322        return Err(error);
323    }
324    Ok(result)
325}
326
327struct Evaluator<'a, H> {
328    plan: &'a ProjectionPlan,
329    streams: &'a mut H,
330}
331
332impl<H: ProjectionStreamHandler> Evaluator<'_, H> {
333    fn apply(
334        &mut self,
335        node: usize,
336        st: &SchemaType,
337        tt: &SchemaType,
338        value: SchemaValue,
339    ) -> Result<SchemaValue, ToolCompatibilityError> {
340        let Some(projection) = self.plan.nodes.get(node).cloned() else {
341            self.discard_value(value);
342            return Err(err("plan", format!("invalid projection node {node}")));
343        };
344        let (st, _) = resolve(&self.plan.source_schema, st);
345        let (tt, _) = resolve(&self.plan.target_schema, tt);
346        let bad = || {
347            err(
348                "value",
349                format!("value shape does not match projection node {node}"),
350            )
351        };
352        let shapes_match = match &projection {
353            ProjectionNode::Identity
354            | ProjectionNode::DynamicChecked
355            | ProjectionNode::Recursive { .. } => true,
356            ProjectionNode::Record { .. } => {
357                matches!(
358                    (st, tt),
359                    (SchemaType::Record { .. }, SchemaType::Record { .. })
360                )
361            }
362            ProjectionNode::Tuple { .. } => {
363                matches!(
364                    (st, tt),
365                    (SchemaType::Tuple { .. }, SchemaType::Tuple { .. })
366                )
367            }
368            ProjectionNode::List { .. } => {
369                matches!((st, tt), (SchemaType::List { .. }, SchemaType::List { .. }))
370            }
371            ProjectionNode::FixedList { .. } => matches!(
372                (st, tt),
373                (SchemaType::FixedList { .. }, SchemaType::FixedList { .. })
374            ),
375            ProjectionNode::Map { .. } => {
376                matches!((st, tt), (SchemaType::Map { .. }, SchemaType::Map { .. }))
377            }
378            ProjectionNode::Option { .. } => {
379                matches!(
380                    (st, tt),
381                    (SchemaType::Option { .. }, SchemaType::Option { .. })
382                )
383            }
384            ProjectionNode::Result { .. } => {
385                matches!(
386                    (st, tt),
387                    (SchemaType::Result { .. }, SchemaType::Result { .. })
388                )
389            }
390            ProjectionNode::Variant { .. } => matches!(
391                (st, tt),
392                (SchemaType::Variant { .. }, SchemaType::Variant { .. })
393            ),
394            ProjectionNode::Enum { .. } => {
395                matches!((st, tt), (SchemaType::Enum { .. }, SchemaType::Enum { .. }))
396            }
397            ProjectionNode::Flags { .. } => {
398                matches!(
399                    (st, tt),
400                    (SchemaType::Flags { .. }, SchemaType::Flags { .. })
401                )
402            }
403            ProjectionNode::Union { .. } => {
404                matches!(
405                    (st, tt),
406                    (SchemaType::Union { .. }, SchemaType::Union { .. })
407                )
408            }
409            ProjectionNode::Stream { item } => matches!(
410                (item, st, tt),
411                (
412                    Some(_),
413                    SchemaType::Stream { inner: Some(_), .. },
414                    SchemaType::Stream { inner: Some(_), .. },
415                ) | (None, SchemaType::Stream { .. }, SchemaType::Stream { .. })
416            ),
417        };
418        if !shapes_match {
419            self.discard_value(value);
420            return Err(err("plan", "projection node does not match its schemas"));
421        }
422        match projection {
423            ProjectionNode::Identity => Ok(value),
424            ProjectionNode::DynamicChecked => {
425                if let Err(errors) = validate_value(&self.plan.target_schema, tt, &value) {
426                    let error = err(
427                        "value",
428                        format!("dynamic value does not match target subtree: {errors:?}"),
429                    );
430                    self.discard_value(value);
431                    return Err(error);
432                }
433                Ok(value)
434            }
435            ProjectionNode::Recursive { node } => self.apply(node, st, tt, value),
436            ProjectionNode::Record { fields, discard } => {
437                let (SchemaType::Record { fields: sf, .. }, SchemaType::Record { fields: tf, .. }) =
438                    (st, tt)
439                else {
440                    self.discard_value(value);
441                    return Err(bad());
442                };
443                let SchemaValue::Record { fields: values } = value else {
444                    self.discard_value(value);
445                    return Err(bad());
446                };
447                let mut values = values.into_iter().map(Some).collect::<Vec<_>>();
448                let mut claimed = HashSet::new();
449                let mut discarded = HashSet::new();
450                if fields.len() != tf.len()
451                    || discard.iter().any(|&index| index >= values.len())
452                    || discard.iter().any(|&index| !discarded.insert(index))
453                    || fields.iter().enumerate().any(|(target_index, field)| {
454                        target_index >= tf.len()
455                            || match field.source_index {
456                                Some(index) => {
457                                    index >= values.len()
458                                        || field.plan.is_none()
459                                        || field.default.is_some()
460                                        || !claimed.insert(index)
461                                }
462                                None => field.plan.is_some() || field.default.is_none(),
463                            }
464                    })
465                    || discard.iter().any(|index| claimed.contains(index))
466                    || claimed.len() + discarded.len() != values.len()
467                    || sf.len() != values.len()
468                {
469                    self.discard_remaining(values);
470                    return Err(err("plan", "invalid record projection"));
471                }
472                for index in discard {
473                    self.discard_value(values[index].take().unwrap());
474                }
475                let mut output = Vec::with_capacity(fields.len());
476                for (target_index, field) in fields.into_iter().enumerate() {
477                    let value = match (field.source_index, field.plan, field.default) {
478                        (Some(source_index), Some(child), None) => match self.apply(
479                            child,
480                            &sf[source_index].body,
481                            &tf[target_index].body,
482                            values[source_index].take().unwrap(),
483                        ) {
484                            Ok(value) => value,
485                            Err(error) => {
486                                self.discard_remaining(output);
487                                self.discard_remaining(values);
488                                return Err(error);
489                            }
490                        },
491                        (None, None, Some(default)) => default,
492                        _ => unreachable!("record projection was validated"),
493                    };
494                    output.push(value);
495                }
496                Ok(SchemaValue::Record { fields: output })
497            }
498            ProjectionNode::Tuple { elements } => {
499                let (
500                    SchemaType::Tuple { elements: ss, .. },
501                    SchemaType::Tuple { elements: ts, .. },
502                    SchemaValue::Tuple { elements: values },
503                ) = (st, tt, value)
504                else {
505                    return Err(bad());
506                };
507                Ok(SchemaValue::Tuple {
508                    elements: self.sequence(elements, ss, ts, values)?,
509                })
510            }
511            ProjectionNode::List { item } | ProjectionNode::FixedList { item, .. } => {
512                let (ss, ts, values, fixed) = match (st, tt, value) {
513                    (
514                        SchemaType::List { element: ss, .. },
515                        SchemaType::List { element: ts, .. },
516                        SchemaValue::List { elements },
517                    ) => (ss.as_ref(), ts.as_ref(), elements, false),
518                    (
519                        SchemaType::FixedList { element: ss, .. },
520                        SchemaType::FixedList { element: ts, .. },
521                        SchemaValue::FixedList { elements },
522                    ) => (ss.as_ref(), ts.as_ref(), elements, true),
523                    _ => return Err(bad()),
524                };
525                let output = self.project_sequence(
526                    std::iter::repeat_n(item, values.len()).collect(),
527                    std::iter::repeat_n(ss, values.len()).collect(),
528                    std::iter::repeat_n(ts, values.len()).collect(),
529                    values,
530                )?;
531                Ok(if fixed {
532                    SchemaValue::FixedList { elements: output }
533                } else {
534                    SchemaValue::List { elements: output }
535                })
536            }
537            ProjectionNode::Map { key, value: vp } => {
538                let (
539                    SchemaType::Map {
540                        key: sk, value: sv, ..
541                    },
542                    SchemaType::Map {
543                        key: tk, value: tv, ..
544                    },
545                    SchemaValue::Map { entries },
546                ) = (st, tt, value)
547                else {
548                    return Err(bad());
549                };
550                let mut remaining = entries.into_iter().map(Some).collect::<Vec<_>>();
551                let mut output = Vec::with_capacity(remaining.len());
552                for i in 0..remaining.len() {
553                    let (k, v) = remaining[i].take().unwrap();
554                    let projected_key = match self.apply(key, sk, tk, k) {
555                        Ok(key) => key,
556                        Err(error) => {
557                            self.discard_value(v);
558                            self.discard_map_entries(output);
559                            self.discard_map_entries(remaining.into_iter().flatten());
560                            return Err(error);
561                        }
562                    };
563                    match self.apply(vp, sv, tv, v) {
564                        Ok(value) => output.push((projected_key, value)),
565                        Err(error) => {
566                            self.discard_value(projected_key);
567                            self.discard_map_entries(output);
568                            self.discard_map_entries(remaining.into_iter().flatten());
569                            return Err(error);
570                        }
571                    }
572                }
573                Ok(SchemaValue::Map { entries: output })
574            }
575            ProjectionNode::Option { some } => {
576                let (
577                    SchemaType::Option { inner: ss, .. },
578                    SchemaType::Option { inner: ts, .. },
579                    SchemaValue::Option { inner },
580                ) = (st, tt, value)
581                else {
582                    return Err(bad());
583                };
584                Ok(SchemaValue::Option {
585                    inner: inner
586                        .map(|v| self.apply(some, ss, ts, *v).map(Box::new))
587                        .transpose()?,
588                })
589            }
590            ProjectionNode::Result { ok, err: ep } => {
591                let (
592                    SchemaType::Result { spec: ss, .. },
593                    SchemaType::Result { spec: ts, .. },
594                    SchemaValue::Result(payload),
595                ) = (st, tt, value)
596                else {
597                    return Err(bad());
598                };
599                let payload = match payload {
600                    ResultValuePayload::Ok { value } => ResultValuePayload::Ok {
601                        value: self.optional_payload(
602                            ok,
603                            ss.ok.as_deref(),
604                            ts.ok.as_deref(),
605                            value,
606                        )?,
607                    },
608                    ResultValuePayload::Err { value } => ResultValuePayload::Err {
609                        value: self.optional_payload(
610                            ep,
611                            ss.err.as_deref(),
612                            ts.err.as_deref(),
613                            value,
614                        )?,
615                    },
616                };
617                Ok(SchemaValue::Result(payload))
618            }
619            ProjectionNode::Variant { cases } => {
620                let (
621                    SchemaType::Variant { cases: ss, .. },
622                    SchemaType::Variant { cases: ts, .. },
623                    SchemaValue::Variant(payload),
624                ) = (st, tt, value)
625                else {
626                    return Err(bad());
627                };
628                let case = cases
629                    .iter()
630                    .find(|c| c.source_index == payload.case as usize)
631                    .cloned();
632                let Some(case) = case else {
633                    self.discard_remaining(payload.payload.map(|value| *value));
634                    return Err(bad());
635                };
636                let (Some(source_case), Some(target_case)) =
637                    (ss.get(case.source_index), ts.get(case.target_index))
638                else {
639                    self.discard_remaining(payload.payload.map(|value| *value));
640                    return Err(err("plan", "invalid variant case projection"));
641                };
642                Ok(SchemaValue::Variant(VariantValuePayload {
643                    case: case.target_index as u32,
644                    payload: self.optional_payload(
645                        case.payload,
646                        source_case.payload.as_ref(),
647                        target_case.payload.as_ref(),
648                        payload.payload,
649                    )?,
650                }))
651            }
652            ProjectionNode::Union { branches } => {
653                let (
654                    SchemaType::Union { spec: ss, .. },
655                    SchemaType::Union { spec: ts, .. },
656                    SchemaValue::Union(payload),
657                ) = (st, tt, value)
658                else {
659                    return Err(bad());
660                };
661                let case = branches.iter().find(|c| {
662                    ss.branches
663                        .get(c.source_index)
664                        .is_some_and(|branch| branch.tag == payload.tag)
665                });
666                let Some(case) = case else {
667                    self.discard_value(*payload.body);
668                    return Err(bad());
669                };
670                let (Some(source_branch), Some(target_branch)) = (
671                    ss.branches.get(case.source_index),
672                    ts.branches.get(case.target_index),
673                ) else {
674                    self.discard_value(*payload.body);
675                    return Err(err("plan", "invalid union branch projection"));
676                };
677                let Some(child) = case.payload else {
678                    self.discard_value(*payload.body);
679                    return Err(err("plan", "union branch has no payload plan"));
680                };
681                Ok(SchemaValue::Union(UnionValuePayload {
682                    tag: target_branch.tag.clone(),
683                    body: Box::new(self.apply(
684                        child,
685                        &source_branch.body,
686                        &target_branch.body,
687                        *payload.body,
688                    )?),
689                }))
690            }
691            ProjectionNode::Enum { cases } => match value {
692                SchemaValue::Enum { case } => Ok(SchemaValue::Enum {
693                    case: *cases.get(case as usize).ok_or_else(bad)? as u32,
694                }),
695                _ => Err(bad()),
696            },
697            ProjectionNode::Flags { flags } => match value {
698                SchemaValue::Flags { bits } => {
699                    let target_len = match tt {
700                        SchemaType::Flags { flags, .. } => flags.len(),
701                        _ => return Err(bad()),
702                    };
703                    let mut out = vec![false; target_len];
704                    for (i, bit) in bits.into_iter().enumerate() {
705                        if bit {
706                            let Some(&target) = flags.get(i) else {
707                                return Err(bad());
708                            };
709                            let Some(target_bit) = out.get_mut(target) else {
710                                return Err(err("plan", "invalid target flag index"));
711                            };
712                            *target_bit = true;
713                        }
714                    }
715                    Ok(SchemaValue::Flags { bits: out })
716                }
717                _ => Err(bad()),
718            },
719            ProjectionNode::Stream { item } => match value {
720                SchemaValue::Stream(stream) => {
721                    let item_plan = match (item, st, tt) {
722                        (
723                            Some(root),
724                            SchemaType::Stream {
725                                inner: Some(ss), ..
726                            },
727                            SchemaType::Stream {
728                                inner: Some(ts), ..
729                            },
730                        ) => Some(ProjectionPlan {
731                            source_schema: graph_for(&self.plan.source_schema, ss),
732                            target_schema: graph_for(&self.plan.target_schema, ts),
733                            nodes: self.plan.nodes.clone(),
734                            root,
735                        }),
736                        (None, _, _) => {
737                            return Ok(SchemaValue::Stream(stream));
738                        }
739                        _ => {
740                            self.streams.discard_stream(stream);
741                            return Err(err(
742                                "plan",
743                                "stream item projection does not match stream schemas",
744                            ));
745                        }
746                    };
747                    self.streams
748                        .project_stream(stream, item_plan)
749                        .map(SchemaValue::Stream)
750                        .map_err(|e| err("stream", e))
751                }
752                _ => Err(bad()),
753            },
754        }
755    }
756
757    fn sequence(
758        &mut self,
759        plans: Vec<usize>,
760        ss: &[SchemaType],
761        ts: &[SchemaType],
762        values: Vec<SchemaValue>,
763    ) -> Result<Vec<SchemaValue>, ToolCompatibilityError> {
764        if plans.len() != values.len() || ss.len() != values.len() || ts.len() != values.len() {
765            self.discard_remaining(values);
766            return Err(err("value", "tuple length does not match projection"));
767        }
768        self.project_sequence(plans, ss.iter().collect(), ts.iter().collect(), values)
769    }
770    fn project_sequence(
771        &mut self,
772        plans: Vec<usize>,
773        ss: Vec<&SchemaType>,
774        ts: Vec<&SchemaType>,
775        values: Vec<SchemaValue>,
776    ) -> Result<Vec<SchemaValue>, ToolCompatibilityError> {
777        let mut remaining = values.into_iter().map(Some).collect::<Vec<_>>();
778        let mut output = Vec::with_capacity(remaining.len());
779        for i in 0..remaining.len() {
780            match self.apply(plans[i], ss[i], ts[i], remaining[i].take().unwrap()) {
781                Ok(value) => output.push(value),
782                Err(error) => {
783                    self.discard_remaining(output);
784                    self.discard_remaining(remaining);
785                    return Err(error);
786                }
787            }
788        }
789        Ok(output)
790    }
791    fn discard_remaining<T: IntoIterator<Item = V>, V: IntoRemainingValue>(&mut self, values: T) {
792        for value in values {
793            if let Some(value) = value.into_remaining_value() {
794                self.discard_value(value);
795            }
796        }
797    }
798    fn discard_map_entries(
799        &mut self,
800        entries: impl IntoIterator<Item = (SchemaValue, SchemaValue)>,
801    ) {
802        for (key, value) in entries {
803            self.discard_value(key);
804            self.discard_value(value);
805        }
806    }
807    fn optional_payload(
808        &mut self,
809        plan: Option<usize>,
810        ss: Option<&SchemaType>,
811        ts: Option<&SchemaType>,
812        value: Option<Box<SchemaValue>>,
813    ) -> Result<Option<Box<SchemaValue>>, ToolCompatibilityError> {
814        match (plan, ss, ts, value) {
815            (None, None, None, None) => Ok(None),
816            (Some(p), Some(s), Some(t), Some(v)) => self.apply(p, s, t, *v).map(Box::new).map(Some),
817            (_, _, _, value) => {
818                self.discard_remaining(value.map(|value| *value));
819                Err(err("value", "payload presence does not match projection"))
820            }
821        }
822    }
823    fn discard_value(&mut self, value: SchemaValue) {
824        match value {
825            SchemaValue::Stream(stream) => self.streams.discard_stream(stream),
826            SchemaValue::Record { fields }
827            | SchemaValue::Tuple { elements: fields }
828            | SchemaValue::List { elements: fields }
829            | SchemaValue::FixedList { elements: fields } => {
830                for v in fields {
831                    self.discard_value(v);
832                }
833            }
834            SchemaValue::Map { entries } => {
835                for (k, v) in entries {
836                    self.discard_value(k);
837                    self.discard_value(v);
838                }
839            }
840            SchemaValue::Variant(v) => {
841                if let Some(value) = v.payload {
842                    self.discard_value(*value);
843                }
844            }
845            SchemaValue::Option { inner: Some(value) } => {
846                self.discard_value(*value);
847            }
848            SchemaValue::Result(
849                ResultValuePayload::Ok { value } | ResultValuePayload::Err { value },
850            ) => {
851                if let Some(value) = value {
852                    self.discard_value(*value);
853                }
854            }
855            SchemaValue::Union(v) => self.discard_value(*v.body),
856            _ => {}
857        }
858    }
859}
860
861trait IntoRemainingValue {
862    fn into_remaining_value(self) -> Option<SchemaValue>;
863}
864
865impl IntoRemainingValue for SchemaValue {
866    fn into_remaining_value(self) -> Option<SchemaValue> {
867        Some(self)
868    }
869}
870
871impl IntoRemainingValue for Option<SchemaValue> {
872    fn into_remaining_value(self) -> Option<SchemaValue> {
873        self
874    }
875}
876
877/// Compiles `expected` middleware-facing inputs to `inner` inputs and `inner`
878/// results/errors back to `expected`. Both descriptors are validated first.
879pub fn compile_tool_compatibility(
880    expected: &Tool,
881    inner: &Tool,
882    mode: ToolCompatibilityMode,
883) -> Result<CompiledToolCompatibility, Vec<ToolCompatibilityError>> {
884    let mut errors = Vec::new();
885    if let Err(es) = validate_tool(expected) {
886        errors.extend(es.into_iter().map(|e| err("expected", e.to_string())));
887    }
888    if let Err(es) = validate_tool(inner) {
889        errors.extend(es.into_iter().map(|e| err("inner", e.to_string())));
890    }
891    if !errors.is_empty() {
892        return Err(errors);
893    }
894
895    if expected.name() != inner.name() {
896        return Err(vec![err("name", "tool names differ")]);
897    }
898
899    if mode == ToolCompatibilityMode::StrictEquality && !strictly_equal(expected, inner) {
900        return Err(vec![err("tool", "tool descriptors are not strictly equal")]);
901    }
902
903    let expected_commands = command_paths(expected);
904    let inner_commands = command_paths(inner);
905    let mut commands = Vec::new();
906    let mut warnings = Vec::new();
907    for (path, &ei) in &expected_commands {
908        let Some(&ni) = inner_commands.get(path) else {
909            errors.push(err(
910                format_path(path),
911                "inner tool does not implement command",
912            ));
913            continue;
914        };
915        let eb = expected.commands.nodes[ei]
916            .body
917            .as_ref()
918            .expect("body command");
919        let nb = inner.commands.nodes[ni]
920            .body
921            .as_ref()
922            .expect("body command");
923        if mode == ToolCompatibilityMode::StrictEquality && eb.constraints != nb.constraints {
924            errors.push(err(format_path(path), "command constraints differ"));
925            continue;
926        }
927        if mode == ToolCompatibilityMode::StructuralSubtype
928            && !nb.constraints.iter().all(|c| eb.constraints.contains(c))
929        {
930            errors.push(err(
931                format_path(path),
932                "inner command adds a constraint not guaranteed by the expected command",
933            ));
934            continue;
935        }
936        if !stream_specs_equal(&eb.stdin, &nb.stdin) {
937            errors.push(err(
938                format!("{}.stdin", format_path(path)),
939                "standard-input stream contracts differ",
940            ));
941            continue;
942        }
943        if !stream_specs_equal(&eb.stdout, &nb.stdout) {
944            errors.push(err(
945                format!("{}.stdout", format_path(path)),
946                "standard-output stream contracts differ",
947            ));
948            continue;
949        }
950        if !stream_specs_equal(&eb.stderr, &nb.stderr) {
951            errors.push(err(
952                format!("{}.stderr", format_path(path)),
953                "standard-error stream contracts differ",
954            ));
955            continue;
956        }
957        let input = compile_inputs(
958            expected,
959            ei,
960            inner,
961            ni,
962            mode,
963            path,
964            &mut errors,
965            &mut warnings,
966        );
967        let result = compile_results(expected, eb, inner, nb, mode, path, &mut errors);
968        let mapped_errors = compile_errors(expected, eb, inner, nb, mode, path, &mut errors);
969        if let Some(input) = input {
970            commands.push(CompiledCommandCompatibility {
971                expected_path: path.clone(),
972                inner_path: path.clone(),
973                input,
974                result,
975                errors: mapped_errors,
976                forward_unknown_errors: false,
977            });
978        }
979    }
980    if mode == ToolCompatibilityMode::StrictEquality {
981        for path in inner_commands.keys() {
982            if !expected_commands.contains_key(path) {
983                errors.push(err(
984                    format_path(path),
985                    "inner tool has an additional command",
986                ));
987            }
988        }
989    }
990    if errors.is_empty() {
991        Ok(CompiledToolCompatibility {
992            mode,
993            commands,
994            warnings,
995        })
996    } else {
997        Err(errors)
998    }
999}
1000
1001fn stream_specs_equal(a: &Option<super::StreamSpec>, b: &Option<super::StreamSpec>) -> bool {
1002    match (a, b) {
1003        (None, None) => true,
1004        (Some(a), Some(b)) => a.mime == b.mime && a.required == b.required,
1005        _ => false,
1006    }
1007}
1008
1009fn compile_inputs(
1010    expected: &Tool,
1011    ei: usize,
1012    inner: &Tool,
1013    ni: usize,
1014    mode: ToolCompatibilityMode,
1015    path: &[String],
1016    errors: &mut Vec<ToolCompatibilityError>,
1017    warnings: &mut Vec<ToolCompatibilityWarning>,
1018) -> Option<ProjectionPlan> {
1019    let ef = expected.canonical_input_fields(ei);
1020    let nf = inner.canonical_input_fields(ni);
1021    let defaults = surface_defaults(inner, ni);
1022    let egraph = match expected.canonical_input_record_schema(ei) {
1023        Ok(graph) => graph,
1024        Err(error) => {
1025            errors.push(err(
1026                format_path(path),
1027                format!("failed to build expected canonical input: {error}"),
1028            ));
1029            return None;
1030        }
1031    };
1032    let ngraph = match inner.canonical_input_record_schema(ni) {
1033        Ok(graph) => graph,
1034        Err(error) => {
1035            errors.push(err(
1036                format_path(path),
1037                format!("failed to build inner canonical input: {error}"),
1038            ));
1039            return None;
1040        }
1041    };
1042    if mode == ToolCompatibilityMode::StrictEquality
1043        && ef.iter().map(|f| &f.name).ne(nf.iter().map(|f| &f.name))
1044    {
1045        errors.push(err(format_path(path), "canonical input fields differ"));
1046        return None;
1047    }
1048    let mut compiler = Compiler::new(&egraph, &ngraph, mode);
1049    let mut fields = Vec::new();
1050    let mut retained = HashSet::new();
1051    for (target, default) in nf.iter().zip(defaults) {
1052        if let Some((source_index, source)) =
1053            ef.iter().enumerate().find(|(_, f)| f.name == target.name)
1054        {
1055            retained.insert(source_index);
1056            let plan = compiler.compile(
1057                &source.type_,
1058                &target.type_,
1059                &format!("{}.input.{}", format_path(path), target.name),
1060                errors,
1061            )?;
1062            fields.push(RecordFieldProjection {
1063                target_name: target.name.clone(),
1064                source_index: Some(source_index),
1065                plan: Some(plan),
1066                default: None,
1067            });
1068        } else if let Some(default) = default.or_else(|| option_none(&target.type_, &ngraph)) {
1069            fields.push(RecordFieldProjection {
1070                target_name: target.name.clone(),
1071                source_index: None,
1072                plan: None,
1073                default: Some(default),
1074            });
1075        } else {
1076            errors.push(err(
1077                format!("{}.input.{}", format_path(path), target.name),
1078                "inner-only input has no representable default",
1079            ));
1080            return None;
1081        }
1082    }
1083    let discard: Vec<_> = (0..ef.len()).filter(|i| !retained.contains(i)).collect();
1084    warnings.extend(discard.iter().map(|&index| ToolCompatibilityWarning {
1085        path: format_path(path),
1086        name: ef[index].name.clone(),
1087    }));
1088    let root = compiler.push(
1089        ProjectionNode::Record { fields, discard },
1090        &format!("{}.input", format_path(path)),
1091        errors,
1092    )?;
1093    Some(compiler.finish(egraph.clone(), ngraph.clone(), root))
1094}
1095
1096fn compile_results(
1097    expected: &Tool,
1098    eb: &CommandBody,
1099    inner: &Tool,
1100    nb: &CommandBody,
1101    mode: ToolCompatibilityMode,
1102    path: &[String],
1103    errors: &mut Vec<ToolCompatibilityError>,
1104) -> Option<ProjectionPlan> {
1105    match (&nb.result, &eb.result) {
1106        (None, None) => None,
1107        (Some(n), Some(e)) => compile_plan(
1108            &inner.schema,
1109            &n.type_,
1110            &expected.schema,
1111            &e.type_,
1112            mode,
1113            &format!("{}.result", format_path(path)),
1114            errors,
1115        ),
1116        _ => {
1117            errors.push(err(
1118                format!("{}.result", format_path(path)),
1119                "result presence differs",
1120            ));
1121            None
1122        }
1123    }
1124}
1125
1126fn compile_errors(
1127    expected: &Tool,
1128    eb: &CommandBody,
1129    inner: &Tool,
1130    nb: &CommandBody,
1131    mode: ToolCompatibilityMode,
1132    path: &[String],
1133    errors: &mut Vec<ToolCompatibilityError>,
1134) -> Vec<ErrorProjection> {
1135    if mode == ToolCompatibilityMode::StrictEquality
1136        && eb
1137            .errors
1138            .iter()
1139            .map(|e| &e.name)
1140            .ne(nb.errors.iter().map(|e| &e.name))
1141    {
1142        errors.push(err(
1143            format!("{}.errors", format_path(path)),
1144            "declared error cases differ",
1145        ));
1146        return Vec::new();
1147    }
1148    if matches!(
1149        mode,
1150        ToolCompatibilityMode::StructuralSubtype | ToolCompatibilityMode::Nominal
1151    ) {
1152        for inner_error in &nb.errors {
1153            if !eb
1154                .errors
1155                .iter()
1156                .any(|expected_error| expected_error.name == inner_error.name)
1157            {
1158                errors.push(err(
1159                    format!("{}.error.{}", format_path(path), inner_error.name),
1160                    "inner tool declares an error case absent from the expected tool",
1161                ));
1162            }
1163        }
1164    }
1165    nb.errors
1166        .iter()
1167        .enumerate()
1168        .map(|(i, n)| {
1169            let found = eb.errors.iter().enumerate().find(|(_, e)| e.name == n.name);
1170            let (expected_index, payload) = match found {
1171                Some((j, e)) => {
1172                    if mode != ToolCompatibilityMode::Nominal
1173                        && (n.kind != e.kind || n.exit_code != e.exit_code)
1174                    {
1175                        errors.push(err(
1176                            format!("{}.error.{}", format_path(path), n.name),
1177                            "error kind or exit code differs",
1178                        ));
1179                    }
1180                    (
1181                        Some(j),
1182                        compile_error_payload(inner, n, expected, e, mode, path, errors),
1183                    )
1184                }
1185                None => (None, None),
1186            };
1187            ErrorProjection {
1188                name: n.name.clone(),
1189                inner_index: i,
1190                expected_index,
1191                payload,
1192            }
1193        })
1194        .collect()
1195}
1196
1197fn compile_error_payload(
1198    source: &Tool,
1199    s: &ErrorCase,
1200    target: &Tool,
1201    t: &ErrorCase,
1202    mode: ToolCompatibilityMode,
1203    path: &[String],
1204    errors: &mut Vec<ToolCompatibilityError>,
1205) -> Option<ProjectionPlan> {
1206    match (&s.payload, &t.payload) {
1207        (None, None) => None,
1208        (Some(a), Some(b)) => compile_plan(
1209            &source.schema,
1210            a,
1211            &target.schema,
1212            b,
1213            mode,
1214            &format!("{}.error.{}", format_path(path), s.name),
1215            errors,
1216        ),
1217        _ => {
1218            errors.push(err(
1219                format!("{}.error.{}", format_path(path), s.name),
1220                "known error payload presence differs",
1221            ));
1222            None
1223        }
1224    }
1225}
1226
1227fn compile_plan(
1228    sg: &SchemaGraph,
1229    s: &SchemaType,
1230    tg: &SchemaGraph,
1231    t: &SchemaType,
1232    mode: ToolCompatibilityMode,
1233    path: &str,
1234    errors: &mut Vec<ToolCompatibilityError>,
1235) -> Option<ProjectionPlan> {
1236    if mode == ToolCompatibilityMode::Nominal {
1237        return Some(dynamic_plan(graph_for(sg, s), graph_for(tg, t)));
1238    }
1239    let mut c = Compiler::new(sg, tg, mode);
1240    let root = c.compile(s, t, path, errors)?;
1241    Some(c.finish(graph_for(sg, s), graph_for(tg, t), root))
1242}
1243
1244struct Compiler<'a> {
1245    sg: &'a SchemaGraph,
1246    tg: &'a SchemaGraph,
1247    mode: ToolCompatibilityMode,
1248    nodes: Vec<ProjectionNode>,
1249    active: HashMap<(TypeId, TypeId), usize>,
1250    completed: HashMap<(TypeId, TypeId), usize>,
1251}
1252impl<'a> Compiler<'a> {
1253    fn new(sg: &'a SchemaGraph, tg: &'a SchemaGraph, mode: ToolCompatibilityMode) -> Self {
1254        Self {
1255            sg,
1256            tg,
1257            mode,
1258            nodes: Vec::new(),
1259            active: HashMap::new(),
1260            completed: HashMap::new(),
1261        }
1262    }
1263    fn push(
1264        &mut self,
1265        n: ProjectionNode,
1266        path: &str,
1267        errors: &mut Vec<ToolCompatibilityError>,
1268    ) -> Option<usize> {
1269        if self.nodes.len() >= MAX_PROJECTION_NODES {
1270            errors.push(err(path, "projection plan exceeds node limit"));
1271            return None;
1272        }
1273        let i = self.nodes.len();
1274        self.nodes.push(n);
1275        Some(i)
1276    }
1277    fn finish(
1278        self,
1279        source_schema: SchemaGraph,
1280        target_schema: SchemaGraph,
1281        root: usize,
1282    ) -> ProjectionPlan {
1283        ProjectionPlan {
1284            source_schema,
1285            target_schema,
1286            nodes: self.nodes,
1287            root,
1288        }
1289    }
1290    fn compile(
1291        &mut self,
1292        source: &SchemaType,
1293        target: &SchemaType,
1294        path: &str,
1295        errors: &mut Vec<ToolCompatibilityError>,
1296    ) -> Option<usize> {
1297        self.compile_at(source, target, path, errors, 0)
1298    }
1299    fn compile_at(
1300        &mut self,
1301        source: &SchemaType,
1302        target: &SchemaType,
1303        path: &str,
1304        errors: &mut Vec<ToolCompatibilityError>,
1305        depth: usize,
1306    ) -> Option<usize> {
1307        if depth >= MAX_PROJECTION_DEPTH {
1308            errors.push(err(path, "projection plan exceeds depth limit"));
1309            return None;
1310        }
1311        if self.mode == ToolCompatibilityMode::Nominal {
1312            return self.push(ProjectionNode::DynamicChecked, path, errors);
1313        }
1314        if self.mode == ToolCompatibilityMode::StrictEquality
1315            && is_equivalent_cross_graph(self.sg, source, self.tg, target)
1316        {
1317            return self.push(ProjectionNode::Identity, path, errors);
1318        }
1319        let (s, sid) = resolve(self.sg, source);
1320        let (t, tid) = resolve(self.tg, target);
1321        if let (Some(a), Some(b)) = (sid, tid) {
1322            let pair = (a.clone(), b.clone());
1323            if let Some(&node) = self.completed.get(&pair) {
1324                return Some(node);
1325            }
1326            if let Some(&node) = self.active.get(&(a.clone(), b.clone())) {
1327                return self.push(ProjectionNode::Recursive { node }, path, errors);
1328            }
1329            let placeholder = self.push(ProjectionNode::Identity, path, errors)?;
1330            self.active.insert(pair.clone(), placeholder);
1331            let body = self.compile_resolved(s, t, path, errors, depth + 1)?;
1332            self.nodes[placeholder] = ProjectionNode::Recursive { node: body };
1333            self.active.remove(&pair);
1334            self.completed.insert(pair, placeholder);
1335            return Some(placeholder);
1336        }
1337        self.compile_resolved(s, t, path, errors, depth + 1)
1338    }
1339    fn compile_resolved(
1340        &mut self,
1341        s: &SchemaType,
1342        t: &SchemaType,
1343        path: &str,
1344        errors: &mut Vec<ToolCompatibilityError>,
1345        depth: usize,
1346    ) -> Option<usize> {
1347        if self.mode == ToolCompatibilityMode::StrictEquality {
1348            errors.push(err(path, "types are not strictly equal"));
1349            return None;
1350        }
1351        let node = match (s, t) {
1352            (SchemaType::Record { fields: a, .. }, SchemaType::Record { fields: b, .. }) => {
1353                let mut fields = Vec::new();
1354                let mut used = HashSet::new();
1355                for bf in b {
1356                    let Some((i, af)) = a.iter().enumerate().find(|(_, f)| f.name == bf.name)
1357                    else {
1358                        errors.push(err(
1359                            format!("{path}.{}", bf.name),
1360                            "source record lacks target field",
1361                        ));
1362                        return None;
1363                    };
1364                    used.insert(i);
1365                    let p = self.compile_at(
1366                        &af.body,
1367                        &bf.body,
1368                        &format!("{path}.{}", bf.name),
1369                        errors,
1370                        depth,
1371                    )?;
1372                    fields.push(RecordFieldProjection {
1373                        target_name: bf.name.clone(),
1374                        source_index: Some(i),
1375                        plan: Some(p),
1376                        default: None,
1377                    });
1378                }
1379                ProjectionNode::Record {
1380                    fields,
1381                    discard: (0..a.len()).filter(|i| !used.contains(i)).collect(),
1382                }
1383            }
1384            (SchemaType::Tuple { elements: a, .. }, SchemaType::Tuple { elements: b, .. })
1385                if a.len() == b.len() =>
1386            {
1387                ProjectionNode::Tuple {
1388                    elements: a
1389                        .iter()
1390                        .zip(b)
1391                        .enumerate()
1392                        .map(|(i, (x, y))| {
1393                            self.compile_at(x, y, &format!("{path}[{i}]"), errors, depth)
1394                        })
1395                        .collect::<Option<_>>()?,
1396                }
1397            }
1398            (SchemaType::List { element: a, .. }, SchemaType::List { element: b, .. }) => {
1399                ProjectionNode::List {
1400                    item: self.compile_at(a, b, &format!("{path}[]"), errors, depth)?,
1401                }
1402            }
1403            (
1404                SchemaType::FixedList {
1405                    element: a,
1406                    length: x,
1407                    ..
1408                },
1409                SchemaType::FixedList {
1410                    element: b,
1411                    length: y,
1412                    ..
1413                },
1414            ) if x == y => ProjectionNode::FixedList {
1415                item: self.compile_at(a, b, &format!("{path}[]"), errors, depth)?,
1416                length: *x,
1417            },
1418            (
1419                SchemaType::Map {
1420                    key: ak, value: av, ..
1421                },
1422                SchemaType::Map {
1423                    key: bk, value: bv, ..
1424                },
1425            ) => ProjectionNode::Map {
1426                // Key conversion must preserve equality and therefore map
1427                // uniqueness. Numeric widening and other merely assignable
1428                // scalar conversions are not safe in this position.
1429                key: if is_equivalent_cross_graph(self.sg, ak, self.tg, bk) {
1430                    self.push(ProjectionNode::Identity, &format!("{path}.key"), errors)?
1431                } else {
1432                    errors.push(err(
1433                        format!("{path}.key"),
1434                        "map key types are not equivalent",
1435                    ));
1436                    return None;
1437                },
1438                value: self.compile_at(av, bv, &format!("{path}.value"), errors, depth)?,
1439            },
1440            (SchemaType::Option { inner: a, .. }, SchemaType::Option { inner: b, .. }) => {
1441                ProjectionNode::Option {
1442                    some: self.compile_at(a, b, &format!("{path}.some"), errors, depth)?,
1443                }
1444            }
1445            (SchemaType::Result { spec: a, .. }, SchemaType::Result { spec: b, .. }) => {
1446                ProjectionNode::Result {
1447                    ok: pair(
1448                        self,
1449                        a.ok.as_deref(),
1450                        b.ok.as_deref(),
1451                        &format!("{path}.ok"),
1452                        errors,
1453                        depth,
1454                    )?,
1455                    err: pair(
1456                        self,
1457                        a.err.as_deref(),
1458                        b.err.as_deref(),
1459                        &format!("{path}.err"),
1460                        errors,
1461                        depth,
1462                    )?,
1463                }
1464            }
1465            (SchemaType::Variant { cases: a, .. }, SchemaType::Variant { cases: b, .. }) => {
1466                ProjectionNode::Variant {
1467                    cases: cases(self, a, b, path, errors, depth)?,
1468                }
1469            }
1470            (SchemaType::Enum { cases: a, .. }, SchemaType::Enum { cases: b, .. })
1471                if a.iter().all(|n| b.contains(n)) =>
1472            {
1473                ProjectionNode::Enum {
1474                    cases: a
1475                        .iter()
1476                        .map(|n| b.iter().position(|x| x == n).unwrap())
1477                        .collect(),
1478                }
1479            }
1480            (SchemaType::Flags { flags: a, .. }, SchemaType::Flags { flags: b, .. })
1481                if a.iter().all(|n| b.contains(n)) =>
1482            {
1483                ProjectionNode::Flags {
1484                    flags: a
1485                        .iter()
1486                        .map(|n| b.iter().position(|x| x == n).unwrap())
1487                        .collect(),
1488                }
1489            }
1490            (SchemaType::Union { spec: a, .. }, SchemaType::Union { spec: b, .. }) => {
1491                let ac: Vec<_> = a
1492                    .branches
1493                    .iter()
1494                    .map(|x| crate::schema::schema_type::VariantCaseType {
1495                        name: x.tag.clone(),
1496                        payload: Some(x.body.clone()),
1497                        metadata: Default::default(),
1498                    })
1499                    .collect();
1500                let bc: Vec<_> = b
1501                    .branches
1502                    .iter()
1503                    .map(|x| crate::schema::schema_type::VariantCaseType {
1504                        name: x.tag.clone(),
1505                        payload: Some(x.body.clone()),
1506                        metadata: Default::default(),
1507                    })
1508                    .collect();
1509                ProjectionNode::Union {
1510                    branches: cases(self, &ac, &bc, path, errors, depth)?,
1511                }
1512            }
1513            (SchemaType::Stream { inner: a, .. }, SchemaType::Stream { inner: b, .. }) => {
1514                if matches!((a, b), (None, None))
1515                    || matches!((a, b), (Some(a), Some(b)) if is_equivalent_cross_graph(self.sg, a, self.tg, b))
1516                {
1517                    ProjectionNode::Identity
1518                } else {
1519                    ProjectionNode::Stream {
1520                        item: pair(
1521                            self,
1522                            a.as_deref(),
1523                            b.as_deref(),
1524                            &format!("{path}.item"),
1525                            errors,
1526                            depth,
1527                        )?,
1528                    }
1529                }
1530            }
1531            (SchemaType::Future { .. }, SchemaType::Future { .. }) => {
1532                errors.push(err(
1533                    path,
1534                    "future projection is unsupported at tool invocation boundaries",
1535                ));
1536                return None;
1537            }
1538            (SchemaType::Secret { spec: a, .. }, SchemaType::Secret { spec: b, .. }) if a == b => {
1539                ProjectionNode::Identity
1540            }
1541            (SchemaType::QuotaToken { spec: a, .. }, SchemaType::QuotaToken { spec: b, .. })
1542                if a == b =>
1543            {
1544                ProjectionNode::Identity
1545            }
1546            (
1547                SchemaType::PermissionCard { spec: a, .. },
1548                SchemaType::PermissionCard { spec: b, .. },
1549            ) if a == b => ProjectionNode::Identity,
1550            _ if scalar_assignable(s, t) => ProjectionNode::Identity,
1551            _ => {
1552                errors.push(err(
1553                    path,
1554                    "source type is not substitutable for target type",
1555                ));
1556                return None;
1557            }
1558        };
1559        self.push(node, path, errors)
1560    }
1561}
1562
1563fn pair(
1564    c: &mut Compiler<'_>,
1565    a: Option<&SchemaType>,
1566    b: Option<&SchemaType>,
1567    path: &str,
1568    e: &mut Vec<ToolCompatibilityError>,
1569    depth: usize,
1570) -> Option<Option<usize>> {
1571    match (a, b) {
1572        (None, None) => Some(None),
1573        (Some(x), Some(y)) => c.compile_at(x, y, path, e, depth).map(Some),
1574        _ => {
1575            e.push(err(path, "payload presence differs"));
1576            None
1577        }
1578    }
1579}
1580fn cases(
1581    c: &mut Compiler<'_>,
1582    a: &[crate::schema::schema_type::VariantCaseType],
1583    b: &[crate::schema::schema_type::VariantCaseType],
1584    path: &str,
1585    e: &mut Vec<ToolCompatibilityError>,
1586    depth: usize,
1587) -> Option<Vec<CaseProjection>> {
1588    a.iter()
1589        .enumerate()
1590        .map(|(i, x)| {
1591            let Some((j, y)) = b.iter().enumerate().find(|(_, z)| z.name == x.name) else {
1592                e.push(err(
1593                    format!("{path}.{}", x.name),
1594                    "target lacks source case",
1595                ));
1596                return None;
1597            };
1598            let payload = pair(
1599                c,
1600                x.payload.as_ref(),
1601                y.payload.as_ref(),
1602                &format!("{path}.{}", x.name),
1603                e,
1604                depth,
1605            )?;
1606            Some(CaseProjection {
1607                name: x.name.clone(),
1608                source_index: i,
1609                target_index: j,
1610                payload,
1611            })
1612        })
1613        .collect()
1614}
1615fn scalar_assignable(s: &SchemaType, t: &SchemaType) -> bool {
1616    let g = SchemaGraph::anonymous(s.clone());
1617    is_assignable(&g, s, t)
1618}
1619
1620fn strictly_equal(expected: &Tool, inner: &Tool) -> bool {
1621    canonical_tool(expected) == canonical_tool(inner)
1622}
1623
1624fn canonical_tool(tool: &Tool) -> Option<serde_json::Value> {
1625    fn discover(
1626        ty: &SchemaType,
1627        defs: &HashMap<TypeId, SchemaTypeDef>,
1628        ids: &mut HashMap<TypeId, TypeId>,
1629        order: &mut Vec<TypeId>,
1630    ) {
1631        walk_refs(ty, &mut |id| {
1632            if !ids.contains_key(id) {
1633                let canonical = TypeId::new(format!("$def{}", ids.len()));
1634                ids.insert(id.clone(), canonical);
1635                order.push(id.clone());
1636                if let Some(def) = defs.get(id) {
1637                    discover(&def.body, defs, ids, order);
1638                }
1639            }
1640        });
1641    }
1642
1643    let mut result = tool.clone();
1644    erase_ignored_tool_documentation(&mut result);
1645    let defs: HashMap<_, _> = result
1646        .schema
1647        .defs
1648        .iter()
1649        .map(|def| (def.id.clone(), def.clone()))
1650        .collect();
1651    let mut ids = HashMap::new();
1652    let mut order = Vec::new();
1653    for root in tool_schema_roots(&result) {
1654        discover(root, &defs, &mut ids, &mut order);
1655    }
1656    discover(&result.schema.root, &defs, &mut ids, &mut order);
1657
1658    // Tool validation permits unused definitions, and strict equality includes
1659    // them. Pick disconnected graph components by their smallest rooted
1660    // serialization, rather than by their allocation IDs or input order.
1661    while ids.len() < defs.len() {
1662        let mut candidates = Vec::new();
1663        for id in defs.keys().filter(|id| !ids.contains_key(*id)) {
1664            let mut local_ids = ids.clone();
1665            let mut local_order = order.clone();
1666            discover(
1667                &SchemaType::ref_to(id.clone()),
1668                &defs,
1669                &mut local_ids,
1670                &mut local_order,
1671            );
1672            let mut bodies = Vec::new();
1673            for original in &local_order[order.len()..] {
1674                let mut def = defs.get(original)?.clone();
1675                def.id = local_ids.get(original)?.clone();
1676                rewrite_refs(&mut def.body, &local_ids);
1677                bodies.push(def);
1678            }
1679            candidates.push((serde_json::to_string(&bodies).ok()?, local_ids, local_order));
1680        }
1681        let (_, next_ids, next_order) = candidates.into_iter().min_by(|a, b| a.0.cmp(&b.0))?;
1682        ids = next_ids;
1683        order = next_order;
1684    }
1685
1686    for root in tool_schema_roots_mut(&mut result) {
1687        rewrite_refs(root, &ids);
1688    }
1689    rewrite_refs(&mut result.schema.root, &ids);
1690    result.schema.defs = order
1691        .into_iter()
1692        .map(|original| {
1693            let mut def = defs.get(&original)?.clone();
1694            def.id = ids.get(&original)?.clone();
1695            rewrite_refs(&mut def.body, &ids);
1696            Some(def)
1697        })
1698        .collect::<Option<Vec<_>>>()?;
1699    serde_json::to_value(result).ok()
1700}
1701
1702fn erase_ignored_tool_documentation(tool: &mut Tool) {
1703    tool.version.clear();
1704    for node in &mut tool.commands.nodes {
1705        node.doc = Default::default();
1706        for option in &mut node.globals.options {
1707            option.doc = Default::default();
1708        }
1709        for flag in &mut node.globals.flags {
1710            flag.doc = Default::default();
1711        }
1712        if let Some(body) = &mut node.body {
1713            for positional in &mut body.positionals.fixed {
1714                positional.doc = Default::default();
1715            }
1716            if let Some(tail) = &mut body.positionals.tail {
1717                tail.doc = Default::default();
1718            }
1719            for option in &mut body.options {
1720                option.doc = Default::default();
1721            }
1722            for flag in &mut body.flags {
1723                flag.doc = Default::default();
1724            }
1725            if let Some(stdin) = &mut body.stdin {
1726                stdin.doc = Default::default();
1727            }
1728            if let Some(stdout) = &mut body.stdout {
1729                stdout.doc = Default::default();
1730            }
1731            if let Some(stderr) = &mut body.stderr {
1732                stderr.doc = Default::default();
1733            }
1734            if let Some(result) = &mut body.result {
1735                result.doc = Default::default();
1736                for formatter in &mut result.formatters {
1737                    formatter.doc = Default::default();
1738                }
1739            }
1740            for error in &mut body.errors {
1741                error.doc = Default::default();
1742            }
1743        }
1744    }
1745    for root in tool_schema_roots_mut(tool) {
1746        erase_schema_documentation(root);
1747    }
1748    erase_schema_documentation(&mut tool.schema.root);
1749    for def in &mut tool.schema.defs {
1750        erase_schema_documentation(&mut def.body);
1751    }
1752}
1753
1754fn erase_schema_documentation(ty: &mut SchemaType) {
1755    ty.metadata_mut().doc = None;
1756    match ty {
1757        SchemaType::Record { fields, .. } => fields.iter_mut().for_each(|field| {
1758            field.metadata.doc = None;
1759            erase_schema_documentation(&mut field.body);
1760        }),
1761        SchemaType::Variant { cases, .. } => cases.iter_mut().for_each(|case| {
1762            case.metadata.doc = None;
1763            if let Some(payload) = &mut case.payload {
1764                erase_schema_documentation(payload);
1765            }
1766        }),
1767        SchemaType::Tuple { elements, .. } => {
1768            elements.iter_mut().for_each(erase_schema_documentation)
1769        }
1770        SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
1771            erase_schema_documentation(element)
1772        }
1773        SchemaType::Map { key, value, .. } => {
1774            erase_schema_documentation(key);
1775            erase_schema_documentation(value);
1776        }
1777        SchemaType::Option { inner, .. }
1778        | SchemaType::Secret {
1779            spec: crate::schema::schema_type::SecretSpec { inner, .. },
1780            ..
1781        } => erase_schema_documentation(inner),
1782        SchemaType::Result { spec, .. } => {
1783            if let Some(ok) = &mut spec.ok {
1784                erase_schema_documentation(ok);
1785            }
1786            if let Some(err) = &mut spec.err {
1787                erase_schema_documentation(err);
1788            }
1789        }
1790        SchemaType::Union { spec, .. } => spec.branches.iter_mut().for_each(|branch| {
1791            branch.metadata.doc = None;
1792            erase_schema_documentation(&mut branch.body);
1793        }),
1794        SchemaType::Future { inner, .. } | SchemaType::Stream { inner, .. } => {
1795            if let Some(inner) = inner {
1796                erase_schema_documentation(inner);
1797            }
1798        }
1799        _ => {}
1800    }
1801}
1802
1803fn walk_refs(ty: &SchemaType, visit: &mut impl FnMut(&TypeId)) {
1804    match ty {
1805        SchemaType::Ref { id, .. } => visit(id),
1806        SchemaType::Record { fields, .. } => fields.iter().for_each(|f| walk_refs(&f.body, visit)),
1807        SchemaType::Variant { cases, .. } => cases
1808            .iter()
1809            .filter_map(|c| c.payload.as_ref())
1810            .for_each(|t| walk_refs(t, visit)),
1811        SchemaType::Tuple { elements, .. } => elements.iter().for_each(|t| walk_refs(t, visit)),
1812        SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
1813            walk_refs(element, visit)
1814        }
1815        SchemaType::Map { key, value, .. } => {
1816            walk_refs(key, visit);
1817            walk_refs(value, visit);
1818        }
1819        SchemaType::Option { inner, .. }
1820        | SchemaType::Secret {
1821            spec: crate::schema::schema_type::SecretSpec { inner, .. },
1822            ..
1823        } => walk_refs(inner, visit),
1824        SchemaType::Result { spec, .. } => {
1825            if let Some(t) = &spec.ok {
1826                walk_refs(t, visit)
1827            }
1828            if let Some(t) = &spec.err {
1829                walk_refs(t, visit)
1830            }
1831        }
1832        SchemaType::Union { spec, .. } => {
1833            spec.branches.iter().for_each(|b| walk_refs(&b.body, visit))
1834        }
1835        SchemaType::Future { inner, .. } | SchemaType::Stream { inner, .. } => {
1836            if let Some(t) = inner {
1837                walk_refs(t, visit)
1838            }
1839        }
1840        _ => {}
1841    }
1842}
1843
1844fn rewrite_refs(ty: &mut SchemaType, ids: &HashMap<TypeId, TypeId>) {
1845    match ty {
1846        SchemaType::Ref { id, .. } => {
1847            if let Some(new) = ids.get(id) {
1848                *id = new.clone();
1849            }
1850        }
1851        SchemaType::Record { fields, .. } => fields
1852            .iter_mut()
1853            .for_each(|f| rewrite_refs(&mut f.body, ids)),
1854        SchemaType::Variant { cases, .. } => cases
1855            .iter_mut()
1856            .filter_map(|c| c.payload.as_mut())
1857            .for_each(|t| rewrite_refs(t, ids)),
1858        SchemaType::Tuple { elements, .. } => {
1859            elements.iter_mut().for_each(|t| rewrite_refs(t, ids))
1860        }
1861        SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
1862            rewrite_refs(element, ids)
1863        }
1864        SchemaType::Map { key, value, .. } => {
1865            rewrite_refs(key, ids);
1866            rewrite_refs(value, ids);
1867        }
1868        SchemaType::Option { inner, .. }
1869        | SchemaType::Secret {
1870            spec: crate::schema::schema_type::SecretSpec { inner, .. },
1871            ..
1872        } => rewrite_refs(inner, ids),
1873        SchemaType::Result { spec, .. } => {
1874            if let Some(t) = &mut spec.ok {
1875                rewrite_refs(t, ids)
1876            }
1877            if let Some(t) = &mut spec.err {
1878                rewrite_refs(t, ids)
1879            }
1880        }
1881        SchemaType::Union { spec, .. } => spec
1882            .branches
1883            .iter_mut()
1884            .for_each(|b| rewrite_refs(&mut b.body, ids)),
1885        SchemaType::Future { inner, .. } | SchemaType::Stream { inner, .. } => {
1886            if let Some(t) = inner {
1887                rewrite_refs(t, ids)
1888            }
1889        }
1890        _ => {}
1891    }
1892}
1893
1894fn tool_schema_roots(tool: &Tool) -> Vec<&SchemaType> {
1895    let mut roots = Vec::new();
1896    for node in &tool.commands.nodes {
1897        collect_command_roots(&node.globals, node.body.as_ref(), &mut roots);
1898    }
1899    roots
1900}
1901
1902fn tool_schema_roots_mut(tool: &mut Tool) -> Vec<&mut SchemaType> {
1903    let mut roots = Vec::new();
1904    for node in &mut tool.commands.nodes {
1905        collect_command_roots_mut(&mut node.globals, node.body.as_mut(), &mut roots);
1906    }
1907    roots
1908}
1909
1910fn collect_command_roots<'a>(
1911    globals: &'a super::Globals,
1912    body: Option<&'a CommandBody>,
1913    roots: &mut Vec<&'a SchemaType>,
1914) {
1915    for option in &globals.options {
1916        roots.push(option_type(&option.shape));
1917    }
1918    if let Some(body) = body {
1919        roots.extend(body.positionals.fixed.iter().map(|p| &p.type_));
1920        if let Some(tail) = &body.positionals.tail {
1921            roots.push(&tail.item_type);
1922        }
1923        roots.extend(body.options.iter().map(|o| option_type(&o.shape)));
1924        if let Some(result) = &body.result {
1925            roots.push(&result.type_);
1926        }
1927        roots.extend(body.errors.iter().filter_map(|e| e.payload.as_ref()));
1928    }
1929}
1930
1931fn collect_command_roots_mut<'a>(
1932    globals: &'a mut super::Globals,
1933    body: Option<&'a mut CommandBody>,
1934    roots: &mut Vec<&'a mut SchemaType>,
1935) {
1936    for option in &mut globals.options {
1937        roots.push(option_type_mut(&mut option.shape));
1938    }
1939    if let Some(body) = body {
1940        roots.extend(body.positionals.fixed.iter_mut().map(|p| &mut p.type_));
1941        if let Some(tail) = &mut body.positionals.tail {
1942            roots.push(&mut tail.item_type);
1943        }
1944        roots.extend(
1945            body.options
1946                .iter_mut()
1947                .map(|o| option_type_mut(&mut o.shape)),
1948        );
1949        if let Some(result) = &mut body.result {
1950            roots.push(&mut result.type_);
1951        }
1952        roots.extend(body.errors.iter_mut().filter_map(|e| e.payload.as_mut()));
1953    }
1954}
1955
1956fn option_type(shape: &OptionShape) -> &SchemaType {
1957    match shape {
1958        OptionShape::Scalar(ty) | OptionShape::OptionalScalar(ty) => ty,
1959        OptionShape::RepeatableList(shape) => &shape.item_type,
1960        OptionShape::RepeatableMap(shape) => &shape.map_type,
1961    }
1962}
1963
1964fn option_type_mut(shape: &mut OptionShape) -> &mut SchemaType {
1965    match shape {
1966        OptionShape::Scalar(ty) | OptionShape::OptionalScalar(ty) => ty,
1967        OptionShape::RepeatableList(shape) => &mut shape.item_type,
1968        OptionShape::RepeatableMap(shape) => &mut shape.map_type,
1969    }
1970}
1971
1972fn resolve<'a>(g: &'a SchemaGraph, t: &'a SchemaType) -> (&'a SchemaType, Option<TypeId>) {
1973    let mut x = t;
1974    let mut id = None;
1975    let mut seen = HashSet::new();
1976    while let SchemaType::Ref { id: i, .. } = x {
1977        if !seen.insert(i.clone()) {
1978            break;
1979        }
1980        id = Some(i.clone());
1981        match g.defs.iter().find(|def| def.id == *i) {
1982            Some(def) => x = &def.body,
1983            None => break,
1984        }
1985    }
1986    (x, id)
1987}
1988fn graph_for(g: &SchemaGraph, t: &SchemaType) -> SchemaGraph {
1989    SchemaGraph {
1990        defs: crate::schema::graph::reachable_defs(g, t),
1991        root: t.clone(),
1992    }
1993}
1994fn dynamic_plan(source_schema: SchemaGraph, target_schema: SchemaGraph) -> ProjectionPlan {
1995    ProjectionPlan {
1996        source_schema,
1997        target_schema,
1998        nodes: vec![ProjectionNode::DynamicChecked],
1999        root: 0,
2000    }
2001}
2002fn option_none(t: &SchemaType, g: &SchemaGraph) -> Option<SchemaValue> {
2003    matches!(resolve(g, t).0, SchemaType::Option { .. })
2004        .then(|| SchemaValue::Option { inner: None })
2005}
2006fn surface_defaults(tool: &Tool, index: usize) -> Vec<Option<SchemaValue>> {
2007    tool.canonical_input_surfaces(index)
2008        .into_iter()
2009        .map(|s| match s {
2010            CanonicalSurfaceRef::GlobalOption { node, index } => {
2011                let option = &tool.commands.nodes[node].globals.options[index];
2012                option
2013                    .default
2014                    .clone()
2015                    .or_else(|| collection_default(&option.shape))
2016            }
2017            CanonicalSurfaceRef::GlobalFlag { node, index } => Some(flag_default(
2018                tool.commands.nodes[node].globals.flags[index].shape,
2019            )),
2020            CanonicalSurfaceRef::BodyPositional { index: i } => tool.commands.nodes[index]
2021                .body
2022                .as_ref()
2023                .unwrap()
2024                .positionals
2025                .fixed[i]
2026                .default
2027                .clone(),
2028            CanonicalSurfaceRef::BodyOption { index: i } => {
2029                let option = &tool.commands.nodes[index].body.as_ref().unwrap().options[i];
2030                option
2031                    .default
2032                    .clone()
2033                    .or_else(|| collection_default(&option.shape))
2034            }
2035            CanonicalSurfaceRef::BodyFlag { index: i } => Some(flag_default(
2036                tool.commands.nodes[index].body.as_ref().unwrap().flags[i].shape,
2037            )),
2038            CanonicalSurfaceRef::BodyTail => tool.commands.nodes[index]
2039                .body
2040                .as_ref()
2041                .unwrap()
2042                .positionals
2043                .tail
2044                .as_ref()
2045                .filter(|tail| tail.min == 0)
2046                .map(|_| SchemaValue::List { elements: vec![] }),
2047        })
2048        .collect()
2049}
2050fn collection_default(shape: &OptionShape) -> Option<SchemaValue> {
2051    match shape {
2052        OptionShape::RepeatableList(_) => Some(SchemaValue::List { elements: vec![] }),
2053        OptionShape::RepeatableMap(_) => Some(SchemaValue::Map { entries: vec![] }),
2054        OptionShape::Scalar(_) | OptionShape::OptionalScalar(_) => None,
2055    }
2056}
2057fn flag_default(shape: FlagShape) -> SchemaValue {
2058    match shape {
2059        FlagShape::BoolFlag(shape) => SchemaValue::Bool(shape.default),
2060        FlagShape::CountFlag(_) => SchemaValue::U32(0),
2061    }
2062}
2063fn command_paths(tool: &Tool) -> BTreeMap<Vec<String>, usize> {
2064    fn walk(t: &Tool, i: usize, p: &mut Vec<String>, r: &mut BTreeMap<Vec<String>, usize>) {
2065        let n = &t.commands.nodes[i];
2066        if n.body.is_some() {
2067            r.insert(p.clone(), i);
2068        }
2069        for c in &n.subcommands {
2070            let j = c.as_usize().unwrap();
2071            p.push(t.commands.nodes[j].name.clone());
2072            walk(t, j, p, r);
2073            p.pop();
2074        }
2075    }
2076    let mut r = BTreeMap::new();
2077    if !tool.commands.nodes.is_empty() {
2078        walk(tool, 0, &mut Vec::new(), &mut r)
2079    }
2080    r
2081}
2082fn format_path(p: &[String]) -> String {
2083    if p.is_empty() {
2084        "root".into()
2085    } else {
2086        p.join(".")
2087    }
2088}
2089fn err(path: impl Into<String>, message: impl Into<String>) -> ToolCompatibilityError {
2090    ToolCompatibilityError {
2091        path: path.into(),
2092        message: message.into(),
2093    }
2094}
2095
2096#[cfg(test)]
2097mod tests;