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/
- The error-response schema —
{ code, error }, matchingApiV1ErrorBodyexactly, plus the status vocabulary: a declaration of every HTTP status a v1 response can carry, reconciled against the shippedDOMAIN_ERROR_STATUSmap (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). - 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. KeepV1_COLLECTIONS(lib/api/v1/pagination.ts:255) as the collection vocabulary the cursor is scoped to. - The rate-limit response headers —
X-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. - The security scheme — the bearer PAT, declared once and referenced by every operation, with the scope vocabulary taken from the shipped
TokenScopeunion (lib/mcp/scopes.ts) rather than re-typed. - The schema-library alignment 11.4.1 pins. If Amendment 4 chose the
zod/v4path, 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_STATUSand 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 shapelib/mcp/scopes.ts's own guard uses. - The library alignment named in Amendment 4 is complete:
pnpm typecheckand the full suite pass, and — if the migration path was taken — no module underlib/api/v1/**imports both schema-library entrypoints. - Every new file is added to the per-file coverage thresholds in
vitest.config.tsat 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.ts—ApiV1ErrorBody,DOMAIN_ERROR_STATUS,classifyApiV1Error.lib/api/v1/pagination.ts—ListEnvelope,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.ts—TokenScopeand 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.