# Turbopuffer OpenAPI Parity Profile

Date: 2026-03-05
Status: Draft (Normative)
Owner: Query Gateway / Ingest Gateway / Search Fabric

## 1. Scope

This document defines the Turbopuffer compatibility contract as full documented OpenAPI path/method coverage with deterministic handling rules, explicit active-profile execution admissibility constraints, and subset-scoped field/value lowering at the canonical boundary.

Goals:
- Provide deterministic handling for every documented OpenAPI path/method in scope.
- Match admitted request and response wire shapes for the declared active subset, except nested billing object field-level schemas (`WriteBilling`, `QueryBilling`) which are out of scope in this profile.
- Keep runtime admissibility constraints explicit and phase-scoped (for example active-profile canonical `doc_id` limitations), not implicit.
- Define the canonical Search Fabric lowering for every admitted Turbopuffer behavior.
- Treat any OpenAPI behavior that cannot be lowered deterministically to canonical APIs as out of profile until the canonical API surface is extended.

Subset posture:
- This profile is not a blanket claim that every OpenAPI value branch is admitted end-to-end.
- The compatibility commitment is:
  - every documented OpenAPI path/method has a deterministic handling rule,
  - only the explicitly admitted request/value subsets are executable in the active profile,
  - all non-admitted branches MUST fail closed rather than being approximated.

Normative references:
- `docs/turbopuffer/openapi.yml`
- `https://turbopuffer.com/api/openapi.yaml`
- Turbopuffer docs:
  - `https://turbopuffer.com/docs/auth`
  - `https://turbopuffer.com/docs/write`
  - `https://turbopuffer.com/docs/query`
  - `https://turbopuffer.com/docs/metadata`
  - `https://turbopuffer.com/docs/export`
  - `https://turbopuffer.com/docs/warm-cache`
  - `https://turbopuffer.com/docs/namespaces`
  - `https://turbopuffer.com/docs/delete-namespace`
  - `https://turbopuffer.com/docs/recall`

Profile rule:
- Checked-in OpenAPI defines the raw Turbopuffer wire surface being mapped.
- Canonical Search Fabric behavior, lowering, and internal contract fit are still governed by `api/proto/searchfabric/v1/search_fabric.proto` plus `docs/specs/core/search-fabric-api.md`.
- If upstream OpenAPI changes, this document and the canonical mapping specs MUST be reviewed and updated together before parity claims change.
- Compatibility non-goal:
  - Billing API field-level compatibility is out of scope (`WriteBilling`, `QueryBilling`).

Ownership applicability boundary:
- Turbopuffer parity commitments are normative only for collections whose authoritative descriptor declares `collection_truth_mode=COLLECTION_TRUTH_MODE_SEARCH_FABRIC`.
- For collections whose authoritative descriptor declares `collection_truth_mode=COLLECTION_TRUTH_MODE_EXTERNAL`, Turbopuffer read/query/write APIs are out of scope; requests MUST be rejected with `409 SF_OWNERSHIP_CONFLICT`.
- If exposed as convenience routing, such behavior is non-normative and MUST NOT be counted in parity acceptance.

## 1.1 Phase/Profile Binding and Gates

- Phase 0 (`2026-03-01` to `2026-03-31`):
  - contract freeze only; no customer delivery.
- Phase 3 (`2026-09-01` to `2026-10-31`):
  - no Turbopuffer delivery milestone is on the Commitment-A/B GA-critical path in this window.
- Profile 1 delivery scope:
  - the active delivered profile is the Phase-4B `Profile 1` subset defined in Sections `3` to `6` of this document.
  - delivery target is gated production traffic on the `2027-01-31` Phase-4B post-GA expansion checkpoint inside the broader Phase-4 window (`2026-11-01` to `2027-01-31`).
- Phase 4B (`2026-11-01` to `2027-01-31`):
  - Profile 1 delivery and its initial hardening/scale-validation close on the same-date post-GA expansion checkpoint, not on the internal engineering qualification critical path.
- Gate alignment:
  - functional gate: Profile-1 conformance suite passes for the declared Section `3` to `6` subset.
  - Phase-4B retrieval-affecting routes delivered by this profile MUST be represented in the fresh checkpoint-closing benchmark bundle whenever they can affect retrieval quality, and they MUST satisfy the roadmap search-quality/evidence-linkage rules defined by `docs/governance/roadmap.md` and `docs/testing/search-quality-evaluation.md`.
  - reliability/economics gates: use the rollout gates, workload bundles, and economics envelopes defined in `docs/governance/roadmap.md` Sections `3.2`, `3.3`, and `6.8`; this document does not redefine them.

## 2. OpenAPI Source-of-Truth Notes

- The in-repo file `docs/turbopuffer/openapi.yml` is the checked-in raw OpenAPI snapshot used as parity input for this profile.
- Parity checks MUST read `docs/turbopuffer/openapi.yml` directly.
- When upstream OpenAPI changes, refresh this file from:
  - `https://github.com/turbopuffer/turbopuffer-openapi/raw/refs/heads/main/openapi.yml`
- Refreshing the raw OpenAPI snapshot alone does not change active parity commitments; any semantic mapping change still requires coordinated updates to this compatibility spec and the canonical Search Fabric contract docs.

## 3. Endpoint Parity Matrix

All OpenAPI paths below are in scope for deterministic handling. In every path shown below, the literal `{namespace}` path segment denotes the Turbopuffer route namespace token, not a canonical namespace catalog object. `Supported` here means the route exists with the subset-constrained request/response contract defined later in this document; it does not imply blanket admission of every OpenAPI field/value branch.

Terminology hazard:
- In upstream Turbopuffer wire syntax, `namespace` names a route-level collection token.
- In canonical Search Fabric vocabulary, `namespace` names a first-class catalog object (`NamespaceDescriptor`).
- Outside exact upstream route strings/field names, this document uses `route namespace token` for the Turbopuffer path token and reserves unqualified `namespace` for quoted upstream syntax only.

| Endpoint | Method | Status | Notes |
|---|---|---|---|
| `/v1/namespaces` | `GET` | Supported | Collection-catalog read via `ListCollections` + gateway projection |
| `/v1/namespaces/{namespace}/schema` | `GET` | Supported | Collection lookup via `GetCollectionMetadata` + schema read via `GetCollectionSchema` |
| `/v1/namespaces/{namespace}/schema` | `POST` | Supported | Collection ensure via `PutCollectionMetadata` + schema update via `PutCollectionSchema` |
| `/v1/namespaces/{namespace}/metadata` | `GET` | Supported | Collection metadata + schema read via `GetCollectionMetadata` + `GetCollectionSchema`; route requires `CollectionDescriptor.completeness.ready` |
| `/v1/namespaces/{namespace}/hint_cache_warm` | `GET` | Supported | Gateway-local warm-cache hint |
| `/v1/namespaces/{namespace}/_debug/recall` | `POST` | Supported | Gateway-local privileged debug endpoint |
| `/v2/namespaces/{namespace}` | `POST` | Supported | Collection-scoped ingest; inline schema/encryption MAY use `PutCollectionMetadata` + `PutCollectionSchema` before mutation emission |
| `/v2/namespaces/{namespace}` | `DELETE` | Supported | Collection lookup by `CollectionNameBinding` + lifecycle deletion via `DropCollection` |
| `/v2/namespaces/{namespace}/query` | `POST` | Supported | Collection-scoped query after `CollectionNameBinding` resolution |
| `/v2/namespaces/{namespace}/query?stainless_overload=multiQuery` | `POST` | Supported | Gateway fan-out over collection-scoped canonical query boundary |
| `/v2/namespaces/{namespace}/explain_query` | `POST` | Supported | Collection-scoped query admission + gateway-local plan rendering |

