# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
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 `readdir` returned duplicate names.** Sibling mounts
under a shared synthetic parent (`/a/b` and `/a/c`) each contributed
an `"a"` entry to `readdir("/")`. Synthetic children are now deduped
at the source.
- **`VFS.Memory`'s `mkdir` missed `:eexist` for implicit directories
and root**, and `parents: true` was non-idempotent (second call errored).
Any existing directory — explicit, implicit, or root — is now
`:eexist`, and `parents: true` is a success no-op over existing
directories, matching `mkdir -p`.
- **Dispatcher errors leaked backend-internal paths in messages.** The
mount table rewrote `:path` into the user's namespace but left the
default `:message` naming the mount-stripped path (`":enoent at /x"`
for a failure at `/repo/x`). New `VFS.Error.put_path/2` regenerates
the default message on rewrite; custom messages are preserved.
- **`VFS.Memory.new/1` accepted non-binary seed keys/values**, deferring
the crash to the first `stat`/`read`. Both now fail at construction
with a clear `ArgumentError`.
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`, `/a` is mounted
separately), `VFS.walk/3` emitted `/a/old` even though
`VFS.read_file(fs, "/a/old")` returned `:enoent`. Walk now filters
each emission against `VFS.__resolve__` to verify it routes back
to the source mount; shadowed paths are dropped.
- **Default walk eagerly consumed lazy `readdir`.** The protocol
permits `readdir/2` to return an unbounded `Enumerable`. The old
walker called `Enum.map` on the names, materializing the full
stream before enqueuing children — making `walk |> Stream.take(N)`
hang on backends like `LazyDir` whose readdir is infinite by design.
Rewritten as recursive `Stream.flat_map` so the consumer's `take/2`
bounds total work.
- **`VFS.Memory.new/1` accepted contradictory seeds.** The constructor
permitted seeds like `%{"/a" => "f", "/a/b" => "c"}` that produced
state where `stat` reported `:regular` and `readdir` simultaneously
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_range` accepted 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 validates `last >= first AND last >= 1`
when `last` is an integer.
### Changed
- Capabilities split: `:write` no longer implies `:mkdir`. Flat-keyed
backends like `VFS.Test.AppService` (and future S3, postgres impls)
declare `:write` without `:mkdir` since they don't model empty
directories. Conformance suite gates `mkdir` tests on `:mkdir in caps`.
- Stream-option handling (`:chunk_size` / `:byte_range` / `:line_range`)
extracted from `VFS.Memory` into the new public `VFS.StreamOptions`
module. Every backend whose `stream_read/3` returns bytes now uses
the same validated helper. Added because `VFS.Test.AppService`
silently 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.Mountable` protocol — pluggable virtual filesystem with state-threading reads.
- `VFS.Stat`, `VFS.Path`, `VFS.Error` foundation modules. Errors are
structured `%VFS.Error{kind, path, mount, message}` exceptions; pattern
match on `:kind` for control flow.
- `VFS.Memory` in-memory backend (read+write).
- `%VFS{}` mount table with longest-prefix routing; itself a `VFS.Mountable`.
- `VFS.Skeleton` macro for backend authors; `VFS.Default` fallback walk impl.
- `VFS.read_file/2` derived from `VFS.Mountable.stream_read/3`; use
`VFS.stream_read/3` for `:chunk_size`, `:byte_range`, and `:line_range`
options.
- 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!/1` for validating values at trust boundaries.
`VFS.mount/3` calls it on every backend, so a struct without a
`VFS.Mountable` impl fails fast at mount time with a helpful
`ArgumentError` instead of a `Protocol.UndefinedError` at 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 by `test/vfs/stream_options_test.exs`.
- Conformance suite extended to `VFS.Test.AppService` (read+write,
no mkdir) and `VFS.Test.LazyFake` (read-only).
[Unreleased]: https://github.com/ivarvong/vfs/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/ivarvong/vfs/releases/tag/v0.1.0