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 on the Scope page, 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.

The package repository type is compat mode and is covered: viv update against one produces Composer's lock byte for byte.

What is not covered

  • Plugins. Only the native adapters viv ships (see docs/plugin-strategy.md) are covered. Any other plugin is out of scope; viv either skips it or the composer shim hands the invocation to the real Composer.
  • Research-chapter surface. viv.lock and viv update --lock native, viv lock convert, viv lock merge, viv workspace list and viv isolate (with the extra.viv.isolate key it writes) are outside this contract. They sit behind explicit opt-in, their behaviour is judged against their chapter in docs/research.md, and they may change between minor releases.
  • Commands Composer keeps. search, config, global, self-update, licenses, depends and the rest of the long tail stay with Composer and aren't part of viv's contract.
  • viv's own init, new and diagnose. viv init writes a composer.json from defaults, without Composer's interactive questions. viv new (also create-project) starts a project in a new directory, and viv diagnose prints viv's own report. Their output isn't promised to match Composer's init, create-project or diagnose; the vendor/ and composer.lock they go on to write follow the same contract as viv install.
  • Progress wording on stdout during a run. Only the files viv writes and the plain-text output of commands such as show, why and validate are contractual. Spinner text, progress counters and similar in-flight chatter can change between releases without notice.
  • Timing. The numbers in the README track viv's speed but aren't a promise; a release can get slower on a given machine without breaking the contract, though the project tries hard not to let that happen.

Works with

  • Repositories: Packagist, Private Packagist and Satis, including a local file:// mirror, path, vcs, git, GitHub and package (inline declarations) sources; redirects are followed.
  • Packages: zip and tar dists, a git checkout when there is no dist, preferred-install: source, sha1 checks, credentials from auth.json and COMPOSER_AUTH.
  • Autoload: PSR-4, PSR-0, classmap and files; --optimize-autoloader and --classmap-authoritative; platform_check.php; vendor/bin proxies; lifecycle scripts.
  • Commands: install, full and partial update including --minimal-changes and Composer's default blocking of versions with a security advisory (--no-blocking to allow them), add, rm, dump-autoload, and offline mode with --offline or COMPOSER_DISABLE_NETWORK. install refuses before writing anything when the lock needs a PHP version, extension or library the detected platform lacks, honouring config.platform, --ignore-platform-reqs and --ignore-platform-req.
  • Plugins: the ones with native adapters, listed under Plugins.

Reasons not to use viv

  • It stays 0.x. No 1.0 is planned. Minor releases can change behaviour and flags; the release notes and JOURNAL.md call those out. The output contract (vendor/ and composer.lock identical to Composer's) is the one thing that does not move.1
  • Windows is not supported. Linux and macOS only.
  • Maintenance is on demand. The compatible mode is complete and no new plugin adapters or Composer commands are planned. A bug in it that a real project hits gets fixed; open an issue with the project's composer.json and lock. Releases continue as research chapters land, and the compat sweep and benchmark gates run on every change so the drop-in behaviour does not regress.
  • Some Composer plugins stop the install. symfony/flex and any plugin without a native adapter make viv exit with an error naming the plugin. --no-plugins installs as Composer would without them, but the plugin's work is not done. Of the 10 pinned test projects, 1 is in this position: symfony/demo, for symfony/flex, which viv refuses by design.
  • The shim needs a real Composer for everything else. It maps install, dump-autoload, normalize, create-project, update, require and remove to viv; search and the rest go to the Composer on your PATH, and with no real Composer installed they fail.
  • vendor/ files are read-only by default. viv hardlinks them from a shared store, so an edit inside vendor/ fails instead of changing every project on the machine. If you patch vendor files by hand, install with --link-mode copy, or --link-mode clone for writable files sharing the store's disk space where the filesystem supports it.
  • A minimum-stability gap. A version filtered out by minimum-stability is still reported as not found rather than as filtered; Composer names the cause.

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.2 The v0.21.0 sweep: all 20 install rows of the pinned corpus are identical, 9 of the 10 pinned projects resolve the same lock (craftcms/craft differs3), and viv lock export reproduces all 10 committed locks byte for byte. In the random sample, all 12 rows that Composer could install are identical; the other 8 were skipped because Composer itself could not resolve the project.4

One pinned project still needs --no-plugins, for a plugin viv refuses by design rather than one it has yet to port.5 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 Speed 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 stays in the 0.x series; no 1.0 is planned, because a 1.0 promises maintenance that nobody has asked for yet (see docs/research.md). Minor releases can make breaking changes, called out in JOURNAL.md. Within that, a minor release may:

  • add new commands or flags,
  • make internals faster,
  • change stdout progress wording or timing.

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").


  1. docs/stability.md states what a minor release may and may not change. ↩

  2. See compat/README.md for how the sweep works. ↩

  3. The v0.18.0 sweep had one lock differ, #316: craftcms/craft's requirements are satisfied by yii2-shell 2.0.6 and by dev-master, and the two solvers search in a different order, so the security-advisories feed, by changing which versions of other packages are available, decides whether Composer's search ends on dev-master. The v0.19.0 and v0.20.0 sweeps resolved the same lock on all 10, and the v0.21.0 sweep differs again, on symfony/var-dumper (v7.4.18 in Composer's lock, v5.4.48 in viv's). The issue stays open because the feed can flip the result. ↩

  4. The skips are packages Composer itself refuses to resolve, not something viv got wrong. In the v0.21.0 sweep, 3 of the 4 skipped projects have only dev- or alpha versions, which the default minimum-stability excludes; Composer could not find a dependency of the fourth. Earlier sweeps also skipped a project when security advisories blocked every matching version, or its platform requirements were unmet. Full results, including which projects and what was skipped, are in compat/results/v0.21.0.md. ↩

  5. Of viv's 10 pinned compatibility projects, the one that needs --no-plugins is symfony/demo, for symfony/flex. Flex does its work in composer require, so installing from a committed lock loses nothing; see docs/plugin-strategy.md. ↩