# Production Standards

Binding rules for a generated client's public surface, not a checklist to work through once. This file's predecessor called itself a checklist and was read as advisory: nothing checked most of its rules, and every client audited against it violated them. Every rule below states whether a machine checks it today. A rule with no check is an invitation to build one, not a permanent exception.

## How to read a rule

Each rule (`S1`, `S2`, ...) has three parts:

- **Rule**: one or two sentences, precise enough to grep for a violation.
- **Why**: usually a real defect the rule closes.
- **Enforcement**: one of:
  - `truewire <command>`: a real, run-today command fails or warns on a violation.
  - review checklist (manual): a reviewer reads the code against the rule; no command checks it.
  - not applicable: the rule belonged to a multi-project release process.

Rule IDs are stable once assigned. Code cites them (`check_reserved_names` cites S8, `no_call.py` cites S29), so a retired number stays retired, gaps included.

## Entry point

Run every mechanically enforced check in one pass:

```bash
truewire standards
```

It runs `truewire check`, `truewire surface`, `truewire examples --require-verified`, `truewire docs check` and `truewire docs lint`, plus six in-process checks (`links`/S1, `docstrings`/S3, `duplicate-schemas`/S6, `secret-placeholders`/S16, `router-coverage`/S26, `no-call`/S29), and reports one summary. `--only`/`--skip` filter by slug; `--list-checks` lists them. `links` is excluded from a plain run because it makes live HTTP requests to documentation hosts; select it with `--only links`. A green run is a precondition for calling a client done, not proof: a rule enforced by the review checklist still needs that review.

## General

**S1. Every upstream documentation URL resolves to the exact endpoint or topic page cited, not a stale redirect or a domain-level landing page.**

Why: a link that 404s or lands somewhere generic reads as verified when it was never checked.

Enforcement: `truewire standards --only links`. It fetches every URL cited in `upstream.md`, `README.md`, `docs/**` and `router.json` files and flags a non-2xx status or a redirect to a domain-level page. `warning`-severity, opt-in, and a heuristic: a human still judges each finding.

**S2. Public docs describe the real, current client surface. A doc section either reflects what the package does, or is deleted, never a stale scaffold placeholder.**

Why: one client shipped its docs as a literal unedited scaffold, down to a misspelled env var.

Enforcement: partial. `truewire docs check` fails a placeholder code example, since a call to a method nobody filled in does not type-check (S20); `truewire docs lint` catches pipeline-voiced prose (S21). Placeholder prose with no code stays manual.

## Docstring Formatting

**S3. Every function, class and module carries a docstring in the Google-compatible section style: `Args:`/`Returns:`/`Raises:`/`Examples:`/`References:`, no custom section markers, and a multi-line docstring's closing `"""` on its own line.**

Why: a docstring reaches a user through editor hover and any generated reference, and a renderer mis-renders a non-standard section rather than erroring on it.

Enforcement: `truewire standards --only docstrings`. It walks the package source with `ast` and flags a closing `"""` sharing a line with prose, and a `Word:` header line outside the five sections above. `warning`-severity. Docstring presence is not checked.

**S26. Every grouping directory under `spec/endpoints/` declares a `router.json` with a real description and a working upstream link, so every hover point in the client's attribute chain carries a real docstring. A directory spanning several pages cites the one covering the most of them, or reuses a parent's link. Inventing one is refused.**

Why: `docs/spec/authoring.md` rule 14. A router class rendered no docstring before this, so a fresh caller had no way to orient from the code.

Enforcement: presence is `truewire standards --only router-coverage`, `error`-severity; shape is validated on load; reachability is `--only links` (S1). Whether the text reaches the generated class's docstring stays manual.

## Typing

**S4. Every published package ships `py.typed`.**

Why: without it a downstream type checker treats every import from the package as untyped.

Enforcement: manual. `truewire init` writes the file; nothing checks that it survives to the built distribution.

**S5. A value with a documented, finite set of possible values is typed `Literal[...]` via a spec `enum`, never a bare `str`/`int`.**

Why: `docs/spec/authoring.md` rule 2. A `Literal` catches a typo at the call site.

Enforcement: `truewire check`, the `enum` rule, `warning`-severity by design: a field name that usually denotes a closed set is a heuristic, not proof.

**S6. A public response or item type carries an idiomatic, semantic name for the API concept it represents (`OrderStatus`, `AccountTrade`), never a generated placeholder (`Response200`), an anonymous schema-position name, or an unreduced duplicate of a shared shape that should have been reused.**

