--- name: api-contract-guard description: Blocks a breaking change to a published contract — a REST/RPC/GraphQL endpoint or its request/response fields, an event/webhook payload schema, or a public library signature — made IN PLACE (renamed/removed/retyped field, newly required param, changed status code/error shape, tightened validation) instead of via expand-contract (add the new shape alongside the old, migrate consumers, deprecate with a window, remove later) and/or a version bump. Best used reactively before changing any endpoint, event schema, or exported function signature with existing consumers. Use when the user says "rename this field", "change the response shape", "make this param required", "update the API", "change the payload", "return a different status code", or after an agent edits a contract other code already calls. DO NOT USE for doc sync (docs-drift-guard), diff scope (clean-diff-guard), or the done-claim (evidence-before-done); this guard is specifically about not breaking existing consumers of a published contract. --- # api-contract-guard Once a contract has a consumer, editing it in place is editing someone else's code. A REST response field, an event payload key, or an exported function's parameter list is a promise other code already relies on — the frontend that reads `fullName`, the internal service that calls `retry(fn)` with two args, the webhook subscriber that treats a 200 with an empty body as "not found yet." The failure mode this guard targets is an agent treating "the contract" as if it were private implementation detail: renaming a field, adding a required parameter, or flipping a status code because the *new* requester wants the *new* shape, without noticing that the *old* shape still has live callers depending on it. Editing in place doesn't fail loudly — it fails the next time an existing consumer runs against the new code and gets `undefined`, a thrown error, or silently wrong behavior. ## When to use — three modes - **Guard-pass (default).** Right before changing an endpoint's request or response shape, an event/message/webhook payload, or an exported function/type signature that already has at least one consumer: classify the change as additive or breaking per the Procedure below, before writing the edit. This is the mode that fires on "rename this field," "change the response shape," "make this param required," "update the API," "change the payload," "return a different status code," or any edit to a contract file with existing callers. - **Live.** While actively editing the contract: before typing the change, ask "does every existing caller of this endpoint/function/event still get what it currently expects after this edit?" If the answer is no, stop and reach for expand-contract (add the new shape alongside, don't overwrite) instead of finishing the in-place edit. - **Review.** When asked to review a diff, PR, or API design doc that touches a contract: audit as a critic. Classify each changed field, parameter, status code, or payload key as additive or breaking, list every known consumer of the changed surface, and state whether the diff as written breaks any of them. Don't silently start rewriting the diff unless asked to fix what you find. ## What this guard blocks 1. **Breaking field rename or removal.** Renaming or deleting a field in a response body, request body, or event/webhook payload that an existing consumer reads or sends. The consumer's code (`user.fullName`, `payload.orderId`) does not get rewritten just because the producer changed — it gets `undefined` or a validation failure. 2. **A newly required parameter or field.** Making a previously optional request field, query param, function argument, or event key required. Every existing caller that omitted it (because it used to be optional) now fails, even though nothing in *their* code changed. 3. **Response-shape or type change.** Changing a field's type (string → number, single object → array, nested vs. flat), renaming a type, or restructuring a payload's shape in place. A consumer written against the old shape either throws or silently misreads the new one. 4. **Status-code or error-shape change.** Changing what status code (or error object shape) an existing, already-handled case returns — e.g. a "not found" that used to be `200` + empty body becoming `404`, or an error payload's field names changing. Consumers that pattern-match on the *old* code/shape (`if (res.status === 200 && isEmpty(body))`) break silently the moment the *meaning* of an existing response changes, even though the new behavior is arguably more correct. 5. **Tightened validation.** Adding a stricter format, range, or enum constraint to an existing input field that some current, previously-valid caller may not satisfy. "Previously valid" data or requests must not start failing without a migration/communication step. 6. **The additive-vs-breaking test.** For every changed field, parameter, status code, or payload key, run the test in `references/additive-vs-breaking.md`: does every existing valid caller/consumer keep working, receiving a result at least as usable as before, after this change, with zero code changes on their side? If yes, it's additive (safe to ship in place). If no — or if you can't prove it — treat it as breaking. 7. **The expand-contract procedure for anything breaking.** A breaking change is never shipped by silently overwriting the old shape. It goes through expand (add the new shape alongside the old, both live at once), migrate (move known consumers over), deprecate (mark the old shape with a stated sunset window), and only then contract (remove the old shape). See `references/expand-contract-playbook.md` for the full sequence and a worked example. 8. **Versioning the breaking bump.** When a clean break genuinely can't be avoided (or once the deprecation window in step 7 has passed), the breaking change ships behind an explicit version signal — a new API path/version header, a semver MAJOR bump for a library, or a documented, dated breaking-change entry in the changelog — never a silent overwrite of the version consumers are already pinned to. ## Procedure — guard-pass steps 1. **Identify the contract's consumers.** Before editing, find who reads or calls the surface being changed: grep the repo for the field name, route path, event/topic name, or function name being touched. Note in-repo consumers (other services, frontend code, tests) and flag whether the surface is also published externally (an npm package, a public API, a webhook other teams subscribe to) where consumers may not even be visible in this repo. 2. **Classify the change** using `references/additive-vs-breaking.md`: additive (new optional field/param/endpoint, widened acceptance) or breaking (rename/remove/retype, new required input, changed status code/error shape, tightened validation). 3. **If additive:** ship it in place. No expand-contract, no version bump, no deprecation ceremony needed — say so explicitly and move on. 4. **If breaking:** do not edit the existing shape in place. Follow `references/expand-contract-playbook.md`: add the new shape alongside the old (both work simultaneously), update the known in-repo consumers found in step 1 to use the new shape, mark the old shape deprecated with a stated sunset (date, version, or event), and — if a clean break is unavoidable — put it behind a version bump rather than overwriting the version already in use. 5. **State the migration/deprecation plan explicitly** in the response: what stays working, what's new, who needs to move, and by when. Do not present a breaking change as simply "updated" without this plan. 6. **Emit the guard footer** (see Output) as the last line of the response. ## Output Emit this verbatim as the last line of any response where a contract (endpoint, event/payload schema, or public function/type signature) was changed, considered, or reviewed: ``` proofguard:api-contract-guard — · Triggered: · Fixed: · Verified: · Remaining gap: ``` - `PASS` — the change was classified as additive (existing consumers unaffected), or it was breaking and has already been routed through expand-contract (old shape still works, new shape added, deprecation and version stated) rather than edited in place. - `FIX-REQUIRED` — a breaking change was about to be (or was) made in place, overwriting the old shape instead of expanding it, and has not yet been resolved (old shape restored/kept alongside the new one, a deprecation window and version stated). - `WAIVED(reason)` — the user, as the actual principal, explicitly accepted breaking existing consumers immediately (e.g. "there are no real consumers yet, it's fine to break it"), with a stated reason. ## References - `references/additive-vs-breaking.md` — the additive-vs-breaking test in detail, applied to REST/JSON APIs, GraphQL, RPC/protobuf, events and webhooks, and public library function/type signatures. - `references/expand-contract-playbook.md` — the expand → migrate → deprecate → contract sequence, the versioning options per contract kind, and a worked before/after example. ## What this guard does NOT do It does not check whether the docs describing the contract were updated (that's `docs-drift-guard`'s job once the contract change itself is correctly shaped), whether the diff is scoped tightly (`clean-diff-guard`), or whether a completion claim about the change has evidence behind it (`evidence-before-done`). It also does not design the API from scratch, pick naming conventions, or judge REST vs. GraphQL vs. RPC style — that's API design, not contract safety. This guard has exactly one job: when a published contract already has consumers, keep changing it from silently breaking the ones that already depend on the old shape.