OpenAPI comment-only omitted endpoints (not in active path map):
- `GET /v1/namespaces/{namespace}` (export)
- `HEAD /v1/namespaces/{namespace}`

## 4. Request Contract Parity

### 4.1 Common Rules

- Global auth: bearer auth required.
- Server template: `https://{region}.turbopuffer.com`.
- Turbopuffer route namespace token path parameter is `namespace` (`string`).
- Terminology split for this profile:
  - canonical namespace means the Search Fabric namespace catalog object (`namespace_id`, `NamespaceDescriptor`);
  - Turbopuffer route namespace token `{namespace}` is an external route token that resolves to a collection name within an already resolved canonical namespace/account scope for collection-scoped data routes;
  - these two meanings intentionally differ and must not be conflated in lowering logic or review language.
- unless explicitly stated as `canonical namespace`, every unqualified `namespace` in quoted upstream route sections and route headings refers to the Turbopuffer route namespace token, not the canonical namespace catalog object. In explanatory prose outside quoted upstream names, this document MUST use `route namespace token`.
- Canonical scope resolution for Turbopuffer is:
  - external Turbopuffer account -> canonical `namespace_id`, but that account-to-`namespace_id` mapping is outside this design scope and belongs to external system control-plane/authz logic.
  - external Turbopuffer `{namespace}` path token -> canonical `CollectionNameBinding{namespace_id=<resolved>, collection_name=<path namespace>}` inside that already-resolved `namespace_id`.
- collection-scoped routes MUST resolve or ensure the authoritative `CollectionRef` through typed collection catalog operations; those catalog operations use `CollectionNameBinding` as the name-based collection identity key before a concrete `CollectionRef` exists.
- `GET /v1/namespaces` lists collections inside the externally resolved `namespace_id` and projects `CollectionDescriptor.collection_name` back to the Turbopuffer wire field `NamespaceSummary.id`.
- Every gateway-emitted canonical unary RPC in this profile MUST carry `RequestMeta`.
- Every Turbopuffer query/read route that lowers to canonical query execution MUST emit the full canonical `QueryRequest` envelope:
  - `meta=<RequestMeta>`
  - `collection=<resolved CollectionRef>`
  - `consistency`
  - `search_payload`
  - `fetch_spec`
- Route legality is validated against the resolved descriptor `collection_truth_mode`; the canonical `QueryRequest` does not carry a public truth-mode field.
- For read-only unary RPCs (`ListCollections`, `GetCollectionMetadata`, `GetCollectionSchema`, `ExecuteQuery`), `RequestMeta.idempotency_key` MAY be omitted, but `RequestMeta.client_id` and `RequestMeta.client_kind=CLIENT_KIND_TP_QUERY_GATEWAY` remain mandatory and explicit.
- For RPCs that require caller-supplied idempotency keys (`PutCollectionMetadata`, `PutCollectionSchema`, `DropCollection`, `StartSnapshotSync`, `CommitSnapshotSync`, `AbortSnapshotSync`), `RequestMeta.idempotency_key` MUST be non-empty.

Canonical resolution flow:

```text
Turbopuffer account
  -> external control plane / authz (out of scope here)
  -> namespace_id

Turbopuffer route namespace token
  -> CollectionNameBinding{namespace_id, collection_name}
  -> GetCollectionMetadata.lookup.name_binding / PutCollectionMetadata.name_binding
  -> CollectionDescriptor.collection
  -> CollectionRef
```

### 4.2 `GET /v1/namespaces`

Supported query parameters:
- `cursor: string`
- `prefix: string`
- `page_size: integer` (`1..1000`)
- Backing contract: canonical collection-catalog read `CatalogControlService.ListCollections` inside the externally resolved `namespace_id`, followed by gateway wire-shape projection.
- `prefix` lowers to canonical `collection_name_prefix`.
- `NamespaceSummary.id` projects from canonical `CollectionDescriptor.collection_name` and is the same Turbopuffer route namespace token used in token-scoped paths.
- `cursor` / `next_cursor` semantics MUST stay coherent with `ListCollections.next_page_token` / `ListCollections.success.next_page.token`.

### 4.3 `GET /v1/namespaces/{namespace}/schema` (route namespace token)

- No request body.
- Returns schema map (`additionalProperties: AttributeSchemaConfig`).
- Backing contract:
  - resolve `{namespace}` through `CatalogControlService.GetCollectionMetadata.lookup.name_binding=CollectionNameBinding{namespace_id=<resolved>, collection_name=<path namespace>}`,
  - read schema through `CatalogControlService.GetCollectionSchema.collection=<resolved CollectionRef>`.

### 4.4 `POST /v1/namespaces/{namespace}/schema` (route namespace token)

Request body:
- `application/json` object
- `additionalProperties: AttributeSchema`
- Backing contract:
  - gateway MUST ensure the target collection descriptor through `CatalogControlService.PutCollectionMetadata.name_binding=CollectionNameBinding{namespace_id=<resolved>, collection_name=<path namespace>}` before schema apply when the collection does not yet exist,
  - gateway MUST then apply the normalized schema through `CatalogControlService.PutCollectionSchema.scope.collection=<authoritative CollectionRef>` with gateway-managed control scope `scope.ddl_commit_ts=0`, authoritative monotonic `schema_version` derived from the current schema lineage of the resolved collection, and explicit non-`UNSPECIFIED` `update_policy`.
  - because both backing RPCs are state-mutating, gateway MUST propagate or synthesize a stable outer `RequestMeta.idempotency_key` and MUST send explicit gateway `RequestMeta.client_id` plus `RequestMeta.client_kind=CLIENT_KIND_TP_WRITE_GATEWAY` on `PutCollectionMetadata` and `PutCollectionSchema`.
  - first schema apply for a brand-new collection MUST use `SCHEMA_UPDATE_POLICY_INITIAL_CREATION`; that initial apply MUST use the full canonical zero-state tuple (`schema_version=1`, `fence.expected_prev_schema_version=0`, `fence.schema_generation_token="__initial__"`, `update_policy=SCHEMA_UPDATE_POLICY_INITIAL_CREATION`, `fence.enforce_monotonic_apply=true`). Later schema writes MUST classify into additive vs rebuild policy according to the canonical schema-diff rules.

`AttributeSchema` parity details:
- `AttributeSchema` is `anyOf(AttributeType, AttributeSchemaConfig)`.
- `AttributeSchemaConfig` is an object with required `type` and optional:
  - `filterable: boolean`
  - `regex: boolean`
  - `full_text_search: FullTextSearch`
  - `ann: Ann`
