# Wire Contract Review

Lifeloop's value is the stability of its serialized contracts. This playbook
describes how to review a change for breaking impact on those contracts. It is
the authoritative reference for every harness's contract-review wrapper.

## Authoritative anchors

- `docs/specs/lifecycle-contract/body.md` — the **target** schema. Event vocabulary,
  manifest shape, capability negotiation, payload envelope, receipt schema,
  failure classes, and retry classes are normative there. When the spec and
  the implementation disagree, the spec is the goal and the implementation
  must move to match (with a schema bump and tombstone if the move is
  breaking).
- `src/lib.rs` — `SCHEMA_VERSION` and the public types that **implement** the
  contract. The serialized JSON shape of these types is the on-the-wire
  promise to consumers right now.
- `tests/wire_contract.rs` — pinned wire shapes. Each public type has at least
  one assertion here.
- `docs/tombstones/` — superseded versions, with removal criteria.

## What counts as a breaking change

Any of the following, applied to a `pub` type in `src/lib.rs`:

1. **Variant rename or removal.** Including:
   - changing a variant name on a `#[serde(rename_all = "snake_case")]` enum
     (the wire name follows the variant name);
   - changing or removing an explicit `#[serde(rename = "...")]`;
   - removing or reordering variants on enums whose serde representation is
     index-sensitive.
2. **Field rename or removal** on any `pub struct` reachable from a public
   API.
3. **Required-vs-optional flip.** Adding `Option<T>` without
   `#[serde(skip_serializing_if = "Option::is_none")]` makes a previously
   absent key suddenly serialize as `null`. Removing `skip_serializing_if`
   does the same.
4. **Tag/representation change** on enums (e.g. switching from external to
   `#[serde(tag = "...")]` internal tagging).
5. **`rename_all` change** — flips snake_case to camelCase, etc.
6. **Type change** for an existing field (e.g. `String` to `u64`).

Adding a new variant to an open enum, or a new optional field with
`skip_serializing_if`, is **not** breaking for current consumers and does not
require a schema bump. It does require a wire test covering the new shape.

## Process

1. Run `scripts/check-wire-contract.sh`. All currently-pinned shapes must
   pass.
2. For each `pub` type touched in the diff, locate its assertion in
   `tests/wire_contract.rs`. If absent, add one — AGENTS.md mandates
   serialization tests for public contracts.
3. For any breaking change identified above:
   - run `scripts/bump-schema.sh <new-version> "<reason>"`;
   - update wire tests to assert the new `schema_version`;
   - fill in the tombstone's removal criteria and migration notes
     ([schema-bump.md](schema-bump.md)).
4. Block the change until all of the above land in the same commit.

## Output format for reviewers

For each finding, cite `file:line`, name which class of breaking change above
applies, and state the required action (schema bump? tombstone? new wire
test?). End with one of:

- `APPROVE` — no breaking changes; any new public types have wire tests.
- `REQUIRES SCHEMA BUMP` — breaking change present; needs the four-surface
  remediation from [schema-bump.md](schema-bump.md).
- `BLOCK` — breaking change without remediation, or missing wire tests for new
  public types.

## Anti-patterns

- A "compatibility shim" that quietly accepts both old and new wire names
  without a tombstone — AGENTS.md forbids hidden compat layers.
- Schema bumps without a tombstone, or tombstones without removal criteria.
- Wire tests that read the type's own constants (e.g.
  `assert_eq!(value["schema_version"], SCHEMA_VERSION)`) instead of literal
  JSON. Refactoring the type silently rewrites the assertion and defeats the
  purpose. Pin literal strings.
