Breaking changes: wai breaking
wai breaking --against <git-ref> compares a WAI checkout’s working tree with a git ref and
reports every change that breaks something written against the ref: a reader of WAI’s bytes, a
verifier of its signed records, or a program built on its libraries. WAI’s repository runs it on
every pull request that touches WAI.
wai breaking --against origin/main # every category
wai breaking --against v0.4.0 --category wire # the bytes a reader must accept, alone
wai breaking --against origin/main --json # the findings as JSON, for a program or an agent
It compares against the merge base of the ref and HEAD, so what the ref gained since a branch
left it is not read as the branch removing it.
What it checks
The categories are cumulative, strictest by default: --category api (the default) reports all
three, record reports wire and record, and wire reports wire alone. --only and --skip
choose surfaces by name.
| Category | Surface | What breaks |
|---|---|---|
| wire | registry | In the registry export (registry/capabilities.json): a capability, deterministic identifier or wire identifier (a magic, document kind, header, path, scheme or tag) that is gone; a capability whose kind, media, role, tier, licence, replicate class, edition or operator set changed, that left a set or joined the mandatory floor, or that was demoted to candidate; a wire identifier whose status fell back |
| wire | corpora | A vector of a media or delivery corpus that is gone or whose bytes changed; in a JSON file, an entry or member that is gone or a value that changed. An added entry is compatible |
| record | domains | A domain-separation prefix, magic or version a signed record is built from (a constant named …DOMAIN…, …MAGIC… or …VERSION… in the WAI crates’ sources) that changed or is gone |
| record | corpora | A vector of a record corpus (claims, receipts, keys, live records) that is gone or changed: a record’s fields, its signing preimage or its verdict |
| record | schemas | A published JSON Schema that refuses a document it accepted: a property gone or newly required, a type or enumeration narrowed |
| api | rust | The public API of the wai and wai-quantum crates, read from the compiler’s own description of it (rustdoc’s JSON output): a public path that is gone, a signature that changed, a struct that can no longer be built with a literal, an enum or struct made non-exhaustive, a field added to a struct a literal builds, a variant added to an exhaustive enum, a required method added to a trait |
| api | c | The C header wai-rs/include/wai.h: a function that is gone or whose return or parameter types changed (parameter names are not compared), a struct whose fields changed, an enumerator or constant that is gone or holds another value |
| api | openapi | The room service’s OpenAPI document: a route that is gone, an operationId that changed, a parameter gone or newly required, a request body newly required, a success response or one of its properties gone |
The corpora hold the bytes the reference code writes and signs, and WAI’s conformance workflow
requires every corpus to regenerate byte for byte from that code. So a change to what the code
writes or signs, such as a field or a signing preimage, reaches a corpus file, where wai breaking reads it.
Each finding
A finding has an id, <category>:<surface>:<item>, and says what changed and why it breaks:
BREAK api:c:wai_envelope_detect_version
changed (api · c): the function wai_envelope_detect_version:
int wai_envelope_detect_version(const uint8_t *, uintptr_t)
→ int wai_envelope_detect_version(const uint8_t *, uint32_t)
why it breaks: a caller compiled against the old prototype passes or reads the
wrong types: undefined behaviour, or a build that fails
Additions are listed as compatible.
An intended break
A break passes only when the changelog names it. Add a line to wai/CHANGELOG.md, under the
current version, in an entry that says Breaking, with the finding’s id in backticks:
- **Breaking:** `api:c:wai_envelope_detect_version`: the length is a 32-bit count; callers pass `uint32_t`.
Only lines the change itself adds count: an entry already in the changelog acknowledges nothing new. For each break it finds, the run prints the line to add.
Exit status and JSON
| Exit | Meaning |
|---|---|
| 0 | No unacknowledged breaking change |
| 1 | A breaking change the changelog does not name |
| 2 | A usage error |
| 5 | The ref, git or the Rust toolchain is not found |
| 7 | An internal error, such as a documentation build that fails |
With --json it prints one JSON envelope, the report as its data (without --json the output
is text, on a terminal or not):
{ "wai": "envelope/1", "op": "compat.check", "ok": false,
"data": {
"against": "origin/main", "base": "<commit>", "category": "api",
"breaking": 1, "acknowledged": 0, "compatible": 0,
"findings": [{ "id": "api:c:wai_envelope_detect_version", "category": "api", "surface": "c",
"item": "wai_envelope_detect_version", "change": "changed", "what": "…",
"why": "…", "breaking": true, "acknowledged": false }],
"notes": [] } }
The API category documents each crate twice, the base and the working tree, with the toolchain
the repository pins, and only when a source it builds from changed. --target-dir <dir> keeps
that build’s files between runs; --skip rust leaves the Rust API out where no Rust toolchain is
installed.