11.4.1 Decision — amend the `/api/v1` ADR: the OpenAPI emission mechanism, the operation registry, the spec's public URL, and the reference surface's home + renderer
Repo: motir-core. One PR — docs/decisions/public-api-conventions.md, Amendment 4. No product code changes here.
Every sibling story in this epic opened with an ADR amendment (11.1.1, 11.2.1, 11.3.1) because a public contract's mechanics belong in the contract, not in whichever card happened to build them. This story's mechanics are the last unrecorded ones: HOW the document is emitted, WHERE it is served, and WHAT the reference surface is. Amendment 2 already pinned the ownership split — "11.4 owns the SHARED envelope, error and pagination schemas, the OpenAPI 3.1 emission, the route↔spec CI guard, the published reference and the stability policy — and authors no per-resource shape" (docs/decisions/public-api-conventions.md:523-534) — so this amendment settles the mechanism inside that boundary, never the ownership.
Answer each question below with its options, a recommendation and rung-1 (mirror) / rung-2 (shipped) evidence. The recommendations here are the planner's, with the rung-2 checks already run and cited; verify them and overturn any that the mirrors contradict.
Q1 — how the OpenAPI document is EMITTED from the zod schemas
The per-resource schemas are shipped and import zod classic: lib/api/v1/workItems/schema.ts, lib/api/v1/projects/schema.ts, lib/api/v1/sprints/schema.ts, lib/api/v1/ready/schema.ts.
- (a) Migrate
lib/api/v1/**to thezod/v4subpath and emit with the first-partyz.toJSONSchema(). RECOMMENDED, on rung-2 evidence: the installedzod@3.25.76already ships the v4 core —node -e "require('zod/v4').toJSONSchema"resolves a function againstnode_modulesonorigin/main— so this adds no dependency. OpenAPI 3.1's schema dialect is JSON Schema 2020-12, which istoJSONSchema's native output, so 3.1 needs no dialect down-conversion (the reason 3.1 rather than 3.0 is the right target at all). - (b) Keep Zod 3, add
@asteasolutions/zod-to-openapi— the de-facto Zod-3 route, but a third-party emitter tracking a schema library the repo is already carrying the successor of. - (c) Keep Zod 3, add
zod-to-json-schemaand hand-assemble the document — most control, most hand-written OpenAPI, which is the drift this story exists to prevent.
If (a): state the BLAST RADIUS as a grep, not a guess. Zod 3 and Zod 4 instances do not interoperate, so the migration must be total across lib/api/v1/** and every importer of those schemas. The amendment records the enumerated import sites; the migration itself is 11.4.3's work, not this card's.
Q2 — where an OPERATION is declared, and what makes route↔spec totality mechanical
An operation is a method, a path, its parameters, its request body, its response envelope, its required scope and its error statuses. Only the response half exists today, and nothing maps GET /api/v1/work-items/{key} to it.
Recommended: a per-resource operations.ts beside each schema.ts — the same "declared with the operation" logic Amendment 2 used — assembled by one registry keyed by `${METHOD} ${path}`, mirroring the totality pattern lib/mcp/registry.ts + lib/mcp/scopes.ts already use, so an operation missing a scope fails typecheck rather than review. Pin whether the registry is the source of the route's declared scope, or merely asserted equal to it.
Q3 — the spec's public URL, and its collision with the shipped route audit
A verified conflict, not a hypothetical. tests/helpers/v1RouteAudit.ts:38 walks every route.ts under app/api/v1 and raises bypasses-wrapper for any exported handler not wrapped in withV1Route (tests/helpers/v1RouteAudit.ts:84-99), and withV1Route authenticates. A spec is public documentation — every mirror serves it unauthenticated — so a route at /api/v1/openapi.json cannot pass the shipped guard as written.
- (a) Serve at
/api/v1/openapi.jsonwith ONE named, asserted exemption in the audit — RECOMMENDED if rung 1 agrees, because a generator looks for the spec beside the API. The exemption is only acceptable if the guard asserts it applies to exactly one path and that that file authenticates nothing, reads no database and takes no user input. - (b) Serve it outside the tree (e.g.
/docs/api/openapi.json), leaving the audit untouched.
Check the mirrors before picking: Gitea serves /swagger.v1.json, GitLab publishes an OpenAPI document for /api/v4, Plane documents /api/v1/. Record what they actually do.
Q4 — the reference surface: its home, its renderer, and its language
- Home:
motir-core, in the public route groupapp/(public)/. RECOMMENDED and rung-2 settled, which retires the placement question the story left open:motir-marketingdoes not exist (gh repo list moooon-B-Vreturnsmotir-ai,motir-meta,motir-core,motir-gateway,nextjs-prisma-vercel-starter,moooon), and its provisioning card 8.3.10 istodobeneath atodostory — an unbuildable target. The epic's own boundary already pins every deliverable in it tomotir-core.app/(public)/explore/is the shipped precedent for an unauthenticated, indexable page group. - Renderer: pick one and SELF-HOST it — no third-party CDN, no runtime fetch to an external host, so the reference works on a self-hosted install with no egress. Weigh a spec-rendering component against rendering from our own primitives; say what the bundle costs.
- Language: page chrome goes through
next-intlwithmessages/en.json+messages/zh.jsonparity (the shipped catalog gate); spec-derived operation text stays English, because the spec is one document and a translated contract is a second one. Record that as a deliberate deviation rather than leaving it to be discovered, and say whether long-form documentation prose follows the same rule.
Q5 — what the PUBLISHED stability + deprecation policy says
ADR §8 (docs/decisions/public-api-conventions.md:324-343) is the internal record; the public page is the promise a third party integrates against. Pin: what v1 guarantees, the exact additive/forbidden lists, the client's obligation to tolerate unknown fields, the deprecation window and how a deprecation is announced, and how a v2 would be introduced alongside v1.
Q6 — how many spec documents there can be
One document per API MAJOR version, not one document. The degenerate case is one today, so the emitter is keyed by version from the start and v2 is a second document rather than a rewrite of the first. Pin info.version's meaning: the API version, not the app's release number.
Scope BOUNDARY
Ends at the ADR amendment and the plan-side records it implies. It writes no emitter, no schema, no route, no page — every one of those is a sibling card that reads this amendment. It does not re-open Amendment 2's ownership split, does not author any per-resource shape, and does not change any shipped endpoint's behaviour.
Acceptance criteria
docs/decisions/public-api-conventions.mdcarries Amendment 4, matching the existing amendment format (context → decision → rejected alternatives), answering Q1–Q6 with a recommendation and its rung-1 / rung-2 evidence for each.- Q1's answer names the emission mechanism AND, if it is the
zod/v4path, the enumerated list of import sites the migration must cover — produced by grep, cited by path. - Q3's answer names the spec's URL and, when it sits under
app/api/v1, states the exemption's exact form and the assertions that bound it. - Q4's answer names the reference's route, the renderer, and the localization stance, and records that
motir-marketingwas rejected as a home because it does not exist. - §8 is cross-linked to the amendment so the published policy page and the internal record cannot say different things.
- If Q1's answer changes what the consumers read, the bodies of 11.5 and 11.6 are updated in the same pass to match — a decision that invalidates a not-yet-built card owes that sweep.
- No file outside
docs/is modified.
Context refs
docs/decisions/public-api-conventions.md— the ADR; §8 stability, §9 thin adapter, Amendments 1–3.lib/api/v1/workItems/schema.ts·lib/api/v1/projects/schema.ts·lib/api/v1/sprints/schema.ts·lib/api/v1/ready/schema.ts— the shipped per-resource schema modules the document is assembled from.lib/api/v1/pagination.ts—ListEnvelope/RankedListEnvelope/V1_COLLECTIONS;lib/api/v1/errors.ts—ApiV1ErrorBody+DOMAIN_ERROR_STATUS.tests/helpers/v1RouteAudit.ts— the route-tree walker and thebypasses-wrapperrule Q3 collides with.lib/mcp/registry.ts·lib/mcp/scopes.ts— the registry-driven totality pattern Q2 mirrors.app/(public)/explore/— the shipped unauthenticated page-group precedent.- Prior amendments: 11.1.1 · 11.2.1 · 11.3.1. Consumers: 11.5 · 11.6. Parent story: 11.4.