- `AttributeType` is string-typed; OpenAPI documents scalar/array families and vector syntax `[DIMS]f16` / `[DIMS]f32`.
- `FullTextSearch` is `oneOf(boolean, FullTextSearchConfig)`.
- `FullTextSearchConfig` is documented by OpenAPI, but this compatibility profile admits only the strict subset that can be expressed by canonical analyzer IDs plus the current analyzer-override registry:
  - admitted booleans/knobs:
    - `k1`
    - `b`
    - `stemming`
    - `remove_stopwords`
    - `ascii_folding`
  - explicitly unsupported in the active profile:
    - `language`, `case_sensitive`, `max_token_length`
    - `tokenizer=pre_tokenized_array|word_v0|word_v1|word_v2|word_v3`
- `Language` values:
  - `arabic`, `danish`, `dutch`, `english`, `finnish`, `french`, `german`, `greek`, `hungarian`, `italian`, `norwegian`, `portuguese`, `romanian`, `russian`, `spanish`, `swedish`, `tamil`, `turkish`
- `Tokenizer` values:
  - `pre_tokenized_array`, `word_v0`, `word_v1`, `word_v2`, `word_v3`
- `Ann` is `oneOf(boolean, AnnConfig)`.
- `AnnConfig` optional field:
  - `distance_metric: DistanceMetric`

### 4.4.1 Canonical Control-Plane Schema Equivalence

For gateway-to-Search-Fabric integration, Turbopuffer schema payloads MUST be normalized into canonical control-plane schema fields before apply:
- wire-level document identity `Row.id` / `Columns.id` -> synthesized `DocIDMapping{source_field=\"id\", source_field_type=SCALAR_FIELD_TYPE_UINT64, immutable=true, unique=true}` for canonical control-plane apply
- `AttributeSchemaConfig.type` -> `FieldSchema.config.field_definition` oneof selection (`scalar_type` / `text` / `vector` / `keyword`); vector families such as `[DIMS]f16` / `[DIMS]f32` map to `FieldSchema.config.field_definition.vector`
- scalar/array family cardinality in `AttributeSchemaConfig.type` MUST be preserved via `FieldSchema.config.capabilities.multi_valued` (`type` -> `false`, `[]type` -> `true`) during canonicalization/projection.
- `filterable`/`regex` -> `FieldSchema.config.capabilities.filterable` / `FieldSchema.config.capabilities.pattern_matchable`
- `full_text_search` normalization is strict-subset only:
  - `false` -> `FieldSchema.config.capabilities.searchable=false`
  - `true` -> `FieldSchema.config.capabilities.searchable=true` + analyzer baseline `builtin_standard_v1` + `FieldSchema.config.field_definition.text.analysis.options["stemming"].atomic_value.string_value="inherit"` + `FieldSchema.config.field_definition.text.analysis.options["stopwords"].atomic_value.string_value="inherit"`
  - admitted `FullTextSearchConfig` subset always implies `FieldSchema.config.capabilities.searchable=true` and lowers to canonical analyzer/override carriers only:
    - `ascii_folding=false|omitted` -> analyzer baseline `builtin_standard_v1`
    - `ascii_folding=true` -> analyzer baseline `builtin_standard_folded_v1`
    - `k1` / `b` -> `FieldSchema.config.field_definition.text.fulltext.default_similarity.bm25`
    - `stemming=true` -> `FieldSchema.config.field_definition.text.analysis.options["stemming"].atomic_value.string_value="enabled"`
    - `stemming=false` -> `FieldSchema.config.field_definition.text.analysis.options["stemming"].atomic_value.string_value="disabled"`
    - omitted `stemming` in `FullTextSearchConfig` -> `FieldSchema.config.field_definition.text.analysis.options["stemming"].atomic_value.string_value="inherit"`
    - `remove_stopwords=true` -> `FieldSchema.config.field_definition.text.analysis.options["stopwords"].atomic_value.string_value="enabled"`
    - `remove_stopwords=false` -> `FieldSchema.config.field_definition.text.analysis.options["stopwords"].atomic_value.string_value="disabled"`
    - omitted `remove_stopwords` in `FullTextSearchConfig` -> `FieldSchema.config.field_definition.text.analysis.options["stopwords"].atomic_value.string_value="inherit"`
    - unsupported `language` / `case_sensitive` / `max_token_length` / `tokenizer=*` branches MUST fail closed with `SF_UNSUPPORTED_OPTION`
  - no wholesale `full_text_search.*` mapping exists in this profile
- `ann.distance_metric` -> `FieldSchema.config.field_definition.vector.distance_metric`
- `ann` enablement intent -> `FieldSchema.config.field_definition.vector.search_tuning.optimization_goal` (profile default mapping: disabled -> `VECTOR_OPTIMIZATION_GOAL_EXACT_MATCH`, enabled -> `VECTOR_OPTIMIZATION_GOAL_READ_LATENCY` unless policy override applies)

Admissibility matrix for `FieldSchema.config.field_definition` oneof selection:

| Turbopuffer source shape | Canonical branch | Extra rules |
|---|---|---|
| non-string scalar or scalar-array (`bool`, integer, float, timestamp, and `[]` variants) | `scalar_type` | `full_text_search` MUST be omitted/`false`; `ann` MUST be omitted/`false`; violating combinations fail closed as `SF_SCHEMA_VIOLATION` |
| `string` / `[]string` with `full_text_search` omitted or `false` | `keyword` | `filterable` / `regex` lower only to capability flags; `ann` MUST be omitted/`false`; string exact-match branches stay `keyword`, not `text` |
| `string` / `[]string` with `full_text_search=true` or admitted `FullTextSearchConfig` | `text` | `FieldSchema.config.capabilities.searchable=true` is mandatory; `ann` MUST be omitted/`false`; unsupported `full_text_search.*` knobs fail closed as `SF_UNSUPPORTED_OPTION` |
| vector family (`[DIMS]f16`, `[DIMS]f32`, and array forms admitted by source schema) with `ann` omitted, `false`, `true`, or admitted `AnnConfig` | `vector` | `full_text_search` MUST be omitted/`false`; `ann.distance_metric` lowers to canonical vector distance metric; unsupported vector tuning/profile combinations fail closed by canonical precedence |

Fail-closed rules for impossible mixed-intent combinations:
- `full_text_search` on any non-string family MUST fail closed as `SF_SCHEMA_VIOLATION`.
- `ann` on any non-vector family MUST fail closed as `SF_SCHEMA_VIOLATION`.
- A source shape that simultaneously implies text and vector semantics for one attribute MUST fail closed; gateway MUST NOT try to approximate this by emitting multiple canonical branches for one field.

Normalization rules:
- gateway MUST keep semantic round-trip stability (`source schema -> canonical -> source schema`) for admitted feature branches.
- gateway MUST preserve scalar-vs-array type family on round-trip (`string` MUST NOT re-project as `[]string`, and vice versa).
- synthesized canonical `DocIDMapping.source_field=\"id\"` refers to the top-level Turbopuffer wire identity carrier, not to a user-defined attribute entry in the schema map.
- canonical `TextAnalysisSpec.options["stemming"].atomic_value.string_value="inherit"` and `["stopwords"].atomic_value.string_value="inherit"` MUST project back to omitted Turbopuffer booleans; `enabled` MUST project to `true`, and `disabled` MUST project to `false`.
- admitted object-form `full_text_search` MUST round-trip as enabled full-text search; canonical lowering MUST NOT keep or re-project `FieldSchema.config.capabilities.searchable=false` when `FullTextSearchConfig` is present.
- any `full_text_search` knob outside the admitted subset MUST fail closed as `SF_UNSUPPORTED_OPTION`; gateway MUST NOT silently drop or best-effort translate unsupported knobs.
- schema branches not representable in canonical model MUST fail closed (`SF_UNSUPPORTED_OPTION` or `SF_SCHEMA_VIOLATION`), not silently dropped.
- schema updates MUST use control-plane fencing/idempotency guarantees (`PutCollectionSchema`) rather than bypassing canonical schema governance.
- readback of the admitted schema generation MUST use `GetCollectionMetadata` + `GetCollectionSchema`; gateway MUST NOT synthesize schema answers from cached request bodies.
- Backing contract: typed collection catalog ensure/read + canonical control-plane write/projection.