Why: found in three generators: `Response200` on every `ping` endpoint, and one shared `Money` shape inlined eight times, each copy titled the same, so the disambiguator appended path segments until the names differed (`LimitLimitGtc0OrderConfigurationAnyOf3LimitLimitGtc`). Two mechanisms: a missing `title`, which rule 1 now requires on an empty object too, and a missing `$ref` for a shared shape (rule 0), which no `title` requirement catches.

Enforcement: the empty-object half is `truewire check`, the `title-empty-object` rule, `warning`-severity. The duplicate half is `truewire standards --only duplicate-schemas`: an inline object schema's property-name signature shared by two or more occurrences across the spec tree (at two properties or more, to cut noise) is one finding naming the locations. `warning`-severity.

## Timestamps

**S7. A wire timestamp declares its real format: `epoch-seconds`, `epoch-millis`, `epoch-micros`, `epoch-nanos`, or `date-time` for an RFC 3339 string, never a bare `string`/`integer` with the shape only in prose. A plain calendar date declares `date`. Applies identically to a request parameter, a request body property and a response field.**

Why: `docs/spec/authoring.md` rule 3. An undeclared timestamp renders as a raw `int`/`str`, and a request-side one is sent to the wire wrong. One client read `0 errors` while declaring a format on 0 of 218 endpoints.

Enforcement: `truewire check`, the `timestamp-format` rule, `warning`-severity: a field name ending in `time`/`timestamp`/`date`/`datetime`/`ts`/`since`/`at` with no declared format is flagged. Only the last word of a split name is matched, so `timeInForce` is never mistaken for a timestamp. Each format's runtime type lives in the project's own core types module, built from `truewire_core.times` converters.

**S27. A project's core types module pairs every timestamp and date type's `BeforeValidator` with a matching `PlainSerializer(converter.dump, when_used='json')`.**

Why: ADR 0008. Without the serializer half, `validator(Type).dump()` (S28) silently renders ISO-8601 regardless of the declared wire format. Latent on every client at the time.

Enforcement: manual. `truewire init`'s core template includes the pairing for every format.

## Validation And Errors

**S8. Every generated `rpc` endpoint method accepts `validate: bool | None = None` (`None` follows the client-level default). When an API's own wire parameter is named `validate`, the codegen backend resolves the Python parameter to a distinct name.**

Why: two hand-rolled generators accepted no override on most methods. Worse, one client's order endpoints reused the wire name `validate` for the API's own dry-run flag, so `validate=True` there meant "don't submit the order," the opposite of its meaning everywhere else.

Enforcement: `truewire surface` reports a `missing_validate` entry for every generated `rpc` method lacking the parameter, and fails like a surface gap. `truewire check`'s `reserved-param` rule flags a spec parameter named `validate`, `warning`-severity: the spec is right to use the real wire name, so this never promotes to `error`.

**S9. API error envelopes map to `truewire_core.exceptions` (`ApiError`, `BadRequest`, `AuthError`, `RateLimited`), not ad hoc local exception classes. A missing credential raises `AuthError` from an early credential-resolution path, never a raw `os.environ[...]` lookup left to surface a bare `KeyError`. A rate-limit failure raises `RateLimited` where the API distinguishes one.**

Why: one client's `resolve_credentials()` correctly raised `AuthError`, and every constructor bypassed it for a raw `os.environ[...]` lookup, so a missing credential raised `KeyError` anyway.

Enforcement: review checklist (manual).

## Core And Instantiation

**S10. A client subclasses `truewire_core` transport (`truewire_core.http.HttpClient`, or `Socket`/`Streams`/`Rpc`/`StreamsRpc` from `truewire_core.ws`) and never constructs a raw `httpx.AsyncClient`, opens a raw `websockets.connect` socket, or defines its own class shadowing a runtime primitive's name without inheriting from it. If the runtime cannot express what an API needs, extend the runtime; never keep a private copy.**

Why: one client overrode `Socket.force_open` with a direct `websockets.connect(...)` call to lift `max_size` for a 5MB snapshot, "otherwise identical" by its own docstring: a private copy of a change every client would benefit from.

Enforcement: review checklist (manual). The repository-wide AST test suite that enforced this in the original monorepo is not applicable to a standalone project; `grep -rn "httpx.AsyncClient\|websockets.connect" src/` covers the first half.

**S23. When an API serves the same request/reply surface over both HTTP and WebSocket, the generated method takes a `transport` keyword choosing between them per call, never a transport chosen once at construction that yields two disconnected objects.**

