F2.1 — Payload views & pagination contract
One vocabulary for partial-vs-full payloads across the whole API, so an agent (or a human) learns it once and it means the same thing on every list/read endpoint. Additive and backward-compatible: every endpoint keeps its existing default and every view word it already shipped — this just makes the vocabulary canonical and documents exactly what each tier carries. Source of truth:apps/api/src/ops/views.ts.
The three views
brief is the intended default for lists (charter, charter.md → “Pagination
- views contract”).
fullis never a default — a caller asks for it.
Per-primitive mapping
- Topics —
GET /topics,GET /topics/search: defaultsummary(the scannable index). For the LIST tiersummaryandbriefcoincide (the index already drops the body; a topic’sbriefwhy is fetched per-topic viaGET /topics/:slug).view=fullreturns the complete canonical context per row.GET /topics/:slugis the full read;POST /topics/retrievetakesbrief(default) /full. - Collections —
GET /collections/:id/records?view=…selects a named collection view (board/table) — a different concept from a payload tier; it is unchanged. Record payloads are already bounded (default page size + cursor).
Legacy spelling aliases
normalizeView maps these onto the canonical tier so a caller may use the
standard word anywhere; the old words never stop working:
An unrecognized
view returns 400 with view must be one of: summary, brief, full — the same coded error contract every endpoint uses.
Pagination contract
Two paging shapes coexist and both stay valid (additive):- Offset paging —
limit/offset, with an envelope{ limit, offset, total, returned, has_more }(topics list). Build it withbuildPageInfo. - Keyset paging —
{ records, nextCursor }over a stable(created_at, id)ordering (collections; broker records). An opaque cursor, ignored if malformed.
DEFAULT_PAGE_LIMIT = 50, MAX_PAGE_LIMIT = 200 (resolvePageLimit clamps).
Topics’ legacy list keeps its own “no limit = unbounded” back-compat.
Error contract
Every error — validation, not-found, conflict, rate-limit, a translated Postgres constraint violation, or an unhandled 500 — is returned as one envelope by theGlobalExceptionFilter:
code (see ops/error-codes.ts), never
on the English message. The code list is additive-only — never renamed or
repurposed.
Why this matters for budgets
The payload ceilings inbudgets.md assume a list defaults to a bounded,
body-free tier. Standardizing the vocabulary is what lets the regression tests
(F2.2) assert “this read stays under its ceiling” against ONE notion of which
tier a hot read returns by default.