### 4.5 `GET /v1/namespaces/{namespace}/metadata` (route namespace token)

- No request body.
- Backing contract:
  - resolve `{namespace}` through `CatalogControlService.GetCollectionMetadata.lookup.name_binding=CollectionNameBinding{namespace_id=<resolved>, collection_name=<path namespace>}`,
  - read typed collection metadata from `GetCollectionMetadata.success.descriptor`,
  - read schema through `CatalogControlService.GetCollectionSchema.collection=<resolved CollectionRef>`.
- route admission requires `GetCollectionMetadata.success.descriptor.completeness.ready`; the resolved collection descriptor must already be complete for metadata-parity consumers.
- gateway MUST project `created_at`, `updated_at`, `approx_*`, `encryption`, and `index` directly from typed canonical collection metadata fields; it MUST NOT synthesize those fields from gateway-local defaults or opaque metadata maps.
- if `GetCollectionMetadata.success.descriptor.completeness.publishing` is the active branch, the route MUST fail closed with `503 SF_CATALOG_UNAVAILABLE` and surface canonical details `descriptor_state=publishing` plus `missing_descriptor_fields=<reported set>` rather than fabricating placeholders or silently degrading the response.
- responsibility boundary for this admission-gated route:
  - `PutCollectionMetadata` directly provisions only the typed metadata modeled in `CollectionMetadataDefinition` (notably `encryption`) plus supplemental `options`.
  - `CollectionDescriptor.approx_stats` and `CollectionDescriptor.index` are populated by catalog-owned metadata publication / index-progress tracking outside the direct request body of this route.
  - canonical readiness for this publication boundary is `CollectionDescriptor.completeness`; there is no route-local best-effort readiness mode.

### 4.6 `GET /v1/namespaces/{namespace}/hint_cache_warm` (route namespace token)

- No request body.
- Backing contract: gateway-local operational hint endpoint. It MAY enqueue cache-warm work, but MUST NOT mutate canonical schema state or canonical query digest inputs.

### 4.7 `POST /v1/namespaces/{namespace}/_debug/recall` (route namespace token)

Request body properties:
- `num: integer`
- `top_k: integer`
- `filters: any` (OpenAPI `x-stainless-any`)
- `include_ground_truth: boolean` (default `false`)
- Backing contract: gateway-local privileged debug workflow that fans out to canonical queries plus exhaustive comparison logic; no public canonical RPC is added solely for this path.

### 4.8 `POST /v2/namespaces/{namespace}` (`Write`, route namespace token)

Every top-level OpenAPI `Write` property listed below has a deterministic handling rule in parity scope, but only the explicitly admitted value branches are executable in the active profile:
- `upsert_columns`
- `upsert_rows`
- `patch_columns`
- `patch_rows`
- `deletes`
- `upsert_condition`
- `patch_condition`
- `delete_condition`
- `distance_metric`
- `schema`
- `copy_from_namespace`
- `delete_by_filter`
- `delete_by_filter_allow_partial`
- `patch_by_filter`
- `patch_by_filter_allow_partial`
- `return_affected_ids`
- `encryption`
- `disable_backpressure`

Key shape notes:
- `copy_from_namespace` supports both:
  - string form
  - object form (`source_namespace`, optional `source_api_key`, optional `source_region`)
- `upsert_columns` / `patch_columns` use `Columns`:
  - required `id: Id[]`
  - optional `vector` as either `Vector[]` or a single `Vector` value
  - additional properties are allowed and must be array-valued (columnar attributes)
- `patch_by_filter` shape (`PatchByFilter`):
  - required `patch` (object, `additionalProperties: true`)
  - required `filters` (`x-stainless-any`)
- `distance_metric` supports `cosine_distance` or `euclidean_squared`.
- `encryption` (`Encryption`) supports optional `cmek` object with required `key_name` when present.
- `return_affected_ids` is boolean with OpenAPI default `false`.
- IDs use OpenAPI `Id` union: `uuid string | string | integer`.
- Write admissibility boundary:
  - this route is write subset-scoped in the active profile.
  - every top-level property listed above has a deterministic handling rule, but only explicitly admitted value branches are executable.
  - unsupported value branches MUST fail closed; gateway MUST NOT silently drop, ignore, or best-effort translate them.
- `distance_metric` intent split:
  - when the request also carries inline `schema`, admitted `distance_metric` MUST be canonicalized into vector schema state;
  - otherwise it is validation-only against the existing schema-declared vector metric and MUST be elided from canonical mutation payload after successful validation.

Active-profile ID constraint (normative):
- OpenAPI `Id` union remains the wire-level source contract, but current canonical execution path admits only lossless-uint64-convertible IDs.
- Accepted in active profile:
  - `string` branch values that are decimal uint64 lexical forms losslessly parseable as base-10 uint64.
  - `integer` branch values when non-negative and losslessly representable as uint64.
- Rejected in active profile:
  - negative integer values.
  - floating-point numeric values (for permissive JSON-decoder paths that do not preserve integer typing).
  - `uuid` branch values.
  - non-decimal or otherwise non-lossless `string` branch values.
- Rejected branches MUST fail closed as `400 SF_INVALID_DOC_ID`.
- UUID/surrogate/non-string normalization support is deferred to a future phase-scoped evolution addendum and is not part of current parity acceptance.

### 4.8.1 Canonical Write Lowering Rules (Gateway -> Search Fabric)

For parity at wire layer and determinism at canonical layer, gateway MUST normalize Turbopuffer write requests into canonical ingest contracts with the following rules:

