# The API contract

`conformance/openapi.yaml` is the **source of truth, not documentation**: handler types, clients,
MCP tool definitions and the conformance suite are generated from it, and the code
conforms to the spec — never the other way round. If the running system and this file
disagree, the system is wrong or the change starts here with a reviewed edit.

The nouns and URL grammar come from [`resource-model.md`](./resource-model.md), agreed
and closed as RUL-171. The full application parity decision of 13 September supersedes its historical v1-only exclusions.

## What the file covers today

All application capabilities are available over the authenticated `/api/trpc` HTTP API and through MCP. Its 206 operations across 28 domains are generated from the app's registered router and exact input schemas, not a manually curated subset. Read the [application action catalog](https://rulework.app/docs/api/actions.json) or the `rulework://actions` MCP resource. MCP advertises 13 everyday/discovery tools while preserving 238 callable capabilities. Use `search_actions`, `get_action`, and `call_action` to discover and execute the rest. All names use `verb_noun` spelling, such as `update_schedule_line`; application-action arguments wrap the original app input in `input`. Runtime schema refinements, permissions, project grants, expected versions and command keys remain enforced by the original procedures.

The OpenAPI resource API provides convenient stable `/v1` workflows for schedules, photographs, client documents and imports. All 24 resource operations are available through MCP. Native `get_photograph` image content is a bounded, upright display derivative; call `list_photographs` first. Binary uploads accept base64 through MCP (24 MiB decoded maximum), with original type and filename. The HTTP upload supports up to 128 MiB. File tools also generate and download documents, export a live client schedule, retain a client-link PDF and create/download project packages. Short-lived download links remain private.

The tRPC API uses JSON GET `input` query parameters for reads and JSON POST bodies for mutations. Its versions and idempotency keys are in the input body; the `/v1` header rules below apply only to resource tools. HTTP errors retain the tRPC `ruleworkCode` envelope, rather than pretending they are RFC 9457. Output schemas are generated from the producer return contracts for all application actions. MCP validates the complete response before presenting it. Producer-defined JSON extension fields remain extensible inside their typed parent contracts. Large reads return bounded `result_page` navigation; use `read_action_result` with the same action/input and returned snapshot/path/offset. A changed snapshot returns a conflict rather than combining different versions. The equivalent HTTP capability is `GET /api/agent-results?name=...&input=...&selection=...`, with the same bearer authorization; it refuses mutations.

OAuth organization consent selects the studio. Switching the browser's studio does not silently retarget an existing MCP token: reconnect and consent to the intended organization. OAuth callbacks and scheduled background jobs are protocol mechanisms, not additional practitioner operations. Existing authorization and confirmation requirements remain in force for transfers, sharing and financial actions.

## Rules a change must keep

- `operationId` is `verb_noun`, snake_case, and **stable forever** — it becomes an MCP
  tool name and a client method name, so a rename is a breaking change.
- Every constrained string is a closed `enum` enumerated from its source, never a prose
  list and never a pattern over usages.
- Money is `{ amount4, currency }`; `amount4` is a scale-4 integer as a **string**, never a
  JSON number — nothing in this API is a float, and the suite checks the whole document for
  one. **`ValueState` is a property of the four priced fields on a schedule line, not of
  money**: the schema carries 77 scale-4 columns and 5 state columns, so a uniform stateful
  wrapper would synthesise a permanent `present` for the rest — a field that can never vary.
  The four are read off the `items` table, so a fifth fails the suite rather than drifting.
  A quantity is `{ amount4, unitId }`: always present, no state, no currency.
- **Displayed figures deliberately do not reconcile.** The shown unit price is the rate
  rounded for display; the shown total is computed from unrounded factors and rounded once,
  so `qty × shown unit ≠ shown total` on correct data (RUL-164). The warning is pinned on
  both `Money` and the client line, because an agent reading the spec without it writes a
  reconciliation assertion that fails on good data.
- Concurrency is `ETag` / `If-Match` (RUL-177): an item's `version` rides in bodies as
  data, but the compare-and-set travels in headers and a stale write refuses with
  `409 VERSION_CONFLICT`. Member GETs answer `If-None-Match` with 304; collections
  deliberately do not.
- Every error names a `code` from `RuleworkErrorCode`
  (`packages/trpc/src/error-codes.ts`), pinned in the response schemas (RUL-175) —
  distinguish failures by code, never by status alone. The contract, and the reason a
  string rather than a class carries it, is in [`errors.md`](./errors.md); the status and
  sentence for each code are in `x-rulework-error-map` in the spec, checked against the
  classifier itself.
