diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
| commit | 8cc1ba8a02c0318b65658f1634de378c780392b9 (patch) | |
| tree | fdd5cb61e488018891a486b25991b87c84220bb8 /crates/mozart-sat-resolver/src/policy.rs | |
| parent | 72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4 (diff) | |
| download | php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.gz php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.zst php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.zip | |
refactor(workspace): consolidate crates into mozart-core
Merged mozart-archiver, mozart-autoload, mozart-registry,
mozart-sat-resolver, and mozart-vcs into mozart-core to align
the source layout with Composer's structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-sat-resolver/src/policy.rs')
| -rw-r--r-- | crates/mozart-sat-resolver/src/policy.rs | 264 |
1 files changed, 0 insertions, 264 deletions
diff --git a/crates/mozart-sat-resolver/src/policy.rs b/crates/mozart-sat-resolver/src/policy.rs deleted file mode 100644 index f45c4f5..0000000 --- a/crates/mozart-sat-resolver/src/policy.rs +++ /dev/null @@ -1,264 +0,0 @@ -use crate::pool::{Literal, Pool}; -use indexmap::IndexMap; - -/// Version selection policy: decides which version to prefer when multiple -/// candidates satisfy a requirement. -/// -/// Port of Composer's DefaultPolicy.php. -pub struct DefaultPolicy { - /// Whether to prefer stable versions. - pub prefer_stable: bool, - /// Whether to prefer lowest versions. - pub prefer_lowest: bool, - /// `name → normalized version` overrides used when more than one - /// candidate could satisfy a requirement: a literal pinned at the - /// preferred version wins outright over the usual highest/lowest pick. - /// Mirrors Composer's `DefaultPolicy::pruneToBestVersion` behavior under - /// `--minimal-changes`, where the lock's previously-installed versions - /// are passed in so the solver only moves a package when a constraint - /// actually forces a different version. - pub preferred_versions: Option<IndexMap<String, String>>, -} - -impl DefaultPolicy { - pub fn new(prefer_stable: bool, prefer_lowest: bool) -> Self { - DefaultPolicy { - prefer_stable, - prefer_lowest, - preferred_versions: None, - } - } - - pub fn with_preferred( - prefer_stable: bool, - prefer_lowest: bool, - preferred_versions: IndexMap<String, String>, - ) -> Self { - DefaultPolicy { - prefer_stable, - prefer_lowest, - preferred_versions: Some(preferred_versions), - } - } - - /// Select preferred packages from a list of candidate literals. - /// Returns the literals sorted by preference (most preferred first). - /// - /// Port of Composer's DefaultPolicy::selectPreferredPackages. - pub fn select_preferred_packages( - &self, - pool: &Pool, - literals: &[Literal], - _required_package: Option<&str>, - ) -> Vec<Literal> { - if literals.is_empty() { - return vec![]; - } - - // Group literals by package name - let mut groups: IndexMap<&str, Vec<Literal>> = IndexMap::new(); - for &lit in literals { - let pkg = pool.literal_to_package(lit); - groups.entry(pkg.name.as_str()).or_default().push(lit); - } - - // Sort each group by version preference - for lits in groups.values_mut() { - lits.sort_by(|&a, &b| self.compare_by_priority(pool, a, b)); - } - - // Prune to best version within each group - for lits in groups.values_mut() { - *lits = self.prune_to_best_version(pool, lits); - } - - // Merge and sort across all packages - let mut selected: Vec<Literal> = groups.into_values().flatten().collect(); - selected.sort_by(|&a, &b| self.compare_by_priority(pool, a, b)); - - selected - } - - /// Compare two package literals by priority. - /// Returns Ordering: negative means a is preferred. - fn compare_by_priority(&self, pool: &Pool, a: Literal, b: Literal) -> std::cmp::Ordering { - let pkg_a = pool.literal_to_package(a); - let pkg_b = pool.literal_to_package(b); - - // If same name, apply Composer's policy ordering. Mirrors - // `DefaultPolicy::versionCompare`: when `prefer_stable` is on and - // the two candidates have different stabilities, the more-stable - // one wins outright — `prefer_lowest` only kicks in within the same - // stability tier. Otherwise sort by version (asc for prefer_lowest, - // desc otherwise). - if pkg_a.name == pkg_b.name { - if self.prefer_stable { - let stab_a = stability_priority(&pkg_a.version); - let stab_b = stability_priority(&pkg_b.version); - if stab_a != stab_b { - return stab_a.cmp(&stab_b); - } - } - let cmp = self.compare_versions(&pkg_a.version, &pkg_b.version); - return if self.prefer_lowest { - cmp - } else { - cmp.reverse() - }; - } - - // Different names: when one package replaces the other, prefer the - // *replaced* original. Mirrors the `replaces()` shortcut in - // Composer's `DefaultPolicy::compareByPriority` (the cross-package - // `ignoreReplace=false` pass). Without this, a request like - // `update a/installed` where the pool also contains an - // `a/replacer` declaring `replace: { "a/installed": "dev-master" }` - // could fall through to package-id tie-break and land on the - // replacer instead of the package the user actually asked for. - if pkg_a.replaces.iter().any(|link| link.target == pkg_b.name) { - return std::cmp::Ordering::Greater; - } - if pkg_b.replaces.iter().any(|link| link.target == pkg_a.name) { - return std::cmp::Ordering::Less; - } - - // Different names, no replace relationship: sort by package ID - // for reproducibility. - pkg_a.id.cmp(&pkg_b.id) - } - - /// Compare two normalized version strings. - fn compare_versions(&self, a: &str, b: &str) -> std::cmp::Ordering { - match ( - mozart_semver::Version::parse(a), - mozart_semver::Version::parse(b), - ) { - (Ok(va), Ok(vb)) => va.cmp(&vb), - _ => a.cmp(b), - } - } - - /// Prune to the best version among a sorted list of literals for the same package. - fn prune_to_best_version(&self, pool: &Pool, literals: &[Literal]) -> Vec<Literal> { - if literals.is_empty() { - return vec![]; - } - - // Mirror Composer's `DefaultPolicy::pruneToBestVersion` short-circuit: - // when a preferred version is set for this package and one of the - // candidates matches it exactly, that wins over the regular - // highest/lowest pick. Falls through otherwise (e.g. the locked - // version no longer satisfies the constraint and was filtered out - // before reaching this method). - if let Some(ref preferred) = self.preferred_versions { - let name = pool.literal_to_package(literals[0]).name.clone(); - if let Some(preferred_ver) = preferred.get(&name) { - let preferred_lits: Vec<Literal> = literals - .iter() - .filter(|&&lit| pool.literal_to_package(lit).version == *preferred_ver) - .copied() - .collect(); - if !preferred_lits.is_empty() { - return preferred_lits; - } - } - } - - // The first literal is the best after sorting - let best_version = &pool.literal_to_package(literals[0]).version; - literals - .iter() - .filter(|&&lit| pool.literal_to_package(lit).version == *best_version) - .copied() - .collect() - } -} - -impl Default for DefaultPolicy { - fn default() -> Self { - DefaultPolicy::new(false, false) - } -} - -/// Map a normalized version string to Composer's stability priority -/// (`BasePackage::STABILITIES`). Lower = more stable. Stable=0, RC=5, beta=10, -/// alpha=15, dev=20. Mirrors `DefaultPolicy::versionCompare`'s comparison -/// when `prefer_stable` is set. -fn stability_priority(version: &str) -> u8 { - let Ok(v) = mozart_semver::Version::parse(version) else { - return 0; - }; - if v.is_dev_branch { - return 20; - } - match v.pre_release.as_deref() { - None => 0, - Some(pre) => { - let lower = pre.to_lowercase(); - if lower.starts_with("dev") { - 20 - } else if lower.starts_with("alpha") || lower == "a" { - 15 - } else if lower.starts_with("beta") || lower == "b" { - 10 - } else if lower.starts_with("rc") { - 5 - } else { - // patch/pl/p / unknown → stable - 0 - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::pool::PoolPackageInput; - - fn make_input(name: &str, version: &str) -> PoolPackageInput { - PoolPackageInput { - name: name.to_string(), - version: version.to_string(), - pretty_version: version.to_string(), - requires: vec![], - replaces: vec![], - provides: vec![], - conflicts: vec![], - is_fixed: false, - is_alias_of: None, - } - } - - #[test] - fn test_prefer_highest() { - let pool = Pool::new( - vec![ - make_input("a/a", "1.0.0.0"), - make_input("a/a", "2.0.0.0"), - make_input("a/a", "3.0.0.0"), - ], - vec![], - ); - let policy = DefaultPolicy::new(false, false); - let result = policy.select_preferred_packages(&pool, &[1, 2, 3], None); - // Should prefer highest version (3.0.0.0 = id 3) - assert_eq!(result[0], 3); - } - - #[test] - fn test_prefer_lowest() { - let pool = Pool::new( - vec![ - make_input("a/a", "1.0.0.0"), - make_input("a/a", "2.0.0.0"), - make_input("a/a", "3.0.0.0"), - ], - vec![], - ); - let policy = DefaultPolicy::new(false, true); - let result = policy.select_preferred_packages(&pool, &[1, 2, 3], None); - // Should prefer lowest version (1.0.0.0 = id 1) - assert_eq!(result[0], 1); - } -} |
