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-scopeallows, try one parent's own pinned record per divergent name (oursfirst, thentheirs), checked against the two locks' ownrequire/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'stimeis 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.phplists a branch alias's (and a lock-level alias's) pretty version whereself.versionappears in that package's ownreplace/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 --verifychecks 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 citingbench/skips.txt(#313)mirror.sh's fetch curls time out on a stalled transfer instead of hanging until the retry budget runs out (#321)