- Every emitted state-mutating canonical RPC in this lowering path MUST carry canonical `RequestMeta`: ingest-plane `MutationBatch.meta.idempotency_key` MUST be non-empty and stable for the admitted outer write identity, and all emitted mutating RPCs (`MutationBatch`, `PutCollectionMetadata`, `PutCollectionSchema`, `DropCollection`, and any snapshot-sync control calls used by admitted routes) MUST send explicit gateway `RequestMeta.client_id` plus `RequestMeta.client_kind=CLIENT_KIND_TP_WRITE_GATEWAY`.
- `schema`:
  - inline write-route `schema` is semantically identical to `POST /v1/namespaces/{namespace}/schema` for the same external namespace token.
  - gateway MUST resolve external account -> canonical `namespace_id` outside this design, ensure the target collection descriptor through `PutCollectionMetadata.name_binding=CollectionNameBinding{namespace_id=<resolved>, collection_name=<path namespace>}`, and use the returned `CollectionDescriptor.collection` as the authoritative `CollectionRef`.
  - gateway MUST canonicalize inline `schema` with the same rules defined in Section `4.4.1` and issue a fenced `PutCollectionSchema.scope.collection=<resolved CollectionRef>` with gateway-managed control scope `scope.ddl_commit_ts=0`, authoritative monotonic `schema_version` derived from the current schema lineage of the resolved collection and explicit non-`UNSPECIFIED` `update_policy` before planning or emitting any row mutations.
  - inline schema ensure/apply MUST carry the same stable outer `RequestMeta.idempotency_key` as the surrounding write request and MUST send explicit gateway `RequestMeta.client_id` plus `RequestMeta.client_kind=CLIENT_KIND_TP_WRITE_GATEWAY` on both backing control-plane RPCs.
  - inline initial schema creation MUST use `SCHEMA_UPDATE_POLICY_INITIAL_CREATION`; when no prior schema generation exists it MUST use the same full canonical zero-state tuple (`schema_version=1`, `fence.expected_prev_schema_version=0`, `fence.schema_generation_token="__initial__"`, `update_policy=SCHEMA_UPDATE_POLICY_INITIAL_CREATION`, `fence.enforce_monotonic_apply=true`). Later inline schema changes MUST classify into additive vs rebuild policy according to the canonical schema-diff rules.
  - inline schema apply MUST use the same retry-stable outer write identity as the surrounding write request:
    - identical replay of the same canonicalized schema payload is replay-safe,
    - replay under the same identity with a different canonicalized schema payload MUST fail closed (`SF_IDEMPOTENCY_CONFLICT` or `SF_VERSION_CONFLICT` by precedence).
  - if inline schema apply fails, gateway MUST emit no canonical row mutations for that request.
  - if inline schema apply succeeds and later row processing partially completes (`continue_on_error`, `*_allow_partial=true`, or condition-based skips), the applied schema remains committed; response reconstruction reflects only emitted mutation outcomes and MUST NOT imply schema rollback.
- `upsert_condition` / `patch_condition` / `delete_condition`:
  - these are wire-only condition DSLs; current canonical proto does not carry a dedicated condition field for row mutations.
  - gateway MUST normalize them with the same admitted predicate subset and canonical field typing used for query `filters`.
  - gateway MUST resolve the explicit target row set first (`upsert_rows`, `upsert_columns`, `patch_rows`, `patch_columns`, `deletes`), then perform deterministic point-read evaluation against each target row's current value before canonical mutation emission.
  - a physically absent row is evaluated as missing/null field state.
  - only rows whose current value satisfies the admitted condition MAY emit canonical mutations.
  - rows failing the condition MUST be skipped deterministically: they emit no canonical mutation, do not contribute to `rows_affected`, and do not appear in `upserted_ids` / `patched_ids` / `deleted_ids`.
  - unsupported predicate branches, parse failures, or schema-incompatible condition typing MUST fail closed before mutation emission.
- `patch_rows` / `patch_columns`:
  - MUST lower to deterministic canonical patch row mutations keyed by canonical `doc_id` (`RowMutation.operation.patch`).
  - explicit ID list cardinality MUST be bounded by existing `max_entries_per_batch`; if request IDs exceed the limit, gateway MUST fail closed during parse/lowering with `400 SF_TOO_MANY_OPERATIONS` (`limit_scope=max_entries_per_batch`, `configured_limit`, `requested_value`).
  - Patch merge semantics are carried natively by canonical patch payload; gateway SHOULD NOT force read-modify-write upsert loops for this path.
  - explicit clear/remove intent MUST lower to `FieldData.atomic_value.null_value = NULL_VALUE_NULL` (not empty typed surrogates).
- `delete_by_filter`:
  - MUST compile selector filter to canonical query, materialize target IDs, sort by `doc_id asc`, then emit deterministic delete mutations.
  - Gateway MUST execute the same selector probe clamp as `patch_by_filter`: `limit=min(caller_requested_limit, max_entries_per_batch, effective_max_result_window)+1` with `track_total_hits=true`, where `caller_requested_limit` is the explicit source-side selector size bound when provided and defaults to `+inf` when omitted.
  - If the probe hits this clamped cap and `delete_by_filter_allow_partial=false` (or omitted), the request MUST be rejected synchronously as `400 SF_TOO_MANY_OPERATIONS` with details bound to the effective limit scope.
  - If the probe hits this clamped cap and `delete_by_filter_allow_partial=true`, gateway MAY apply only the first `probe_limit-1` sorted target IDs, MUST reconstruct `rows_remaining=true`, and MUST keep `rows_affected` / `deleted_ids` limited to that admitted prefix; current OpenAPI wire shape exposes no resume token.
- `patch_by_filter`:
  - MUST compile to selector query + deterministic `doc_id asc` patch mutation plan.
  - Gateway MUST execute a probe query with `limit=min(caller_requested_limit, max_entries_per_batch, effective_max_result_window)+1` and `track_total_hits=true`.
  - `caller_requested_limit` is the explicit source-side selector size bound when provided; when omitted, `caller_requested_limit` defaults to `+inf`.
  - If the probe hits this clamped cap and `patch_by_filter_allow_partial=false` (or omitted), the request MUST be rejected synchronously as `400 SF_TOO_MANY_OPERATIONS` with details bound to the effective limit scope.
  - If the probe hits this clamped cap and `patch_by_filter_allow_partial=true`, gateway MAY apply only the first `probe_limit-1` sorted target IDs, MUST reconstruct `rows_remaining=true`, and MUST keep `rows_patched` / `patched_ids` limited to that admitted prefix.
- `copy_from_namespace`:
  - MUST lower to snapshot-consistent source read + canonical snapshot/ingest flow.
  - source-namespace resolution uses the same external-account -> `namespace_id` plus `{namespace}` -> `collection_name` model on the source side.
  - Object-form source auth/region parameters are gateway-local and MUST NOT cross canonical gRPC boundary.
  - when this route emits snapshot mutating RPCs, `StartSnapshotSync.meta.idempotency_key`, `SnapshotUploadInit.meta.idempotency_key`, `CommitSnapshotSync.meta.idempotency_key`, and `AbortSnapshotSync.meta.idempotency_key` MUST all be non-empty and replay-stable for the same outer copy operation, with explicit gateway `client_id` and `client_kind=CLIENT_KIND_TP_WRITE_GATEWAY` on each mutating request.
- `encryption`:
  - write-request `encryption` is collection-descriptor intent, not row payload.
  - gateway MUST canonicalize admitted encryption intent through `CatalogControlService.PutCollectionMetadata.name_binding=CollectionNameBinding{namespace_id=<resolved>, collection_name=<path namespace>}` before mutation emission.
  - active admitted subset is omitted or `{cmek:{key_name}}`.
  - when the collection already exists, provided encryption MUST exactly match `GetCollectionMetadata.success.descriptor.encryption`; changing encryption through the write route is outside the active subset and MUST fail closed.
  - dedicated encryption data MUST NOT be forwarded as an ingest-plane mutation field.
