All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
While the library is pre-1.0, breaking changes may land in minor versions; they will always be flagged here.
Unreleased
0.1.0 - 2026-06-12
First public release.
Release-readiness review (2026-06-10): four more contract bugs fixed
A multi-agent pre-release audit found four contract violations, each now
locked in by a property in test/vfs/contracts_test.exs (written first,
RED → GREEN):
Mount-table
readdirreturned duplicate names. Sibling mounts under a shared synthetic parent (/a/band/a/c) each contributed an"a"entry toreaddir("/"). Synthetic children are now deduped at the source.VFS.Memory'smkdirmissed:eexistfor implicit directories and root, andparents: truewas non-idempotent (second call errored). Any existing directory — explicit, implicit, or root — is now:eexist, andparents: trueis a success no-op over existing directories, matchingmkdir -p.Dispatcher errors leaked backend-internal paths in messages. The mount table rewrote
:pathinto the user's namespace but left the default:messagenaming the mount-stripped path (":enoent at /x"for a failure at/repo/x). NewVFS.Error.put_path/2regenerates the default message on rewrite; custom messages are preserved.VFS.Memory.new/1accepted non-binary seed keys/values, deferring the crash to the firststat/read. Both now fail at construction with a clearArgumentError.
Also: VFS.readdir/2's @spec claimed [String.t()] where the
protocol (and the dispatcher's own unbounded branch) returns
Enumerable.t(String.t()); the spec and docs now match the protocol.
Audit (2026-05-02): four contract bugs found and fixed
A staff-level review surfaced four bugs that the existing 100%-coverage,
93%-mutation-kill-rate test suite missed because every test verified
"the code does what we wrote" rather than "the code matches the
published contract." Each is now a property in test/vfs/contracts_test.exs.
walk leaked mount-shadowed paths. When a longer-prefix mount overlaps a shorter one (e.g.
/mount has/a/old,/ais mounted separately),VFS.walk/3emitted/a/oldeven thoughVFS.read_file(fs, "/a/old")returned:enoent. Walk now filters each emission againstVFS.__resolve__to verify it routes back to the source mount; shadowed paths are dropped.Default walk eagerly consumed lazy
readdir. The protocol permitsreaddir/2to return an unboundedEnumerable. The old walker calledEnum.mapon the names, materializing the full stream before enqueuing children — makingwalk |> Stream.take(N)hang on backends likeLazyDirwhose readdir is infinite by design. Rewritten as recursiveStream.flat_mapso the consumer'stake/2bounds total work.VFS.Memory.new/1accepted contradictory seeds. The constructor permitted seeds like%{"/a" => "f", "/a/b" => "c"}that produced state wherestatreported:regularandreaddirsimultaneously treated the same path as a directory. Now validates: rejects/as a file key, rejects any pair of paths in a strict prefix relationship.:line_rangeaccepted malformed(first, last)pairs.{2, 0},{1, -1},{3, 2}slipped past validation and returned silent surprising slices. Critical for LLM tool boundaries that use line ranges to retrieve precise context windows: silent-wrong is worse than loud:einval. Now validateslast >= first AND last >= 1whenlastis an integer.
Changed
Capabilities split:
:writeno longer implies:mkdir. Flat-keyed backends likeVFS.Test.AppService(and future S3, postgres impls) declare:writewithout:mkdirsince they don't model empty directories. Conformance suite gatesmkdirtests on:mkdir in caps.Stream-option handling (
:chunk_size/:byte_range/:line_range) extracted fromVFS.Memoryinto the new publicVFS.StreamOptionsmodule. Every backend whosestream_read/3returns bytes now uses the same validated helper. Added becauseVFS.Test.AppServicesilently ignored those options before the audit caught it.
Numbers after the audit
- 331 tests / 45 properties / 36 doctests / 0 failures across all scopes
- 100% line coverage
- 97.7% mutation kill rate (up from 93%)
- mix check clean: format, compile -W, credo, dialyzer, coverage
- static perf audit (mix vfs.audit) clean: 0 high / 0 medium / 0 low findings
Added
VFS.Mountableprotocol — pluggable virtual filesystem with state-threading reads.VFS.Stat,VFS.Path,VFS.Errorfoundation modules. Errors are structured%VFS.Error{kind, path, mount, message}exceptions; pattern match on:kindfor control flow.VFS.Memoryin-memory backend (read+write).%VFS{}mount table with longest-prefix routing; itself aVFS.Mountable.VFS.Skeletonmacro for backend authors;VFS.Defaultfallback walk impl.VFS.read_file/2derived fromVFS.Mountable.stream_read/3; useVFS.stream_read/3for:chunk_size,:byte_range, and:line_rangeoptions.- Telemetry events under the
[:vfs, _, _]prefix for the data-flow ops (read_file,stream_read,write_file,mkdir,rm,walk,materialize) plus[:vfs, :cache, :hit | :miss]from lazy backends. VFS.assert_implemented!/1for validating values at trust boundaries.VFS.mount/3calls it on every backend, so a struct without aVFS.Mountableimpl fails fast at mount time with a helpfulArgumentErrorinstead of aProtocol.UndefinedErrorat first use.- Conformance test harness (
VFS.ConformanceCase) parametrized over backend impls. test/vfs/contracts_test.exs: property tests over the published protocol contract — observation consistency (stat/readdir/exists?/ read_file agreement), write/read round-trip, rm+read agreement, materialize idempotence, byte_range and line_range validation, capabilities reflect behavior, adversarial inputs to constructors, walk = read-reachable namespace, walk + take terminates over unbounded readdir.lib/vfs/stream_options.ex: shared option handler for backend authors, 100% covered bytest/vfs/stream_options_test.exs.- Conformance suite extended to
VFS.Test.AppService(read+write, no mkdir) andVFS.Test.LazyFake(read-only).