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

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 the zod/v4 subpath and emit with the first-party z.toJSONSchema(). RECOMMENDED, on rung-2 evidence: the installed zod@3.25.76 already ships the v4 core — node -e "require('zod/v4').toJSONSchema" resolves a function against node_modules on origin/main — so this adds no dependency. OpenAPI 3.1's schema dialect is JSON Schema 2020-12, which is toJSONSchema'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-schema and 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.json with 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 group app/(public)/. RECOMMENDED and rung-2 settled, which retires the placement question the story left open: motir-marketing does not exist (gh repo list moooon-B-V returns motir-ai, motir-meta, motir-core, motir-gateway, nextjs-prisma-vercel-starter, moooon), and its provisioning card 8.3.10 is todo beneath a todo story — an unbuildable target. The epic's own boundary already pins every deliverable in it to motir-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-intl with messages/en.json + messages/zh.json parity (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.md carries 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/v4 path, 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-marketing was 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.tsListEnvelope / RankedListEnvelope / V1_COLLECTIONS; lib/api/v1/errors.tsApiV1ErrorBody + DOMAIN_ERROR_STATUS.
  • tests/helpers/v1RouteAudit.ts — the route-tree walker and the bypasses-wrapper rule 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.