Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2275Done

`/api/v1` ADVERTISES its contract version on every response — an `x-motir-api-version` header, so a client's skew gate costs nothing

Nothing on a /api/v1 response says which contract version served it. Verified by grep on origin/main @ 6d472611 during 11.5.1:

  • withV1Route (lib/api/v1/route.ts) stamps exactly x-request-id and x-ratelimit-{limit,remaining,reset} on every exit path. No version header.
  • GET /api/v1/me returns meSchema, which is .strict() over { user: { id, name, email }, workspaceId, scopes } (lib/api/v1/identity/schema.ts). No version field, and .strict() means one cannot appear by accident.
  • V1_CONTRACT_VERSION is referenced by emit.ts, lib/apiDocs/reference.ts and two tests — never by a response path.

So the only surface carrying the number is the spec document at /api/openapi/v1.json, and a client that wants to know what it is talking to must download a specification to read one string.

Why this is a follow-up and not part of 11.5

11.5's boundary forbids server changes, and the story that would have owned this — 11.7 — shipped before the gap was written up. docs/decisions/cli-v1-client.md Q3 therefore pins a lazy probe: the CLI fetches the spec only after a boundary parse failure or an unrouted 404, once per process, and only then compares majors/minors. That design is correct on its own and this card is not a prerequisite for it.

This card makes the probe free rather than possible. When the header lands, the client reads it off a response it already made and skips the spec fetch entirely; the probe stays as the fallback for a server that does not send it (every server older than this card).

What to build

  • withV1Route stamps x-motir-api-version: <V1_CONTRACT_VERSION> into responseHeaders alongside the request id — before the try block, so it survives a 401, a 403, a 429, a mapped domain error and a 500 alike, exactly as x-request-id does. A client that gets a 404 for an absent route still learns the version from the 404 it got for a route that exists.
  • Declare it in the emitted document — add the header to V1_SHARED_RESPONSE_HEADERS (lib/api/v1/openapi/headers.ts) with a description saying it is the CONTRACT's MAJOR.MINOR.PATCH (ADR Amendment 4 Q6), not the deployment's release number, so a reader cannot mistake it for one.
  • Do NOT add it to meSchema. That schema is .strict() and its three fields are deliberate; a version belongs on the transport, not in one endpoint's body, and putting it there would make it unavailable on every other response.

Acceptance criteria

  • Every /api/v1 response carries x-motir-api-version equal to V1_CONTRACT_VERSION — asserted on a 200, a 401, a 403, a 429, a mapped domain error and a 500, driving the real wrapper, not a fixture.
  • The header is declared in V1_SHARED_RESPONSE_HEADERS and therefore appears on every operation in the emitted OpenAPI document — asserted against the emitted document, not against the constant.
  • meSchema is unchanged, and a test asserts it still has exactly its three keys.
  • The header value is read from V1_CONTRACT_VERSION, never restated — asserted by importing the constant in the test rather than hard-coding 1.0.0.
  • Adding the header is documented as an ADR §8 additive change in public-api-conventions.md, with V1_CONTRACT_VERSION bumped to the next MINOR in the same PR (a new response header is exactly §8's additive case, and a contract version that does not move when the contract grows is the one thing this header must never be).
  • The per-file coverage floor (≥90%) holds.

Context refs

  • lib/api/v1/route.tswithV1Route, responseHeaders, and the stamp helper every exit path runs through.
  • lib/api/v1/openapi/headers.tsV1_SHARED_RESPONSE_HEADERS, where x-request-id and the rate-limit trio are declared.
  • lib/api/v1/openapi/emit.tsV1_CONTRACT_VERSION, and Amendment 4 Q6's meaning for it.
  • lib/api/v1/identity/schema.tsmeSchema, the .strict() shape this card must NOT widen.
  • docs/decisions/cli-v1-client.md Q3 — the consumer, the grep this card records, and why the probe stands without it.
  • docs/decisions/public-api-conventions.md §8 — the additive-change rule this lands under.