aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src
AgeCommit message (Collapse)Author
2026-05-06feat(core): port Factory::createComposer and AutoloadGenerator::dumpnsfisis
Add the Composer state-container types (LocalRepository, RepositoryManager, InstallationManager, AutoloadGenerator, AutoloadDumpOptions, PlatformRequirementFilter, Locker) plus the factory wiring that builds them from composer.json and vendor/composer/installed.json. AutoloadGenerator::dump lives in mozart-autoload as an extension trait so the orchestrating algorithm sits next to the classmap scanner while the state container stays in mozart-core. Rework dump-autoload to drive both, mirroring $composer->getAutoloadGenerator()->dump(...).
2026-05-06fix(clear-cache): write info messages to stderr to match Composernsfisis
Composer's ClearCacheCommand uses $io->writeError() for the per-cache status lines and the final summary; Mozart was writing them to stdout via console.info(). Switch to console_writeln_error\! so the output stream matches Composer.
2026-05-06refactor(console): rename color helpers and migrate call sites to ↵nsfisis
console_format! The six tag-style color functions (info, comment, error, question, highlight, warning) are pub only so that console_format! can call them from generated code; they are not part of the public API. Rename them to __format_*_message to make that intent visible, add a doc-comment saying not to call them directly, and replace every remaining direct call site with console_format!. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06refactor(console): add write macros and migrate commands to use themnsfisis
2026-05-06fix(cache): mirror Composer no-cache and cache-files-maxsize handlingnsfisis
--no-cache redirects COMPOSER_CACHE_DIR to /dev/null (mirrors Application::doRun). cache_files_maxsize is now u64 with a "300MiB"-style string deserializer. Cache::new() takes readonly instead of enabled; is_usable() detects null devices.
2026-05-05feat(core): port Factory::createConfig() as factory::create_config()nsfisis
Adds crates/mozart-core/src/factory.rs with get_cache_dir(), get_data_dir(), and create_config() — a Rust port of Composer\Factory::createConfig() (auth loading and htaccess creation are out of scope for now). Also fixes a correctness bug on Linux: the previous Config::default() resolved cache-dir to $XDG_CONFIG_HOME/composer/cache via the {$home}/cache placeholder, whereas Composer uses the XDG cache base ($XDG_CACHE_HOME or ~/.cache), giving ~/.cache/composer. Callers updated: - Composer::load() uses create_config() as the global baseline before merging project-level config. - config command execute_read() builds the global baseline with create_config() and overlays local config on top when not --global, matching Composer's actual layering order. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05refactor(core): replace ComposerConfig with typed Config structnsfisis
Config uses serde with kebab-case field mapping; known properties are strongly-typed fields and unknown keys flow into an extra BTreeMap. resolve_references is moved to the new config module.
2026-05-05chore: remove redundant commentsnsfisis
2026-05-05feat(dump-autoload): warn and exit 1 on missing installed packagesnsfisis
Mirror Composer's DumpAutoloadCommand by scanning the local repository for packages whose install path is absent on disk, emitting the same "Not all dependencies are installed" warning, and returning exit code 1 when any are missing. Also reorders the surrounding flow to follow Composer's command sequence.
2026-05-05refactor(commands): consolidate working_dir resolution into Cli methodnsfisis
2026-05-05feat(core): add Composer struct mirroring requireComposer/tryComposernsfisis
Introduce mozart_core::composer::Composer with require()/try_load() constructors and a config() accessor, modelled on PHP Composer's BaseCommand::requireComposer / tryComposer. ComposerConfig and composer_home move into mozart-core so Composer::load can resolve placeholders consistently. Migrate dump-autoload, archive, exec and run-script away from ad-hoc composer.json reads. exec and run-script now fail when composer.json is missing instead of silently falling back to "vendor/bin", matching the upstream requireComposer contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05chore(clear-cache): remove verbose type declarationnsfisis
2026-05-05feat(cache): include cache-vcs-dir in clear-cache commandnsfisis
Composer's clear-cache deletes cache-vcs-dir alongside repo/files caches, and GCs it via gcVcsCache (TTL-based, top-level subdir deletion, no size cap). Mozart was silently leaving VCS mirrors on disk forever — every clear-cache run grew the cache monotonically. Add cache_vcs_dir to CacheConfig (default {cache-dir}/vcs, env override COMPOSER_CACHE_VCS_DIR), wire it into clear-cache, and add Cache::gc_vcs mirroring Composer's gcVcsCache semantics.
2026-05-05refactor(depends): share execution path with prohibits via do_executensfisis
Mirror composer's BaseDependencyCommand::doExecute by collapsing the duplicated working-dir/load/lookup/print pipeline in depends.rs and prohibits.rs into a single dependency::do_execute helper driven by an `inverted` flag. The clap arg structs stay in their per-command modules and just forward to the shared entry point. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05refactor(core): centralize version constant in mozart-corensfisis
2026-05-04refactor(spdx-licenses): remove unused functionsnsfisis
2026-05-04feat(show): expand license output with name, OSI flag, and URLnsfisis
Mirror Composer's Command\ShowCommand::printLicenses(): emit one line per license identifier, expanded to "<full name> (<id>) [(OSI approved)] <url>" when the id is in the SPDX database, or just the id otherwise. The URL is built by mozart-spdx-licenses' new LicenseInfo::url() so the construction lives in the SPDX crate, like Composer's SpdxLicenses::getLicenseByIdentifier().
2026-05-04feat(validate): port ValidatingArrayLoader license checksnsfisis
Mirror Composer's Package\Loader\ValidatingArrayLoader::load() license block: warn on non-string/wrong-shape values, validate the SPDX expression with proprietary→MIT substitution, and surface "extra spaces" diagnostics. Validity is gated on the manifest's `time` field (checked only for releases without a date or within the last 8 days), mirroring Composer's strtotime('-8days') window.
2026-05-04feat(validate): warn on deprecated SPDX license identifiersnsfisis
Mirror Composer's Util\ConfigValidator::validate() license handling: treat empty string and empty array as missing, accept array form, and emit deprecation warnings (with GPL-specific -only/-or-later suggestions) for identifiers flagged deprecated in the SPDX database.
2026-05-04feat(http): honor config.cafile and config.capathnsfisis
Composer's config.cafile/config.capath were accepted by the config command but ignored by every HTTP request. Centralize reqwest client construction in mozart_core::http, pre-load the configured CA bundle at startup, and route every callsite (registry, vcs drivers, diagnose, self-update) through the shared builder so user-supplied roots are actually used during HTTPS verification.
2026-05-04fix(update): preserve locked refs and aliases on partial updatensfisis
Partial update of a non-allow-listed dev package now resolves and emits the locked-repo entry verbatim, mirroring Composer's `PoolBuilder`. Three coordinated changes: - resolver: `lock_filter_allows` accepts the locked package's branch- alias normalized versions, not just the base. Without this, root constraints like `~2.1` against a `dev-master` locked package whose branch alias is `2.1.x-dev` failed with "no matching package found". - lockfile: new `lock_pinned_names` field on `LockFileGenerationRequest` routes non-allow-listed packages through `previous_lock_lookup` before `inline_lookup`, so the lock's source/dist references survive even when the inline metadata has moved to a newer commit. - update: `apply_partial_update` skips alias entries — re-pinning their pretty `version` to the base would collapse the alias label and emit a self-referential entry in the new lock's `aliases[]` block. Unblocks partial_update_forces_dev_reference_from_lock_for_non_updated_packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04fix(update): run full resolve under --lock to surface alias changesnsfisis
Drop the content-hash-only short-circuit for `--lock` and route the flag through the same updateMirrors flow Composer uses (`UpdateCommand::execute` line 219). Locked packages are pinned at their lock versions, but the resolver still runs and the installer still emits the operation trace — including MarkAliasInstalled lines for aliases the lock declares but installed.json hasn't recorded yet. Three follow-on fixes the new flow needs: - Re-attach `<lock-version> as <alias>` from `lock.aliases` when building the mirrors-mode require list, so the resolver's alias extractor materializes the alias entry. The bare `<version>` form is required because `==<version>` fails Composer's normalize. - Don't `continue` past Action::Skip in the install loop. Composer's Transaction::calculateOperations emits MarkAliasInstalled even when the target package is already at the right version, as long as the alias is missing from installed.json. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03fix(update): honor symlink:false on path repos during partial updatensfisis
A path repo locked with `transport-options.symlink: false` is in copy-mode and Composer's PoolBuilder keeps that entry pinned at its lock version on a partial update. The previous unconditional skip treated every path-repo dist as "always reload from disk", which caused non-allow-listed copy-mode packages to drift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03feat(registry): support type: path repositoriesnsfisis
Adds a `mozart-php-serialize` crate (a byte-compatible port of PHP's `serialize()`) and a `mozart-registry::path_repository` module that expands `type: path` entries into synthetic `type: package` repositories. Each synthesized package carries the same SHA-1 dist reference Composer computes (`sha1(\$json . serialize(\$options))`) so the lockfile and trace lines match Composer byte-for-byte. Two latent bugs surfaced once the path-repo flow exercised real resolutions: - `apply_partial_update` swapped path-repo packages back to their locked version, defeating Composer's "path repos always reload" rule (`PoolBuilder` treats them as canonical, not lock-bound). Mirror the path-repo skip already used when constructing `locked_packages`. - `normalize_root_alias_atom` returned the raw input string for stable numeric atoms (e.g. `1.1.1`), so the alias matcher's `input.version \!= alias.version_normalized` check — comparing against pool inputs that carry the 4-segment normalized form — silently never matched. Run the parsed Version through Display so both sides are in the same shape. `install/update::run` gain a `path_repo_base_override: Option<&Path>` parameter for the in-process test harness: Composer's PHPUnit `InstallerTest::setUp` does `chdir(__DIR__)` so relative path-repo URLs resolve against `composer/tests/Composer/Test/`, but the Rust harness writes `composer.json` into a per-test tempdir and can't chdir safely under parallel tests. Production callers pass `None` and resolve against `working_dir`. Greens 3 ignored installer fixtures: partial_update_loads_root_aliases_for_path_repos alias_in_lock alias_in_lock2
2026-05-03fix(update): pattern-match allow-list specifiers and reuse locked metadatansfisis
Three related parity gaps surfaced by the `update-allow-list-patterns` fixture: 1. `mozart-semver`'s wildcard parser turned `*.*` into `>=0 <1` (a single-major range) because stripping the trailing `.*` left `*` in the major slot, which `parse()` quietly read as `0`. Composer reduces such patterns to a plain `*` (unconstrained) — match that and short-circuit when the stripped base is `*`. 2. `expand_wildcards` passed any non-wildcard specifier straight through, so a typo like `notexact/Test` (lock has `notexact/testpackage`) entered the resolver as a real package name and failed lookup. Mirror Composer's regex-based `isUpdateAllowed`/`warnAboutNonMatchingUpdateAllowList`: every specifier — wildcard or not — is matched against locked names *and* current root-require names, with `*` expanded to `.*`, and unmatched specs are warned and dropped instead of forwarded. 3. The lockfile generator's metadata loop hit the empty test repo set when a partial update kept a non-allow-listed package at its locked version that the inline repo no longer advertised, and bailed with "Could not find version". Add a `previous_lock` fallback that synthesizes a `PackagistVersion` straight off the `LockedPackage` so the lock entry's own metadata stays authoritative for packages that aren't moving.
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(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(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(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(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>