aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src/commands/update.rs
AgeCommit message (Collapse)Author
2026-05-11test(commands): remove per-command testsnsfisis
2026-05-11refactor(package): rename traits and switch dep maps to IndexMapnsfisis
Rename Package/CompletePackage to PackageInterface/CompletePackageInterface to mirror Composer's interface names, and split each into its own module under crates/mozart-core/src/package/. Switch dependency-link and metadata maps from BTreeMap to indexmap::IndexMap so serialized JSON preserves the original key ordering rather than sorting alphabetically — matching PHP associative-array semantics. The --sort-packages behaviour in `require` is preserved via sort_unstable_keys.
2026-05-10refactor(downloader): turn DownloadManager into downloader registrynsfisis
Reshape DownloadManager from a hard-coded VCS match into a registry of DownloaderInterface instances keyed by source type, mirroring Composer's DownloadManager — with prefer-source/dist preferences, an IO handle, and a files cache. ArchiveManager now resolves dist sources through a shared DownloadManager instead of calling download_dist directly, and Composer::require / try_load take an IO so it flows through the factory wiring.
2026-05-10refactor(clippy): deny clippy::unused_trait_namesnsfisis
2026-05-10refactor(io): introduce IoInterface trait mirroring Composer IOInterfacensfisis
Add an `IoInterface` trait in mozart-core::console that mirrors `\Composer\IO\IOInterface`, implement it for `Console`, and switch commands, the auditor, and the suggested-packages reporter to accept the abstracted IO (typically `Arc<Mutex<Box<dyn IoInterface>>>` at the command boundary, `&dyn IoInterface` deeper down) instead of `&Console`. The console_writeln\!/write\! macros now go through `IoInterface::verbosity()` via the lock so any implementor works.
2026-05-10refactor(workspace): consolidate crates into mozart-corensfisis
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>
2026-05-09refactor(composer): move Composer and Factory from mozart-core to mozartnsfisis
Composer needs DownloadManager (from mozart-registry), but mozart-core sits below mozart-registry in the dependency graph — adding the field would create a dependency cycle. Moving Composer and create_composer to the mozart CLI crate breaks the cycle and lets the root state container hold a DownloadManager. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09refactor(update): Slice A — align update.rs with Composer's UpdateCommand ↵nsfisis
pipeline - Rename ChangeKind::Remove → Uninstall, drop Unchanged variant (propagate to remove.rs / require.rs); mirrors Composer's UninstallOperation naming - Fix magic-keyword mirrors detection: use Composer's count-diff semantic (any keyword triggers mirrors mode) instead of all-are-magic; mutex check now fires correctly for mixed lists like `update foo lock` - Add --patch-only pre-solve lock-presence check (previously missing; Composer throws InvalidArgumentException when no lock exists) - Add --patch-only pre-solve constraint injection: inject >=M.N.P.0,<M.(N+1).0.0 into temporary_constraints before the resolver runs, mirroring UpdateCommand::execute 177-195 - Delegate --bump-after-update to bump::do_bump (mirrors Composer's BumpCommand::doBump delegate); add \!--lock guard per Composer 280-282 - Drop parse_minimum_stability wrapper; call package::Stability::parse directly - Replace is_platform_dep with is_platform_package from mozart_core::platform (the local variant was incomplete: missed hhvm and composer pseudo-packages) - Wrap "Lock file operations" and "Writing lock file" in <info> tags - Update --no-suggest deprecation wording to match Composer 3 message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09refactor(install): Slice A — align install.rs with Composer's ↵nsfisis
InstallCommand pipeline - Match Composer's deprecation wording for --dev and --no-suggest (A1) - Add missing URL to lock-fallback warning (A2) - Reorder arg validation to match InstallCommand::execute order (A3) - Rename collect_install_* helpers to verify_lock_* and consolidate into a single verify_lock() entry point (A4) - Couple classmap_authoritative=true → optimize_autoloader=true, mirroring Composer's setter side-effects (Installer.php 1263-1272) (A5) - Centralise is_platform_package in mozart_core::platform, removing divergent local copies from install.rs and update.rs (A6) - Move compute_operations, topological_sort, alias helpers, and locked_to_installed_entry into mozart-registry::installer_executor:: transaction, re-exported from the parent module (A8) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08fix(licenses): align with Composer's LicensesCommand pipelinensfisis
Drive the command from Composer::require() and route the (installed | locked) branch through the ported PackageSorter, RepositoryUtils::filterRequiredPackages, and PackageInfo helpers in mozart-core. --no-dev for installed packages now filters via root.require closure instead of dev_package_names membership; text output annotates the name cell with an OSC 8 hyperlink to the view-source/homepage URL; summary ties resolve in first-seen order via IndexMap + stable sort_by_key(Reverse(count)) to mirror PHP's arsort().
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-05chore: remove redundant commentsnsfisis
2026-05-05refactor(commands): consolidate working_dir resolution into Cli methodnsfisis
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(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(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(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-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-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(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(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-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-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): 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-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-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-02fix(update): normalize locked version to 4-segment form on partial pinnsfisis
`apply_partial_update` and `apply_patch_only` both pinned non-listed packages back to the lock by copying `LockedPackage.version_normalized` verbatim, falling back to the raw pretty `version` when the field was missing. Lock files written by Composer always include the field, but hand-written fixtures (every `--LOCK--` block in the installer fixtures, in particular) typically only carry `version`. The 3-segment form ("1.0.0") then leaked into the resolved package, where `LockFileGenerationRequest::inline_lookup` compares against the 4-segment normalizer output ("1.0.0.0") and missed inline `type: package` entries — triggering a Packagist fetch (and proxy-blocked failure under the test harness) for a package that should never need one. Extract a single `locked_version_normalized` helper that runs the pretty version through `mozart_semver::Version::parse(...).to_string()` when the lock omits `version_normalized`, and use it from both call sites. Mirrors `packagist_to_pool_inputs` and `inline_lookup`, which already produce the 4-segment form. Unblocks 26 installer fixtures: the entire update-allow-list cluster (16, minus the alias subcase), five partial-update cases, and five others (full-update-minimal-changes, load-replaced-package-if-replacer-dropped, remove-deletes-unused-deps, remove-does-nothing-if-removal-requires-update-of-dep, update-changes-url). Scoreboard: 107 → 133 of 187 installer fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01feat(core): reject root composer.json that requires its own namensfisis
Mirrors Composer\Package\Loader\RootPackageLoader::load(): if the root package's "name" appears as a key in its own "require" or "require-dev" map, fail loudly before reaching the resolver. Without this, Mozart would silently let the request hit Packagist (which has no entry for the root's vendor/name) and report a misleading "could not be found" error. Wired into install::execute (when a lock file is present) and update::execute (the no-lock fallback path). Carries the same wording as Composer's RuntimeException so a future EXPECT-OUTPUT comparison will match. Also extends the installer test harness: when a fixture sets EXPECT-EXCEPTION but no EXPECT-EXIT-CODE, assert that Mozart exits non-zero. Full exception-class matching remains a follow-up (see .ken/test_design.md §7.2). Closes the gap exercised by the install-self-from-root installer fixture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-02-24fix(cache): enable dist archive caching for all commandsnsfisis
files_cache was Option<&Cache> and install_from_lock always passed None, so downloaded zip/tar archives were never cached. Make the parameter non-optional (&Cache) and wire it through every command that downloads dist archives (install, update, require, remove, create-project, archive). The Cache internally respects --no-cache via its enabled flag, so the Option wrapper was unnecessary. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24feat(cache): enable repo cache for all Packagist API callsnsfisis
Remove the Option wrapper from repo_cache in ResolveRequest, LockFileGenerationRequest, and fetch_package_versions. All commands now initialize a Cache via build_cache_config(cli.no_cache), ensuring Packagist metadata is cached to disk (respecting --no-cache flag). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23refactor(cli): route command output through Console abstractionnsfisis
Replace direct println\!/eprintln\! calls with console.write(), console.info(), and console.write_stdout() across all command handlers to respect verbosity settings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23fix(cli): align install/update output with Composer conventionsnsfisis
- Migrate eprintln\! to Console for consistent colored output - Use Composer terminology in lock file operations: Locking instead of Installing, Upgrading/Downgrading instead of Updating - Add is_downgrade() helper to distinguish upgrades from downgrades - Pass Console through install_from_lock for proper output handling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23feat(vcs): add mozart-vcs crate for VCS repository supportnsfisis
Implement VCS driver/downloader infrastructure mirroring Composer's VCS subsystem. Includes drivers for GitHub, GitLab, Bitbucket, Forgejo, Git, Hg, and SVN with API-based metadata resolution, plus source downloaders for Git/Hg/SVN. Integrates into mozart-registry via vcs_bridge module to scan VCS repositories and feed discovered packages into the SAT resolver. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23fix(update): implement --with constraints, inline shorthand, and APCu ↵nsfisis
passthrough - Parse and apply --with temporary constraints to the resolver - Support inline constraint shorthand (vendor/pkg:1.0.*) - Reject --lock combined with specific package names - Filter magic keywords (lock/nothing/mirrors) from package list - Pass APCu CLI flags through to InstallConfig instead of hardcoding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23fix(install): add CLI option validation, download-only wiring, and ↵nsfisis
apcu-prefix implicit enable - Restrict --prefer-install to source/dist/auto and --audit-format to table/plain/json/summary via clap value_parser - Error when --prefer-install is combined with --prefer-source/--prefer-dist - Wire --download-only through InstallConfig to skip autoloader and installed.json - Implicitly enable --apcu-autoloader when --apcu-autoloader-prefix is set - Apply same validation fixes to update, require, remove, create-project commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>