An endpoint with more than one entry in `spec.transports` renders one method with `transport: Literal[...]`, in declaration order, defaulting to the first. A single-transport endpoint gets no parameter.

Why: binding the transport at construction forces a caller to pick one for the client's lifetime, or hold two instances.

Enforcement: review checklist (manual). A `surface`-style check that the method carries the keyword is buildable but not built.

## Package Structure

**S11. One endpoint method per module. The last-level package `__init__.py` composes sibling endpoint classes through inheritance (`MarketData(AssetPairs, Assets, Depth)`). A higher-level composite, root or mid-level, is a plain `@dataclass(kw_only=True)` composing grouped objects as fields, never further inheritance. A mid-level composite is defined inside the subpackage it composes (`Spot` in `spot/__init__.py`), never inline in the root's wiring file.**

"Plain" means wired through `__init__`, never `field(init=False)` + `__post_init__` + `object.__setattr__`, the pattern a composite falls into when it also subclasses a frozen endpoint base. Expose a `classmethod new(cls, *, client)` that builds every child and passes them to `cls(...)` instead.

Why: one generator did the `object.__setattr__` dance on every mid-level composite, found after the client's root class had been cited as a clean example. Review a composite at every level it appears.

Enforcement: review checklist (manual). `grep -rn "object\.__setattr__" src/` is a strong signal, as is any `class` beyond the root in the root wiring file.

**S12. A request body whose valid or required fields differ by a discriminant (order type, side) is spec'd as an `anyOf` of the variant shapes, never a flat list of named parameters with every variant-specific field optional.**

Why: two clients flattened `price`/`stopPrice`/`timeInForce` as `NotRequired` regardless of order type, so a market order with a `price`, or a limit order without one, type-checks cleanly. A spec defect, not a codegen gap: rule 0 states the `anyOf` shape, and the generator renders it as a tagged union.

Enforcement: review checklist (manual).

**S28. A generated `rpc` endpoint method with a request body serializes it through `content=validator(BodyType).dump(body)`, not per-field conversion lines.**

Why: ADR 0008. Per-field conversion rewrites formatted fields one level deep; one client's order body nested `decimal-string` fields inside an `anyOf` past that walk and raised `TypeError` on a real call. Needs a module-level body adapter named distinctly from the response adapter, and S27 first.

Enforcement: review checklist (manual).

**S29. No generated class defines `__call__`. An endpoint's method is always inherited directly onto its parent, real and distinctly named, never reached through a `cached_property` wrapping a `__call__`-only leaf class, at any depth, including a client's outermost transport root.**

Why: a leaf endpoint class defines no `__init__`, so multiply-inheriting several leaves onto a parent works at any depth; `__call__` is never needed. The motivating bug was a leaf with both a named sibling method and a `__call__`, producing two disagreeing docstrings for one call.

Enforcement: `truewire standards --only no-call`, `error`-severity.

**S30. A `spec/endpoints/` directory is a leaf endpoint or a router grouping, never both.**

Why: `docs/spec/authoring.md` rule 16. A directory holding its own `endpoint.json` beside an endpoint-bearing descendant has no name left to render its leaf under; the only mechanical answer is `__call__`, which S29 forbids. Every real instance found already had its leaf's `function` one segment deeper than its directory, which is exactly the subdirectory it needs to move into.

Enforcement: `truewire check`, `error`-severity. The generator also raises, naming the directory, when a leaf's resolved function collides with a node already in the function tree.

## Spec Coverage

**S13. `spec/` reflects the implemented public surface: one `endpoint.json` per endpoint, `upstream.md` where upstream docs exist, and at least one paired example where it is safe to capture one. An endpoint that is unsafe, stateful, credential-blocked or otherwise unavailable declares why on the endpoint itself (`unverified: {reason, detail}`), not as prose in a status document.**

Why: ADR 0001. A per-endpoint declaration is checkable; a paragraph is not. One status document asserted `0 errors` while a fresh run reported 202.

Enforcement: `truewire examples --require-verified` and `truewire check`. Whether any prose still agrees with what they report is manual: re-run before trusting a claim.

**S14. Pagination is declared per `docs/spec/authoring.md` rule 8 / ADR 0002, not hand-rolled per endpoint and not left for a caller to infer from bare `limit`/`cursor` parameters.**

Enforcement: `truewire check`, the `pagination` rule, `error`-severity.