- `return_affected_ids`:
  - Gateway MUST reconstruct affected-ID arrays from canonical per-item outcomes (`MutationAck.success.item_outcomes[]`).
  - For explicit-ID source operations, emitted affected-ID arrays MUST preserve source-request order for the realized operation subset.
  - For selector-driven operations (`patch_by_filter`, `delete_by_filter`), emitted affected-ID arrays MUST use the realized deterministic mutation-plan order (`doc_id asc`).
- `disable_backpressure`:
  - `disable_backpressure=true` MUST lower intent to canonical `MutationBatch.policy.ingest_mode=INGEST_MODE_BATCH`.
  - `disable_backpressure=false` (or omitted) MUST lower to `MutationBatch.policy.ingest_mode=INGEST_MODE_STREAMING`.
  - This is best-effort only; canonical service hard limits remain authoritative and MAY still return `429 SF_RATE_LIMITED`.

### 4.8.2 `DELETE /v2/namespaces/{namespace}`

- No request body.
- Backing contract:
  - resolve external account -> canonical `namespace_id` outside this design,
  - resolve `{namespace}` through `CatalogControlService.GetCollectionMetadata.lookup.name_binding=CollectionNameBinding{namespace_id=<resolved>, collection_name=<path namespace>}`,
  - when the collection exists, lower lifecycle deletion to `CatalogControlService.DropCollection.collection=<resolved CollectionRef>`.
- There is no implicit `collection_id=1` delete target in this revision.
- There is no namespace-level delete primitive in the canonical service for Turbopuffer parity; the wire route is a collection delete projected under Turbopuffer namespace naming.
- Route-level idempotency applies: an absent collection MAY be projected as a success response, and canonical `DropCollectionResponse.success.accepted` / `DropCollectionResponse.success.already_dropped` both reconstruct to the same wire success shape.
- The backing `DropCollection` call is state-mutating and therefore MUST carry non-empty `RequestMeta.idempotency_key` plus explicit gateway `RequestMeta.client_id` and `RequestMeta.client_kind=CLIENT_KIND_TP_WRITE_GATEWAY`; delete replay safety is implemented through those canonical carriers rather than route-local implicit state.

### 4.9 `POST /v2/namespaces/{namespace}/query` (`QueryConfig` + `Query`, route namespace token)

Request body is `allOf(QueryConfig, Query)`.

OpenAPI `QueryConfig` properties:
- `vector_encoding` (`float` or `base64`)
- `consistency` object:
  - `level`: `strong` or `eventual`
  - canonical lowering is mandatory:
    - `strong -> CONSISTENCY_MODE_STRICT` with a gateway-acquired fresh `read_ts > 0`
    - `eventual -> CONSISTENCY_MODE_BEST_EFFORT` with `read_ts=0`

OpenAPI `Query` properties:
- `rank_by`
- `top_k`
- `filters`
- `include_attributes`
- `exclude_attributes`
- `aggregate_by`
- `group_by`
- `distance_metric`
- `limit`

Query shape notes:
- `rank_by` and `filters` are `x-stainless-any` in the active OpenAPI schema (no additional wire-level structural restriction encoded at this layer).
- `exclude_attributes` is `string[]`.
- `aggregate_by` is object-typed with `additionalProperties: true`.
- `group_by` is `string[]`.
- `distance_metric` supports `cosine_distance` or `euclidean_squared`.
- Gateway MUST validate query-time `distance_metric` against the admitted collection schema. If the requested metric mismatches the schema-declared `distance_metric`, the request MUST fail closed with `400 SF_INVALID_QUERY`. After successful validation, the metric is elided and canonical execution continues against the schema-declared metric only.

`include_attributes` supports:
- boolean form (`true` = include all attributes, `false` = include none)
- array form (`string[]` = include exactly listed attributes)
- normalization is deterministic: `include_attributes=false` suppresses all attributes and makes `exclude_attributes` inert; array form applies `exclude_attributes` as set subtraction before canonical emission; final attribute field order must be byte-ascending.

`limit` supports:
- integer form, or
- object form (`total`, optional `per` with `attributes` + `limit`)

### 4.9.1 Canonical Query / Explain Lowering Rules

- `rank_by` MUST lower to exactly one admitted canonical primary-ranking leaf wrapped as `search_payload.root_retrieval=<RetrievalNode>` (`VectorClause` for vector ranking or `TextClause.mode_override.plain_match` for lexical ranking). Non-lowerable or multi-strategy `rank_by` branches MUST fail closed.
- `top_k` lowers to canonical `search_payload.global_candidate_topk`.
- `filters` lowers to canonical `search_payload.global_filter=<PredicateNode>`.
- `consistency.level` lowers to canonical `ReadConsistencySpec`:
  - `strong` => `ReadConsistencySpec{mode=CONSISTENCY_MODE_STRICT, read_ts=<gateway-acquired fresh snapshot>, max_staleness_ms=0}`
  - `eventual` => `ReadConsistencySpec{mode=CONSISTENCY_MODE_BEST_EFFORT, read_ts=0, max_staleness_ms=0}`
- `aggregate_by` MUST lower to canonical `search_payload.aggregations`.
- because OpenAPI leaves `aggregate_by` object internals unconstrained (`additionalProperties: true`), gateway owns the Turbopuffer aggregation DSL parser and MUST fail closed on any branch it cannot deterministically lower to canonical `AggregationRequest[]`.
- `group_by` defines grouping-key order and MUST be preserved when gateway lowers grouped requests to canonical bucket aggregations; multi-attribute grouping lowers to `CompositeAggregation.sources[]` in source order.
- `limit` integer form lowers to top-level `fetch_spec.limit`.
- `limit` object form lowers to canonical aggregation/result-budget controls:
  - `total` lowers to the outer result budget (`fetch_spec.limit` for direct-row queries, or canonical bucket limit such as `CompositeAggregation.limit` for grouped aggregation output).
  - `per.attributes` defines the group key order for per-group row output; when `group_by` is also present, the two lists MUST match after deduplication with order preserved.
  - `per.limit` lowers to per-group `TopHitsAggregation.limit`.
- `distance_metric` is validation-only for pure query requests and MUST be elided after successful schema validation; it MUST NOT perturb canonical query digest generation.
- `vector_encoding` is response-side vector serialization policy only and MUST NOT alter canonical query-input parsing or ranking semantics.
- vector-ranking dimension contract follows the canonical MRL rule: decoded query dimensions `Dq` must satisfy `0 < Dq <= Dschema`; `Dq < Dschema` is admitted as prefix-compatible retrieval, while `Dq = 0` or `Dq > Dschema` MUST fail closed.
- `include_attributes` / `exclude_attributes` normalize only attribute projection. Vector inclusion is controlled independently by vector-return policy.
- attribute-projection normalization is:
  - `include_attributes=true` -> return all attributes, then apply `exclude_attributes` as subtraction.
  - `include_attributes=false` -> return no attributes; `exclude_attributes` has no effect.
  - `include_attributes=[...]` -> build explicit attribute set, subtract any `exclude_attributes`, deduplicate, then byte-sort before canonical emission.
- active-profile row-return constraint:
  - row-return queries admit exactly one vector field, and that field name MUST be `vector`.
  - gateway MUST source wire-level `Row.vector` only from canonical `fetch_spec.vector_projections["vector"]`.
  - collections or query shapes that would require multiple vector fields in one `Row` are out of profile and MUST fail closed.
