From 2a680f571eae31737525ef96105e929ae420b061 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 3 May 2026 22:26:55 +0900 Subject: fix(resolver): strip inline #ref and gate alias trace on alias stability Two related parity gaps surfaced by the `alias-with-reference` fixture: 1. A root require like `dev-main#abcd as 1.0.0` left the SAT-side constraint as `dev-main#abcd`, which no candidate matched, so resolution failed before the alias could be materialized. Mirror Composer's `extractAliases` regex (which captures only the constraint up to `#`) and `RootPackageLoader::extractReferences` (which records the hash separately): drop the trailing `#hex` from the resolver-side constraint and from the alias's left-hand side. Lockfile generation already pulls the reference back out of the raw require map for the post-resolve override. 2. `MarkAliasInstalled`'s trace line gated the reference suffix on the *target* package's stability, so a stable alias like `1.0.0` pointing at a dev-branch target rendered as `1.0.0 abcd`. Mirror `AliasPackage::getFullPrettyVersion`: the alias decides on its own whether to append the suffix based on its own normalized version, so a stable alias skips the suffix even when the target is dev. --- .../mozart-registry/src/installer_executor/mod.rs | 24 +++++++++++++++++ .../src/installer_executor/trace_recorder.rs | 7 ++--- crates/mozart-registry/src/resolver.rs | 31 +++++++++++++++++----- crates/mozart/tests/installer.rs | 2 +- 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/crates/mozart-registry/src/installer_executor/mod.rs b/crates/mozart-registry/src/installer_executor/mod.rs index c344ba4..c29e32c 100644 --- a/crates/mozart-registry/src/installer_executor/mod.rs +++ b/crates/mozart-registry/src/installer_executor/mod.rs @@ -110,6 +110,30 @@ pub fn format_full_pretty_with_pretty(pretty_version: &str, pkg: &LockedPackage) ) } +/// Render an alias's full pretty version: the alias's own pretty form for +/// the visible text, the alias's *normalized* version for the dev-stability +/// gate, and the target package's source/dist references for the suffix. +/// Mirrors `AliasPackage::getFullPrettyVersion`, where the alias decides on +/// its own whether to append a reference based on its own stability — so a +/// stable alias like `1.0.0` skips the suffix even when the target is a dev +/// branch. +pub fn format_full_pretty_alias( + alias_pretty: &str, + alias_version: &str, + target: &LockedPackage, +) -> String { + let source_ref = target.source.as_ref().and_then(|s| s.reference.as_deref()); + let dist_ref = target.dist.as_ref().and_then(|d| d.reference.as_deref()); + let source_type = target.source.as_ref().map(|s| s.source_type.as_str()); + format_full_pretty_with_refs( + alias_pretty, + alias_version, + source_ref, + dist_ref, + source_type, + ) +} + /// Same as [`format_full_pretty_version_for_installed`] but lets the caller /// supply an alternate pretty version. Used when emitting /// `MarkAliasUninstalled`: the alias's `1.0.x-dev` text needs to be rendered diff --git a/crates/mozart-registry/src/installer_executor/trace_recorder.rs b/crates/mozart-registry/src/installer_executor/trace_recorder.rs index 159d854..b60a869 100644 --- a/crates/mozart-registry/src/installer_executor/trace_recorder.rs +++ b/crates/mozart-registry/src/installer_executor/trace_recorder.rs @@ -17,8 +17,8 @@ use mozart_semver::Version; use super::{ - ExecuteContext, InstallerExecutor, PackageOperation, format_full_pretty_version, - format_full_pretty_with_pretty, + ExecuteContext, InstallerExecutor, PackageOperation, format_full_pretty_alias, + format_full_pretty_version, }; /// Recording-only executor. Construct with [`TraceRecorderExecutor::new`], @@ -84,7 +84,8 @@ impl InstallerExecutor for TraceRecorderExecutor { )); } PackageOperation::MarkAliasInstalled { alias, target } => { - let alias_full = format_full_pretty_with_pretty(&alias.alias, target); + let alias_full = + format_full_pretty_alias(&alias.alias, &alias.alias_normalized, target); let target_full = format_full_pretty_version(target); self.trace.push(format!( "Marking {} ({}) as installed, alias of {} ({})", diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs index 67650e6..9fb5cec 100644 --- a/crates/mozart-registry/src/resolver.rs +++ b/crates/mozart-registry/src/resolver.rs @@ -288,7 +288,11 @@ struct RootAlias { /// the cleaned constraint plus the `(left, right)` pieces when an alias is /// present. Mirrors Composer's `VersionParser::parseConstraint` `as`-strip: /// the constraint passed to the resolver is the LEFT side, and a separate -/// alias entry is recorded for the RIGHT side. +/// alias entry is recorded for the RIGHT side. A trailing `#hex` reference +/// (`dev-main#abcd`) is also stripped — Composer's `extractAliases` regex +/// `([^,\s#|]+)(?:#[^ ]+)?` excludes it from the captured constraint, and +/// `RootPackageLoader::extractReferences` records the hash separately for +/// the post-resolve `setSourceDistReferences` pass. fn strip_root_alias_clause(constraint: &str) -> (String, Option<(String, String)>) { let trimmed = constraint.trim(); if let Some(idx) = trimmed.find(" as ") { @@ -299,13 +303,28 @@ fn strip_root_alias_clause(constraint: &str) -> (String, Option<(String, String) && !before.contains([' ', '\t', ',', '|']) && !after.contains([' ', '\t', ',', '|']) { - return ( - before.to_string(), - Some((before.to_string(), after.to_string())), - ); + let cleaned = strip_inline_reference(before); + return (cleaned.clone(), Some((cleaned, after.to_string()))); } } - (trimmed.to_string(), None) + (strip_inline_reference(trimmed), None) +} + +/// Drop a trailing `#hex` reference from a single-atom `dev-*` / `*-dev` +/// constraint, matching Composer's `'{^[^,\s@]+?#([a-f0-9]+)$}'` guard. +/// Lockfile generation records the reference separately via +/// `extract_root_references` and applies it after resolution, so the SAT +/// constraint itself only needs the bare branch name. +fn strip_inline_reference(s: &str) -> String { + if let Some((head, hash)) = s.rsplit_once('#') + && !hash.is_empty() + && hash.chars().all(|c| c.is_ascii_hexdigit()) + && !head.contains([' ', '\t', ',', '@']) + && (head.to_lowercase().starts_with("dev-") || head.to_lowercase().ends_with("-dev")) + { + return head.to_string(); + } + s.to_string() } // ───────────────────────────────────────────────────────────────────────────── diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index 5e285a9..0759177 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -222,7 +222,7 @@ installer_fixture!(alias_in_lock2, ignore); installer_fixture!(alias_on_unloadable_package); installer_fixture!(alias_solver_problems); installer_fixture!(alias_solver_problems2); -installer_fixture!(alias_with_reference, ignore); +installer_fixture!(alias_with_reference); installer_fixture!(aliased_priority); installer_fixture!(aliased_priority_conflicting); installer_fixture!(aliases_with_require_dev, ignore); -- cgit v1.3.1