Skip to main content

player_plugin/
resolver.rs

1//! Deterministic, metadata-only provider resolution.
2
3use std::cmp::Ordering;
4use std::collections::{BTreeMap, BTreeSet};
5
6use semver::{Version, VersionReq};
7use thiserror::Error;
8
9use crate::{
10    MAX_PLUGIN_ARCHITECTURE_BYTES, MAX_PLUGIN_CATALOG_RECORDS, MAX_PLUGIN_REQUIREMENTS,
11    MAX_PLUGIN_TARGET_BYTES, PluginArtifactTransport, PluginCatalog, PluginCatalogRecord,
12    PluginPlan, PluginPlanError, PluginRequirement, PluginTransport, validate_plugin_requirements,
13};
14
15/// Upper bound for accumulated root and transitive requirement constraints.
16pub const MAX_PLUGIN_RESOLUTION_CONSTRAINTS: usize =
17    MAX_PLUGIN_CATALOG_RECORDS * MAX_PLUGIN_REQUIREMENTS;
18/// Upper bound for deterministic backtracking states derived from catalog size.
19pub const MAX_PLUGIN_RESOLUTION_STATES: usize = MAX_PLUGIN_CATALOG_RECORDS * 4;
20
21/// Host-owned compatibility and preference inputs for one resolution.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct PluginResolverPolicy {
24    transport: PluginArtifactTransport,
25    target: String,
26    architecture: String,
27    abi_major: u16,
28    abi_minor: u16,
29    plugin_priorities: BTreeMap<String, i32>,
30}
31
32impl PluginResolverPolicy {
33    pub fn new(
34        transport: PluginArtifactTransport,
35        target: impl Into<String>,
36        architecture: impl Into<String>,
37        abi_major: u16,
38        abi_minor: u16,
39    ) -> Result<Self, PluginResolutionError> {
40        let target = target.into();
41        let architecture = architecture.into();
42        validate_policy_text("target", &target, MAX_PLUGIN_TARGET_BYTES)?;
43        validate_policy_text("architecture", &architecture, MAX_PLUGIN_ARCHITECTURE_BYTES)?;
44        if abi_major == 0 {
45            return Err(PluginResolutionError::InvalidPolicy {
46                field: "abi_major".to_owned(),
47                message: "must be greater than zero".to_owned(),
48            });
49        }
50        Ok(Self {
51            transport,
52            target,
53            architecture,
54            abi_major,
55            abi_minor,
56            plugin_priorities: BTreeMap::new(),
57        })
58    }
59
60    pub const fn transport(&self) -> PluginArtifactTransport {
61        self.transport
62    }
63
64    pub fn target(&self) -> &str {
65        &self.target
66    }
67
68    pub fn architecture(&self) -> &str {
69        &self.architecture
70    }
71
72    pub const fn abi_major(&self) -> u16 {
73        self.abi_major
74    }
75
76    pub const fn abi_minor(&self) -> u16 {
77        self.abi_minor
78    }
79
80    pub fn set_plugin_priority(
81        &mut self,
82        plugin_id: impl Into<String>,
83        priority: i32,
84    ) -> Result<(), PluginResolutionError> {
85        let plugin_id = plugin_id.into();
86        let transport = match self.transport {
87            PluginArtifactTransport::Native => PluginTransport::Native,
88            PluginArtifactTransport::Wasm => PluginTransport::Wasm,
89        };
90        crate::PluginReference::new(plugin_id.clone(), None, transport).map_err(|error| {
91            PluginResolutionError::InvalidPolicy {
92                field: "plugin_priorities.plugin_id".to_owned(),
93                message: error.to_string(),
94            }
95        })?;
96        if !self.plugin_priorities.contains_key(&plugin_id)
97            && self.plugin_priorities.len() >= MAX_PLUGIN_CATALOG_RECORDS
98        {
99            return Err(PluginResolutionError::InvalidPolicy {
100                field: "plugin_priorities".to_owned(),
101                message: format!("must contain at most {MAX_PLUGIN_CATALOG_RECORDS} entries"),
102            });
103        }
104        self.plugin_priorities.insert(plugin_id, priority);
105        Ok(())
106    }
107
108    pub fn plugin_priority(&self, plugin_id: &str) -> i32 {
109        self.plugin_priorities
110            .get(plugin_id)
111            .copied()
112            .unwrap_or_default()
113    }
114
115    pub fn plugin_priorities(&self) -> &BTreeMap<String, i32> {
116        &self.plugin_priorities
117    }
118
119    fn accepts(&self, record: &PluginCatalogRecord) -> bool {
120        let descriptor = record.descriptor();
121        descriptor.transport == self.transport
122            && descriptor.target == self.target
123            && descriptor.architecture == self.architecture
124            && descriptor.abi_major == self.abi_major
125            && descriptor.abi_minor_min <= self.abi_minor
126            && descriptor.abi_minor_max >= self.abi_minor
127    }
128}
129
130/// One selected service provider and its immutable artifact metadata.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct PluginResolvedProvider {
133    service: String,
134    provided_version: String,
135    artifact: PluginCatalogRecord,
136}
137
138impl PluginResolvedProvider {
139    pub fn service(&self) -> &str {
140        &self.service
141    }
142
143    pub fn provided_version(&self) -> &str {
144        &self.provided_version
145    }
146
147    pub fn artifact(&self) -> &PluginCatalogRecord {
148        &self.artifact
149    }
150}
151
152/// Pure result of provider resolution. Artifacts are ordered dependency-first.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct PluginResolution {
155    catalog_fingerprint: String,
156    providers: Vec<PluginResolvedProvider>,
157    artifacts: Vec<PluginCatalogRecord>,
158}
159
160impl PluginResolution {
161    pub fn catalog_fingerprint(&self) -> &str {
162        &self.catalog_fingerprint
163    }
164
165    pub fn providers(&self) -> &[PluginResolvedProvider] {
166        &self.providers
167    }
168
169    pub fn artifacts(&self) -> &[PluginCatalogRecord] {
170        &self.artifacts
171    }
172}
173
174#[derive(Debug, Error, Clone, PartialEq, Eq)]
175pub enum PluginResolutionError {
176    #[error("invalid plugin resolver policy field `{field}`: {message}")]
177    InvalidPolicy { field: String, message: String },
178    #[error("invalid root plugin requirements: {message}")]
179    InvalidRequirements { message: String },
180    #[error("catalog invariant failed for `{artifact_identity}`: {message}")]
181    InvalidCatalogRecord {
182        artifact_identity: String,
183        message: String,
184    },
185    #[error(
186        "no provider for service `{service}` matches the host policy; requirements={requirements:?}, catalog_candidates={catalog_candidates}, policy_candidates={policy_candidates}"
187    )]
188    MissingProvider {
189        service: String,
190        requirements: Vec<String>,
191        catalog_candidates: usize,
192        policy_candidates: usize,
193    },
194    #[error(
195        "no version of service `{service}` satisfies {requirements:?}; available versions={available_versions:?}"
196    )]
197    VersionConflict {
198        service: String,
199        requirements: Vec<String>,
200        available_versions: Vec<String>,
201    },
202    #[error(
203        "plugin `{plugin_id}` would resolve to conflicting artifacts `{selected_identity}` and `{candidate_identity}`"
204    )]
205    PluginIdentityConflict {
206        plugin_id: String,
207        selected_identity: String,
208        candidate_identity: String,
209    },
210    #[error("plugin dependency cycle: {artifact_identities:?}")]
211    DependencyCycle { artifact_identities: Vec<String> },
212    #[error("plugin resolution exceeded {limit} accumulated constraints")]
213    ConstraintLimitExceeded { limit: usize },
214    #[error("plugin resolution exceeded {limit} deterministic search states")]
215    SearchLimitExceeded { limit: usize },
216}
217
218/// Resolves service providers from an immutable catalog without artifact I/O.
219pub struct PluginResolver<'a> {
220    catalog: &'a PluginCatalog,
221    policy: PluginResolverPolicy,
222}
223
224impl<'a> PluginResolver<'a> {
225    pub fn new(catalog: &'a PluginCatalog, policy: PluginResolverPolicy) -> Self {
226        Self { catalog, policy }
227    }
228
229    pub fn policy(&self) -> &PluginResolverPolicy {
230        &self.policy
231    }
232
233    pub fn catalog(&self) -> &PluginCatalog {
234        self.catalog
235    }
236
237    pub fn resolve(
238        &self,
239        root_requirements: &[PluginRequirement],
240    ) -> Result<PluginResolution, PluginResolutionError> {
241        validate_plugin_requirements(root_requirements).map_err(|error| {
242            PluginResolutionError::InvalidRequirements {
243                message: error.to_string(),
244            }
245        })?;
246
247        let mut initial = ResolverState::default();
248        for requirement in root_requirements {
249            initial.add_constraint(requirement, RequirementOwner::Host)?;
250        }
251        let mut pending = vec![initial];
252        let mut examined = 0_usize;
253        let mut first_failure = None;
254
255        while let Some(state) = pending.pop() {
256            if examined >= MAX_PLUGIN_RESOLUTION_STATES {
257                return Err(PluginResolutionError::SearchLimitExceeded {
258                    limit: MAX_PLUGIN_RESOLUTION_STATES,
259                });
260            }
261            examined += 1;
262            let Some(service) = state.next_unresolved_service() else {
263                match self.finish(state) {
264                    Ok(resolution) => return Ok(resolution),
265                    Err(error) => {
266                        remember_first(&mut first_failure, error);
267                        continue;
268                    }
269                }
270            };
271
272            let candidates = match self.candidates_for(&state, &service) {
273                Ok(candidates) => candidates,
274                Err(error) => {
275                    remember_first(&mut first_failure, error);
276                    continue;
277                }
278            };
279            let mut next_states = Vec::with_capacity(candidates.len());
280            let mut candidate_failure = None;
281            for candidate in candidates {
282                let mut next = state.clone();
283                match self.apply_candidate(&mut next, &service, candidate) {
284                    Ok(()) => next_states.push(next),
285                    Err(error) => remember_first(&mut candidate_failure, error),
286                }
287            }
288            if next_states.is_empty()
289                && let Some(error) = candidate_failure
290            {
291                remember_first(&mut first_failure, error);
292            }
293            for next in next_states.into_iter().rev() {
294                if pending.len() + examined >= MAX_PLUGIN_RESOLUTION_STATES {
295                    return Err(PluginResolutionError::SearchLimitExceeded {
296                        limit: MAX_PLUGIN_RESOLUTION_STATES,
297                    });
298                }
299                pending.push(next);
300            }
301        }
302
303        Err(
304            first_failure.unwrap_or_else(|| PluginResolutionError::InvalidRequirements {
305                message: "resolution ended without a result or typed provider failure".to_owned(),
306            }),
307        )
308    }
309
310    /// Resolves and freezes a canonical metadata-only plan.
311    pub fn resolve_plan(
312        &self,
313        root_requirements: &[PluginRequirement],
314    ) -> Result<PluginPlan, PluginPlanError> {
315        let resolution = self.resolve(root_requirements)?;
316        PluginPlan::from_resolution(&self.policy, root_requirements, self.catalog, resolution)
317    }
318
319    fn candidates_for(
320        &self,
321        state: &ResolverState,
322        service: &str,
323    ) -> Result<Vec<Candidate>, PluginResolutionError> {
324        let constraints = state.requirements.get(service).ok_or_else(|| {
325            PluginResolutionError::InvalidRequirements {
326                message: format!("unresolved service `{service}` has no constraints"),
327            }
328        })?;
329        let mut catalog_candidates = 0_usize;
330        let mut policy_candidates = 0_usize;
331        let mut available_versions = Vec::new();
332        let mut candidates = Vec::new();
333
334        for (record_index, record) in self.catalog.records().iter().enumerate() {
335            let plugin_version = Version::parse(&record.descriptor().version).map_err(|error| {
336                PluginResolutionError::InvalidCatalogRecord {
337                    artifact_identity: record.canonical_identity_key(),
338                    message: format!("invalid plugin version: {error}"),
339                }
340            })?;
341            for (provision_index, provision) in record.descriptor().provides.iter().enumerate() {
342                if provision.service != service {
343                    continue;
344                }
345                catalog_candidates += 1;
346                if !self.policy.accepts(record) {
347                    continue;
348                }
349                policy_candidates += 1;
350                let service_version = Version::parse(&provision.version).map_err(|error| {
351                    PluginResolutionError::InvalidCatalogRecord {
352                        artifact_identity: record.canonical_identity_key(),
353                        message: format!("invalid provided service version: {error}"),
354                    }
355                })?;
356                available_versions.push((service_version.clone(), provision.version.clone()));
357                if constraints
358                    .iter()
359                    .all(|constraint| constraint.parsed.matches(&service_version))
360                {
361                    candidates.push(Candidate {
362                        record_index,
363                        provision_index,
364                        service_version,
365                        plugin_version: plugin_version.clone(),
366                        priority: self.policy.plugin_priority(&record.descriptor().plugin_id),
367                        plugin_id: record.descriptor().plugin_id.clone(),
368                        artifact_identity: record.canonical_identity_key(),
369                    });
370                }
371            }
372        }
373
374        if catalog_candidates == 0 || policy_candidates == 0 {
375            return Err(PluginResolutionError::MissingProvider {
376                service: service.to_owned(),
377                requirements: describe_constraints(constraints),
378                catalog_candidates,
379                policy_candidates,
380            });
381        }
382        if candidates.is_empty() {
383            available_versions.sort();
384            available_versions.dedup_by(|left, right| left.1 == right.1);
385            return Err(PluginResolutionError::VersionConflict {
386                service: service.to_owned(),
387                requirements: describe_constraints(constraints),
388                available_versions: available_versions
389                    .into_iter()
390                    .map(|(_, version)| version)
391                    .collect(),
392            });
393        }
394        candidates.sort_by(candidate_order);
395        Ok(candidates)
396    }
397
398    fn apply_candidate(
399        &self,
400        state: &mut ResolverState,
401        service: &str,
402        candidate: Candidate,
403    ) -> Result<(), PluginResolutionError> {
404        let record = self
405            .catalog
406            .records()
407            .get(candidate.record_index)
408            .ok_or_else(|| PluginResolutionError::InvalidCatalogRecord {
409                artifact_identity: candidate.artifact_identity.clone(),
410                message: "candidate record index is outside the catalog".to_owned(),
411            })?;
412        let artifact_identity = record.canonical_identity_key();
413        if let Some(selected_identity) = state.plugin_artifacts.get(&record.descriptor().plugin_id)
414            && selected_identity != &artifact_identity
415        {
416            return Err(PluginResolutionError::PluginIdentityConflict {
417                plugin_id: record.descriptor().plugin_id.clone(),
418                selected_identity: selected_identity.clone(),
419                candidate_identity: artifact_identity,
420            });
421        }
422
423        state.selected_services.insert(
424            service.to_owned(),
425            SelectedProvider {
426                record_index: candidate.record_index,
427                provision_index: candidate.provision_index,
428            },
429        );
430        if state
431            .selected_artifacts
432            .insert(artifact_identity.clone(), candidate.record_index)
433            .is_none()
434        {
435            state.plugin_artifacts.insert(
436                record.descriptor().plugin_id.clone(),
437                artifact_identity.clone(),
438            );
439            for requirement in &record.descriptor().requires {
440                state.add_constraint(
441                    requirement,
442                    RequirementOwner::Artifact(artifact_identity.clone()),
443                )?;
444                if let Some(selected) = state.selected_services.get(&requirement.service) {
445                    self.validate_selected_service(state, &requirement.service, *selected)?;
446                }
447            }
448        }
449        Ok(())
450    }
451
452    fn validate_selected_service(
453        &self,
454        state: &ResolverState,
455        service: &str,
456        selected: SelectedProvider,
457    ) -> Result<(), PluginResolutionError> {
458        let record = self
459            .catalog
460            .records()
461            .get(selected.record_index)
462            .ok_or_else(|| PluginResolutionError::InvalidCatalogRecord {
463                artifact_identity: format!("record-index:{}", selected.record_index),
464                message: "selected provider index is outside the catalog".to_owned(),
465            })?;
466        let provision = record
467            .descriptor()
468            .provides
469            .get(selected.provision_index)
470            .ok_or_else(|| PluginResolutionError::InvalidCatalogRecord {
471                artifact_identity: record.canonical_identity_key(),
472                message: "selected provision index is outside the descriptor".to_owned(),
473            })?;
474        let version = Version::parse(&provision.version).map_err(|error| {
475            PluginResolutionError::InvalidCatalogRecord {
476                artifact_identity: record.canonical_identity_key(),
477                message: format!("invalid selected service version: {error}"),
478            }
479        })?;
480        let constraints = state.requirements.get(service).ok_or_else(|| {
481            PluginResolutionError::InvalidRequirements {
482                message: format!("selected service `{service}` has no constraints"),
483            }
484        })?;
485        if constraints
486            .iter()
487            .all(|constraint| constraint.parsed.matches(&version))
488        {
489            return Ok(());
490        }
491        Err(PluginResolutionError::VersionConflict {
492            service: service.to_owned(),
493            requirements: describe_constraints(constraints),
494            available_versions: vec![provision.version.clone()],
495        })
496    }
497
498    fn finish(&self, state: ResolverState) -> Result<PluginResolution, PluginResolutionError> {
499        let artifact_order = self.topological_order(&state)?;
500        let providers = state
501            .selected_services
502            .iter()
503            .map(|(service, selected)| {
504                let record = self
505                    .catalog
506                    .records()
507                    .get(selected.record_index)
508                    .ok_or_else(|| PluginResolutionError::InvalidCatalogRecord {
509                        artifact_identity: format!("record-index:{}", selected.record_index),
510                        message: "selected provider index is outside the catalog".to_owned(),
511                    })?;
512                let provision = record
513                    .descriptor()
514                    .provides
515                    .get(selected.provision_index)
516                    .ok_or_else(|| PluginResolutionError::InvalidCatalogRecord {
517                        artifact_identity: record.canonical_identity_key(),
518                        message: "selected provision index is outside the descriptor".to_owned(),
519                    })?;
520                Ok(PluginResolvedProvider {
521                    service: service.clone(),
522                    provided_version: provision.version.clone(),
523                    artifact: record.clone(),
524                })
525            })
526            .collect::<Result<Vec<_>, PluginResolutionError>>()?;
527        let artifacts = artifact_order
528            .iter()
529            .map(|identity| {
530                let index = state.selected_artifacts.get(identity).ok_or_else(|| {
531                    PluginResolutionError::InvalidCatalogRecord {
532                        artifact_identity: identity.clone(),
533                        message: "topological artifact is absent from selection".to_owned(),
534                    }
535                })?;
536                self.catalog.records().get(*index).cloned().ok_or_else(|| {
537                    PluginResolutionError::InvalidCatalogRecord {
538                        artifact_identity: identity.clone(),
539                        message: "topological artifact index is outside the catalog".to_owned(),
540                    }
541                })
542            })
543            .collect::<Result<Vec<_>, _>>()?;
544        Ok(PluginResolution {
545            catalog_fingerprint: self.catalog.fingerprint().to_owned(),
546            providers,
547            artifacts,
548        })
549    }
550
551    fn topological_order(
552        &self,
553        state: &ResolverState,
554    ) -> Result<Vec<String>, PluginResolutionError> {
555        let mut adjacency = state
556            .selected_artifacts
557            .keys()
558            .map(|identity| (identity.clone(), BTreeSet::new()))
559            .collect::<BTreeMap<_, _>>();
560        let mut indegree = state
561            .selected_artifacts
562            .keys()
563            .map(|identity| (identity.clone(), 0_usize))
564            .collect::<BTreeMap<_, _>>();
565        let mut order_keys = BTreeMap::new();
566        for (identity, record_index) in &state.selected_artifacts {
567            let record = self.catalog.records().get(*record_index).ok_or_else(|| {
568                PluginResolutionError::InvalidCatalogRecord {
569                    artifact_identity: identity.clone(),
570                    message: "selected artifact index is outside the catalog".to_owned(),
571                }
572            })?;
573            order_keys.insert(identity.clone(), ArtifactOrderKey::from_record(record)?);
574        }
575
576        for (dependent_identity, record_index) in &state.selected_artifacts {
577            let record = self.catalog.records().get(*record_index).ok_or_else(|| {
578                PluginResolutionError::InvalidCatalogRecord {
579                    artifact_identity: dependent_identity.clone(),
580                    message: "selected artifact index is outside the catalog".to_owned(),
581                }
582            })?;
583            for requirement in &record.descriptor().requires {
584                let provider = state
585                    .selected_services
586                    .get(&requirement.service)
587                    .ok_or_else(|| PluginResolutionError::MissingProvider {
588                        service: requirement.service.clone(),
589                        requirements: vec![requirement.requirement.clone()],
590                        catalog_candidates: 0,
591                        policy_candidates: 0,
592                    })?;
593                let provider_record = self
594                    .catalog
595                    .records()
596                    .get(provider.record_index)
597                    .ok_or_else(|| PluginResolutionError::InvalidCatalogRecord {
598                        artifact_identity: format!("record-index:{}", provider.record_index),
599                        message: "dependency provider index is outside the catalog".to_owned(),
600                    })?;
601                let provider_identity = provider_record.canonical_identity_key();
602                let inserted = adjacency
603                    .entry(provider_identity.clone())
604                    .or_default()
605                    .insert(dependent_identity.clone());
606                if inserted {
607                    let value = indegree.get_mut(dependent_identity).ok_or_else(|| {
608                        PluginResolutionError::InvalidCatalogRecord {
609                            artifact_identity: dependent_identity.clone(),
610                            message: "dependency target is absent from selection".to_owned(),
611                        }
612                    })?;
613                    *value = value.saturating_add(1);
614                }
615            }
616        }
617
618        let mut ready = indegree
619            .iter()
620            .filter_map(|(identity, count)| {
621                (*count == 0)
622                    .then(|| order_keys.get(identity).cloned())
623                    .flatten()
624            })
625            .collect::<BTreeSet<_>>();
626        let mut order = Vec::with_capacity(indegree.len());
627        while let Some(key) = ready.pop_first() {
628            let identity = key.identity;
629            order.push(identity.clone());
630            if let Some(dependents) = adjacency.get(&identity) {
631                for dependent in dependents {
632                    let count = indegree.get_mut(dependent).ok_or_else(|| {
633                        PluginResolutionError::InvalidCatalogRecord {
634                            artifact_identity: dependent.clone(),
635                            message: "dependency target is absent from indegree map".to_owned(),
636                        }
637                    })?;
638                    *count = count.saturating_sub(1);
639                    if *count == 0 {
640                        let key = order_keys.get(dependent).cloned().ok_or_else(|| {
641                            PluginResolutionError::InvalidCatalogRecord {
642                                artifact_identity: dependent.clone(),
643                                message: "dependency target has no stable order key".to_owned(),
644                            }
645                        })?;
646                        ready.insert(key);
647                    }
648                }
649            }
650        }
651        if order.len() == indegree.len() {
652            return Ok(order);
653        }
654        let adjacency = adjacency
655            .into_iter()
656            .map(|(identity, dependents)| {
657                let mut dependents = dependents.into_iter().collect::<Vec<_>>();
658                dependents.sort_by(|left, right| {
659                    order_keys
660                        .get(left)
661                        .cmp(&order_keys.get(right))
662                        .then_with(|| left.cmp(right))
663                });
664                (identity, dependents)
665            })
666            .collect::<BTreeMap<String, Vec<String>>>();
667        Err(PluginResolutionError::DependencyCycle {
668            artifact_identities: find_cycle(&adjacency, &order_keys),
669        })
670    }
671}
672
673#[derive(Debug, Clone)]
674struct RequirementConstraint {
675    requirement: String,
676    parsed: VersionReq,
677    owner: RequirementOwner,
678}
679
680#[derive(Debug, Clone)]
681enum RequirementOwner {
682    Host,
683    Artifact(String),
684}
685
686impl RequirementOwner {
687    fn label(&self) -> &str {
688        match self {
689            Self::Host => "host",
690            Self::Artifact(identity) => identity,
691        }
692    }
693}
694
695#[derive(Debug, Clone, Copy)]
696struct SelectedProvider {
697    record_index: usize,
698    provision_index: usize,
699}
700
701#[derive(Debug, Clone, Default)]
702struct ResolverState {
703    requirements: BTreeMap<String, Vec<RequirementConstraint>>,
704    selected_services: BTreeMap<String, SelectedProvider>,
705    selected_artifacts: BTreeMap<String, usize>,
706    plugin_artifacts: BTreeMap<String, String>,
707    constraint_count: usize,
708}
709
710impl ResolverState {
711    fn add_constraint(
712        &mut self,
713        requirement: &PluginRequirement,
714        owner: RequirementOwner,
715    ) -> Result<(), PluginResolutionError> {
716        if self.constraint_count >= MAX_PLUGIN_RESOLUTION_CONSTRAINTS {
717            return Err(PluginResolutionError::ConstraintLimitExceeded {
718                limit: MAX_PLUGIN_RESOLUTION_CONSTRAINTS,
719            });
720        }
721        let parsed = VersionReq::parse(&requirement.requirement).map_err(|error| {
722            PluginResolutionError::InvalidRequirements {
723                message: format!(
724                    "service `{}` has invalid semver requirement: {error}",
725                    requirement.service
726                ),
727            }
728        })?;
729        self.requirements
730            .entry(requirement.service.clone())
731            .or_default()
732            .push(RequirementConstraint {
733                requirement: requirement.requirement.clone(),
734                parsed,
735                owner,
736            });
737        self.constraint_count += 1;
738        Ok(())
739    }
740
741    fn next_unresolved_service(&self) -> Option<String> {
742        self.requirements
743            .keys()
744            .find(|service| !self.selected_services.contains_key(*service))
745            .cloned()
746    }
747}
748
749#[derive(Debug)]
750struct Candidate {
751    record_index: usize,
752    provision_index: usize,
753    service_version: Version,
754    plugin_version: Version,
755    priority: i32,
756    plugin_id: String,
757    artifact_identity: String,
758}
759
760#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
761struct ArtifactOrderKey {
762    plugin_id: String,
763    plugin_version: Version,
764    transport: PluginArtifactTransport,
765    target: String,
766    architecture: String,
767    format: crate::PluginArtifactFormat,
768    identity: String,
769}
770
771impl ArtifactOrderKey {
772    fn from_record(record: &PluginCatalogRecord) -> Result<Self, PluginResolutionError> {
773        let descriptor = record.descriptor();
774        let plugin_version = Version::parse(&descriptor.version).map_err(|error| {
775            PluginResolutionError::InvalidCatalogRecord {
776                artifact_identity: record.canonical_identity_key(),
777                message: format!("invalid plugin version: {error}"),
778            }
779        })?;
780        Ok(Self {
781            plugin_id: descriptor.plugin_id.clone(),
782            plugin_version,
783            transport: descriptor.transport,
784            target: descriptor.target.clone(),
785            architecture: descriptor.architecture.clone(),
786            format: descriptor.format,
787            identity: record.canonical_identity_key(),
788        })
789    }
790}
791
792fn candidate_order(left: &Candidate, right: &Candidate) -> Ordering {
793    right
794        .service_version
795        .cmp(&left.service_version)
796        .then_with(|| right.priority.cmp(&left.priority))
797        .then_with(|| right.plugin_version.cmp(&left.plugin_version))
798        .then_with(|| left.plugin_id.cmp(&right.plugin_id))
799        .then_with(|| left.artifact_identity.cmp(&right.artifact_identity))
800}
801
802fn describe_constraints(constraints: &[RequirementConstraint]) -> Vec<String> {
803    constraints
804        .iter()
805        .map(|constraint| {
806            format!(
807                "{} required by {}",
808                constraint.requirement,
809                constraint.owner.label()
810            )
811        })
812        .collect()
813}
814
815fn remember_first(target: &mut Option<PluginResolutionError>, error: PluginResolutionError) {
816    if target.is_none() {
817        *target = Some(error);
818    }
819}
820
821fn validate_policy_text(
822    field: &str,
823    value: &str,
824    maximum_bytes: usize,
825) -> Result<(), PluginResolutionError> {
826    if value.is_empty() || value.len() > maximum_bytes {
827        return Err(PluginResolutionError::InvalidPolicy {
828            field: field.to_owned(),
829            message: format!("must contain 1 to {maximum_bytes} UTF-8 bytes"),
830        });
831    }
832    Ok(())
833}
834
835fn find_cycle(
836    adjacency: &BTreeMap<String, Vec<String>>,
837    order_keys: &BTreeMap<String, ArtifactOrderKey>,
838) -> Vec<String> {
839    let mut colors = adjacency
840        .keys()
841        .map(|identity| (identity.clone(), 0_u8))
842        .collect::<BTreeMap<_, _>>();
843    let starts = order_keys.values().cloned().collect::<BTreeSet<_>>();
844    for start in starts.iter().map(|key| &key.identity) {
845        if colors.get(start).copied().unwrap_or_default() != 0 {
846            continue;
847        }
848        colors.insert(start.clone(), 1);
849        let mut path = vec![start.clone()];
850        let mut stack = vec![(start.clone(), 0_usize)];
851        while let Some((identity, next_index)) = stack.last_mut() {
852            let neighbors = adjacency
853                .get(identity)
854                .map(Vec::as_slice)
855                .unwrap_or_default();
856            if *next_index >= neighbors.len() {
857                colors.insert(identity.clone(), 2);
858                stack.pop();
859                path.pop();
860                continue;
861            }
862            let neighbor = neighbors[*next_index].clone();
863            *next_index += 1;
864            match colors.get(&neighbor).copied().unwrap_or_default() {
865                0 => {
866                    colors.insert(neighbor.clone(), 1);
867                    path.push(neighbor.clone());
868                    stack.push((neighbor, 0));
869                }
870                1 => {
871                    if let Some(start_index) = path.iter().position(|node| node == &neighbor) {
872                        let mut cycle = path[start_index..].to_vec();
873                        cycle.push(neighbor);
874                        return cycle;
875                    }
876                }
877                _ => {}
878            }
879        }
880    }
881    adjacency.keys().cloned().collect()
882}