| Age | Commit message (Collapse) | Author |
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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`.
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
`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>
|
|
The pool builder and rule-set generator only consulted an exact-match
HashSet, so `--ignore-platform-reqs` (no value) and `--ignore-platform-req=ext-foo-*`
fell through to the SAT layer and produced "no matching package found"
for transitive platform deps. Track the bool flag separately and run
each platform name through `mozart_core::matches_wildcard` against the
configured patterns.
Unblocks four installer fixtures:
install-{ignore-platform-package-requirement-wildcard,ignore-platform-package-requirements}
update-{ignore-platform-package-requirement-wildcard,ignore-platform-package-requirements}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Composer fixtures with inline `package` repositories often set
`dist.url` / `source.url` to RFC 2606 placeholders like
`https://example.org`. Composer's PHPUnit suite swaps in
InstallationManagerMock; Mozart's harness invokes the real binary,
so its `reqwest`-based downloader actually hit the network and
hung for ~30s per fixture before failing.
Setting HTTP_PROXY / HTTPS_PROXY / NO_PROXY on the child process
routes every HTTP request through 127.0.0.1:1, which fails the
TCP connect immediately. The current 103 green installer tests
remain green (resolver short-circuits before download); the
ignored update_downgrades_unstable_packages now errors in 0.1s
instead of 30s, which is the safety net we want as more
inline-package fixtures get unignored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Composer's PackageRepository lets composer.json embed full package
metadata under repositories[].package, mirroring the on-disk
Packagist response shape. The vast majority of installer fixtures
under composer/tests/Composer/Test/Fixtures/installer (179 of 189)
rely on this — they declare every package they need inline rather
than hitting the network.
Three pieces wire this into Mozart:
1. mozart-core::package::RawRepository: relax `url` to Option<String>
(Composer enforces presence per repo type, not at JSON parse) and
add `package: Option<Value>` to receive the inline definition,
which can be a single object or an array.
2. mozart-registry::inline_package: a new module that walks
`&[RawRepository]`, picks out type=package entries, and reshapes
each `package` payload into a PackagistVersion (auto-computing
version_normalized when omitted, matching Packagist's output).
3. resolver::resolve and lockfile::generate_lock_file: feed inline
packages into the SAT pool builder and short-circuit the Packagist
fetch when generating the lock entry for a resolved inline package.
The package-name set is shared with the existing VCS-skip logic so
the seed and transitive loops don't double-fetch.
One additional install-time change: in install_from_lock, packages
that have neither dist nor source are now skipped silently instead
of bailing with "no dist or source information". This mirrors
Composer's MetapackageInstaller (no installer for type=metapackage)
and is also what Composer's own AllFunctionalTest exercises via
InstallationManagerMock — most inline-package fixtures define
synthetic packages with no download metadata, expecting the install
operation to be recorded but not actually run.
Net effect: installer fixture scoreboard jumps from 7/187 to 103/187.
The 84 fixtures still ignored hit issues unrelated to inline-package
plumbing — aliases, replace/provide chains, dev-reference handling,
allow-list updates, etc. — and are tracked separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Composer's InstalledFilesystemRepository::write() dumps the full
package via ArrayDumper, so flags like `abandoned` and
`default-branch` (which Mozart parks in LockedPackage::extra_fields)
should round-trip from composer.lock into vendor/composer/installed.json.
locked_to_installed_entry was zeroing the destination's extra_fields,
silently stripping these flags every time installed.json got
rewritten.
Carry the extra_fields map across verbatim. The
install-forces-reinstall-if-abandon-changes installer fixture is
already exit-0 green at the harness layer; this aligns the actual
end-state with Composer's EXPECT-INSTALLED so a future EXPECT-INSTALLED
comparison won't re-flag this gap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Composer's `Locker` treats `content-hash` as optional with BC support
(see Locker::isLocked() / isFresh() lines 142-147): if a lock predates
the field — or, in the case of installer fixtures, deliberately omits
it — Composer simply considers the lock "not fresh" against any
composer.json. Mozart's deserializer was strict, rejecting the lock
with `missing field content-hash` before any of the install-time
checks could run.
Default the field to empty via `#[serde(default)]`. With an empty
hash, `is_fresh()` returns false (matching Composer's BC behavior, so
the freshness warning still fires) and downstream code that overwrites
`content_hash` continues to work unchanged.
Closes the parsing barrier exercised by the
updating-dev-from-lock-removes-old-deps installer fixture. Note:
matching Composer's exact operations trace ("Upgrading a/devpackage
…", alias-removal lines) requires a `compute_operations` that compares
package source references — out of scope for this change and tracked
in .ken/test_design.md §7.2 under "EXPECT (operations trace) 比較".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
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>
|
|
Mirrors Composer's platform-requirements check during
Installer::doInstall(): merge platform requires from the lock's
platform/platform-dev fields with the root composer.json
require/require-dev (composer.json wins on duplicate keys), then
verify them against the detected runtime platform. If any are missing
or unsatisfied, print the standard "Your lock file does not contain a
compatible set of packages" message followed by Problem 1..N entries
and exit with DEPENDENCY_RESOLUTION_FAILED (2) instead of silently
proceeding to "Nothing to install" with exit 0. Closes the gap
exercised by the outdated-lock-file-with-new-platform-reqs-fails
installer fixture.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Mirrors Composer's Installer::doInstall() check: before installing from
an existing composer.lock, walk every root require (and require-dev in
dev mode) and confirm the lock contains a satisfying package. If any
are missing or fail the constraint, print the standard bullet-list
diagnostic and exit with LOCK_FILE_INVALID (4) instead of blindly
attempting to install and failing later with a misleading "no dist or
source information" error. Closes the gap exercised by the
outdated-lock-file-fails-install installer fixture.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Replace if-let/else-return with `?`, swap `as_ref().map(|k| k.as_slice())`
for `as_deref()`, and switch test fixtures from `vec\![]` to array literals
where ownership is unneeded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Composer treats _readme as write-only metadata: Locker.php injects it on
write but the loader never requires it. Mozart's deserializer was strict,
so any composer.lock without _readme failed to parse — including most
fixtures under composer/tests/.../installer/*.test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Composer's FilesystemRepository::initialize branches on
isset($data['packages']) — object form is v2, bare array is v1 —
and treats dev-package-names/dev as optional. Mirror that in
InstalledPackages::read so Mozart consumes shared .test fixtures
(which use v1) without harness preprocessing, and so installs over
v1-era vendor directories keep working. Drop the v1→v2 wrapper
that was added to mozart-test-harness for the same reason.
Removes #[ignore] from update_to_empty_from_locked (2/187 green).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Composer's RootPackageLoader assigns the root name "__root__" when
composer.json omits the "name" field. Mozart was failing deserialization
in that case, blocking any installer fixture with a nameless root
manifest. Apply the same serde default and unignore
update-to-empty-from-blank as the first green entry on the .test
scoreboard.
|
|
Add per-fixture #[test] entries (187 total) via a small declarative
macro that reads files directly from the composer submodule. For now
each test only asserts the file parses; execution and EXPECT-* checks
will be layered on as the harness gains comparison helpers.
|
|
Foundation for porting Composer's installer integration fixtures.
Parser covers the 13 sections of InstallerTest.php; runner sets up a
tempdir from COMPOSER/LOCK/INSTALLED and invokes the mozart binary.
No fixtures are migrated in this commit.
|
|
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>
|
|
The watch graph's propagateLiteral used a cloned chain for iteration
while checking the live chain for bounds. After move_watch removed a
node, the stale clone kept re-reading the same node index, causing an
infinite loop on large dependency sets (e.g. laravel/laravel).
Now re-fetch the live chain each iteration, matching Composer's
SplDoublyLinkedList semantics where remove() advances the iterator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
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>
|
|
Add #[tracing::instrument] and debug logs to all HTTP-calling
functions in mozart-registry and mozart-vcs for request-level
observability (URL, status code, cache hits, download size).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
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>
|
|
- 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>
|
|
`parse_hyphen_range` was using `<=` for all upper bounds, but Composer
treats partial versions (1-2 segments) differently: `8.1 - 8.5` should
mean `>=8.1.0 <8.6.0-dev`, not `>=8.1.0 <=8.5.0`. This caused
constraints like `php 8.1 - 8.5` to reject PHP 8.5.3.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
Improves testability and ensures proper resource cleanup by returning
errors through the existing MozartError/exit_code mechanism instead of
terminating the process directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
Sync descriptions with upstream Composer. Replace product name
references (Composer/composer) with Mozart.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
Replace manual tokio::runtime::Handle::current().block_on() calls with
native async/await throughout all VCS drivers. Introduce AnyVcsDriver
enum for static dispatch to avoid dyn trait with async methods.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
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>
|