# API Contract Conformance Matrix

This document assigns stable invariant IDs to validator-sensitive contract requirements.
It is a testing/discoverability aid, not a second semantic owner.
Normative semantics remain owned by:
- `docs/specs/core/search-fabric-api.md`
- `docs/specs/core/error-codes.md`
- `docs/specs/core/search-fabric-architecture.md`
- the owning `.proto` file named by the semantic owner stack

## Stable Invariant IDs

- `SF-CONF-ERR-001` — emitted error-correlation invariant:
  - canonical emitted error field is `ErrorStatus.correlation_id`
  - when caller provides `RequestMeta.correlation_id`, the error envelope MUST echo that value
  - when caller omits `RequestMeta.correlation_id`, the server MUST emit a service-generated correlation ID rather than exposing caller replay identity
  - compatibility error headers use `X-SF-Correlation-Id`

- `SF-CONF-PROTO-001` — public protobuf-set invariant:
  - `api/proto/searchfabric/public-proto-set.txt` is the machine-readable manifest for the public / cross-language protobuf contract set
  - the current revision keeps `searchfabric/v1/search_fabric.proto` in that public set

- `SF-CONF-PROTO-002` — system protobuf-set invariant:
  - `api/proto/searchfabric/system-proto-set.txt` is the machine-readable manifest for the full checked-out public/internal protobuf contract set
  - the system manifest must enumerate every checked-in `api/proto/searchfabric/v1/*.proto` file exactly once and all listed protos must compile cleanly

- `SF-CONF-PROTO-003` — public/internal protobuf package-boundary invariant:
  - `api/proto/searchfabric/v1/search_fabric.proto` remains in `package searchfabric.v1`
  - every internal-only protobuf owner file listed by `api/proto/searchfabric/system-proto-set.txt` other than `searchfabric/v1/search_fabric.proto` uses `package searchfabric.v1.internal`
  - the current revision keeps file-path ownership under `api/proto/searchfabric/v1/` while making the public/internal namespace split machine-visible through package naming

- `SF-CONF-STREAM-001` — public StreamQuery failure-transport invariant:
  - if execution fails before the first frame is emitted, the RPC uses non-`OK` gRPC status plus canonical `ErrorStatus`
  - once any frame has been emitted, terminal execution failure uses in-band `StreamQueryFrame.outcome=error`

- `SF-CONF-STREAM-002` — internal streamed terminal-receipt invariant:
  - internal streamed terminal accounting uses the same-stream `QueryBrokerStreamEvent.terminal_receipt`
  - `stream_attempt_id` is the sole deterministic join key for that receipt path

- `SF-CONF-QUERY-001` — StreamQuery framing/dictionary/resume invariant:
  - StreamQuery is hit-streaming only and only the terminal success frame may carry `warnings[]` / `total_hits`
  - projection/highlight/inner-hits dictionaries are emitted only on the first successful frame carrying one or more hits for one RPC attempt, and resumed attempts re-emit them on their own first hit-bearing success frame
  - every hit-bearing success frame carries a non-empty resumable `next_page.token`; the only legal zero-hit success frame is the terminal no-more-hits completion frame, and public failure transport stays split between pre-first-frame gRPC failure and post-frame in-band `outcome=error`

- `SF-CONF-QUERY-002` — projected/primary relational-route invariant:
  - `ExecuteProjectedQueryOrFallback` uses `ProjectedQueryRequest`, `ExecutePrimaryQuery` uses `PrimaryQueryRequest`, and only the projected route admits `snapshot_overlay` / `snapshot_read_ts`
  - projected-route overlay state split is implementation-validated/conformance-backed in the current revision rather than machine-closed by proto annotations, continuation reuses the same canonical overlay semantics, and token validation binds the canonical overlay digest
  - projected-route transport-`OK` normal results always carry the full observability tuple including non-empty `relational_execution_digest`, while the primary route remains success/error only with no fallback branch

- `SF-CONF-QUERY-003` — optimizer-statistics lineage/freshness invariant:
  - `GetCollectionStatistics` selects snapshots only from the current query-serving `latest READY` lineage
  - `collected_at_unix_ms` is the authoritative freshness timestamp for successful statistics reads and for `max_staleness_ms` budget evaluation
  - optimizer-statistics snapshots are durably identified by composite identity (`applied_schema_generation_token`, `stats_commit_ts`), and schema-only lineage promotion still creates a distinct immutable snapshot identity

- `SF-CONF-QUERY-004` — Query Broker internal stream/aggregation observability invariant:
  - `StreamAttemptId.value` is the required non-empty join key for the internal stream request and terminal receipt path
  - `QUERY_BROKER_PHASE_AGGREGATION_REDUCTION` covers metadata partial reduction plus semantic/vector-dominant broker-local facet reduction, distinct from `QUERY_BROKER_PHASE_FUSION`
  - `QueryBrokerExecutionReceipt` reports fusion counters separately from metadata-aggregation and facet-reduction counters

- `SF-CONF-RESP-001` — public exact-one response-shape invariant:
  - public response carriers that model outcomes/states/success branches use exact-one branch-presence semantics rather than status-enum-plus-payload ambiguity
  - transport-`OK` direct-success RPCs do not reintroduce legacy public success/error envelopes
  - `sf_required_oneof`-backed exact-one validation remains the machine-readable closure for wire-representable unset states on the admitted public carriers

