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-2184Done

11.4.3 The shared v1 wire-schema layer — the error envelope, the TWO page envelopes, the rate-limit headers and the bearer security scheme, declared once

Repo: motir-core. One PR. The shared half of the schema source — the shapes that are NOT per-resource and that every operation in the document composes. ADR Amendment 2 assigns exactly these to this story: "11.4 owns the SHARED envelope, error and pagination schemas" (docs/decisions/public-api-conventions.md:530-532).

Today those shapes exist only as TypeScript interfaces, which a generator cannot read: ApiV1ErrorBody (lib/api/v1/errors.ts:19), ListEnvelope<T> (lib/api/v1/pagination.ts:44) and RankedListEnvelope<T> (lib/api/v1/pagination.ts:279). An interface disappears at compile time; a zod schema is a value the emitter can turn into an OpenAPI component. Converting them is this card's substance.

What to build, in lib/api/v1/openapi/

  1. The error-response schema{ code, error }, matching ApiV1ErrorBody exactly, plus the status vocabulary: a declaration of every HTTP status a v1 response can carry, reconciled against the shipped DOMAIN_ERROR_STATUS map (lib/api/v1/errors.ts:103) and the ADR §4 table, so a status the code can return but the document has no vocabulary for is a typecheck or test failure, not a documentation gap. Include the statuses 11.2 added (409, 412).
  2. The TWO page envelopes, named separately — the plain cursor page and the ranked collection page carrying totalCount. This is an explicit ADR obligation, not a choice: "the OpenAPI assembly emits two named envelope schemas, and each operation references the one its route returns" (Amendment 3, Q2). Both are generic over their item schema, so an operation composes the envelope with its resource's own schema rather than restating either. Keep V1_COLLECTIONS (lib/api/v1/pagination.ts:255) as the collection vocabulary the cursor is scoped to.
  3. The rate-limit response headersX-RateLimit-* as declared header components, since the wrapper sets them on every response including errors (lib/api/v1/route.ts), so they belong to the shared layer rather than to any operation.
  4. The security scheme — the bearer PAT, declared once and referenced by every operation, with the scope vocabulary taken from the shipped TokenScope union (lib/mcp/scopes.ts) rather than re-typed.
  5. The schema-library alignment 11.4.1 pins. If Amendment 4 chose the zod/v4 path, perform that migration here, across the enumerated import sites the amendment lists — total, because Zod 3 and Zod 4 instances do not interoperate, so a half-migrated tree fails at the seam. If the amendment chose a converter library instead, add it here and no migration is needed. Read the amendment; do not assume which.

The pattern to copy is the shipped per-resource one: lib/api/v1/workItems/schema.ts declares a shape and maps to it field by field, never by spreading a DTO, for the reason its own header gives — a column added by a later migration must not become public API by accident.

Scope BOUNDARY

Ends at the shared shapes and the library alignment. It authors no per-resource shape — work items, projects, sprints and the ready set already own theirs and re-declaring one here would recreate the two-artifact drift this story exists to prevent. It emits no document and declares no operation: the registry and the generator are 11.4.4, the per-resource operation declarations are 11.4.5. It adds and changes no endpoint and no response body — the shapes it declares must match what the shipped routes already return, and a mismatch found here is a finding to record, not an edit to smuggle in.

Acceptance criteria

  • lib/api/v1/openapi/ exports the error-response schema, the two named page envelopes, the rate-limit headers and the bearer security scheme as declared schemas — values, not interfaces.
  • The two envelopes are distinct, named types; nothing collapses them into one with an optional totalCount, which the ADR rejected by name.
  • The error schema's shape is asserted equal to what a real error response carries — driven through classifyApiV1Error / the shared wrapper, not compared to a fixture written from the same assumption.
  • The status vocabulary covers every status in DOMAIN_ERROR_STATUS and the ADR §4 table including 409 and 412, and a test proves a status added to the map with no vocabulary entry FAILS.
  • The security scheme's scopes are derived from TokenScope, so a new scope cannot be added without appearing here; asserted by a totality test in the shape lib/mcp/scopes.ts's own guard uses.
  • The library alignment named in Amendment 4 is complete: pnpm typecheck and the full suite pass, and — if the migration path was taken — no module under lib/api/v1/** imports both schema-library entrypoints.
  • Every new file is added to the per-file coverage thresholds in vitest.config.ts at the project's ≥90% branch/function/line floor.
  • No file under app/api/v1/** changes behaviour; any response-shape mismatch discovered is recorded as a bug, not fixed here.

Context refs

  • lib/api/v1/errors.tsApiV1ErrorBody, DOMAIN_ERROR_STATUS, classifyApiV1Error.
  • lib/api/v1/pagination.tsListEnvelope, RankedListEnvelope, V1_COLLECTIONS, the cursor codec.
  • lib/api/v1/route.ts — the wrapper that sets the rate-limit headers and maps errors on every response.
  • lib/mcp/scopes.tsTokenScope and the totality-guard pattern the security scheme copies.
  • lib/api/v1/workItems/schema.ts — the shipped per-resource idiom (field-by-field mapping, never a spread) this layer mirrors.
  • docs/decisions/public-api-conventions.md — §4 errors, §5 pagination, §6 rate limits, Amendment 2 (ownership), Amendment 3 Q2 (the two-envelope obligation), Amendment 4 (the library choice).
  • vitest.config.ts — the per-file coverage-threshold map new files join.
  • Blocker: 11.4.1. Producers of the per-resource halves: 11.2 · 11.3. Parent story: 11.4.