Skip to main content

wesley_core/domain/
capability.rs

1//! Rust-native module capability descriptors and host policy checks.
2//!
3//! This module intentionally models capability metadata and pre-execution
4//! decisions only. It does not load dynamic modules, execute WASM, or preserve
5//! the legacy Node module loader shape.
6
7use std::{
8    collections::{BTreeMap, BTreeSet},
9    fmt,
10};
11
12/// Stable name of the first Wesley capability ABI.
13pub const WESLEY_CAPABILITY_ABI: &str = "wesley-capability-abi";
14
15/// Current capability ABI version supported by the Rust host policy layer.
16pub const CURRENT_CAPABILITY_ABI_VERSION: CapabilityContractVersion =
17    CapabilityContractVersion::new(0, 1, 0);
18
19/// Semver-like capability contract version.
20#[derive(
21    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
22)]
23#[serde(rename_all = "camelCase")]
24pub struct CapabilityContractVersion {
25    /// Major version.
26    pub major: u64,
27    /// Minor version.
28    pub minor: u64,
29    /// Patch version.
30    pub patch: u64,
31}
32
33impl CapabilityContractVersion {
34    /// Builds a capability contract version.
35    pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
36        Self {
37            major,
38            minor,
39            patch,
40        }
41    }
42}
43
44impl fmt::Display for CapabilityContractVersion {
45    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
47    }
48}
49
50/// Capability ABI version range required by a module target.
51#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct CapabilityVersionRequirement {
54    /// Capability ABI name.
55    pub abi: String,
56    /// Inclusive minimum ABI version.
57    pub minimum: CapabilityContractVersion,
58    /// Exclusive maximum ABI version.
59    pub maximum_exclusive: CapabilityContractVersion,
60}
61
62impl CapabilityVersionRequirement {
63    /// Builds a capability ABI range.
64    pub fn new(
65        abi: impl Into<String>,
66        minimum: CapabilityContractVersion,
67        maximum_exclusive: CapabilityContractVersion,
68    ) -> Self {
69        Self {
70            abi: abi.into(),
71            minimum,
72            maximum_exclusive,
73        }
74    }
75
76    /// Returns the current first-profile compatibility range.
77    pub fn current() -> Self {
78        Self::new(
79            WESLEY_CAPABILITY_ABI,
80            CURRENT_CAPABILITY_ABI_VERSION,
81            CapabilityContractVersion::new(0, 2, 0),
82        )
83    }
84
85    /// Whether the named ABI version satisfies this range.
86    pub fn allows(&self, abi: &str, version: CapabilityContractVersion) -> bool {
87        self.abi == abi && self.minimum <= version && version < self.maximum_exclusive
88    }
89}
90
91impl fmt::Display for CapabilityVersionRequirement {
92    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93        write!(
94            formatter,
95            "{} >={} <{}",
96            self.abi, self.minimum, self.maximum_exclusive
97        )
98    }
99}
100
101/// Execution mode declared by a module capability.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
103#[serde(rename_all = "kebab-case")]
104pub enum CapabilityExecutionMode {
105    /// Capability is implemented in Rust and runs in the native host.
106    RustNative,
107    /// Capability is implemented as a portable WASM component.
108    Wasm,
109    /// Capability runs through an explicit external process protocol.
110    ExternalProcess,
111}
112
113/// Minimum portability floor promised by a module capability.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
115#[serde(rename_all = "kebab-case")]
116pub enum CapabilityPortabilityFloor {
117    /// Capability is only expected to run in the current native host.
118    HostNative,
119    /// Capability is portable across hosts that implement the WASM ABI profile.
120    PortableWasm,
121    /// Capability requires an external process protocol and is not in-process.
122    ExternalProcess,
123}
124
125/// Runtime state model declared by a module capability.
126#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
127#[serde(rename_all = "kebab-case")]
128pub enum CapabilityRuntimeModel {
129    /// Capability output must depend only on its explicit input envelope.
130    #[default]
131    Stateless,
132    /// Capability requests explicit host-created resource handles.
133    ResourceHandles,
134}
135
136/// Module-provided target capability metadata.
137#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct ModuleTargetDescriptor {
140    /// Name of the module that contributes the target.
141    pub module: String,
142    /// Stable target name selected by `wesley compile` or a future Rust verb.
143    pub target: String,
144    /// Whether this target is the default target when no explicit target is requested.
145    pub is_default: bool,
146    /// Execution mode needed to run this target capability.
147    pub execution_mode: CapabilityExecutionMode,
148    /// Minimum portability floor promised by this target capability.
149    pub portability_floor: CapabilityPortabilityFloor,
150    /// Capability ABI range required by this target.
151    #[serde(default = "CapabilityVersionRequirement::current")]
152    pub required_contract: CapabilityVersionRequirement,
153    /// Runtime state model needed by this target.
154    #[serde(default)]
155    pub runtime_model: CapabilityRuntimeModel,
156    /// Host imports requested by this target, using stable dotted import names.
157    #[serde(default, skip_serializing_if = "Vec::is_empty")]
158    pub requested_host_imports: Vec<String>,
159    /// Explicit resource handles requested by this target.
160    #[serde(default, skip_serializing_if = "Vec::is_empty")]
161    pub requested_resource_handles: Vec<String>,
162}
163
164/// Deterministic module target registry for Rust-native dispatch planning.
165#[derive(Debug, Default, Clone, PartialEq, Eq)]
166pub struct ModuleTargetRegistry {
167    targets: BTreeMap<String, ModuleTargetDescriptor>,
168}
169
170impl ModuleTargetRegistry {
171    /// Builds a registry from module target descriptors.
172    pub fn from_targets(
173        targets: impl IntoIterator<Item = ModuleTargetDescriptor>,
174    ) -> Result<Self, ModuleCapabilityError> {
175        let mut registry = Self::default();
176        for target in targets {
177            registry.register(target)?;
178        }
179
180        Ok(registry)
181    }
182
183    /// Registers one target descriptor.
184    pub fn register(
185        &mut self,
186        target: ModuleTargetDescriptor,
187    ) -> Result<(), ModuleCapabilityError> {
188        if let Some(existing) = self.targets.get(&target.target) {
189            return Err(ModuleCapabilityError::DuplicateTarget {
190                target: target.target,
191                first_module: existing.module.clone(),
192                second_module: target.module,
193            });
194        }
195
196        self.targets.insert(target.target.clone(), target);
197        Ok(())
198    }
199
200    /// Resolves an explicit target, or the single default target when none is requested.
201    pub fn resolve_target(
202        &self,
203        requested: Option<&str>,
204    ) -> Result<&ModuleTargetDescriptor, ModuleCapabilityError> {
205        if self.targets.is_empty() {
206            return Err(ModuleCapabilityError::NoTargets);
207        }
208
209        if let Some(requested) = requested {
210            return self.targets.get(requested).ok_or_else(|| {
211                ModuleCapabilityError::UnknownTarget {
212                    target: requested.to_string(),
213                    available: self.targets.keys().cloned().collect(),
214                }
215            });
216        }
217
218        let defaults = self
219            .targets
220            .values()
221            .filter(|target| target.is_default)
222            .collect::<Vec<_>>();
223
224        match defaults.as_slice() {
225            [target] => Ok(target),
226            [] => Err(ModuleCapabilityError::NoDefaultTarget {
227                available: self.targets.keys().cloned().collect(),
228            }),
229            targets => Err(ModuleCapabilityError::MultipleDefaultTargets {
230                targets: targets.iter().map(|target| target.target.clone()).collect(),
231            }),
232        }
233    }
234
235    /// Builds a report that names requested, granted, and denied target capabilities.
236    pub fn capability_report(
237        &self,
238        requested: impl IntoIterator<Item = impl AsRef<str>>,
239    ) -> CapabilityReport {
240        let mut requested_targets = Vec::new();
241        let mut granted = Vec::new();
242        let mut denied = Vec::new();
243
244        for target in requested {
245            let target = target.as_ref().to_string();
246            requested_targets.push(target.clone());
247            if self.targets.contains_key(&target) {
248                granted.push(target);
249            } else {
250                denied.push(target);
251            }
252        }
253
254        CapabilityReport {
255            requested: requested_targets,
256            granted,
257            denied,
258        }
259    }
260}
261
262/// Requested/granted/denied capability report.
263#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct CapabilityReport {
266    /// Capabilities requested by the caller.
267    pub requested: Vec<String>,
268    /// Requested capabilities the registry can grant.
269    pub granted: Vec<String>,
270    /// Requested capabilities absent from the registry.
271    pub denied: Vec<String>,
272}
273
274/// Host-side capability contract support.
275#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct HostCapabilityContract {
278    /// Host profile name.
279    pub host: String,
280    /// Supported capability ABI name.
281    pub abi: String,
282    /// Supported capability ABI version.
283    pub version: CapabilityContractVersion,
284}
285
286impl HostCapabilityContract {
287    /// Returns the current Rust host contract.
288    pub fn current() -> Self {
289        Self {
290            host: "wesley-rust-host".to_string(),
291            abi: WESLEY_CAPABILITY_ABI.to_string(),
292            version: CURRENT_CAPABILITY_ABI_VERSION,
293        }
294    }
295
296    /// Evaluates one target's capability contract against this host.
297    pub fn evaluate_contract(&self, target: &ModuleTargetDescriptor) -> CapabilityContractReport {
298        let mut diagnostics = Vec::new();
299
300        if target.required_contract.abi != self.abi {
301            diagnostics.push(CapabilityContractDiagnostic {
302                code: "MODULE_CONTRACT_ABI_MISMATCH".to_string(),
303                target: target.target.clone(),
304                host: self.host.clone(),
305                host_version: self.version.to_string(),
306                required: target.required_contract.to_string(),
307            });
308        } else if !target.required_contract.allows(&self.abi, self.version) {
309            diagnostics.push(CapabilityContractDiagnostic {
310                code: "WASM_ABI_UNSUPPORTED".to_string(),
311                target: target.target.clone(),
312                host: self.host.clone(),
313                host_version: self.version.to_string(),
314                required: target.required_contract.to_string(),
315            });
316        }
317
318        CapabilityContractReport {
319            target: target.target.clone(),
320            host: self.host.clone(),
321            host_version: self.version.to_string(),
322            required: target.required_contract.to_string(),
323            accepted: diagnostics.is_empty(),
324            diagnostics,
325        }
326    }
327
328    /// Rejects a target before execution when the capability contract is incompatible.
329    pub fn reject_incompatible_contract_before_execution(
330        &self,
331        target: &ModuleTargetDescriptor,
332    ) -> Result<CapabilityContractReport, ModuleCapabilityError> {
333        let report = self.evaluate_contract(target);
334        if report.accepted {
335            Ok(report)
336        } else {
337            Err(ModuleCapabilityError::IncompatibleCapabilityContract {
338                target: target.target.clone(),
339                diagnostic_codes: report
340                    .diagnostics
341                    .iter()
342                    .map(|diagnostic| diagnostic.code.clone())
343                    .collect(),
344            })
345        }
346    }
347}
348
349/// Compatibility report for a target capability contract.
350#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct CapabilityContractReport {
353    /// Target being evaluated.
354    pub target: String,
355    /// Host profile used for the decision.
356    pub host: String,
357    /// Host capability ABI version.
358    pub host_version: String,
359    /// Required capability ABI range.
360    pub required: String,
361    /// Whether the target can run under the host contract.
362    pub accepted: bool,
363    /// Typed compatibility diagnostics.
364    pub diagnostics: Vec<CapabilityContractDiagnostic>,
365}
366
367/// Typed compatibility diagnostic emitted before execution.
368#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
369#[serde(rename_all = "camelCase")]
370pub struct CapabilityContractDiagnostic {
371    /// Stable diagnostic code.
372    pub code: String,
373    /// Target that failed compatibility.
374    pub target: String,
375    /// Host profile used for the decision.
376    pub host: String,
377    /// Host capability ABI version.
378    pub host_version: String,
379    /// Required capability ABI range.
380    pub required: String,
381}
382
383/// Host function policy used before running a WASM capability.
384#[derive(Debug, Clone, PartialEq, Eq)]
385pub struct HostFunctionPolicy {
386    profile: String,
387    allowed_imports: BTreeSet<String>,
388}
389
390impl HostFunctionPolicy {
391    /// Returns the default pure profile that denies all ambient host imports.
392    pub fn pure() -> Self {
393        Self {
394            profile: "pure".to_string(),
395            allowed_imports: BTreeSet::new(),
396        }
397    }
398
399    /// Returns a profile with an explicit allowlist of host imports.
400    pub fn allowing(
401        profile: impl Into<String>,
402        imports: impl IntoIterator<Item = impl Into<String>>,
403    ) -> Self {
404        Self {
405            profile: profile.into(),
406            allowed_imports: imports.into_iter().map(Into::into).collect(),
407        }
408    }
409
410    /// Evaluates a target's requested host imports against this policy.
411    pub fn evaluate(&self, target: &ModuleTargetDescriptor) -> HostImportReport {
412        let requested = sorted_unique(&target.requested_host_imports);
413        let mut granted = Vec::new();
414        let mut denied = Vec::new();
415
416        for requested_import in &requested {
417            if self.allowed_imports.contains(requested_import) {
418                granted.push(requested_import.clone());
419            } else {
420                denied.push(requested_import.clone());
421            }
422        }
423
424        HostImportReport {
425            profile: self.profile.clone(),
426            target: target.target.clone(),
427            requested,
428            granted,
429            denied,
430        }
431    }
432
433    /// Rejects a target before execution when required host imports are unavailable.
434    pub fn reject_unavailable_imports_before_execution(
435        &self,
436        target: &ModuleTargetDescriptor,
437    ) -> Result<HostImportReport, ModuleCapabilityError> {
438        let report = self.evaluate(target);
439        if report.denied.is_empty() {
440            Ok(report)
441        } else {
442            Err(ModuleCapabilityError::DeniedHostImports {
443                target: target.target.clone(),
444                denied: report.denied,
445            })
446        }
447    }
448}
449
450/// Host import policy report for one target.
451#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
452#[serde(rename_all = "camelCase")]
453pub struct HostImportReport {
454    /// Host policy profile used for the decision.
455    pub profile: String,
456    /// Target whose imports were evaluated.
457    pub target: String,
458    /// Host imports requested by the target.
459    pub requested: Vec<String>,
460    /// Requested imports granted by policy.
461    pub granted: Vec<String>,
462    /// Requested imports denied by policy.
463    pub denied: Vec<String>,
464}
465
466/// Runtime resource policy used before running a capability.
467#[derive(Debug, Clone, PartialEq, Eq)]
468pub struct RuntimeResourcePolicy {
469    model: CapabilityRuntimeModel,
470    allowed_resource_handles: BTreeSet<String>,
471}
472
473impl RuntimeResourcePolicy {
474    /// Returns the default stateless policy that denies all resource handles.
475    pub fn stateless_default() -> Self {
476        Self {
477            model: CapabilityRuntimeModel::Stateless,
478            allowed_resource_handles: BTreeSet::new(),
479        }
480    }
481
482    /// Returns a future resource-handle profile with an explicit allowlist.
483    pub fn allowing_resource_handles(handles: impl IntoIterator<Item = impl Into<String>>) -> Self {
484        Self {
485            model: CapabilityRuntimeModel::ResourceHandles,
486            allowed_resource_handles: handles.into_iter().map(Into::into).collect(),
487        }
488    }
489
490    /// Evaluates a target's requested resource handles against this policy.
491    pub fn evaluate(&self, target: &ModuleTargetDescriptor) -> RuntimeResourceReport {
492        let requested = sorted_unique(&target.requested_resource_handles);
493        let mut granted = Vec::new();
494        let mut denied = Vec::new();
495
496        for requested_handle in &requested {
497            if self.model == CapabilityRuntimeModel::ResourceHandles
498                && self.allowed_resource_handles.contains(requested_handle)
499            {
500                granted.push(requested_handle.clone());
501            } else {
502                denied.push(requested_handle.clone());
503            }
504        }
505
506        RuntimeResourceReport {
507            model: self.model,
508            target: target.target.clone(),
509            requested,
510            granted,
511            denied,
512        }
513    }
514
515    /// Rejects a target before execution when required resource handles are unavailable.
516    pub fn reject_resource_handles_before_execution(
517        &self,
518        target: &ModuleTargetDescriptor,
519    ) -> Result<RuntimeResourceReport, ModuleCapabilityError> {
520        let report = self.evaluate(target);
521        if report.denied.is_empty() {
522            Ok(report)
523        } else {
524            Err(ModuleCapabilityError::DeniedResourceHandles {
525                target: target.target.clone(),
526                denied: report.denied,
527            })
528        }
529    }
530}
531
532/// Runtime resource policy report for one target.
533#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
534#[serde(rename_all = "camelCase")]
535pub struct RuntimeResourceReport {
536    /// Runtime model used for the decision.
537    pub model: CapabilityRuntimeModel,
538    /// Target whose resource handles were evaluated.
539    pub target: String,
540    /// Resource handles requested by the target.
541    pub requested: Vec<String>,
542    /// Requested resource handles granted by policy.
543    pub granted: Vec<String>,
544    /// Requested resource handles denied by policy.
545    pub denied: Vec<String>,
546}
547
548/// One deterministic host fixture for cross-host capability checks.
549#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
550#[serde(rename_all = "camelCase")]
551pub struct HermeticCapabilityFixture {
552    /// Host profile that produced the fixture.
553    pub host: String,
554    /// Target capability under test.
555    pub target: String,
556    /// Canonical input envelope digest.
557    pub input_digest: String,
558    /// Canonical output envelope digest.
559    pub output_digest: String,
560}
561
562impl HermeticCapabilityFixture {
563    /// Builds a hermetic host fixture.
564    pub fn new(
565        host: impl Into<String>,
566        target: impl Into<String>,
567        input_digest: impl Into<String>,
568        output_digest: impl Into<String>,
569    ) -> Self {
570        Self {
571            host: host.into(),
572            target: target.into(),
573            input_digest: input_digest.into(),
574            output_digest: output_digest.into(),
575        }
576    }
577
578    /// Verifies that all fixtures for one target/input agree across hosts.
579    pub fn verify_cross_host_outputs(
580        fixtures: impl IntoIterator<Item = HermeticCapabilityFixture>,
581    ) -> Result<HermeticCapabilityReport, ModuleCapabilityError> {
582        let fixtures = fixtures.into_iter().collect::<Vec<_>>();
583        let Some(first) = fixtures.first() else {
584            return Err(ModuleCapabilityError::EmptyHermeticFixtures);
585        };
586
587        let target = first.target.clone();
588        let input_digest = first.input_digest.clone();
589
590        if fixtures
591            .iter()
592            .any(|fixture| fixture.target != target || fixture.input_digest != input_digest)
593        {
594            return Err(ModuleCapabilityError::MixedHermeticFixtureInputs);
595        }
596
597        let output_digests = fixtures
598            .iter()
599            .map(|fixture| fixture.output_digest.clone())
600            .collect::<BTreeSet<_>>();
601
602        if output_digests.len() != 1 {
603            return Err(ModuleCapabilityError::NonHermeticCapabilityFixture {
604                target,
605                input_digest,
606                output_digests: output_digests.into_iter().collect(),
607            });
608        }
609
610        Ok(HermeticCapabilityReport {
611            target,
612            input_digest,
613            output_digest: output_digests.into_iter().next().unwrap_or_default(),
614            hosts: fixtures
615                .iter()
616                .map(|fixture| fixture.host.clone())
617                .collect::<BTreeSet<_>>()
618                .into_iter()
619                .collect(),
620        })
621    }
622}
623
624/// Verified cross-host hermetic capability report.
625#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
626#[serde(rename_all = "camelCase")]
627pub struct HermeticCapabilityReport {
628    /// Target capability under test.
629    pub target: String,
630    /// Canonical input envelope digest.
631    pub input_digest: String,
632    /// Shared canonical output envelope digest.
633    pub output_digest: String,
634    /// Host profiles that produced identical output.
635    pub hosts: Vec<String>,
636}
637
638/// Module capability registry and host policy errors.
639#[derive(Debug, thiserror::Error, PartialEq, Eq)]
640pub enum ModuleCapabilityError {
641    /// No module targets are registered.
642    #[error("no module targets are registered; load an external target module")]
643    NoTargets,
644    /// Two modules contributed the same target name.
645    #[error(
646        "duplicate module target '{target}' from modules '{first_module}' and '{second_module}'"
647    )]
648    DuplicateTarget {
649        /// Duplicate target name.
650        target: String,
651        /// Module that registered the target first.
652        first_module: String,
653        /// Module that attempted to register the duplicate target.
654        second_module: String,
655    },
656    /// Requested target is absent from the registry.
657    #[error("unknown module target '{target}'; available targets: {}", available.join(", "))]
658    UnknownTarget {
659        /// Requested target name.
660        target: String,
661        /// Available target names.
662        available: Vec<String>,
663    },
664    /// No target is marked as default.
665    #[error("no default module target is registered; available targets: {}", available.join(", "))]
666    NoDefaultTarget {
667        /// Available target names.
668        available: Vec<String>,
669    },
670    /// More than one target is marked as default.
671    #[error("multiple default module targets are registered: {}", targets.join(", "))]
672    MultipleDefaultTargets {
673        /// Target names marked as default.
674        targets: Vec<String>,
675    },
676    /// A target requested unavailable host imports.
677    #[error("module target '{target}' requested unavailable host imports: {}", denied.join(", "))]
678    DeniedHostImports {
679        /// Target that requested denied imports.
680        target: String,
681        /// Denied host import names.
682        denied: Vec<String>,
683    },
684    /// A target requested an incompatible capability contract.
685    #[error(
686        "module target '{target}' requires incompatible capability contract: {}",
687        diagnostic_codes.join(", ")
688    )]
689    IncompatibleCapabilityContract {
690        /// Target with an incompatible contract.
691        target: String,
692        /// Diagnostic codes explaining the incompatibility.
693        diagnostic_codes: Vec<String>,
694    },
695    /// A target requested unavailable resource handles.
696    #[error(
697        "module target '{target}' requested unavailable resource handles: {}",
698        denied.join(", ")
699    )]
700    DeniedResourceHandles {
701        /// Target that requested denied resource handles.
702        target: String,
703        /// Denied resource handle names.
704        denied: Vec<String>,
705    },
706    /// No hermetic fixtures were provided.
707    #[error("no hermetic capability fixtures were provided")]
708    EmptyHermeticFixtures,
709    /// Hermetic fixtures mixed targets or input digests.
710    #[error("hermetic capability fixtures must use one target and input digest")]
711    MixedHermeticFixtureInputs,
712    /// Host fixtures produced different output digests for the same target/input.
713    #[error("module target '{target}' is not hermetic for input {input_digest}")]
714    NonHermeticCapabilityFixture {
715        /// Target under test.
716        target: String,
717        /// Input digest under test.
718        input_digest: String,
719        /// Divergent output digests.
720        output_digests: Vec<String>,
721    },
722}
723
724fn sorted_unique(values: &[String]) -> Vec<String> {
725    values
726        .iter()
727        .cloned()
728        .collect::<BTreeSet<_>>()
729        .into_iter()
730        .collect()
731}