0.20.0

Released 2026-09-29

Added

  • viv lock export writes composer.lock from viv.lock through the one existing writer, byte-equal on all ten pinned compat projects; --check reports whether the committed composer.lock matches. viv install in a project with viv.lock and no composer.lock generates it first and says so; with both present it writes nothing and refuses a divergence with a hint naming viv lock export and viv lock convert. viv init --lock native and viv lock convert mark composer.lock linguist-generated. A viv.lock record now keeps Composer's own package block (raw) and the commit time, which is what makes the export exact and the merge rule below decidable; re-run viv lock convert on a lock written before this release (#344, #347)
  • viv lock merge identifies a dev-* record by its commit and, when both branches moved it to different commits, keeps the one with the later commit time and never re-solves that name; equal or missing times are a conflict naming both commits. When the re-solve needs a pinned commit the registry's branch entry no longer describes, viv fetches that commit's composer.json from the package's git source, once per machine (fetching <package> at <sha> on stderr), and uses it in place of the registry head; --offline errors naming package and commit (#343, #345)

Research

  • Candidate 3.3, a lock that pins commits and merges by record: holds and built. Replayed with the shipped driver over the 52 client merges chapter 1's driver left to a person, 46 finish, 6 do not (three wpackagist plugins that no longer exist, one commit gone upstream, one malformed manifest, one unclassified), none time out; 22 commit fetches on a cold cache, one on a warm one. Chapter 1's floor of 51 unfinished merges in 355 is 6, 1.7% (#331)