Skip to main content

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.

CategorySurfaceWhat breaks
wireregistryIn 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
wirecorporaA 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
recorddomainsA 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
recordcorporaA 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
recordschemasA published JSON Schema that refuses a document it accepted: a property gone or newly required, a type or enumeration narrowed
apirustThe 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
apicThe 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
apiopenapiThe 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

ExitMeaning
0No unacknowledged breaking change
1A breaking change the changelog does not name
2A usage error
5The ref, git or the Rust toolchain is not found
7An 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.