0.18.0

Released 2026-09-27

Added

  • viv lock merge --offline-rung (opt-in): once the registry escalation has failed at every rung --max-scope allows, try one parent's own pinned record per divergent name (ours first, then theirs), checked against the two locks' own require/conflict/replace/provide/platform data with no registry fetch. On 355 client merges it finishes 37 of the 52 that otherwise end in conflict markers; 30 of those 37 install from an empty cache, 7 fail on one licence-gated download, and 13 keep a version older than the discarded side's. Off by default: in live merges it only ever fires when a branch head has moved or a package has been removed since the lock was written (#314)

Fixed

  • installed.json's time is written as Composer does: RFC 3339, with an explicit offset kept instead of shifted to UTC; the lock writer's own time normaliser (#121) shared the same offset bug, fixed here too (#317)
  • A mixed-case package name installs under its own casing — vendor path, install path, autoload paths — instead of a lowercased one; an install over an existing lowercase directory moves it (#318)
  • For a class declared twice, the classmap keeps the same file as Composer: whichever the directory walk reaches first, not whichever sorts first by path (#319)
  • installed.php lists a branch alias's (and a lock-level alias's) pretty version where self.version appears in that package's own replace/provide (#322)

Research

  • Candidate B, install from a lock years later: measured, not built. 40 locks from 2015 to 2025 all install with viv from an empty cache; no dist gone and no hash wrong, so a lock carrying tree hashes would have saved nothing. The old locks reached code the pinned corpus never does, and turned up #317, #318 and #319 (#307)

Tooling

  • compat/lock-age.py --verify checks an old lock against a cached Composer reference, warm caches, four in parallel: about 2 minutes instead of hours (#328)
  • A crashed viv lock merge (killed, or exiting by signal) is counted as its own outcome instead of vanishing from every replay table without a note (#320)
  • run.sh's skip-update message names its real source instead of always citing bench/skips.txt (#313)
  • mirror.sh's fetch curls time out on a stalled transfer instead of hanging until the retry budget runs out (#321)