Versioning policy
Weft’s users depend on three surfaces that change at different rates: the scenario DSL, the event-log format (weft-log), and the CLI. Each has its own compatibility contract, defined here. The Rust crate APIs are a fourth, weaker surface. Crate versions follow SemVer; this document defines what “breaking” means for each surface, because SemVer alone doesn’t.
Current status: pre-1.0 (0.0.x). Until 1.0, breaking changes are allowed
on any surface with (a) a CHANGELOG.md entry under Unreleased marked
BREAKING, and (b) for the log format, a version-number bump (that rule is
absolute even pre-1.0, see below). There is no deprecation cycle yet. From
1.0 on, the contracts below get the deprecation policy in the final section.
1. Event-log format (weft-log) — the strictest contract
A recording is a bug report that replays forever. People archive these files; a format change that orphans old recordings destroys exactly the value the tool exists to provide. So this surface is versioned explicitly and independently of everything else:
- The header carries
"format":"weft-log","version":N. Any change to record semantics — new required fields, changed digest computation, changed meaning of an existing field, changed op numbering — incrementsN. This applies pre-1.0 too (docs/recording-format.md). - Current version: 2. v2 added the per-
sendsend_vtanchor (deliv = send_vt + latency) and the headerwindow_nsfield, groundwork for windowed multi-host recordings. v1 is rejected on read — its latency-only deliveries would silently diverge under the send-time-anchored core, so there is no in-place upgrade; re-record under the current tool. (Pre-1.0, and Weft was published only days earlier, so no long-lived archives are orphaned in practice.) - Readers MUST reject unknown versions rather than guess.
weft replaydoes; your tooling should too. - Non-breaking: adding keys inside the header’s
metaobject (readers MUST ignore unknownmetakeys; it is informational by definition), and gzip vs. plain encoding (detected by content). - Breaking, requires version bump: everything else. In particular the digest chain: two weft-log files with equal digests MUST describe byte-identical executions across all Weft releases that can read that version.
- Aspiration for 1.0+: ship a
weft log migratetool or retain read support for at least one previous version. Not promised pre-1.0.
2. Scenario DSL — strict on reading, additive-friendly
A scenario file encodes institutional knowledge about how to break a system; teams check them into their repos and expect them to keep working.
- Non-breaking: adding a new optional field with a default that
preserves old behavior; adding a new event
type; adding a new latency distribution or net-spec clause; improving error messages. - Breaking: removing or renaming a field; changing a default; making an
optional field required; changing validation so a previously-valid file is
rejected (tightening) or a previously-rejected file is accepted with
different semantics; changing the meaning of an existing value (e.g.
units of
time_ns). - Note the DSL deliberately has no version field today. Before 1.0 it will either gain one or the format freezes as-is; until then, breaking DSL changes are CHANGELOG-flagged and the parser’s “unknown field” strictness is the compatibility tripwire (a file using newer fields fails loudly on older Weft, never silently misbehaves).
- Removed surface is rejected by name where feasible: e.g. YAML input was never implemented and its vestigial API was removed pre-release rather than left as a JSON-parsing trap.
3. CLI — the everyday contract
Scripts and CI pipelines consume the CLI. “Breaking” here includes output that machines parse, not just flags:
- Breaking: removing/renaming a subcommand or flag; changing a flag’s
default; changing any documented exit code (
weft fuzz‘s 0/2/1 and the checkers’ 0/2/3/1 are load-bearing CI contracts); changing the format of machine-parsed output lines (replay identical: N op(s), stream digest %016x, theshrunk : X → Y opsreport lines); changing env-var names or activation semantics (WEFT_*— presence-activated variables are especially sensitive). - Non-breaking: adding subcommands, flags, or env vars; adding output lines; improving human-facing prose on stderr; adding new exit codes for new failure modes (never reusing 0/1/2/3 meanings).
- Seed semantics are part of the CLI contract in one narrow, important way: we do not promise that seed N produces the same schedule across Weft releases (engine improvements legitimately change the seed→schedule map); we DO promise a recording made by any release replays identically on every later release that reads its log version. Archive recordings, not seeds.
4. Rust crate APIs
weft-scenario, weft-replay, weft-chord, weft-raft expose small
intentional APIs (see docs/REFERENCE.md §7) and follow standard Rust SemVer.
Everything not listed there is implementation detail and may change in any
release regardless of version number. weft-shim’s exported C symbols are
an interposition surface, not an API — they mirror libc by construction and
carry no compatibility promise of their own.
5. From 1.0 onward
- Breaking changes on any surface require a minor-version deprecation window (old form keeps working with a warning for ≥ 1 minor release) except where technically impossible (log-format semantics — handled by version rejection instead).
- The workspace releases in lockstep (one version number across crates), so “Weft 1.3” unambiguously names the behavior of every surface.
CHANGELOG.mdgains a per-surface compatibility table per release: DSL / log / CLI / crates, each marked unchanged · additive · breaking.