aboutsummaryrefslogtreecommitdiffhomepage
AgeCommit message (Collapse)Author
2026-05-03fix(resolver): honor config.audit.block-insecure security-advisory filternsfisis
Mozart silently ignored the `security-advisories` block on inline `type: package` repositories and the `config.audit.block-insecure` audit flag, so a `composer update` succeeded with packages a Composer run would have refused to load. Mirror Composer's `SecurityAdvisoryPoolFilter` for the slice that feeds the pool: - Plumb a `security-advisories` field through `RawRepository` and a `block_insecure` flag through `ResolveRequest`, lifted off `composer.json`'s `config.audit.block-insecure`. - Collect every advisory's `affectedVersions` constraint at resolve time. When `block_insecure` is set and an inline package's normalized version satisfies the constraint, drop it from the pool before solving — root requires with no unaffected candidate then fail with the standard "could not be resolved" error.
2026-05-03fix(update): wire up the bare-keyword mirrors modensfisis
`update lock`, `update nothing`, and `update mirrors` were treated as ordinary full updates: the resolver picked the highest matching version of every root require and the install step rewrote refs from the repository, masquerading transport metadata refreshes as content changes (and accepting brand-new root requires the lock had never seen). Mirror Composer's `setUpdateMirrors(true)` flow: - Detect the bare-keyword form and skip composer.json's require / require-dev entirely; require each locked package by exact version instead. This drops fresh root requires Mozart shouldn't yet honor and pins existing ones to their lock version. - After lockfile generation, walk each new entry and copy the OLD lock's source/dist reference back when the source/dist *type* matches, mirroring `LockTransaction::updateMirrorAndUrls`. URL and mirrors update; ref stays put — so a repo rename or mirror flip emits no Update operation, but a real type change (`hg` → `git`) still does.
2026-05-03fix(install): reject lock when a locked dep's require excludes the rootnsfisis
Mozart's install verification didn't surface the slice of Composer's SAT-verify failure where a locked package's `require` targets the current root by name but the root's `version` no longer satisfies the declared constraint (e.g. lock has `b/requirer` requiring `root/pkg ^1`, root composer.json now ships `2.x-dev`). The install ran package operations against a lock that Composer would have rejected with exit-code 2. Add a targeted check that walks each locked package's requires, looks for ones aimed at the root's name, and fails with the same "found root/pkg[X.x-dev] but it does not match the constraint" pointer Composer prints from `Problem::getPrettyString`.
2026-05-03fix(resolver): strip inline #ref and gate alias trace on alias stabilitynsfisis
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.
2026-05-03fix(resolver): reject partial update when locked version fails stabilitynsfisis
A partial update reuses every non-allow-listed locked package as a fixed pool entry, ignoring stability filters. So when a user tightens `minimum-stability` (or drops a `stability-flags` entry the lock used to ride on), Mozart silently kept the rejected version and produced a plan Composer would have failed on. Mirror Composer's `Pool::isUnacceptableFixedOrLockedPackage` path: walk the locked packages before pool construction, surface every entry whose version no longer passes `passes_stability_filter`, and bail with the same "fixed to <v> (lock file version) ... rejected by your minimum-stability" pointer Composer prints from `Problem::getPrettyString`.
2026-05-03fix(update): apply --minimal-changes preferred versions on partial updatensfisis
The previous --minimal-changes wiring only populated the policy's preferred-version map when no packages were named on the CLI, so a partial update like `update foo --with-all-dependencies --minimal-changes` saw an empty map and the resolver picked the highest matching version for transitive deps that should have stayed at their lock version. Mirror Composer's `Installer::createPolicy(minimalUpdate=true)` directly: build the map from the lock and skip only the packages explicitly named by the user (the `updateAllowList`), so deps unlocked transitively by `--with-(all-)dependencies` still prefer their lock version when the constraint allows it.
2026-05-03fix(update): unlock replacer when --with-deps walks a replaced requirensfisis
`expand_with_(direct|all)_dependencies` only looked up dependencies by their literal name in the lock. When a transitive require pointed at a virtual / replaced name (e.g. `replaced/pkg1`) and the lock owned it through another package's `replace` map (e.g. `dep/pkg1` replaces `replaced/pkg1`), the replacer never entered the unlock set. The partial-update resolver then left it pinned at its lock version and silently kept the user on the old release. Mirror Composer's replace branch in `PoolBuilder::loadPackage`: build a `replaced → replacers` index over the lock and route every dep walked during expansion through it before recursing.
2026-05-03fix(update): apply --minimal-changes via policy preferred versionsnsfisis
The previous implementation pinned every resolved package back to its locked version after the resolve, which discarded the new versions the solver had to pick when a root constraint moved off the lock (e.g. a require bumped from `1.*` to `2.*`). The lock effectively never moved, so transitive cascades from a forced root-level update were lost. Mirror Composer's `Installer::createPolicy(forUpdate=true, minimalUpdate=true)` instead: thread the lock's `name → normalized version` map through the policy as `preferred_versions`. The solver now picks the locked version as a tiebreaker when it still satisfies the active constraints, but moves freely when a constraint forces a different version. Drop the post-process hook entirely.
2026-05-03fix(install): reject lock when locked packages conflict with each othernsfisis
Composer's `install` runs a SAT verify over the locked repository so a declared `conflict` between two locked packages (including via a branch-alias or the lock's top-level `aliases` block) fails fast with exit-code 2 and "Your lock file does not contain a compatible set of packages." Mozart skipped that step and proceeded to install both packages. Add a targeted check that walks each locked package's `conflict` map against every name a locked package effectively advertises (own version, `extra.branch-alias` target, lock-level `aliases` entry, `replace` constraint) and bails with the same exit code when a match is found.
2026-05-03fix(resolver): expand root branch-alias and self.version replace linksnsfisis
Two related parity gaps surfaced by the `circular-dependency` fixture: 1. The root's `extra.branch-alias` entry was never materialized in the pool, and root-level `replace`/`provide`/`conflict` constraints written as `self.version` were forwarded verbatim. Mirror Composer's `RootAliasPackage`: resolve `self.version` against the root's declared version for the base entry, then add an extra alias entry (carrying the base links plus a duplicate link per `self.version` original retagged at the alias's version) when the root's version matches an `extra.branch-alias` key. 2. `Pool::matches_package` returned on the first link to a target name even when its constraint did not match the query, hiding any later link to the same target. With the alias above, that masked the second `replace` link tagged at the alias version. Keep iterating when target matches but constraint does not, so a later link can still satisfy.
2026-05-03fix(lockfile): carry abandoned flag through to LockedPackage extrasnsfisis
`packagist_version_to_locked_package` was forwarding only `extra` and `notification-url` into `extra_fields`, so an `abandoned: "<replacement>"` declared in the package metadata never reached the lock. The same-version update detector then saw the lock and installed.json agreeing on "not abandoned" and skipped the resync, leaving the deprecation state stale on disk. Emit the field when truthy (string or `true`), matching Composer's `ArrayDumper::dump`.
2026-05-03fix(update): mirror Composer's always-include-dev resolution pathnsfisis
Composer's `Installer::doUpdate` hardcodes `includeDevRequires=true` for the first solve, so a `--no-dev` update still considers require-dev during resolution and writes a complete lock file (the flag only gates what gets installed). Mozart was passing `include_dev: dev_mode`, dropping require-dev from both the resolver pool and the lock when `--no-dev` was set, which broke fixtures where a non-dev requirement was satisfied by a package pulled in transitively through require-dev (e.g. `provided/pkg` provided by a require-dev metapackage). Also extend `classify_dev_packages` to walk `provide`/`replace` edges so the production BFS reaches packages that satisfy a `require` virtually, matching what Composer's `extractDevPackages` second-Solver run achieves through a real solve.
2026-05-03fix(install): align partial-update operation order with Composernsfisis
Three coordinated changes to make `update --with-dependencies` produce the same operation trace Composer emits: - LockFileGenerationRequest gains a previous_lock field. When a resolved package matches an entry in the old lock at the same name + version_normalized, its relationship-shaped fields (require / require-dev / conflict / replace / provide / suggest) are carried over verbatim. Source/dist refs and version-shaped fields still refresh from upstream metadata so dev packages can still pick up new commits. Without this carry-over, partial updates regenerated lock entries from upstream COMPOSER repo definitions, which can declare different requires than the lock — and topological_sort then sees a graph Composer's transaction never built. - Transaction's topological_sort and get_root_packages now expand replace/provide targets when matching `require` links to result packages, mirroring Composer's getProvidersInResult. Previously a package was only treated as required when matched by its own name, so packages reached only via replace/provide were mis-classified as roots and the DFS stack visited deps in the wrong order. - compute_operations iterates installed.json in reverse when emitting removals, mirroring Composer's array_unshift onto operations. Two co-orphaned packages otherwise emit removals in the wrong order vs Composer's trace. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(install): emit reference-suffixed removal and default-branch alias tracensfisis
Composer's UninstallOperation::show renders the package's getFullPrettyVersion(), which for dev packages includes the (truncated) source reference. Mozart was passing only the bare pretty version, so removal lines for dev packages dropped the ref. The MarkAliasUninstalled detection also missed the synthetic 9999999-dev alias that ArrayLoader::getBranchAlias surfaces for default-branch dev packages without an explicit branch-alias. Those aliases were never being retired alongside their targets. The new lock's implicit branch-aliases (from extra.branch-alias and the default-branch fallback) now count as "still present", so packages that remain in the lock don't trigger spurious uninstall traces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(update): union lock and repo requires when expanding --with-depsnsfisis
Previously requires_for_name returned the lock entry's requires when the package was already locked, falling back to repo requires only when not. That missed the case where the resolver would pick a *newer* version of the locked package that added a new requirement on another locked package — the new dependency stayed pinned and the upgrade was silently suppressed. Union both sources so every candidate version's requires contribute to the unlock cascade. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03feat(repository): support only/exclude/canonical repo filtersnsfisis
Composer's FilterRepository wraps a repository with three knobs: `only` / `exclude` to drop packages by name, and `canonical: false` to relax the repo's authoritative claim on its package names so lower-priority repos can still answer. Mozart was ignoring all three, so first-listed inline / composer-repo entries always shadowed later repos and `only` / `exclude` lists were silently no-ops. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(update): unlock new-package deps via repo requires for --with-depsnsfisis
`expand_with_direct_dependencies` only walked the lock map, so an allow-listed package not yet in the lock (a freshly added root require) contributed nothing to the unlock cascade. The resolver then kept transitive deps pinned to their lock versions and bailed when the new package's require could not be satisfied. Mirror Composer's `PoolBuilder::loadPackage` by also walking inline / composer-repo require lists for not-yet-locked packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(install): honour config.allow-missing-requirementsnsfisis
Composer's Installer::doInstall prints the missing-requirement warnings and continues when config.allow-missing-requirements is true, rather than bailing with ERROR_LOCK_FILE_INVALID. Mozart was always bailing, diverging on the install-from-incomplete-lock-with-ignore fixture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(install): skip MarkAliasInstalled when alias was already presentnsfisis
Composer's `Transaction::calculateOperations` only emits a MarkAliasInstalledOperation when the alias isn't already in `presentAliasMap`. Mirror that here: walk installed.json for each package being installed/updated, recover its prior alias set (explicit `extra.branch-alias` entries plus the synthetic `9999999-dev` alias for `default-branch: true` dev packages), and suppress the trace line when the new lock's alias normalized version was already there. Avoids the spurious "Marking ... as installed" emitted on a same-alias dev ref bump.
2026-05-03feat(resolver): honour audit.block-abandoned confignsfisis
Read `config.audit.block-abandoned` from composer.json (defaults to false) and propagate it to the resolver. When set, the pool builder skips packages whose `abandoned` field is truthy (`true` or a non-empty replacement string), matching `SecurityAdvisoryPoolFilter`'s behavior in `Composer\DependencyResolver`. With no candidates left, a root require that only matches abandoned versions fails resolution with exit 2.
2026-05-03fix(install): honour branch-alias when checking lock requirementsnsfisis
Mirror Composer's `Locker::getLockedRepository` flow when validating that every root require is satisfied by the lock and when emitting trace operations: a `dev-*` package's `extra.branch-alias` entry surfaces an AliasPackage at the alias version, so requirement matching considers that version too and `MarkAliasInstalled` fires for the branch-alias when the lock has no matching `aliases[]` entry. Dedupe by `alias_normalized` so packages aliased through both sources don't get two trace lines.
2026-05-03fix(resolver): load inline type:package entries by name, not eagerlynsfisis
Mirror Composer's PackageRepository (extends ArrayRepository) which only emits packages whose own name matches a queried name. Eager-loading every inline package let the SAT resolver pick a replacer that nothing required by name, masking broken transitive dependencies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(install): reinstall when locked package's abandoned flag driftsnsfisis
Composer's Transaction::calculateOperations fires an UpdateOperation on same-version packages when isAbandoned() or getReplacementPackage() shifts between installed and locked, so vendor/composer/installed.json picks up the refreshed metadata. Mozart only checked source/dist references and treated the abandon-flag drift as a no-op skip. Mirror the canonical bool/string reduction Composer uses (false/null → not abandoned, true → abandoned without replacement, string → abandoned with that replacement) so the check is symmetric across the lock and installed.json shapes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(install): reject locks where two packages claim the same namensfisis
Mirror Composer's `RuleSetGenerator::addConflictRules` SAME_NAME pass on the locked package set: a package's `getNames(false)` is its canonical name plus the names it claims via `replace`, and any name with two providers makes the lock-verify solve unsatisfiable. Mozart's `install` skips that solve, so the conflict slipped through and both packages were installed; surface it explicitly and exit DEPENDENCY_RESOLUTION_FAILED. `provide` targets are deliberately excluded — `getNames(false)` excludes them, since multiple providers of a virtual name may co-exist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(resolver): seed locked packages into pool and honour root-require barriernsfisis
Mirror Composer's PoolBuilder/Request semantics for partial updates: each non-allow-listed locked package becomes a non-fixed pool entry restricted to its locked version, so `replace`-providing peers cannot silently displace it. Path-repo packages are exempt — Composer always reloads them from disk. Threading `--with-dependencies` through `expand_with_direct_dependencies` now performs transitive expansion with a root-require barrier matching UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE, so root requires stay locked when reached via a transitive dep. Newly green: remove_does_nothing_if_removal_requires_update_of_dep, update_allow_list_removes_unused, github_issues_4795, partial_update_with_deps_warns_root. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(policy): prefer replaced original over replacer in cross-name picknsfisis
Mirrors the `replaces()` shortcut in Composer's `DefaultPolicy::compareByPriority` (the cross-package `ignoreReplace=false` pass). When two candidates with different names both satisfy a request — say `update a/installed` finds the real `a/installed` package alongside an `a/replacer` declaring `replace: { "a/installed": "..." }` — the policy now picks the replaced original. Without this, the comparison falls through to the package-id tie-break and silently lands on whichever entry was inserted first (here, the replacer with the wrong source ref). Net effect on installer fixtures: `update_dev_ignores_providers` newly green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(resolver): apply root "X as Y" aliases via pool second passnsfisis
Mirrors Composer's `RootPackageLoader::extractAliases` + `PoolBuilder::loadPackage` flow: strip the `as` clause from each root require so the SAT side sees only the LEFT-hand constraint, and after every package is loaded run a second pass that materializes an alias entry for any input matching `(name, version_normalized)`. Locked-only packages in a partial update are excluded via a new `ResolveRequest::locked_package_names` so they don't pick up the alias (`propagateUpdate=false` in Composer). Two adjacent fixes uncovered while making `install_aliased_alias` green: - `Version::cmp` treated unnamed wildcard branches (`1.0.x-dev`, `is_dev_branch=true && name=None`) as below every numeric version. They are semantically the same as the four-segment `*-dev` form Composer's `normalizeBranch` emits, so let only *named* branches take the shortcut. - `Constraint::Exact` / `NotEqual` used the derived `==`, which compared `is_dev_branch` field-by-field and missed the wildcard/numeric equivalence. Switch to `cmp` so both forms count as equal. - `Pool::matches_package` now falls back to parsing `pretty_version` when the `version` parse doesn't match the constraint, so a `dev-master` query lines up with a pool entry stored as the internal `9999999.x.x.x-dev` expansion. Net effect on installer fixtures: `install_aliased_alias` newly green, plus `aliased_priority`, `aliased_priority_conflicting`, and `install_dev_using_dist` come along for the ride. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(resolver): carry root composer.json conflicts onto the in-pool root entrynsfisis
The root pool entry now seeded from composer.json carried provides and replaces but no conflicts, so a root-level conflict like \`{"some/dep": ">=1.3"}\` was silently dropped. Composer keeps these on the RootPackage (which lives in the pool via RootPackageRepository), and the SAT generator turns them into rules that forbid any candidate matching the constraint — including a branch alias that would resolve to a matching version. Without that, Mozart cheerfully installs both the required dev branch and its conflicting alias. Plumb composer.json's \`conflict\` map through ResolveRequest as root_conflict and project it onto the root pool entry as PoolLink conflicts; all callers updated. Unblocks conflict_on_root_with_alias_prevents_update_if_not_required and conflict_with_alias_prevents_update installer fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(install): emit MarkAliasUninstalled and fix dev-branch upgrade directionnsfisis
Two pieces of Composer's update-trace machinery were missing: 1. VersionParser::isUpgrade in Composer\Package\Version (which overrides the upstream Semver one) substitutes dev-master / dev-trunk / dev-default with the 9999999-dev default-branch alias, then returns true whenever either side starts with `dev-`. Mozart's is_upgrade compared via the generic version order, so dev-master → dev-foo came out as Downgrading. Port the override. 2. Transaction::calculateOperations seeds removeAliasMap from the currently-installed AliasPackages and emits MarkAliasUninstalled for every entry not covered by the new lock. Mozart never emitted those, so updating away from a branch-aliased package produced no trace line for the alias retirement. Walk installed.json's `extra.branch-alias` map, compare against the new lock's aliases[] block, and emit a MarkAliasUninstalled PackageOperation (a new variant on the executor surface — no filesystem effects, only the trace recorder cares). Unblocks update_alias, update_alias_lock2, and update_no_dev_still_resolves_dev installer fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(resolver): seed root package into pool as fixed entrynsfisis
Composer's RootPackageRepository puts a clone of the root package into the pool as a fixed entry — its `require` / `require-dev` cleared, but its name, version, provides, and replaces preserved. That way a transitive `require` pointing back at the root resolves through the pool the same way any other reference would, and legal circular dependencies (root requires A, A requires root) work. Mozart had no such seed: the rule generator only knew about the root through the explicit root-require / root-provide / root-replace tables, so a transitive consumer requiring the root by name failed with no provider. Plumb root_version through ResolveRequest (RawPackageData gains a matching `Option<String>` field), build a fixed PoolPackageInput for the root with provides/replaces lifted from request.root_provide / root_replace, and skip the root by name when collecting the resolver's output so it doesn't leak into the lock file. Falls back to `1.0.0+no-version-set` (Composer's RootPackage::DEFAULT_PRETTY_VERSION) when the root composer.json omits `version`. Unblocks circular_dependency2, conflict_against_replaced_package_problem, and provider_conflicts installer fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(install): switch update trace to dist-ref mode when source refs matchnsfisis
Composer's UpdateOperation::format renders the from/to versions through DISPLAY_SOURCE_REF_IF_DEV first, but if both sides come out identical it re-renders in DISPLAY_SOURCE_REF (when source refs differ) or DISPLAY_DIST_REF (when only dist refs differ) so the trace doesn't show a useless `pkg (X => X)` line. Mozart skipped the switch and emitted the default form on both halves, so a same-version-different-dist-ref update showed up as `dev-master def000 => dev-master def000` instead of `dev-master def000 => dev-master`. Add format_update_pretty_versions to render the pair Composer's way and plumb the resolved to_full_pretty through PackageOperation::Update so the trace recorder uses it verbatim. Unblocks update_installed_reference and update_picks_up_change_of_vcs_type installer fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(install): treat dev-reference shifts as upgradesnsfisis
Composer's Transaction fires an UpdateOperation when an installed package's source/dist reference moved, even if the version string is unchanged — that is how a `dev-main#abcd` root require pinning a new commit propagates through `composer install`. Mozart was checking only (name, version) and short-circuiting to Skip, so the package stayed pinned to whatever reference installed.json carried. Compare references in compute_operations and route mismatches into Action::Update. The trace recorder needs the from-side display string to include the reference suffix (`dev-master abc123`) so the EXPECT output matches Composer's UpdateOperation::format; thread that through PackageOperation::Update as a separate from_full_pretty field while keeping from_version (sans suffix) for the upgrade-vs-downgrade direction check, which has to compare normalized versions like Composer's VersionParser::isUpgrade does. Unblocks update_reference, update_reference_picks_latest, and updating_dev_updates_url_and_reference installer fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(install): keep one cycle survivor as root for install orderingnsfisis
Mozart's install-order topological sort marked every package required by any other as non-root, so cycle members all fell out of the root set and the cycle fallback emitted them in input (alphabetical) order. Composer instead walks the sorted result map and removes each package's required providers as it goes, skipping outer packages already removed — leaving the highest-sort-key cycle member as a root and giving DFS a deterministic entry point. Mirror that. Unblocks the prefer_lowest_branches installer fixture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03refactor: switch internal maps/sets from HashMap to IndexMapnsfisis
Adopt indexmap workspace-wide so iteration order is deterministic and follows insertion order. The non-deterministic order of std HashMap otherwise leaks into resolver decisions when multiple valid solutions exist (e.g. cyclic require pairs under prefer-lowest), making behavior flaky and divergent from Composer's PHP-array semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03feat(resolver): apply config.platform overrides on top of detected platformnsfisis
Mirrors `Composer\Repository\PlatformRepository`'s `$overrides` handling: each override either replaces a detected platform package version or adds a virtual one (e.g. ext-dummy), and `false` disables the package. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(resolver): fail when a root require has no matching providersnsfisis
Mirror Composer's `Solver::checkForRootRequireProblems`: a root require that resolves to zero pool providers produces no SAT rule, so the solver previously succeeded with an empty plan instead of reporting the unresolvable requirement. `RuleSetGenerator::generate` now returns those misses alongside the rule set, and `resolve()` short-circuits into `ResolveError::NoSolution` so install/update exit with code 2 to match Composer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(registry): respect priority order across inline package reposnsfisis
When a `type: package` repository declares a name already declared by a higher-priority `type: package` entry, drop it. Mirrors Composer's RepositorySet first-repo-wins semantics so duplicate names across inline repositories cannot promote a lower-priority version into the pool. Greens 4 installer fixtures: install_prefers_repos_over_package_versions, repositories_priorities2, repositories_priorities4, update_package_present_in_lower_repo_prio_but_not_main_due_to_min_stability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(resolver): substitute self.version in pool link constraintsnsfisis
Composer's ArrayLoader and AliasPackage rewrite "self.version" to the declaring package's own version when building Link objects, so a package's replace/provide/conflict/require constraints carry a concrete "= <version>" rather than the literal string. Mozart was passing "self.version" through verbatim, which then failed to parse in Pool::matches_package and caused replace_alias.test to fail to find a provider for c/c 1.* via a/a's branch alias. Push the substitution into make_pool_links, threading the source package's normalized version through the call sites in resolver.rs (packagist/inline/composer-repo) and vcs_bridge.rs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02fix(resolver): honor root self-provide/replace as require fulfilmentnsfisis
Port Composer's RuleSetGenerator::createRequireRule self-fulfilling branch: when the root composer.json's `provide` or `replace` covers a name it also requires (with intersecting constraints), skip emitting an install-one-of rule for that root require. Composer relies on the root package being a fixed entry in the pool so whatProvides() includes it; Mozart does not yet add the root to the pool, so the same decision is made via explicit `root_provide` / `root_replace` tables threaded through ResolveRequest. Without this, an inline repo package whose name matches the root's provide was being force-installed. Fixes installer fixtures `provider_satisfies_its_own_requirement` and `replacer_satisfies_its_own_requirement`.
2026-05-02feat(resolver): support inline #ref pin and default-branch aliasnsfisis
Adds the missing pieces for installer fixtures that pin a dev package via `dev-foo#hex` or rely on Composer's `default-branch: true` synthetic `9999999-dev` alias. Mirrors Composer at four layers: 1. `mozart_semver::parse_single` strips `dev-...#hex` / `....x-dev#hex` suffixes from constraints (Composer's `parseConstraint` regex). 2. `PackagistVersion` carries `default_branch`. When set on a `dev-` package with no numeric prefix, `packagist_to_pool_inputs` emits the synthetic `9999999-dev` alias — but skips it when an explicit `extra.branch-alias` already covers the version (matches `ArrayLoader::getBranchAlias`). 3. `RuleSetGenerator::generate` picks up `addRulesForRootAliases`: any pool alias whose target was added gets its own alias↔target rules so the SAT solver pulls them in together. 4. `lockfile::generate_lock_file` extracts root `#hex` overrides from `require`/`require-dev` and rewrites source/dist references (and github/gitlab/bitbucket archive URLs) on the matched package, the `setSourceDistReferences` ladder Composer runs in `PoolBuilder`. Resolver also infers `Stability::Dev` from a `dev-foo` style single-atom constraint when no explicit `@flag` is given, mirroring the second loop of `RootPackageLoader::extractStabilityFlags` so the package isn't filtered out under default `stable` minimum-stability. Newly green: install_branch_alias_composer_repo, install_reference, conflict_with_alias_prevents_update_if_not_required, unbounded_conflict_matches_default_branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02feat(resolver): add branch-alias support across the resolution pipelinensfisis
Plumb Composer's `extra.branch-alias` mechanism end-to-end so a dev branch (e.g. `dev-foobar`) can be installed alongside its numeric alias (e.g. `3.2.x-dev`) and resolve constraints written against the alias target. Concretely: - `mozart-semver`: stop treating pure-numeric `-dev` as a wildcard branch — `3.2.9999999.9999999-dev` (the form `normalizeBranch` emits) now parses as a classical version with `is_dev_branch=false`, so constraints like `3.2.*` match it. - `mozart-registry/composer_repo`: load `type: composer` repositories from `file://` URLs (legacy embedded `packages.json`). - `mozart-registry/resolver`: emit pool entries in pairs for dev branches with `extra.branch-alias`, link them via `is_alias_of`, and apply `@dev`/`@beta` etc. stability suffix flags from root requires. - `mozart-sat-resolver`: alias rules (`PackageAlias` / `PackageInverseAlias`) so alias and target install together; alias packages skipped from same-name conflict indexing. - `mozart-sat-resolver/policy`: `DefaultPolicy` now honors `prefer_stable` via Composer's stability-tier comparison. - `mozart-registry/lockfile`: split resolved set into real packages vs. alias entries; populate the `aliases[]` block. - `mozart-registry/installer_executor`: new `MarkAliasInstalled` operation; `format_full_pretty_version` mirroring `BasePackage::getFullPrettyVersion` (appends source ref[0..7] for dev/git packages). - Test harness rewrites fixture-relative `file://` URLs to absolute paths. Newly green fixtures: `install_branch_alias_composer_repo`, `alias_solver_problems`, `alias_solver_problems2`, `conflict_with_all_dependencies_option_dont_recommend_to_use_it`, `unbounded_conflict_does_not_match_default_branch_with_branch_alias`, `unbounded_conflict_does_not_match_default_branch_with_numeric_branch`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02fix(installer): match Composer's transaction order and uninstall labelnsfisis
Three coupled changes that bring `compute_operations` + the in-process trace recorder into byte-parity with Composer's `Transaction::__toString` output: - `TraceRecorderExecutor`: emit "Removing X (V)" instead of "Uninstalling X (V)" — Composer's `UninstallOperation::__toString` uses "Removing". - `install_from_lock`: run removals before installs/updates to mirror `Transaction::moveUninstallsToFront`. Both dry-run and real-execution branches now emit the same prefix order. - `topological_sort`: replace recursive DFS with the stack-based DFS that Composer uses in `Transaction::calculateOperations`. Roots are seeded reverse-alphabetically (matching `setResultPackageMaps`'s uasort with `strcmp(b, a)`), and `getProvidersInResult` is mirrored by treating a package's `provide`/`replace` keys as additional name targets when resolving a `require` link. To make the third change work end-to-end, `LockedPackage` gains typed `provide` and `replace` fields (Composer's lock preserves them; Mozart was silently dropping them). `packagist_version_to_locked_package` now copies them through. Unignores 13 installer fixtures (10 newly green from the fix, 3 that were already green-but-still-flagged): conflict_downgrade_nested, install_from_lock_removes_package, install_security_advisory_matching_dependency, load_replaced_package_if_replacer_dropped, partial_update_keeps_older_dep_* (×2), partial_update_security_advisory_matching_locked_dep, provider_packages_can_be_installed_together_with_provided_if_both_installable, remove_deletes_unused_deps, replace_priorities, update_allow_list_require_new_replace, update_allow_list_with_dependencies_require_new_replace, update_requiring_decision_reverts_and_learning_positive_literals. Installer scoreboard: 75/187 → 88/187. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02Merge branch 'test/di'nsfisis
2026-05-02test(installer): switch fixtures to in-process harnessnsfisis
Replaces the spawn-based runner in tests/installer.rs with the in-process harness from Step E. Every fixture now goes through mozart::commands::{install,update}::run with an empty RepositorySet (Composer's `'packagist' => false` test config) and a TraceRecorderExecutor (Composer's InstallationManagerMock), and the EXPECT section is now asserted against the recorder's trace - load-bearing for behavior parity, not just exit-code. The original CI failure (suggest_replaced) is now legitimately tested: the empty RepositorySet makes b/b unreachable just like Composer's test config, the inline package repo's eager preload finds c/c which replaces b/b, and the topological install order in compute_operations produces the c/c -> a/a trace the fixture pins. Strict trace assertion surfaced 60 Mozart-vs-Composer divergences that the exit-code-only spawn runner had been silently ignoring. Each is marked `installer_fixture\!(name, ignore)` for now; the categories break down roughly as: - alias handling (alias_in_lock2, install_aliased_alias, update_alias*) - replace / provider trace shape (replace_priorities, provider_satisfies_its_own_requirement, replacer_*) - update direction strings (update_changes_url, update_reference, update_dev_*) - partial-update + lock interactions (partial_update_*) - allow-list with replace/dependency interactions (update_allow_list_with_dependencies_require_new*) These each become individual follow-up Mozart bugs rather than mass silent-pass. Also marks prefer_lowest_branches as ignore: it's a real flake driven by HashSet iteration order in the resolver, where two equivalent candidates can be picked in either order. That's a separate determinism bug worth its own fix. The proxy-hack env-vars in mozart-test-harness::runner are removed - no test currently spawns the binary, and the in-process harness expresses Packagist disablement directly via RepositorySet::empty rather than relying on TCP failure to suppress network calls. Headline numbers: 75 passed (in-process, exit-code + EXPECT trace) + 112 ignored, vs prior 136 passed (spawn, exit-code only) + 51 ignored. The drop in passing count reflects the stricter assertion bar, not new regressions. Also removes tests/installer_in_process.rs - its single proof-of- concept fixture (suggest_replaced) is now part of the unified installer.rs harness. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02feat(installer): add trace recorder and topo install ordernsfisis
Adds TraceRecorderExecutor (Composer's InstallationManagerMock analog), which records every install/update/uninstall as a string matching Composer's *Operation::__toString output (after strip_tags) - the load-bearing assertion target for in-process fixture tests. Two changes were needed to make the recorder useful: - InstallerExecutor::uninstall_package gains a version parameter, and install_from_lock now looks up both the uninstall and the Update-from-version from installed.json. Previously the Update path passed the new version as a placeholder; the recorder needs the real old version to emit `Upgrading pkg (old => new)`. - compute_operations now topologically sorts the lock contents (deps before dependents) before computing actions, mirroring Composer's Transaction::calculateOperations. Without this, packages would install in alphabetical order and the trace would diverge from Composer's expectation. Also adds crates/mozart/tests/installer_in_process.rs with the in-process harness scaffold: parses the same .test fixtures, builds a tempdir, calls commands::install::run / update::run with an empty RepositorySet (no Packagist) and a TraceRecorderExecutor, then asserts exit code + EXPECT trace. One fixture wired up: suggest_replaced - the original CI failure that motivated this whole DI refactor. It now passes on the in-process path because the empty RepositorySet makes b/b unreachable just like Composer's `'packagist' => false` test config, and the resolver finds c/c (which replaces b/b) via the inline package repo's eager preload. Step F will migrate every fixture currently in installer.rs to the new harness; remaining divergences (alias handling, output ordering, replace trace shape, etc.) will surface as individual follow-ups. All 136 existing spawn-based fixtures + 114 mozart-registry tests + 541 mozart lib tests still green; clippy clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02refactor(commands): split install/update into CLI execute + library runnsfisis
Carve commands::install::execute and commands::update::execute into thin CLI-arg-driven wrappers + run() entry points that take (working_dir, args, console, repositories, executor) directly. The wrappers build a production RepositorySet (Packagist) + FilesystemExecutor from cli, then dispatch to run; in-process tests will call run directly with an empty RepositorySet (Composer's `'packagist' => false` test config) and a tracing InstallerExecutor. The install -> update fallback (no composer.lock present) now goes through update::run, forwarding the caller's repositories + executor so test mocks survive the edge. Also drop the now-dead InstallConfig::no_cache field — install_from_lock stopped consuming the cache when FilesystemExecutor was extracted in the earlier DI plumbing pass, so the field has no effect. All 136 enabled installer fixtures + 114 mozart-registry tests + 541 mozart lib tests still green; clippy clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02refactor(registry): plumb RepositorySet and executor through callersnsfisis
ResolveRequest and LockFileGenerationRequest now take Arc<RepositorySet> instead of a raw Cache. install_from_lock now accepts &mut dyn InstallerExecutor instead of constructing FilesystemExecutor internally. Both changes expose the DI injection points needed by the upcoming in-process test harness, where Packagist must be replaced with an empty RepositorySet (Composer's `'packagist' => false` test config) and filesystem install execution must be replaced with a tracing recorder (Composer's InstallationManagerMock). The eager VCS scan and inline-package preload still happen inside resolve(), so the RawRepository array is kept on ResolveRequest as raw_repositories - migrating those through RepositorySet remains a follow-up. RepositorySet gains with_packagist and empty constructors so production callers and future tests have a uniform construction shape. All 136 enabled installer fixtures + 114 mozart-registry tests + 541 mozart lib tests still green; clippy clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02refactor(registry): route Packagist queries through RepositorySetnsfisis
Replace direct packagist::fetch_package_versions calls in resolver::resolve (seed + transitive loops) and lockfile::generate_lock_file with repo_set.load_packages calls. PackagistRepository now propagates errors instead of swallowing them, so the seed loop's strictness and the transitive loop's local-leniency are both preserved exactly. VCS and inline-package repositories are still preloaded directly into the pool builder for now, with their names tracked in skip lists so we don't double-load them through the trait. Migrating them through RepositorySet is a follow-up - vcs_to_pool_inputs and packagist_to_pool_inputs differ in dev-branch handling that needs to be unified first. All 136 enabled installer fixtures + 114 mozart-registry tests + 541 mozart lib tests remain green; clippy clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02refactor(registry): introduce Repository and InstallerExecutor traitsnsfisis
Sets up DI scaffolding for in-process installer E2E tests, mirroring how Composer's PHPUnit suite swaps Packagist (FactoryMock) and the install manager (InstallationManagerMock) without touching the network or filesystem. Additions: - Repository trait + RepositorySet (Composer's RepositoryInterface analog), with PackagistRepository, InlinePackageRepository, VcsRepository impls. - InstallerExecutor trait (Composer's InstallationManager analog) with FilesystemExecutor extracted from install_from_lock. install_from_lock now delegates per-package install/uninstall verbs to FilesystemExecutor; console output orchestration stays in the caller so existing --EXPECT-OUTPUT-shape assertions remain comparable. No behavior change - all 136 enabled installer fixtures still pass. Also tightens the installer_fixture\! ignore form to a single token (installer_fixture\!(name, ignore)) for readability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02feat(resolver): real constraint intersection and replace-aware same-name ↵nsfisis
conflicts `Pool::what_provides` previously accepted any provide/replace candidate because `constraints_intersect` returned true unconditionally; the same-name conflict pass also looked only at canonical names, so a package replacing another at v1.0.0 was treated as a valid provider for a v2.0.0 require and could coexist with the replaced package. Implement interval-based `VersionConstraint::intersects` and index packages by their `replace` targets (matching Composer's `getNames(false)`) when generating same-name conflict rules. Greens 3 installer fixtures: conflict_against_replaced_by_dep_package_problem, provider_conflicts3, replaced_packages_should_not_be_installed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>