- `POST /v2/namespaces/{namespace}/explain_query` is gateway-local textual rendering over the admitted canonical query/aggregation plan; emitting or omitting `plan_text` MUST NOT change canonical execution semantics or digest binding.

### 4.10 `POST /v2/namespaces/{namespace}/query?stainless_overload=multiQuery` (route namespace token)

Request body:
- `allOf(QueryConfig, { queries: Query[] })`, where `queries` is required.
- Ordered fan-out is mandatory.
- When outer `QueryConfig.consistency.level=strong`, gateway MUST acquire one strict snapshot and reuse the same `read_ts` across every lowered child canonical query in that `multiQuery` request.
- When outer `QueryConfig.consistency.level=eventual`, every lowered child canonical query MUST use `ReadConsistencySpec{mode=CONSISTENCY_MODE_BEST_EFFORT, read_ts=0}`.

### 4.11 `POST /v2/namespaces/{namespace}/explain_query` (route namespace token)

Request body:
- `allOf(QueryConfig, Query)`

## 5. Response Contract Parity

### 5.1 List, Schema, Metadata, Warm-Cache, Recall

- `GET /v1/namespaces`:
  - object with `namespaces: NamespaceSummary[]`, optional `next_cursor`
  - each `NamespaceSummary` requires `id: string`
  - `NamespaceSummary.id` projects from canonical `CollectionDescriptor.collection_name`
  - payload is projected from canonical `ListCollections.success.collections`; cursor semantics must stay coherent with `ListCollections.success.next_page.token`
- `GET /v1/namespaces/{namespace}/schema`:
  - object map of `AttributeSchemaConfig`
  - payload is projected from canonical `GetCollectionSchema.success.schema` on the resolved collection
- `POST /v1/namespaces/{namespace}/schema`:
  - object map of `AttributeSchemaConfig`
  - payload MUST reflect the authoritative post-apply schema snapshot on the resolved collection (`PutCollectionSchema` result plus `GetCollectionSchema` readback when required for projection)
- `GET /v1/namespaces/{namespace}/metadata`:
  - `NamespaceMetadata` with required fields:
    - `schema`, `approx_logical_bytes`, `approx_row_count`, `created_at`, `updated_at`, `encryption`, `index`
  - `schema` is projected from canonical `GetCollectionSchema.success.schema` on the resolved collection
  - `approx_logical_bytes` and `approx_row_count` are projected from canonical `GetCollectionMetadata.success.descriptor.approx_stats`
  - `created_at` and `updated_at` are projected from canonical `GetCollectionMetadata.success.descriptor.{created_at_unix_ms,updated_at_unix_ms}`
  - `encryption` and `index` are projected from canonical `GetCollectionMetadata.success.descriptor.{encryption,index}`
  - `encryption` oneOf branches:
    - SSE branch requires `sse`
    - CMEK branch requires `cmek.key_name`
  - `index` oneOf branches:
    - up-to-date branch requires `status=up-to-date`
    - updating branch requires `status=updating` and `unindexed_bytes`
  - route admission requires `GetCollectionMetadata.success.descriptor.completeness.ready`; gateway MUST NOT synthesize placeholder values when typed fields are absent.
  - when descriptor completeness is still `publishing`, the route MUST fail closed with `503 SF_CATALOG_UNAVAILABLE` plus canonical completeness details rather than degrading the response.
- `GET /v1/namespaces/{namespace}/hint_cache_warm`:
  - `202` with object containing `status=ACCEPTED`, optional `message`
- `POST /v1/namespaces/{namespace}/_debug/recall`:
  - required top-level fields: `avg_recall`, `avg_exhaustive_count`, `avg_ann_count`
  - optional `ground_truth[]` where each item requires:
    - `query_vector`
    - `nearest_neighbors`

### 5.2 Write and Delete

- `POST /v2/namespaces/{namespace}`:
  - `WriteResult` with required: `status`, `message`, `rows_affected`, `billing`
  - optional: `rows_upserted`, `rows_patched`, `rows_deleted`, `rows_remaining`, `upserted_ids`, `patched_ids`, `deleted_ids`
  - `rows_remaining` type is boolean
  - `upserted_ids`, `patched_ids`, `deleted_ids` are `Id[]` when present
- `DELETE /v2/namespaces/{namespace}`:
  - object with `status=OK`
- success response is projected from either route-level idempotent absent-collection handling or canonical `DropCollectionResponse.success.accepted` / `DropCollectionResponse.success.already_dropped`

#### 5.2.1 Deterministic `WriteResult` Reconstruction

- successful write routes MUST emit `status=OK`; whole-request failures use `ErrorResponse`, not `WriteResult`.
- `rows_upserted`, `rows_patched`, and `rows_deleted` MUST be reconstructed from the deterministic admitted source operation plan by operation kind, not from raw canonical ingest counters, so replay-safe retries reproduce the same wire-visible result.
- `rows_affected` MUST equal the sum of the operation-kind counters emitted in that response.
- `rows_remaining=true` is reserved for filter-based partial-completion flows where the gateway intentionally stops after an ordered admitted prefix; current OpenAPI wire shape does not expose a resume token, so `rows_remaining` is only a boolean continuation hint. Otherwise it MUST be omitted or `false`.
- `upserted_ids`, `patched_ids`, and `deleted_ids` are emitted only when `return_affected_ids=true`.
- For explicit-ID source operations, those arrays MUST preserve the original source-request order for the realized operation subset.
- For selector-driven operations (`patch_by_filter`, `delete_by_filter`), those arrays MUST preserve the realized deterministic mutation-plan order (`doc_id asc`).
- `message` is required by OpenAPI but is gateway-local explanatory text; clients MUST NOT treat it as a semantic contract input.

### 5.3 Query, Multi-Query, Explain

- `POST /v2/namespaces/{namespace}/query`:
  - `QueryResult` (`SingleQueryResult` + required `performance` + required `billing`)
- `POST /v2/namespaces/{namespace}/query?stainless_overload=multiQuery`:
  - `MultiQueryResult` with required `results`, `performance`, `billing`
- `POST /v2/namespaces/{namespace}/explain_query`:
  - object with optional `plan_text: string`
- `SingleQueryResult` shape:
  - optional `aggregations: object`
  - optional `aggregation_groups: AggregationGroup[]`
  - optional `rows: Row[]`
- grouped query lowering MUST reconstruct `aggregation_groups[]` deterministically from canonical bucket results (and per-group `top_hits` payloads when row payloads are requested inside groups).
- non-grouped row-return queries populate `rows[]`; aggregation-only requests MAY leave `rows[]` empty while populating `aggregations` and/or `aggregation_groups`.
- `plan_text` is a gateway-local explain artifact and MAY be omitted when explain output is disabled by policy or when the lowered plan has no textual rendering.
- `QueryPerformance` required fields:
  - `cache_hit_ratio`
  - `cache_temperature`
  - `server_total_ms`
  - `query_execution_ms`
  - `exhaustive_search_count`
  - `approx_namespace_size`
- Billing note (out-of-scope for field-level compatibility):
  - OpenAPI envelope requires `billing` in query responses.
  - This compatibility goal treats nested billing object schemas (`QueryBilling`) as opaque/non-contract.

#### 5.3.1 Deterministic `QueryPerformance` Reconstruction

