Compatibility and scope

What viv's byte-identical promise actually covers, what stays out of scope, and how that's checked before every release.

What viv promises

For the commands listed in the README's Scope section, viv writes a vendor/ directory — including vendor/composer/* — and, for update, update-lock and require/remove, a composer.lock that are byte-identical to what Composer 2.10 (the version pinned in devbox.json) writes for the same composer.json, lock file and cache state. This is the contract, not an implementation detail: if viv's output differs from Composer's for a covered command, that's a bug.

Composer moves; viv tracks one version of it at a time. Each release states which Composer version it targets, and the compat sweep (see below) is run against that version before the release ships.

What is not covered

Works with

Reasons not to use viv

Is it safe to try

viv's contract is that its output matches Composer's byte for byte. Before every release, a compatibility sweep installs a mix of pinned popular projects and a random sample of Packagist packages with both Composer and viv, then compares the results. The v0.13.0 sweep: 34 rows identical, 0 differ, 6 skipped, and all 10 pinned projects resolve the same composer.lock as Composer as well as installing the same vendor/.

One pinned project still needs --no-plugins, for a plugin viv refuses by design rather than one it has yet to port. See Plugins below.

How compatibility is checked

Every release runs the compatibility sweep against a mix of pinned popular projects and a random sample of Packagist packages, comparing viv's vendor/ and composer.lock against Composer's own. Per-release results live in compat/results/; ongoing sweeps of public projects outside that pinned set are tracked in compat/hunted.md. The Compare page turns the newest numbers into a table.

For the curious, here's exactly what a plain install writes, and how a path renders inside it — the detail the sweep checks byte for byte.

vendor/autoload.php, and in vendor/composer/:

File When
autoload_namespaces.php, autoload_psr4.php, autoload_classmap.php, autoload_static.php, autoload_real.php always
autoload_files.php any files entry, else deleted
platform_check.php config.platform-check not false and at least one PHP or ext requirement, else deleted
ClassLoader.php, InstalledVersions.php, LICENSE verbatim copies from Composer (MIT); vivace embeds them from src/autoload/templates/
installed.json, installed.php always
vendor/bin/* packages with bin, per config.bin-compat (src/bin.rs)

No .gitignore is written. Files are only rewritten when their bytes change.

Standard layout gives $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir);. A path under the vendor dir renders as $vendorDir . '/psr/log/src', anything else relative to the project as $baseDir . '/src'. Paths are normalised first: // collapsed, ./ and .. resolved, trailing slash stripped, so "src/" and "src" both give /src. In autoload_static.php the same paths render as __DIR__ . '/..' . '/psr/log/src' and __DIR__ . '/../..' . '/src'.

Versioning

viv is pre-1.0: expect breaking changes between minor releases, and expect them to be called out in JOURNAL.md. Within that, a minor release may:

A minor release may not change the output bytes (vendor/, composer.lock, or the contractual stdout of show/why/validate) that Composer's pinned version would produce for a given input, unless it's fixing a bug in a previous release's output.

When a release moves the pinned Composer version, its JOURNAL.md entry says so, and the compat sweep results in compat/results/ are regenerated against the new version as part of that release (see AGENTS.md, "Before a release").