| 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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
- 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.
- 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).
- If additive: ship it in place. No expand-contract, no version bump,
no deprecation ceremony needed — say so explicitly and move on.
- 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.
- 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.
- 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 — <PASS | FIX-REQUIRED | WAIVED(reason)> · Triggered: <what fired it> · Fixed: <n> · Verified: <additive-vs-breaking classification + consumers checked> · Remaining gap: <what's unchecked>
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.