- `x-openai-isConsequential: true` on every write; `x-rulework-scope` on every
  operation, drawn from the `StudioPermission` union in
  `packages/trpc/src/contracts-a.ts` and nowhere else. Nesting, caller kinds, capability
  tokens and rate limits are in [`authorisation.md`](./authorisation.md).
- Every operation description names at least one alternative operation, so an agent that
  picked the plausible neighbour is told where the right one is.
- **Every collection carries a cursor and almost none needs one.** Measured on live data:
  450 lines in the largest project, 17 sections at most, three projects over a hundred
  lines. `limit` defaults above that so the common case is one request — the cursor is for
  the shape, not the size. Never offset: the schedule reorders. The default order is the
  document's own and `sort` carries no `default:`, because the natural order is not one of
  its values. Filters are a named set, not a query language, and no `fields` parameter —
  field selection is one more place a denied field could be asked for.
- **The client document is its own resource, not a filtered project.** It shares no
  structural type with the practitioner schedule — only the closed `ValueState` enum, which
  carries no data and would be worse duplicated. A filter is a runtime decision that can be
  got wrong; two types is a compile-time one that cannot. Its register is the client's: a
  row is a **piece**, never a line or an item, and *plate* never appears in a field name.
  It carries **two timestamps**, because photographs are frozen to mint time and the pieces
  are not (RUL-134) — hiding that behind one would settle a decision that is the
  practitioner's (RUL-170).

## Validate and conform

```sh
sh conformance/validate.sh              # the file is a legal OpenAPI 3.1 document
sh conformance/conformance.sh           # the file agrees with the code, and with itself
sh conformance/conformance.sh --bites   # every check proved red against its own defect
```

`validate.sh` runs `@seriousme/openapi-schema-validator` against the OpenAPI 3.1
meta-schema and prints `{ "valid": true }`. It says nothing about whether the spec is
*true* — a document can be perfectly legal and describe a vocabulary the code does not
have. Run both before committing any edit to the spec.

`conformance.sh` asserts the rules above rather than restating them. Each closed set is
**imported from the module that defines it** and compared member for member — never
matched by a pattern over usages, which is how a scope enumeration once returned ten of
eleven because it disallowed a hyphen in the first segment. The remaining checks hold
the spec to its own preamble: 409 not 412, 404 never 403 on a nested id, 304 on member
GETs and never on collections, and no field anywhere under `ClientDocument` that could
carry the studio's margin.

**`/v1` reads are served from `apps/web/app/v1`** (RUL-286), and the writes follow. The
checks here are still the static half of RUL-182: nothing in this directory fires an HTTP
request. The handler tests beside the routes (`apps/web/lib/server/v1/`) validate each
response against the spec's own schema; the request-driven half of the conformance suite
has not joined yet and is not claimed.

`--bites` is not optional ceremony. It mutates the document once per defect and asserts
**which** check goes red, because a guard can fail on its own vacuity floor rather than
on the defect — the same colour pointing at the wrong place. Add a check and you add its
defect, or the check is decoration.

## Synthetic workflow examples

For an ordinary line edit: `get_line` → retain its exact quoted `etag` → `update_line` with that value in `If-Match` → `get_line` to confirm the saved fields. On VERSION_CONFLICT, refresh and explain; do not replay the update automatically. For creation, retain one UUID Idempotency-Key and identical input for an uncertain retry. `packages/mcp/test/server.test.ts` exercises these HTTP contracts with synthetic records.

For document rendering: create the render operation using the discovered schema, then poll its returned operation identifier. Honor Retry-After; stop on a terminal failure and return its stable code. A queued operation is not a completed document. See the OpenAPI operation schemas and local handler tests.

Large result example: call a read action with `{ "input": { "projectId": "project_synthetic" } }`. A `result_page` names its snapshot, paths and nextOffset. Discover `read_action_result`, keep the exact original input and snapshot, and select one returned path. Continue only the branches needed for the question; never treat references as missing records. The snapshot digest is a consistency check, not an access credential.

## Workflow contracts

[The Arazzo workflows](https://rulework.app/arazzo.yaml) describe a version-checked read–edit–verify sequence and a client-document freeze followed by operation inspection. Run writes only for the requested change. A conflict ends the write sequence; an uncertain creation retains its original idempotency key.

Large application reads return pages bounded to 8,000 JSON bytes. Continue with `read_action_result`, preserving the action, input and snapshot. A returned `node` is a snapshot-bound reference, including for deeply nested values and long object keys; `nextOffset` continues the selected node. A changed snapshot requires a fresh read.
