Versioning

Three versions, moving independently, with one rule each.

AxisLooks likeChanges whenBreaking?
API contract/wine/v1/…, X-API-Versiona breaking change to the request or response contractyes, by definition
Datasetttb-permits-2026.1upstream data or the build changesno — never on its own
Service release1.2.0any deployno

Machine-readable: /wine/v1/version and /wine/api/v1/versions.json.

The test that settles arguments

Could a correct client, written against the current contract and not changed, now break or silently misread a response? If yes, it is breaking. If you have to reason about whether a client should have depended on something, it is breaking — callers depend on what was shipped, not on what was meant.

Additive — no version bump

Breaking — new path version, old one keeps serving

That last one is the subtle one: the static datasets are published files, so people build against a producer record's shape exactly as they build against an endpoint. Adding a field to it is additive; renaming one is breaking, even though no route changed.

Deprecation flow

current ──► deprecated ──────────────────► sunset ──► gone
            (≥6 months notice)   (≥12 months total support)

From the moment of deprecation, every response on the old version carries:

Deprecation: Wed, 01 Apr 2026 00:00:00 GMT      (RFC 9745)
Sunset: Thu, 01 Oct 2026 00:00:00 GMT           (RFC 8594)
Link: <https://apis.allanninal.dev/wine/versioning.html>; rel="deprecation"

…plus a deprecation block in the JSON body, because a client reading only the body should not be the last to find out.

Reproducibility

Every response carries datasetVersions. Any response can be regenerated from those versions plus the repository at the matching tag — the raw upstream payloads are cached and the build is a pure function of them. A rebuild that changes output when upstream did not is a bug, and the validation gate is what catches it.

How it is enforced

Not by memory. scripts/check-contract.mjs boots the app, walks every route, response shape and dataset record shape, and diffs them against a recorded snapshot. Anything breaking fails the build. It exists because the change that hurts a caller is never the one you meant to make.