- `server_total_ms` is server-side end-to-end time including queue/concurrency wait and response assembly.
- `query_execution_ms` is canonical execution time excluding queue/concurrency wait; when unavailable, gateway MUST emit the same value as `server_total_ms`.
- `cache_hit_ratio` is engine/gateway cache-hit ratio when available, otherwise `0.0`.
- `cache_temperature` is derived from `cache_hit_ratio` (`hot` when `>= 0.95`, `warm` when `>= 0.50 && < 0.95`, otherwise `cold`).
- `exhaustive_search_count` is exact-scan/brute-force candidate count when available, otherwise `0`.
- `approx_namespace_size` is the latest admitted collection row-count estimate from `GetCollectionMetadata.success.descriptor.approx_stats.approx_row_count`, projected back into the Turbopuffer wire field name for the resolved route namespace token; canonical namespace-level stats MUST NOT be used for this field. Query routes that need this field MUST first observe `GetCollectionMetadata.success.descriptor.completeness.ready`; if the descriptor is still `publishing`, gateway MUST fail closed with `503 SF_CATALOG_UNAVAILABLE` instead of returning `0`.

### 5.4 Shared `Row` Shape (Explicit Parity)

OpenAPI `Row` is used in recall and query response payloads:
- Recall: `ground_truth[].nearest_neighbors[]`
- Query results: `rows[]` in `SingleQueryResult`

`Row` wire shape:
- type: object
- required field:
  - `id: Id`
- optional field:
  - `vector: Vector`
- active-profile constraint:
  - `vector` is the only admitted vector-return slot.
  - it maps only to the canonical schema field named `vector`.
  - collections with additional vector fields are out of row-return profile scope and MUST fail closed when `rows[]` reconstruction would be ambiguous.
- `Vector` is `oneOf(float32 number array, base64 string)`
- additional properties: allowed (`additionalProperties: true`)
  - carries per-document attributes in row form

## 6. Error Contract Parity

All default error responses use OpenAPI `ErrorResponse`:
- required `status` (const: `error`)
- required `error` (string)

No additional JSON body error fields are part of OpenAPI parity.
Canonical correlation/diagnostic metadata MUST be preserved through headers on every Turbopuffer-parity error response:
- `X-SF-Code: <canonical SF_*>`
- `X-SF-Correlation-Id: <canonical correlation_id>`
- these headers are mandatory even when the JSON body is limited to OpenAPI `ErrorResponse`, because upstream OpenAPI has no body field for canonical correlation metadata.

Deterministic scenario mapping:
- canonical code selection MUST follow `docs/specs/core/error-codes.md` precedence and `docs/specs/core/search-fabric-api.md`; this document does not redefine precedence.
- direct Turbopuffer-native routes outside ownership applicability MUST map to canonical `409 SF_OWNERSHIP_CONFLICT` before OpenAPI error projection.
- invalid `Id` branches (negative integer, floating numeric, `uuid`, non-decimal/non-lossless string) MUST map to canonical `400 SF_INVALID_DOC_ID`.
- unsupported or non-lowerable request branches MUST map to canonical `400 SF_UNSUPPORTED_OPTION`, `400 SF_UNSUPPORTED_QUERY_FEATURE`, or `400 SF_INVALID_QUERY` according to the canonical rules, then be projected into OpenAPI `ErrorResponse`.

## 7. Compatibility Clarifications and Limitations

- This document defines external wire parity together with the canonical lowering boundary used to realize that parity.
- For write routes, active-profile parity is subset-scoped by value-branch admissibility, not blanket execution parity for every OpenAPI branch.
- Every top-level write property has a deterministic handling rule, but only explicitly admitted value branches are executable in the active profile.
- Non-admitted write value branches MUST fail closed rather than being silently ignored, approximated, or gateway-local rewritten into a different semantic contract.
- Control-plane schema operations (`GET/POST /v1/namespaces/{namespace}/schema`) are wire-compatible at gateway boundary and canonically normalized into Integration API schema payloads for internal admission.
- Internal implementation details (for example internal error taxonomies, ownership routing, throttling internals, storage semantics) are non-contract details unless surfaced by OpenAPI.
- Turbopuffer route namespace tokens are projected collection names, not canonical namespace names: external account resolution yields canonical `namespace_id` outside this design, and external Turbopuffer route namespace token `{namespace}` maps to `CollectionDescriptor.collection_name` inside that resolved canonical namespace.
- Unless explicitly qualified as `canonical namespace`, quoted upstream route prose and headings that mention `namespace` refer to the Turbopuffer route namespace token, not to the canonical namespace catalog object; explanatory prose in this document uses `route namespace token` to keep the distinction mechanically obvious.
- Canonical namespace catalog RPCs (`ListNamespaces`, `GetNamespaceMetadata`) describe true namespace objects and are not the backing surface for Turbopuffer collection-scoped `{namespace}` routes in the active profile.
- There is no implicit single-collection `collection_id=1` model in this revision.
- Active-profile ID limitation is intentional and normative: executable `Id` forms are decimal-string uint64 and non-negative integer uint64 only; negative integers, floating numerics, `uuid`, and non-lossless/non-decimal strings fail closed (`SF_INVALID_DOC_ID`).
- Active-profile vector-return limitation is intentional and normative: row-return queries admit exactly one vector field, and that field name MUST be `vector`; collections with additional vector fields are out of row-return profile scope and MUST fail closed when `Row.vector` reconstruction would be ambiguous.
- Billing API field-level semantics are out of scope for this compatibility goal:
  - `WriteBilling` and `QueryBilling` nested fields are intentionally non-contract in this profile.
  - Clients MUST NOT depend on nested billing-field presence/shape beyond top-level envelope requirements.

### 7.1 Canonical Lowering Limitations

- The synthesized canonical doc-id schema for Turbopuffer is fixed to `DocIDMapping{source_field=\"id\", source_field_type=SCALAR_FIELD_TYPE_UINT64, immutable=true, unique=true}` in the active profile.
- `DocIDMapping.source_field_type=SCALAR_FIELD_TYPE_STRING` and `DocIDMapping.source_field_type=SCALAR_FIELD_TYPE_BYTES` are intentionally unsupported in this profile, even though Turbopuffer wire `Id` remains a union type. Only `Id` values that lower losslessly to canonical `DocumentID.id_value.u64` are admitted.
- External account -> canonical `namespace_id` resolution is intentionally outside this profile and MUST be provided by external system control-plane/authz infrastructure.
- Any Turbopuffer schema/metadata/listing/lifecycle behavior that would require canonical catalog/control operations beyond `ListCollections`, `GetCollectionMetadata`, `PutCollectionMetadata`, `GetCollectionSchema`, `DropCollection`, or canonical query/write RPCs is outside the current profile until the canonical API surface is extended.
- Any Turbopuffer write value branch outside the explicitly admitted subset (for example unsupported `Id` forms, unsupported `full_text_search` knobs, unsupported condition/filter branches, or unsupported write-route metadata mutations) is outside the active profile even when the top-level write property itself is listed in parity scope.
- `CollectionDescriptor.options` is supplemental metadata only. Typed metadata required by Turbopuffer parity (`approx_*`, `created_at`, `updated_at`, `encryption`, `index`) MUST come from dedicated canonical collection metadata fields, not from opaque option keys.