- `SF-CONF-CATALOG-001` — descriptor completeness / missing-fields invariant:
  - `CollectionDescriptor` and `NamespaceDescriptor` completeness/index/lifecycle carriers use exact-one branch semantics
  - successful descriptor readback always carries `encryption`, while missing lagging typed fields force `completeness.publishing`
  - `publishing.missing_fields[]` is restricted to canonical names `approx_stats` and `index`, kept unique, returned byte-ascending, and mirrored into `missing_descriptor_fields` using that same ordered list

- `SF-CONF-CATALOG-002` — metadata-mutation success / lifecycle-conflict invariant:
  - successful `PutCollectionMetadata` / `PutNamespaceMetadata` responses emit exactly one of `created`, `updated`, or `already_applied`
  - post-resolution stable-id-owned lifecycle races fail as `SF_VERSION_CONFLICT` through the canonical lifecycle-conflict detail family rather than implementation-local diagnostics
  - transport replay for metadata mutations remains distinct from those lifecycle-conflict semantics

- `SF-CONF-INGEST-001` — batch-lane policy / atomicity invariant:
  - `INGEST_MODE_BATCH` is the admitted `MutationBatch` batch-build lane and is distinct from the snapshot-sync session lifecycle
  - batch lane is admitted only with `durability_policy=ingest_ack` plus `visibility_policy=wait_for_visible`, and `DURABILITY_POLICY_WAL_CHUNK_DURABLE` remains streaming-only in the current revision
  - internal chunking and publish behavior stay subordinate to the original admitted `MutationBatch.policy.atomicity`; batch execution must not invent a weaker visibility boundary than the original canonical batch

- `SF-CONF-INGEST-002` — mutation-ack outcome coverage invariant:
  - any admitted batch containing one or more `ROW_MUTATION` entries emits `MutationAck.success.item_outcomes[]` for every admitted entry in canonical order regardless of `continue_on_error`
  - progress-only / barrier-only batches may omit `item_outcomes[]`
  - the same completeness/order rule applies to both streaming and batch execution, preserving original `entry_index` / `source_seq` mapping for deterministic response reconstruction

- `SF-CONF-INGEST-003` — internal ingest frame / completion invariant:
  - broker-private coalescing preserves original canonical `MutationBatch` payloads unchanged inside one `IngestBrokerDispatchFrame`
  - dispatch-frame compatibility includes canonical `policy.durability`, `policy.visibility`, `policy.ingest_mode`, and `schema_generation_token`, and mark-bearing batches remain singleton frames
  - trusted internal transport stays fail-closed: planner responses echo `expected_batch_count` and cover each original batch exactly once, while pre-completion fatal failures use non-`OK` gRPC status plus canonical `ErrorStatus`

- `SF-CONF-SNAPSHOT-001` — snapshot session binder / resume invariant:
  - `StartSnapshotSync` binds one immutable session to `CollectionRef`, `snapshot_ts`, `schema_version`, `schema_generation_token`, and `client_session_token`
  - replay with the same session token and the same admitted binders returns the original session identity, while mismatched binders fail as `SF_IDEMPOTENCY_CONFLICT`
  - snapshot upload resume-token expiry maps to `SF_INVALID_QUERY`, and recovery is to start a fresh `StartSnapshotSync` session because used sessions do not admit tokenless resume in the current revision

- `SF-CONF-CDC-001` — CDC replay-watermark invariant:
  - `last_acked_watermark` is the only authoritative replay-suppression boundary
  - `last_sent_watermark` is observational-only and may be used only for in-process duplicate coalescing before ACK
  - restart recovery persists and reloads the last acknowledged cursor plus returned `applied_watermarks`; pre-crash sent-but-unacknowledged watermarks remain replay-eligible

- `SF-CONF-TIDB-001` — TiDB projected-route snapshot invariant:
  - `ProjectedQueryRequest` is the only TiDB query route that may carry `snapshot_overlay` / `snapshot_read_ts`
  - `STRICT` and `BOUNDED_STALENESS` require `snapshot_read_ts = query.consistency.read_ts`
  - `BEST_EFFORT` keeps `query.consistency.read_ts=0` but uses `snapshot_read_ts` as the authoritative execution snapshot, and continuation validation reuses the canonical overlay digest

- `SF-CONF-TIDB-002` — TiDB primary-route transport invariant:
  - `PrimaryQueryRequest` is overlay-free and has no fallback branch
  - `RelationalQueryService.ExecutePrimaryQuery` is success/error only: transport `OK` direct success body on success, non-`OK` gRPC status plus canonical `ErrorStatus` on failure

- `SF-CONF-TIDB-003` — TiDB statistics-helper invariant:
  - `GetCollectionStatistics` collection-only fast path requires `target_fields=[]` and empty `extended_requests[]`, returns `collection_stats`, and success always carries `applied_schema_version` plus `applied_schema_generation_token`
  - failure taxonomy remains fail-closed across `SF_RESOURCE_NOT_FOUND`, canonical schema-bootstrap details, and canonical schema-convergence details
  - `collected_at_unix_ms` is the authoritative freshness timestamp

- `SF-CONF-TIDB-004` — TiDB shadow-read parity invariant:
  - aux-index shadow reads mirror `ProjectedQueryRequest` on `RelationalQueryService.ExecuteProjectedQueryOrFallback`
  - federated-table shadow reads mirror `PrimaryQueryRequest` on `RelationalQueryService.ExecutePrimaryQuery`
  - projected-route shadow reads carry the same validated `snapshot_overlay` and `snapshot_read_ts`