**S15. A `kind: 'stream'` endpoint declares `envelope.verb`: the field, and its two literal values, that states subscribe-vs-unsubscribe intent on the wire.**

Why: ADR 0004. Without it the mock server guesses intent rather than reading a declared fact.

Enforcement: `truewire check`, the `ws-verb` rule, `warning`-severity while existing specs migrate; promoting it to `error` is that migration's last step.

**S16. A response field carrying a genuine secret, credential or PII gets an obviously fake, shape-preserving placeholder in a captured example: never the real value, and never something implausible about the field's shape.**

Why: `docs/spec/authoring.md` rule 6. `REDACTED_CLIENT_SECRET` in an API-key listing's response is the worked reference.

Enforcement: `truewire standards --only secret-placeholders`. The mock server never compares or redacts a response body (unlike a request-side credential, which `redacted`/ADR 0007 strips), so the check reads the project's `[secrets]` names from `truewire.toml` plus a built-in denylist (`apiKey`, `secret`, `privateKey`, ...) and flags a credential-shaped field in any recorded example whose value carries no fake marker (`REDACTED`, `FAKE`, `TEST`, ...). `warning`-severity: a heuristic, not proof.

## Runtime And Type Tests

**S17. A type-usage test file exists, exercises representative public call patterns (constructors, endpoint calls, response types, stream usage), and passes pyright. Not an empty stub, and not orphaned by a `pyrightconfig.json` pointing at a file that does not exist.**

Why: the one committed guardrail against a change silently degrading a public return type to `Any`, missing or empty on six of eight clients in one audit.

Enforcement: manual. `truewire generate` runs pyright when `[python].pyright` is set or a `pyrightconfig.json` sits at the project root, but nothing checks that the file exists or is non-trivial; what counts as non-trivial is a design call.

**S18. Every endpoint method with pagination parameters has an ergonomic `_paged` variant (unless the API's own pagination model cannot be made safe), covered by a mock-backed test walking several pages. One page cannot distinguish a correct walk from one that happens to terminate immediately.**

Why: off-by-one, cursor-advance and termination bugs are what a single-page test cannot see. A declared block generates the walk but does not prove it.

Enforcement: review checklist (manual).

**S24. A `_paged` variant is `truewire_core.util.paging.PaginatedResponse`-shaped (`async for page in x(...)` and `await x(...)` both work) wherever the generator has that rendering: `token` with `absent_cursor`, `page` with a `total` terminator and `done.rows`, and plain `seek` with no `overlap`. A plain async generator is the wrong choice on those three, and the only choice elsewhere until the rendering widens.**

Why: strictly more capable at zero cost to a caller who only iterates. One client's published, awaitable `_paged` methods were silently replaced with plain async generators during a regeneration, breaking downstream `await` callers.

Enforcement: review checklist (manual). Before regenerating a client, check whether it already has a published awaitable contract worth preserving.

**S19. Both HTTP and WebSocket examples are covered by mock-backed tests when the client supports both transports. A mock test uses a local base-URL override, never a live network call.**

Enforcement: review checklist (manual).

## Docs And Examples

**S20. Every code block in `README.md` and `docs/` type-checks against the package it documents.**

Enforcement: `truewire docs check`.

**S21. Docs describe a fact about the API or the credential when stating a limit ("this endpoint requires a partner whitelist"), never a fact about the build ("out of scope for this run," "deliberately deferred"), and never an internal path (`spec/`, `truewire.toml`, `codegen`).**

Enforcement: `truewire docs lint`, the `internal-reference` rule.

**S25. Every relative link in `README.md`, `docs/**/*.md` and `docs/quickstart.yaml` resolves to a real source file.**

Why: a same-repository link is knowable before publish and should never depend on a live HTTP probe.

Enforcement: `truewire docs lint`, the `broken-doc-link` rule, no network access. The rule's original second half, canonical routes on a hosted documentation site, is not applicable outside that host; see your own release process. Heading fragments are not validated.

## Export And Publish

**S22. Retired.** It tied a catalog tier to export sync between a monorepo and a public mirror, not applicable outside a multi-project monorepo; see your own release process. The point stands: source-tree quality that never reaches the published package reaches nobody.

## Notes For Future Rules

- Don't add a rule restating something `docs/spec/authoring.md` or an ADR states more precisely; point at it (S5, S7, S14, S15, S16 do). Two sources of truth for one fact is how this file's predecessor rotted.
- A rule with `manual` enforcement is a to-do, not a lesser rule. If you build the check, update the rule's `Enforcement` line and the slug list above in the same change.
