Reference
Rule Index
Every rule has an ID. Lint messages cite it, and reviews should too. Each v4 rule ID links to the rule box on its page.
Lint
A rule in lint/ruleset.yaml fails CI on any finding. The chips name the rules.
Review
A person checks it in the pull request.
v4 rules
| ID | Rule | Enforced by |
|---|---|---|
| V4-URL-1 | Paths start with /v4/; kebab-case segments; camelCase path parameters ending in Id, accounts and projects included. | Lintv4-path-shape |
| V4-URL-2 | Only the URL shapes: a container, then one level of nesting under an owner, then re-root at the container. | Lintv4-path-shape |
| V4-URL-3 | Collections are plural nouns; namespaces and singletons are registered words; no action verbs. | Lintv4-path-shape |
| V4-URL-4 | One canonical URL per resource; resources with an ID live at /v4/{container}/{containerId}/{collection}/{id}, a bucket or project at the root at /v4/buckets/{bucketId}; one parameter name per collection. | Lintv4-canonical-url |
| V4-URL-5 | A collection that creates resources has a canonical item URL for them, in the same container. | Lintv4-canonical-url |
| V4-URL-6 | Accounts, projects and buckets are the containers; every path starts with one. Projects and buckets are listed and created in their account and addressed at the root by their ID; what a project deploys lives under /v4/projects/{projectId}, and gateway data under /v4/buckets/{bucketId}. | Lintv4-path-shapeReview |
| V4-URL-7 | A collection name is one resource type across all of v4; product collections live in the product's namespace (…/ai/pools), never behind a prefix. | Lintv4-path-shapev4-canonical-urlReview |
| V4-METH-1 | Fixed methods per URL shape; PUT only on singletons and association items; an association is added with a POST to its collection or set with a PUT on its item, never both. | Lintv4-method-shape |
| V4-METH-2 | Fixed success codes; 201 has Location; 204 has no body; bodies are JSON with named schemas. | Lintv4-success-responses |
| V4-IDEM-1 | GET, PUT, PATCH, DELETE and association upserts are retry-safe; PATCH sets absolute values; no side effects on GET. | Review |
| V4-IDEM-2 | No operation declares Idempotency-Key: v4 does not support it. | Lintv4-idempotency |
| V4-IDEM-3 | No operation declares If-Match, ETag or 412: v4 does not support them. | Lintv4-conditional-requests |
| V4-IDEM-4 | When the client knows the identity, the request carries its key (in the body of a POST to the association collection, or the URL of a PUT on its item) and replaces an existing association. | Review |
| V4-COLL-1 | Every list paginates with limit/offset and returns exactly { data, limit, offset, total, hasMore, next } as {Resource}List. | Lintv4-list-envelope |
| V4-COLL-2 | include[] is the one parameter that adds to a response: an enum of nullable response properties, null unless asked for. No include*, with* or expand[] flags. | Lintv4-parametersv4-enum-values |
| V4-ERR-1 | Every operation documents the errors it can return (400/401/403/404/429/default by shape, 409 where it can occur). | Lintv4-error-responses |
| V4-NAME-1 | Schema, parameter and response names are PascalCase with acronyms written as words (AiPool, ApiKey; never AIPool). | Lintv4-schema-names |
| V4-DATA-1 | No untyped JSON: types everywhere, typed maps, discriminated unions. A consumer's metadata is the one exception. | Lintv4-typed-schemas |
| V4-DATA-2 | Money in US dollars, units in names (PerMillionTokens, Seconds). | Review |
| V4-DATA-3 | Secrets are write-only on input and revealed once (…WithSecret), masked afterwards; only the person who holds a personal consumer can read its keys' secrets again (…/secret). Lint checks that responses have no writeOnly. | Lintv4-response-schemasReview |
| V4-DATA-4 | Separate request (*Request, *Input, closed, bounded) and response (open, all required) schemas. | Lintv4-request-schemasv4-response-schemas |
| V4-DATA-5 | People are always identified by their Zuplo user ID (user_…), through the shared UserId schema (userId, ownerId, …UserId); a sub is a read-only property (UserSub, or DeveloperPortalUserSub for developer portal users). | Lintv4-user-ids |
| V4-OAS-1 | Summary at most 60 characters, a real description, exactly one declared tag; each tag in exactly one x-tagGroups group. | Lintv4-operation-docsv4-document |
| V4-OAS-2 | operationIds derived from method and URL shape. | Lintv4-operation-id |
| V4-OAS-3 | Bodies only on POST/PUT/PATCH, application/json, named {Verb}{Resource}Request; success bodies reference a named schema too. | Lintv4-request-bodiesv4-success-responses |
| V4-OAS-4 | OpenAPI 3.1, the api.zuplo.com server, rate limits stated, shared components present. | Lintv4-document |
| V4-OAS-5 | config/routes.oas.json is generated from spec/openapi.yaml, and every operation is a Zuplo route. | Lintv4-zuplo-routeCI: check:routes |
Shared rules, as applied in v4
| ID | Shared rule | In v4 | Enforced by |
|---|---|---|---|
| SCOPE-1..3 | Applicability, standards-only naming exceptions, queue endpoints | Applies | Review |
| SCOPE-4 | Grandfathering | Does not applyv4 has no grandfathered violations and no ignore files | CI |
| URL-1 | Kebab plural segments, camelCase parameters | Applies | Lintv4-path-shape |
| URL-2 | No audience-tier prefixes | Appliesthere is no /internal or /admin in v4 | Lintv4-path-shape |
| URL-3 | Bare kebab verb actions | Does not applyv4 has no actions | Lintv4-path-shape |
| URL-4 | Short Stripe-like shapes; a parent is a filter, not a path | Changedflat inside the container (/v4/buckets/{bucketId}/ai/apps?poolId=…); only associations and credentials (…/users/{userId}/consumers) are nested (V4-URL-2/4/5) | Lintv4-path-shapev4-canonical-url |
| URL-4a | Account-rooted, by-name paths are legacy | Changedaccounts are a container, addressed by ID (/v4/accounts/{accountId}/buckets) (V4-URL-6) | Lintv4-path-shapeReview |
| URL-5 | Webhook ingress prefix | Applieswhen v4 receives webhooks | Review |
| VER-1 | Path major version per subsystem | Changedone /v4 for the whole API | Lintv4-path-shape |
| VER-2, VER-3, VER-4 | Deprecation, breaking changes, x-stability | Applies | Lintv4-documentReview |
| NAME-1 | camelCase properties | Appliesstrictly (no baseURL) | Lintv4-property-names |
| NAME-2 | -On timestamps | Appliesboth ways (-On is always a timestamp) | Lintv4-property-names |
| NAME-3 | kebab-case enums | Applies | Lintv4-enum-values |
| NAME-4 | camelCase query parameters | Applies | Lintv4-parameters |
| NAME-5 | kebab verb-noun operationIds | Narrowedto V4-OAS-2 | Lintv4-operation-id |
| DATA-1 | RFC 3339 UTC date-time | Applies | Lintv4-property-names |
| DATA-2 | null not omitted; PATCH tri-state | Applies | Lintv4-response-schemasv4-request-schemas |
| DATA-3 | Prefixed opaque IDs | Changedopaque IDs exactly as the owning service mints them, prefixed or not: accounts are UUIDs, projects prj_…, people user_… (V4-DATA-5), AI pools, apps and user apps config_…, environments and deployments unprefixed, built-in AI providers by name, custom ones aiprov_…, provider assignments aip_… | Lintv4-property-namesReview (string IDs; that an ID is the service's in review) |
| COLL-1, COLL-2 | Envelope, limit bounds | Appliesto every list | Lintv4-list-envelope |
| COLL-3 | Cursor for append-only collections | Applieswhen they arrive | Review |
| COLL-4, COLL-5 | sort, search | Reserved | Lintv4-parameters (no synonyms) |
| COLL-6 | Reject unknown query parameters | Applies | Reviewruntime: request validation |
| EXP-1 | expand[] | Changedinclude[] instead, and no other flags (V4-COLL-2) | Lintv4-parameters |
| ERR-1..3 | Problem JSON, title/detail, zup.fail types | Applies | Lintv4-error-responsesv4-documentReview |
| ERR-4 | application/problem+json | Applies | Lintv4-error-responses |
| ERR-5 | 401 / 403 / 404 semantics | Applies | LintReview (WWW-Authenticate) |
| ERR-6 | Errors documented | Widenedto V4-ERR-1 | Lintv4-error-responses |
| AUTH-1, AUTH-2 | Bearer only; security declared | Applies | Lintv4-securityv4-parameters |
| METH-1, METH-2 | PATCH/PUT semantics, status codes | Narrowedto V4-METH-1/2 | Lint |
| METH-3 | Create-then-poll LROs | Applies | Review |
| ROB-1 | Idempotency-Key | Does not applynot applied: v4 has no Idempotency-Key, so no operation declares it (V4-IDEM-2) | Lintv4-idempotency |
| ROB-2 | ETag / If-Match | Does not applynot applied: v4 has no conditional requests and the last write wins, so no operation declares them (V4-IDEM-3) | Lintv4-conditional-requests |
| ROB-3 | Rate-limit contract | Applies | Lintv4-error-responsesv4-document (429 + Retry-After) |
| ROB-4 | Bounded, closed requests | Applieswith separate schemas (V4-DATA-4) | Lintv4-request-schemas |
| ROB-5..7 | Request ID, bulk, outbound webhooks | Applies | Reviewruntime |
| SPEC-1 | Specs generated from code | Changedv4 is design-first (spec/openapi.yaml) | CI: check:routes |
| SPEC-2..4 | x-internal/x-zuplo-route, lint gate, descriptions | Applies | Lint |
Last modified on