# Search Fabric Emulator Design (Contract-Aligned, Extensible CDC Upstream Layer)

Date: 2026-03-09
Status: Draft (Normative for Test/Integration Environments)
Owner: TiDB Integration / Search Fabric Emulator

## 1. Goal

Build a deterministic, high-fidelity **Search Fabric Emulator** for:

1. TiDB integration development (`aux_index` + federated-table query paths).
2. Query-plane and control-plane contract testing without requiring full production deployment.
3. CDC-free fast test lanes and CDC-compatible replay lanes in the same runtime.

Implementation and deployment contract for this design:

1. `emulator` and its internal CDC upstream adapters are implemented in **Golang**.
2. Emulator MUST keep one internal CDC upstream abstraction that lowers row, schema, and progress inputs into the same canonical emulator state paths.
3. Embedded `mini-cdc` is the TiDB-specific implementation of that abstraction in the current revision; additional adapters may be added only if they preserve the same canonical contracts.
4. Code paths SHOULD maximize reuse of proven upstream CDC implementations when available, but upstream-specific runtime/schema/codec logic MUST stay outside the canonical emulator core.
5. Emulator supports two runtime forms with identical contract behavior:
   - standalone process exposing canonical gRPC services,
   - embedded mode started inside TiDB for local development/debug.
6. Language boundary is strict: all components outside emulator domain are implemented in **Rust**.

The emulator is a **test infrastructure component**, not a production serving cluster, but it MUST preserve canonical contracts for:

- identity: `(namespace_id, collection_id, doc_id:uint64)`
- visibility and consistency: `read_ts`, `safe_ts`, `effective_read_ts`
- deterministic replay/idempotency
- canonical `SF_*` error behavior and precedence

---

## 2. Revision Note

This document is the normative current-state emulator contract.

1. Historical review ledgers and resolved-gap narratives are intentionally kept outside this source-of-truth spec.
2. The sections below describe only currently active emulator requirements, constraints, and validation rules.

---

## 3. Normative Inputs and Requirement Baseline

Normative references:

- `docs/specs/core/search-fabric-api.md` (latest canonical contract baseline)
- `docs/testing/tidb-emulator-integration-test-plan.md` (short-circuited test suite contract)
- `docs/architecture/tidb-search-fabric-integration.md` (TiDB-side integration and fallback semantics)
- `docs/specs/core/embedded-mini-cdc.md` (embedded CDC constraints)
- `docs/specs/core/error-codes.md` (canonical error code and precedence source of truth)

Baseline requirement summary:

1. Canonical ingest/query/control semantics must be replay-deterministic.
2. Test APIs must provide fast deterministic setup and readiness gating.
3. `safe_ts` is derived from applied modality timestamps; never directly written.
4. Strict mode must not silently return stale data for federated-table / `COLLECTION_TRUTH_MODE_SEARCH_FABRIC` routes.
5. All surfaced errors must be canonical `SF_*` and precedence-stable.

---

## 4. Scope and Non-Goals

## 4.1 In Scope

1. Canonical ingest emulation (stream batches, idempotency, progress marks, reconnect logic).
2. Canonical query-plane emulation for:
   - shared query routes: `ExecuteQuery`, `StreamQuery`, `GetReadiness`, and `GetCollectionStatistics`
   - trusted relational routes: `RelationalQueryService.ExecuteProjectedQueryOrFallback` and `RelationalQueryService.ExecutePrimaryQuery`
   - route families exercised by TiDB integration tests: `aux_index`, projected-query fallback, and federated-table / Search-Fabric-truth primary-query paths
3. Canonical schema-oriented control-plane emulation for `PutCollectionSchema`, `DropCollection`, `GetCollectionSchema`, and `GetSchemaSyncStatus` fencing/readback behavior.
4. Snapshot sync session emulation (start/upload/commit/abort/progress).
5. Static policy emulation (query/ingest/control limits and flags; no runtime negotiation RPCs).
6. Test-only setup APIs (`inject_*`, readiness, fault profile, optional retention controls).
7. Optional internal CDC upstream adapters that feed the same canonical emulator state paths; Embedded Mini-CDC is the TiDB-specific implementation in the current revision.
8. Minimal internal debug emulation for `InternalDebugService.DebugDigestProbe` (read-only digest attestation semantics only).

## 4.2 Non-Goals

1. Production HA/distributed sharding/failover.
2. Persistent durability across emulator process restarts.
3. Full external wire-level parity for Elasticsearch/Turbopuffer protocols.
4. ANN/text ranking quality benchmarking.
5. Replacing full-chain TiCDC transport validation suites.
6. Running Mini-CDC as an independent standalone service process.

## 4.3 Contract Coverage Levels

1. **Level A (Required)**: query/control + readiness/fallback contracts used by TiDB integration tests.
2. **Level B (Required)**: ingest stream idempotency/order/replay determinism.
3. **Level C (Required for conformance lane)**: snapshot session + continuation token determinism.

---

## 5. Architecture

```text
+------------------------+    +-------------------------------+
| Test Setup API         |--->| Canonical Normalization Layer |
| inject_writes          |    | - key normalize               |
| inject_changefeed      |    | - schema resolve              |
| inject_progress        |    | - write envelope normalize    |
+-----------+------------+    +---------------+---------------+
            ^                                 |
            |                                 v
+-----------+------------+      +-----------------------------+
| CDC Upstream Adapter   |----->| Single Writer Core          |
| Layer                  |      | - ingest admission order    |
| - Embedded Mini-CDC    |      | - idempotency ledger        |
| - other uint64-version |      | - MVCC apply                |
|   sources              |      | - progress safe_ts update   |
+-----------+------------+      +------+----------------------+
            |                          |
            | schema/control bridge    |
            v                          v
+-------------------------+   +-------------------------+   +-------------------------+
| Control Surface         |   | Query Engine            |   | Snapshot Session Mgr    |
| PutCollectionSchema/... |   | Execute/Stream query    |   | start/upload/commit/... |
+-----------+-------------+   | continuation tokens     |   | lease + resume          |
            |                 +-------------+-----------+   +-------------+-----------+
            |                               |                             |
            v                               v                             v
+-------------------------+   +-------------------------+   +-------------------------+
| Capability + Readiness  |   | Token Service           |   | Catalog / Schema State  |
| query/ingest/control    |   | HMAC token sign/verify  |   | generation/readiness    |
+-------------------------+   +-------------------------+   +-------------------------+
```

Design choices:

1. **Single writer core** for deterministic ordering and no lock contention in mutation apply.
2. **One CDC upstream seam above the canonical core**: test HTTP and all internal CDC upstream adapters share one canonical lowering path and must not invent source-private replay, ordering, or readiness semantics.
3. **Policy/token manager as first-class component** to emulate deterministic reconnect and admission behavior.
4. **Upstream-specific isolation**: schema fetch, CDC pull, codec decode, and source lifecycle logic stay in upstream adapters; `singlewriter`, `control`, `readiness`, and query logic remain source-agnostic.
5. **Go + reuse-first implementation**: prioritize direct reuse of existing TiDB/TiCDC validated modules for Mini-CDC before adding emulator-local logic.
6. **Rust outside emulator**: query/ingest/control/stateful/background production components are out of emulator domain and are implemented in Rust.

---

## 6. API Surfaces and Compatibility Strategy

## 6.1 Canonical Service Surface (Emulated)

The emulator MUST emulate the behavior (not necessarily full production throughput) of:

1. `IngestGatewayService`
2. `QueryGatewayService`
3. `RelationalQueryService`
4. default `CatalogControlService` subset (`PutCollectionMetadata`, `PutCollectionSchema`, `DropCollection`, `ListCollections`, `GetCollectionMetadata`, `GetCollectionSchema`, `PutNamespaceMetadata`, `ListNamespaces`, `GetNamespaceMetadata`, `GetSchemaSyncStatus`)
5. `IdAllocatorService` (`AllocateIds`)
6. `InternalDebugService` (minimal): `DebugDigestProbe` read-only digest-match/mismatch contract.

Default emulator scope includes the metadata/name-binding catalog surface because canonical gateway/control flows now depend on it directly: gateway-managed schema ensure paths use `PutCollectionMetadata` before first schema apply, namespace provisioning/admin caller rules are first-class canonical contract, and catalog pagination/readback semantics are part of the default validation inventory. Internal lifecycle-admin workflows that are still outside the public RPC surface remain optional and may be modeled only by specialized test profiles. Allocator emulation is not optional in the default scope because `DOC_ID_GENERATION_POLICY_ALLOCATOR_OPTIONAL` is part of the canonical admitted schema contract and emulator tests must be able to exercise both allocator-issued and allocator-reserved create/insert identity. Relational query emulation is also part of the default scope because the canonical query owner set now routes TiDB projected-query and primary-query behavior through `RelationalQueryService` rather than through ad hoc reuse of the shared `ExecuteQuery` surface.

Runtime forms:

1. Standalone mode: emulator runs as an independent gRPC process.
2. Embedded mode: emulator handlers are started inside TiDB process for development/debug convenience.
3. Both forms MUST share the same Go handler implementation and MUST preserve identical canonical behavior.

For deterministic tests, in-process adapters may invoke shared handlers directly, but handler behavior MUST match canonical contract semantics.

Cross-plane metadata rule:

1. All unary requests must carry `RequestMeta`.
2. Requests that operate on an already resolved concrete collection instance must carry `CollectionRef`; name-based collection catalog lookup/ensure RPCs may instead use typed collection name bindings before a `CollectionRef` exists.
3. Gateway and internal service callers must set `RequestMeta.client_id` and enum-typed `RequestMeta.client_kind`; identity mismatch with authenticated caller fails closed (`SF_FORBIDDEN`).
4. Requests carrying `client_kind=CLIENT_KIND_UNSPECIFIED` are malformed; when no higher-tier violation is selected by canonical precedence, emitted code must be `SF_BAD_REQUEST`.
5. If `RequestMeta.principal_subject` is present, server must verify principal match; mismatch fails closed (`SF_FORBIDDEN`).
6. Unknown enum values in typed protocol fields (including `client_kind`, logical/predicate/fusion/sort operators, and schema/index enums) must fail closed by canonical precedence (`SF_BAD_REQUEST` or `SF_UNSUPPORTED_OPTION` by tier).
7. RPC deadline governance remains transport-level metadata/policy in this revision; emulator tests must verify transport deadline handling separately from canonical `RequestMeta` payload validation.

## 6.2 Test-Only Setup Surface

Test APIs remain mandatory and map to canonical handlers:

1. `POST /__test__/inject_writes`
2. `POST /__test__/inject_changefeed_batch`
3. `POST /__test__/inject_progress`
4. `GET /__test__/readiness?namespace_id=&collection_id=`
5. Optional `POST /__test__/fault_profile`
6. Optional `POST /__test__/set_gc_cutoff`

Guardrails:

1. Disabled by default; enabled only in test profile.
2. Loopback bind by default.
3. Namespace/collection access restricted to configured test-owned ranges.
4. Scope violations fail with `SF_FORBIDDEN`.

## 6.3 Canonical Replay Normalization Rule

`inject_changefeed_batch` accepts only canonical `entries[]` (`ROW_MUTATION/PROGRESS_MARK/BARRIER_MARK`).

Emulator MUST derive canonical metadata deterministically from that payload:

1. omitted `batch_id` -> `blake3("batch:" + collection_ref + normalized payload)`.
2. omitted `idempotency_key` -> `blake3("request:" + collection_ref + normalized payload)`; request-level network dedupe identity MUST stay distinct from batch-level replay identity even when both are derived.
3. omitted `stream_identity`:
   - caller SHOULD provide explicit `stream_identity` or `source_table`;
   - when `source_table{keyspace_id?,table_id}` is provided, derive `source_partition` as `keyspace:<keyspace_id>/table:<table_id>` or `table:<table_id>`.
   - otherwise derive fallback `source_partition="__test__/changefeed/<namespace_id>/<collection_id>"`, `source_epoch=1` for convenience fixtures only.
4. omitted `entries[].source_seq` -> derive as the 1-based canonical order after normalization.
5. derived `batch_fingerprint` MUST follow the canonical API exclusion rules (`RowMutation.commit_ts` and envelope fields excluded).
6. Golden fixtures for emulator replay and integration tests MUST use canonical `entries[]` only; compatibility-form fixture schemas are out of scope.

Applicability guardrail:

1. Emulator must enforce profile ownership and applicability boundaries by authoritative `collection_truth_mode` before request admission.
2. Out-of-scope tuples fail closed (`SF_OWNERSHIP_CONFLICT` or `SF_UNSUPPORTED_ENDPOINT` by precedence).

## 6.4 Allocator Service Contract (Emulator)

The emulator MUST implement the canonical `IdAllocatorService.AllocateIds` contract directly rather than fencing it out.

Allocator rules:

1. `AllocateIds` requires non-empty `RequestMeta.idempotency_key` and resolved `CollectionRef`; replay scope uses server-authenticated caller identity + `client_id` + `client_kind` + `rpc_method` + collection scope + `idempotency_key`.
2. The only admitted route intent is `ID_ALLOCATION_ROUTE_INTENT_CREATE_INSERT`; patch/delete/update-style attempts MUST be rejected by canonical malformed/ownership precedence.
3. Fresh allocator admission MUST execute one ordered mixed-mode `plan.items[]` sequence under one replay-governed request lineage, one returned `schema_generation_token`, and one returned `allocator_receipt`; `plan.items[]` MUST be non-empty, explicit `explicit_doc_id` values within one plan MUST be unique, and plan cardinality MUST respect the canonical allocator request-item ceiling.
4. Success MUST return `realized_doc_ids[]` in the same order and cardinality as `plan.items[]`: `issue` items emit allocator-issued IDs with `allocator_issued=true`, explicit plan items echo the caller-supplied IDs with `allocator_issued=false`, and later plan items observe watermark changes caused by earlier explicit reservations in that same ordered execution.
5. Successful replay lookup MUST short-circuit fresh allocator admissibility re-evaluation; later collection tombstone, policy removal, caller-profile change, or schema-generation rollover MUST NOT invalidate a previously recorded successful replay result.
6. Successful replay MUST return the originally recorded `realized_doc_ids[]`, the originally recorded `schema_generation_token`, and an `allocator_receipt` bound to that same successful result tuple; fresh allocator admission returns the currently observed write-admission `schema_generation_token`, which the emulator MUST later revalidate again on the follow-on `StreamMutations` path.
7. Within one emulator runtime, successful allocator outcomes are the replay-authoritative record for identical retries; restart persistence remains out of scope for the default emulator profile, while failed attempts MAY be logged for audit/operations but MUST remain ordinary non-`OK` gRPC + canonical `ErrorStatus` failures rather than a second success-shaped replay record family.
8. `COLLECTION_TRUTH_MODE_EXTERNAL` collections MUST reject allocator use, MUST keep `DocIDMapping.generation_policy=DOC_ID_GENERATION_POLICY_CALLER_REQUIRED`, and MUST keep `CollectionSettings.allocator_admission_policy` absent; collections with `DOC_ID_GENERATION_POLICY_CALLER_REQUIRED` MUST also reject allocator use.
9. Collections with `DOC_ID_GENERATION_POLICY_ALLOCATOR_OPTIONAL` MUST carry valid `CollectionSettings.allocator_admission_policy`, publish `caller_profile=ALLOCATOR_CALLER_PROFILE_ES_WRITE_GATEWAY_ONLY`, require positive `max_plan_items_per_request` plus positive `max_forward_reservation_jump`, and reject any other caller profile in the current revision.
10. Existing collections whose authoritative allocator admission facts are unavailable/incomplete MUST fail closed as `SF_CATALOG_UNAVAILABLE`; when no active schema generation has been admitted yet, the emulator MUST emit `ErrorStatus.context.allocator.catalog_unavailable.schema_bootstrap.schema_bootstrap_state=no_active_schema_generation` plus `catalog_unavailable_reason=schema_bootstrap` / `schema_bootstrap_state=no_active_schema_generation`.
11. The allocator watermark is dense-burn: once `next_doc_id` advances to `N`, every lower `doc_id < N` becomes permanently unavailable for fresh allocation, including IDs skipped by earlier high explicit reservations or discarded lease tails.
12. Fresh explicit reservation of any `doc_id < next_doc_id` MUST fail closed as `SF_VERSION_CONFLICT` with `ErrorStatus.context.allocator.reservation_conflict.{doc_id, conflict_reason=ALLOCATOR_RESERVATION_CONFLICT_REASON_BELOW_WATERMARK_BURNED_GAP}` and canonical allocator-conflict diagnostics `doc_id` / `allocator_conflict_reason=below_watermark_burned_gap`.
13. Explicit reservation with `doc_id >= next_doc_id` MUST advance allocator watermark to `doc_id + 1` during allocator admission itself when it stays within the admitted forward-reservation window; over-window jumps MUST fail as `SF_TOO_MANY_OPERATIONS` with `ErrorStatus.context.allocator.limit_rejection.limit_scope=ALLOCATOR_LIMIT_SCOPE_FORWARD_RESERVATION_WINDOW`, later ingest failure burns the reservation, and exhausted identity space MUST fail as `SF_TOO_MANY_OPERATIONS` with `ErrorStatus.context.allocator.limit_rejection.limit_scope=ALLOCATOR_LIMIT_SCOPE_IDENTITY_SPACE_REMAINING` rather than returning that identity to the free pool.

---

## 7. Ingest Plane Contract (Emulator)

## 7.1 Canonical Mutation Entry Model

The emulator MUST implement the canonical ingest contract exactly as defined by:

1. `api/proto/searchfabric/v1/search_fabric.proto`
2. `docs/specs/core/search-fabric-api.md` Section `4`

This document intentionally does not restate proto-shaped message definitions. Emulator guidance here is limited to emulator-specific semantics, admission rules, and deterministic test hooks layered on top of the canonical contract.

Framing rules (must fail closed):

1. All `ROW_MUTATION` entries first.
2. At most one `PROGRESS_MARK`; if present, after all row entries.
3. At most one `BARRIER_MARK`; if present, final entry.
4. If `MutationBatch.policy.continue_on_error=true`, batch payload must contain only `ROW_MUTATION` entries; `PROGRESS_MARK`/`BARRIER_MARK` are malformed.
5. Violations -> `SF_BAD_REQUEST`.
6. `MutationBatch.meta.idempotency_key` must be non-empty for admitted mutating ingest requests; empty value is malformed (`SF_BAD_REQUEST`).

## 7.2 Deterministic Ordering and Invariants

Ordering key:

```text
(commit_ts, source_partition, source_epoch, source_seq)
```

Invariants:

1. stream scope is represented by mandatory `MutationBatch.stream_identity` (`source_partition`, `source_epoch`).
2. `MutationBatch.schema_generation_token` must be non-empty and must resolve to catalog-known generation lineage for `(namespace_id, collection_id)`:
   - Flow B (`commit_ts=0`): token must match active generation.
   - Flow A (`commit_ts>0`): historical token is allowed only when batch max row `commit_ts` does not cross token invalidation `ddl_commit_ts` boundary.
   - Boundary-crossing reject MUST return `SF_VERSION_CONFLICT` with `ErrorStatus.context.ingest_schema_boundary_invalidation.invalidated_at_commit_ts=<ddl_commit_ts>` so replay can split deterministically at the invalidation point; `ErrorStatus.diagnostics.invalidated_at_commit_ts=<ddl_commit_ts>` may mirror the same value for compatibility/string-only consumers.
   - Unknown/stale/out-of-bound token -> `SF_VERSION_CONFLICT`.
3. Allocator-backed create/insert batches on `COLLECTION_TRUTH_MODE_SEARCH_FABRIC` collections with `DocIDMapping.generation_policy=DOC_ID_GENERATION_POLICY_ALLOCATOR_OPTIONAL` MUST carry `MutationBatch.allocator_receipt`; the emulator MUST validate that receipt against authenticated caller identity, `client_id`, `client_kind`, resolved collection scope, `route_intent=ID_ALLOCATION_ROUTE_INTENT_CREATE_INSERT`, canonical ordered realized `doc_id` result, and `schema_generation_token` before row admission.
4. Caller-required collections, `COLLECTION_TRUTH_MODE_EXTERNAL` collections, and non-allocator-backed batches MUST leave `MutationBatch.allocator_receipt` absent.
5. Allocator-receipt validation failures MUST use the canonical split:
   - `ALLOCATOR_RECEIPT_VALIDATION_REASON_CALLER_MISMATCH` -> `SF_FORBIDDEN`
   - `ALLOCATOR_RECEIPT_VALIDATION_REASON_MISSING_REQUIRED_RECEIPT`, `MALFORMED_RECEIPT`, `SIGNATURE_INVALID`, `EXPIRED_RECEIPT`, `SCOPE_MISMATCH`, `REALIZED_DOC_IDS_MISMATCH`, `ROUTE_INTENT_MISMATCH`, and `SCHEMA_GENERATION_TOKEN_MISMATCH` -> `SF_BAD_REQUEST`
   - when the protobuf typed branch is available, `ErrorStatus.context.allocator.receipt_validation.reason` is authoritative and `diagnostics["receipt_validation_reason"]` is only the compatibility mirror.
5. every `MutationEntry.source_seq` must be `> 0`.
6. when reconnecting, `MutationBatch.resume_cursor.source_partition` must be non-empty, `MutationBatch.resume_cursor.source_epoch` must be `> 0`, `MutationBatch.resume_cursor.source_seq` must be `> 0`, and cursor scope must match `MutationBatch.stream_identity`.
7. `source_epoch` must be monotonic per partition.
8. within same `(source_partition, source_epoch)`, `source_seq` must be strictly increasing.
9. reconnect or producer failover that resets sequence numbering must increment `source_epoch`.
10. stateless/direct test ingress must not use `RequestMeta.idempotency_key` as data-plane stream scope; when test APIs derive stream identity automatically, `source_partition` must come from retry-stable batch identity, `source_epoch` defaults to `1`, and omitted `source_seq` values are derived from canonical entry order as 1-based indexes.
11. stream-invariant mismatches (scope mismatch, non-monotonic epoch, or sequence-order violation) -> `SF_VERSION_CONFLICT`.
10. reconnect token-binding mismatches (cursor/session payload mismatch against token-bound scope) -> `SF_IDEMPOTENCY_CONFLICT`.

## 7.3 Idempotency Rules

Entry idempotency key:

```text
(namespace_id, collection_id, source_partition, source_epoch, doc_id, commit_ts, source_seq)
```

Rules:

1. Same key + same semantic payload -> replay-safe duplicate.
2. Same key + different payload -> `SF_IDEMPOTENCY_CONFLICT`.

Batch idempotency key:

```text
(namespace_id, collection_id, source_partition, batch_id)
```

Rules:

1. Same key + same fingerprint -> replay no-op, including legal failover retransmit where `source_epoch` is higher.
2. Same key + different fingerprint -> `SF_IDEMPOTENCY_CONFLICT`.

Fingerprint contract:

1. Algorithm fixed: `BLAKE3`.
2. Input fixed: canonical binary serialization of admitted business content only.
3. `RowMutation.commit_ts` is excluded from fingerprint hashing.
4. Stream/envelope metadata (`stream_identity`, `schema_generation_token`, request metadata, resume cursor/token fields) is excluded from fingerprint hashing.
5. Deterministic map-key ordering, float canonicalization, and default-field normalization are required.

## 7.4 First-Error Stop Contract

If a non-retryable violation occurs at entry `N`:

1. Entries after `N` in same batch must not be applied.
2. ACK must expose deterministic first reject cursor in `RejectionContext`:
   - `first_rejected_commit_ts`
   - `first_rejected_source_epoch`
   - `first_rejected_source_seq`
   - `first_rejected_error_code`

## 7.5 Undeclared Field Policy Hint

`MutationBatch.policy.undeclared_field` governs handling of row fields not declared in admitted schema snapshot:

1. `UNDECLARED_FIELD_POLICY_UNSPECIFIED` defaults to strict reject behavior in this revision.
2. `UNDECLARED_FIELD_POLICY_REJECT`: first undeclared field rejects batch with `SF_SCHEMA_VIOLATION` and must emit deterministic first-reject cursor.
3. `UNDECLARED_FIELD_POLICY_IGNORE`: undeclared fields are dropped before apply; declared fields continue.
4. Emulator behavior is intentionally closed to the canonical enum surface above; deleted or out-of-contract undeclared-field branches MUST fail closed rather than being emulated as hidden additive-schema behavior.

## 7.6 Durability and Visibility Policies

ACK timing is the stricter gate of:

1. `policy.durability` (`INGEST_ACK` / `WAL_CHUNK_DURABLE`)
2. `policy.visibility` (`ASYNC` / `WAIT_FOR_VISIBLE`)

Even in test mode, emulator MUST model this contract in ACK state fields.

## 7.7 Progress Watermark Contract

Per collection progress model:

```go
type CollectionProgress struct {
    AppliedTSMetadataSearch uint64
    AppliedTSSemanticSearch uint64
    AppliedTSMetadataAux    *uint64 // nil means metadata-aux modality is absent/disabled
    SafeTS                  uint64
}
```

Invariant:

```text
safe_ts = min(enabled modality applied_ts, with metadata_aux included only when AppliedTSMetadataAux != nil)
```

`safe_ts` is derived only; direct write is rejected with `SF_BAD_REQUEST`.

## 7.8 Reconnect and Token Contract

`StreamCursor.resume` must bind:

1. scope: namespace/collection/partition
2. cursor tuple: `commit_ts`, `source_epoch`, `source_seq`
3. principal binding + TTL + `key_id`
4. token purpose (`mutation_stream_resume`)

Reconnect rules:

1. Reconnect with non-zero `resume_cursor` but empty `resume_cursor.resume.token` -> `SF_BAD_REQUEST`.
2. Cursor-only reconnect is not admitted in this revision.
3. Malformed token envelope or signature failure -> `SF_BAD_REQUEST`.
4. Token `key_id` outside verify overlap window -> `SF_BAD_REQUEST`.
5. Token principal/scope mismatch -> `SF_FORBIDDEN` (precedence before conflict tier).
6. Token/cursor mismatch -> `SF_IDEMPOTENCY_CONFLICT`.
7. Expired token -> `SF_INVALID_QUERY` with restart hint.
8. If retention window exceeded and deterministic replay cannot be verified -> retryable `SF_BACKEND_UNAVAILABLE`.

ACK contract:

1. `MutationAck.success.acked_cursor.resume.expires_at_unix_ms` must be returned whenever `acked_cursor.resume.token` is non-empty.

Token key rotation:

1. Current signing key issues new tokens.
2. Previous key remains verify-only during overlap window.
3. Tokens outside verify window fail deterministically.

## 7.9 Ingest Admission Order (Deterministic)

Admission check order:

1. access scope
2. request-level network dedupe (`RequestMeta.idempotency_key` short-window replay check)
3. token/session validity
4. hard limits + static policy checks
5. catalog ownership/schema admissibility
6. ordering/idempotency invariants
7. apply + ACK

Backpressure contract:

1. Overload rejection must use retryable `SF_RATE_LIMITED`.
2. Error details must include `retry_after_ms`, `throttled_dimension` (`ingest_bytes`, `ingest_entries`, `snapshot_bytes`, `snapshot_chunks`), and `server_limit`.
3. Operation-budget rejection on ingest hard-count limits (for example `max_entries_per_batch`) must emit `SF_TOO_MANY_OPERATIONS` with required details: `limit_scope=max_entries_per_batch`, `configured_limit`, `requested_value`.

---

## 8. Snapshot Sync Emulation

## 8.1 Session Lifecycle

State machine:

```text
preparing -> streaming -> committing -> committed
                     \-> aborted | expired | failed
```

APIs:

1. `StartSnapshotSync`
2. `UploadSnapshotChunks` (stream)
3. `CommitSnapshotSync`
4. `AbortSnapshotSync`
5. `GetSnapshotSyncProgress`

## 8.2 Start Idempotency

Idempotency key:

```text
(namespace_id, collection_id, client_session_token)
```

Rules:

1. Same key + same parameters -> return original session id.
2. Same key + different parameters -> `SF_IDEMPOTENCY_CONFLICT`.
3. Token must be non-empty.
4. `StartSnapshotSync.meta.idempotency_key` must be non-empty; empty idempotency key is malformed (`SF_BAD_REQUEST`).
5. `StartSnapshotSyncSuccessResponse` must expose `lease_expire_at_unix_ms`.

## 8.3 Upload Stream Framing

`UploadSnapshotChunks` frames:

1. first frame must be `SnapshotUploadInit`
2. `SnapshotUploadInit.meta.idempotency_key` must be non-empty; empty idempotency key is malformed (`SF_BAD_REQUEST`).
3. after a valid init, stream may carry `SnapshotChunk` frames and must terminate with exactly one `SnapshotUploadComplete` frame.
4. every admitted `SnapshotChunk` must carry the full canonical metadata prefix before payload bytes: `chunk_checksum`, `checksum_algorithm`, `payload_row_count`, `payload_uncompressed_bytes`, `payload_encoding`, `compression_codec`, and `payload_compressed_bytes`.
5. `SnapshotChunk.payload_row_count` must exactly equal the number of length-delimited `RowMutation` messages contained in the payload; `payload_uncompressed_bytes` and `payload_compressed_bytes` must exactly describe the transmitted chunk framing so emulator validation matches canonical pre-allocation and checksum behavior.
6. duplicate init frame -> `SF_BAD_REQUEST`.
7. duplicate complete frame or any post-complete chunk frame -> `SF_BAD_REQUEST` (or `SF_VERSION_CONFLICT` by precedence for state-conflict branch).
8. `SnapshotUploadComplete.total_chunks_sent` must equal final acknowledged chunk count (`next_expected_seq - 1`); mismatch is deterministic conflict.

Reconnect rules:

1. Fresh upload should use empty `resume_token`; `resume_from_chunk_seq` MAY be omitted or set to `1`.
2. Reconnect upload must use token + sequence that match server-acknowledged `next_expected_seq`.
3. Malformed upload token envelope or signature failure -> `SF_BAD_REQUEST`.
4. Upload token `key_id` outside verify overlap window -> `SF_BAD_REQUEST`.
5. `resume_token` principal/scope mismatch -> `SF_FORBIDDEN` (precedence before conflict tier).
6. token-bound session/sequence mismatch (including `resume_from_chunk_seq` mismatch against token-bound `next_expected_seq`) -> `SF_IDEMPOTENCY_CONFLICT`.
7. Parallel upload streams for one `sync_session_id` are rejected (`SF_VERSION_CONFLICT`).
8. Expired upload token -> `SF_INVALID_QUERY`.
9. Empty `resume_token` is allowed only for omitted/`1` `resume_from_chunk_seq` on an unused session.

Session binding rules:

1. `sync_session_id` is immutably bound to one collection identity.
2. `SnapshotUploadInit.collection` must exactly match the collection bound to `sync_session_id`; mismatch fails closed (`SF_FORBIDDEN` or `SF_BAD_REQUEST` by precedence).
3. `CommitSnapshotSync` / `AbortSnapshotSync` / `GetSnapshotSyncProgress` must carry the same bound `CollectionRef`; mismatch fails closed (`SF_FORBIDDEN` or `SF_BAD_REQUEST` by precedence).

Chunk-sequence rules:

1. `chunk_seq == next_expected_seq` -> normal admit path.
2. `chunk_seq < next_expected_seq` -> replay is allowed only when checksum/payload equivalence holds; otherwise `SF_IDEMPOTENCY_CONFLICT`.
3. `chunk_seq > next_expected_seq` -> `SF_VERSION_CONFLICT`.

## 8.4 Checksum and Root Hash Rules

1. `chunk_checksum`: CRC32C, lowercase hex, fixed width 8.
2. `expected_root_hash`: BLAKE3, lowercase hex, fixed width 64.
3. `CommitSnapshotSync` must carry `expected_chunks`, `expected_root_hash`, and `expected_root_hash_algorithm`; emulator must reject omission or malformed branch exactly as the canonical API would.
4. `expected_chunks` must equal the accepted `SnapshotUploadComplete.total_chunks_sent`.
5. Invalid format/width -> `SF_BAD_REQUEST`.
6. `chunk_checksum` is computed over transmitted bytes (`payload`) after compression, not decoded rows.
7. Payload encoding is fixed to canonical row-mutation payload shape; compression codecs must be admitted by static ingest policy.
8. Root-hash algorithm mismatch at commit is malformed (`SF_BAD_REQUEST`); same-algorithm digest mismatch is conflict (`SF_VERSION_CONFLICT`).

Root hash is computed over ordered acknowledged chunk checksums, not decoded objects.

## 8.5 Lease and Session Governance

1. Session has lease TTL.
2. Successful upload/commit refreshes lease.
3. Expired session rejects upload/commit with `SF_VERSION_CONFLICT`.
4. Invalid session state transitions (for example `expired -> streaming`) are rejected with `SF_VERSION_CONFLICT`.
5. `SnapshotChunkAck.success.upload_resume.expires_at_unix_ms` must be returned whenever `upload_resume.token` is non-empty for deterministic reconnect behavior.
6. `CommitSnapshotSync` / `AbortSnapshotSync` requests must carry non-empty `RequestMeta.idempotency_key`; empty value is malformed (`SF_BAD_REQUEST`).
7. `CommitSnapshotSync` before accepted upload EOF (`SnapshotUploadComplete` -> completed ACK) is rejected with `SF_VERSION_CONFLICT`.

---

## 9. Query Plane Contract (Emulator)

## 9.1 Query Modes

The emulator must support two normative collection truth modes:

1. `COLLECTION_TRUTH_MODE_EXTERNAL`
2. `COLLECTION_TRUTH_MODE_SEARCH_FABRIC`

Route split:

1. Shared canonical query routes remain on `QueryGatewayService`:
   - `ExecuteQuery`
   - `StreamQuery`
   - `GetReadiness`
   - `GetCollectionStatistics`
2. Trusted relational routes remain on `RelationalQueryService`:
   - `ExecuteProjectedQueryOrFallback(ProjectedQueryRequest)` for projected-query execution and explicit fallback ownership
   - `ExecutePrimaryQuery(PrimaryQueryRequest)` for Search-Fabric-truth primary-query execution without caller-native fallback outcomes
3. The emulator MUST preserve the current canonical route split rather than collapsing federated-table and projected-query semantics into generic `ExecuteQuery` handling.
4. `ProjectedQueryRequest` carries projected-route-only execution inputs (`snapshot_overlay`, `snapshot_read_ts`) and therefore has additional admission rules beyond the inner `QueryRequest`.
5. `PrimaryQueryRequest` carries only the shared canonical `QueryRequest` because the active primary-query route does not admit snapshot-overlay carriers.

## 9.2 Consistency Semantics

Internal readiness variables:

```text
strict/bounded: effective_read_ts = min(read_ts, safe_ts)
best_effort:    effective_read_ts = safe_ts
```

Mode-coupled `ReadConsistencySpec` admission constraints (canonical mirror):

1. `CONSISTENCY_MODE_STRICT`: `read_ts > 0`, `max_staleness_ms = 0`.
2. `CONSISTENCY_MODE_BOUNDED_STALENESS`: `read_ts > 0`, `max_staleness_ms > 0`.
3. `CONSISTENCY_MODE_BEST_EFFORT`: `read_ts = 0`, `max_staleness_ms = 0`.

Deterministic behavior:

1. `aux_index + strict + effective_read_ts < read_ts` -> fallback required signal.
2. `projected-query + strict + effective_read_ts < read_ts` -> fallback required signal.
3. bounded lag violation:
   - aux-index -> fallback required
   - projected-query -> fallback required
4. `primary-query` and shared Search-Fabric-truth query routes (`ExecuteQuery`) fail closed as `SF_VISIBILITY_TIMEOUT` when exact or bounded-staleness visibility cannot be met.
5. default best-effort execution uses `effective_read_ts = safe_ts`; emulator MUST NOT synthesize a non-zero `read_ts` for this mode.
6. overlay-bearing `ProjectedQueryRequest` is the explicit best-effort exception: when `snapshot_overlay.rows` is non-empty under `CONSISTENCY_MODE_BEST_EFFORT`, the emulator MUST use `effective_read_ts = snapshot_read_ts` for both persisted-reader evaluation and overlay visibility/LWW reduction rather than substituting `safe_ts`.
7. strict mode never returns silently downgraded best-effort results.

## 9.3 Query Shape and Deterministic Ordering

Rules:

1. `search_payload.global_filter` is authoritative hard filter.
2. `search_payload.root_retrieval` and `NestedQueryPayload.root_retrieval` are the only retrieval roots; flat top-level clause lists are invalid in this revision.
3. every `VectorClause.local_filter` is optimization-only and cannot relax global eligibility.
4. `TextClause` / `VectorClause` / `ScoringScalarClause` / `NestedClause` `clause_id` values must be non-empty and globally unique across the entire retrieval tree in one request.
5. `RetrievalNode` carries only `boost` plus its `clause` oneof in this revision; required-vs-optional retrieval semantics are expressed only through `RetrievalGroup.must_clauses[]` and `ShouldMatchSpec`, not through any node-local `occur` carrier.
6. `TextClause.fields[]` must be non-empty, byte-unique after normalization, and all entries must resolve to schema-declared text-searchable fields.
7. each `RetrievalGroup` must carry at least one retrieval branch (`must_clauses[]` and/or `should_match`); `ShouldMatchSpec.clauses[]` must be non-empty when present; `ShouldMatchSpec.threshold` fallback and `percentage_ratio` evaluation must follow the canonical API spec; `tie_breaker` is legal only when `fusion_strategy=GROUP_FUSION_STRATEGY_DIS_MAX`.
8. `TextClause.similarity_override`, when present, applies only to this clause ranking baseline.
9. `search_payload.aggregations` is a recursive tree; sibling `AggregationRequest.name` values must be unique at each depth.
10. `metric` and `top_hits` aggregation nodes are leaf-only and must not carry `sub_aggregations`.
11. `bucket` aggregation nodes may carry `sub_aggregations`; bucket strategy params must be valid (`terms.size>0`, `histogram.interval>0`, exactly one of `date_histogram.calendar_interval/fixed_interval`, non-empty `range.ranges`, and `composite.limit>0`).
12. `composite` aggregation contract must stay deterministic: non-empty `sources[]`, unique `source.name`, exactly-one source strategy, explicit source `order`, `after_key` key-set exactly matching `sources[].name`, lexicographic source-order comparator, reader-side pruning for buckets `<= after_key`, and broker-side k-way merge with `BucketResult.composite.after_key.fields` from the last emitted bucket when more buckets remain.
13. metric-branch exclusivity: `MetricAggregation.metric_type` is a oneof and MUST set exactly one branch (`builtin_function` or `cardinality`).
14. metric field-source rules are deterministic: `COUNT` with empty `field` means document count; `COUNT` with non-empty `field` means value-count of non-null field values; non-`COUNT` builtin and `cardinality` require non-empty `field`.
15. Final ordering is deterministic and MUST execute admitted normalized `fetch_spec.sort_keys`.
16. If caller sort is empty, emulator materializes default `score desc` + `doc_id asc`; if caller sort omits `doc_id`, emulator appends `doc_id asc`.

## 9.4 Result Payload Contract

Transport-`OK` unary query success payloads (`ExecuteQuerySuccessResponse`), projected-query normal-result payloads (`ProjectedQueryResponse`), and primary-query success payloads (`ExecuteQuerySuccessResponse` returned by `ExecutePrimaryQuery`) MUST follow the canonical contract in `docs/specs/core/search-fabric-api.md`; this emulator document MUST NOT restate or fork individual response-message field semantics.

Emulator-specific obligations are limited to:

1. `_count`-style requests still execute through canonical aggregation responses, which means `success.aggregations` is populated and `success.hits` remains empty.
2. requests carrying mismatched `expected_schema_generation_token` MUST reject before execution with `SF_VERSION_CONFLICT`.
3. emulator implementations MUST preserve canonical dictionary/header alignment (`projected_field_names`, `highlight_field_names`, `inner_hits_headers`), total-hit semantics, vector-projection semantics, and aggregation result-shape rules exactly as defined by the canonical API spec.
4. projected-query success/fallback behavior MUST follow the canonical ownership split:
   - `ExecuteProjectedQueryOrFallback` returns transport-`OK` normal-result `ProjectedQueryResponse`
   - `ExecutePrimaryQuery` remains success/error only and MUST NOT synthesize caller-native fallback outcomes
5. when `ProjectedQueryRequest` is admitted, emulator implementations MUST preserve the projected-route observability split between:
   - `canonical_query_digest` over the inner `QueryRequest`
   - `relational_execution_digest` over the canonical query semantics plus projected-route-only execution inputs (`snapshot_read_ts` and canonical overlay digest)

## 9.5 Pagination and Continuation Tokens

Token binds:

1. canonical query digest (`BLAKE3`)
2. `effective_read_ts`
3. `applied_schema_generation_token`
4. committed manifest pin
5. collection identity and normalized sort contract
6. continuation lineage metadata (page-window mode plus token-bound continuation branch and page ordinal)
7. when the admitted route is `ProjectedQueryRequest`, token/search-context binding MUST additionally include:
   - `snapshot_read_ts`
   - canonical overlay digest derived from normalized columnar `snapshot_overlay`

Canonical digest must include:

1. truth-mode and consistency semantics (`collection_truth_mode`, admitted consistency mode, `read_ts` / staleness parameters),
2. normalized sort contract (including auto-appended `doc_id asc` tie-break),
3. retrieval candidate semantics (`search_payload.global_candidate_topk`) and deterministic semantic defaults affecting result-set definition,
4. canonicalized hard-filter semantics (`search_payload.global_filter`) plus retrieval-tree semantics (`RetrievalNode.boost`, `RetrievalGroup.must_clauses[]` / `should_match` / `fusion_strategy` / `tie_breaker`, `ShouldMatchSpec.threshold`, leaf clause IDs, typed `TextClause.mode_override` branch options, `TextClause.similarity_override`, and normalized vector local-filter strategy),
5. projection/output semantics (`fetch_spec.attribute_projection`, canonicalized `fetch_spec.vector_projections`, `fetch_spec.return_score`, `fetch_spec.text_options`, `fetch_spec.rescore`),
6. routing semantics (`search_payload.routing.routing_keys`, `search_payload.routing.partition_filter`, canonicalized `search_payload.routing.target_tiers`),
7. aggregation tree semantics (`search_payload.aggregations`) with canonical recursive ordering (cursor payloads excluded),
8. nested extraction semantics (`InnerHitsSpec` payloads).

Cursor payloads (`CompositeAggregation.after_key`) and transport pagination/session controls (`fetch_spec.limit`, `fetch_spec.page_window`, `fetch_spec.search_context`) must not alter `canonical_query_digest`. Projected-route-only execution inputs (`snapshot_read_ts`, canonical overlay digest) also do not redefine `canonical_query_digest`; they participate in token binding and projected-route execution digest instead.

Coherence rules:

1. each request must set exactly one page-window branch (`first_page`, `continuation_page`, or `composite_page`).
2. first-page requests must use `first_page`; continuation branches are invalid in first-page semantics.
3. first-page requests must not carry `fetch_spec.search_context.token`; violation is malformed (`SF_INVALID_QUERY`).
4. requests carrying non-empty `CompositeAggregation.after_key` are aggregation-continuation requests, must use `composite_page`, and must carry non-empty bound `fetch_spec.composite_page.next_page_token` whose token-bound cursor exactly matches the request `after_key`.
5. continuation requests must use `continuation_page` with non-empty `next_page_token`, and must not include `first_page`; empty `next_page_token` is malformed (`SF_BAD_REQUEST`).
6. `limit` semantics are mode-coupled:
   - `ExecuteQuery` first-page requests: `limit=0` is allowed only when `search_payload.aggregations` is non-empty or `fetch_spec.track_total_hits` is explicitly configured (`enabled`/`threshold`); otherwise malformed (`SF_BAD_REQUEST`).
   - `StreamQuery` first-page requests: `limit=0` is allowed and means unbounded export mode.
   - `ExecuteQuery` continuation requests: `limit` must remain `> 0`.
   - `StreamQuery` continuation requests: `limit` may remain `> 0` or `0`.
7. `sort_keys` must be omitted or exactly match token-bound contract.
8. continuation requests must preserve the token-bound lineage/page ordinal contract; reusing a token against the wrong continuation branch, wrong logical page step, or wrong search-context lineage is invalid (`SF_INVALID_QUERY`).
9. malformed token envelope, signature failure, or key-id outside verify overlap -> `SF_BAD_REQUEST`.
10. token principal/scope mismatch -> `SF_FORBIDDEN` (precedence before query-contract tier).
11. token expiry or token/query-parameter mismatch -> `SF_INVALID_QUERY`.
12. payload bytes exceeding `max_query_payload_bytes` MUST fail closed as `SF_PAYLOAD_TOO_LARGE` with required details: `limit_scope=dynamic_payload_budget`, `effective_limit`, `requested_bytes`.
13. `InListPredicate.values.values[]` cardinality exceeding `max_in_list_elements` MUST fail closed as `SF_TOO_MANY_OPERATIONS` with required details: `limit_scope=max_in_list_elements`, `configured_limit`, `requested_value`, `field`.
14. total predicate-tree nodes exceeding `max_predicate_ast_nodes` MUST fail closed as `SF_TOO_MANY_OPERATIONS` with required details: `limit_scope=max_predicate_ast_nodes`, `configured_limit`, `requested_value`.
15. exceed `max_page_depth` -> `SF_TOO_MANY_OPERATIONS`.
16. `max_page_depth` rejection must include required details: `limit_scope=max_page_depth`, `configured_limit`, `requested_value`.
17. `(first_page.offset + limit) > effective_max_result_window` must reject with `SF_TOO_MANY_OPERATIONS` and required details: `limit_scope=max_result_window`, `max_result_window`, `requested_offset`, `requested_limit`, `requested_window`.
18. dynamic vector payload/window budget rejections (`SF_PAYLOAD_TOO_LARGE` or `SF_TOO_MANY_OPERATIONS`) must include required details: `limit_scope=dynamic_payload_budget|dynamic_result_window`, `effective_limit`, and `requested_bytes`/`requested_window` by branch.
19. token-bound schema generation mismatch -> `SF_VERSION_CONFLICT`.
20. default page-token TTL is authoritative from static query policy (`default_page_token_ttl_ms`).
21. default search-context TTL is authoritative from static query policy (`default_search_context_ttl_ms`).
22. page-token key-rotation outside verify overlap (`key_id` unknown/out-of-window) -> `SF_BAD_REQUEST`.
23. when response issues non-empty `success.pagination.next_page.token`, it should also expose `success.pagination.next_page.expires_at_unix_ms` aligned with the issued token TTL.
24. when response issues non-empty `success.pagination.search_context.token`, it should also expose `success.pagination.search_context.expires_at_unix_ms` aligned with the effective admitted search-context TTL.

## 9.6 Query Admission Order (Deterministic)

Admission check order:

1. access scope
2. payload budget and AST complexity pre-admission (`max_query_payload_bytes`, `max_in_list_elements`, `max_predicate_ast_nodes`)
3. ownership/applicability
4. continuation token validity/coherence
5. feature compatibility
6. hard limits (`limit`, `topk`, page depth, result window, aggregation ceilings, payload-tightening branches)
7. consistency admissibility (`read_ts`, `safe_ts`, lag bound)

## 9.7 StreamQuery Rules

1. request must use either `fetch_spec.first_page` or `fetch_spec.continuation_page`.
2. `StreamQuery` is hit-streaming only; `StreamSearchPayload` must not expose aggregation carriers, and emulator tests must reject any non-canonical attempt to smuggle aggregation semantics onto the stream path.
3. first-page mode requires the offset-free `fetch_spec.first_page` branch.
4. resume mode requires non-empty `fetch_spec.continuation_page.next_page_token`.
5. stream request shape excludes PIT/search-context and composite-page controls in this revision; emulator tests must not admit hidden alternate carriers for those unary-only branches.
6. `fetch_spec.limit > 0` means bounded streamed hit count; `fetch_spec.limit = 0` means unbounded export mode and defaults omitted total-hit tracking to disabled.
7. unbounded export mode MUST reject explicit `fetch_spec.track_total_hits.enabled=true` as `SF_BAD_REQUEST`.
8. output order must match unary query deterministic ordering.
9. every success frame carrying one or more hits must include non-empty `success.pagination.next_page.token`.
10. non-terminal success frames must carry one or more hits; zero-hit success is legal only on the terminal no-more-hits completion frame, which must keep `success.pagination.next_page.token` empty.
11. projection/highlight/inner-hits dictionary arrays are emitted only on the first successful stream frame carrying one or more hits; zero-hit success frames must not consume that emission opportunity, later hit-bearing frames in the same RPC attempt must leave those header arrays empty, and resumed RPC attempts must re-emit them on their own first hit-bearing frame.
12. non-terminal success frames must use the `ok` branch and must not emit `warnings[]` or `total_hits`.
13. only the terminal success frame may emit `warnings[]` or `total_hits`.

## 9.8 PIT-Equivalent Search Context Lifecycle

1. Unary query responses may issue `success.pagination.search_context.token` when request carries `fetch_spec.search_context.keep_alive_ms`:
   - `ExecuteQuerySuccessResponse.success`
   - `ProjectedQueryResponse.success`
2. `search_context.token` is snapshot-scoped; continuation requests may carry both `next_page_token` and `token`, and both must bind to the same admitted query snapshot, including committed-manifest pin and any projected-route overlay binding.
3. default search-context TTL is authoritative from static query policy (`default_search_context_ttl_ms`) and is independent from page-token TTL.
4. continuation requests MAY present `fetch_spec.search_context.keep_alive_ms` to request TTL extension, but emulator MUST clamp/refresh the context only within static policy bounds and MUST surface the resulting admitted expiry through `success.pagination.search_context.expires_at_unix_ms` when a live context token is returned.
5. There is no public `CloseSearchContext` RPC in the current revision:
   - PIT-equivalent retention ends only by natural expiry,
   - continuation requests MAY still ask for bounded keep-alive refresh through `fetch_spec.search_context.keep_alive_ms`.
6. malformed context-token envelope/signature -> `SF_BAD_REQUEST`; scope/principal mismatch -> `SF_FORBIDDEN`.
7. Query-execution continuation paths treat expired tokens as `SF_INVALID_QUERY`; no separate close-only success branch exists.

---

## 10. Control Plane Contract (Emulator)

## 10.1 Structured Schema Payload

Emulator schema state must store structured canonical fields:

1. `DocIDMapping`
2. `FieldSchema[]`
3. optional `CollectionSettings` (including optional `IndexSortSpec`, optional `max_result_window`, optional `lifecycle_policy`, optional `allocator_admission_policy`, and supplemental `options`)
4. `DocIDMapping` identity contract follows the active cross-ecosystem parse guardrail: gateway-facing ID tokens may be decimal strings or non-negative integer tokens when losslessly representable as canonical `uint64`; canonical stored identity remains `DocumentID.id_value.u64`.
5. `FieldSchema` must include typed semantic fields through `FieldConfig` (`config.capabilities`, oneof `config.field_definition` with `scalar_type`/`text`/`vector`/`keyword`/`object`/`nested`, plus `subfields[].config`) for deterministic ES/Turbopuffer/MySQL mapping.
6. `CollectionSettings` must support collection-level `index_sort`, `max_result_window`, `lifecycle_policy`, optional `allocator_admission_policy`, and supplemental `options`; when `allocator_admission_policy` is present it must preserve `caller_profile`, `max_plan_items_per_request`, and `max_forward_reservation_jump`.
7. `DocIDMapping.generation_policy` plus `CollectionSettings.allocator_admission_policy` determine whether create/insert routes are caller-required or allocator-optional; emulator schema state MUST preserve and enforce that combination exactly, including the truth-mode restriction (`COLLECTION_TRUTH_MODE_EXTERNAL -> CALLER_REQUIRED + no allocator policy`), the positive allocator guardrail ceilings on allocator-optional collections, and the current-revision caller-profile restriction to `ALLOCATOR_CALLER_PROFILE_ES_WRITE_GATEWAY_ONLY`.
8. Emulator validation must honor the canonical proto `sf_non_zero_enum` contract on allocator/core enum fields, including `RequestMeta.client_kind`, `AllocateIdsRequest.route_intent`, `DocIDMapping.generation_policy`, allocator limit/conflict typed-error enums, and allocator receipt-validation reasons.

Opaque TiDB internal blob payload is not allowed.

## 10.2 CAS Fencing and Monotonic Apply

`PutCollectionSchema` acceptance requires:

1. request carries `scope.collection` plus caller-class-correct `scope.ddl_commit_ts`:
   - TiDB DDL sync -> authoritative non-zero DDL commit TS
   - gateway-managed native-search schema apply -> reserved sentinel `0`
2. request carries authoritative monotonic `schema_version`
3. request carries explicit non-`UNSPECIFIED` `update_policy`
4. matching `expected_prev_schema_version`
5. matching monotonic sequence
6. valid `schema_generation_token`
7. ownership/profile checks passed
8. `FieldSchema.config.field_definition` and typed-schema constraint checks passed
9. typed schema semantic validation passed (text-analysis/vector constraints and cross-ecosystem schema coherence/fingerprint checks); charset/collation, when preserved, are supplemental metadata only and MUST NOT participate as typed-schema gatekeepers

`DropCollection` replay safety is always enforced through non-empty `RequestMeta.idempotency_key`; optional `fence` is added only when CAS-safe delete / ABA protection is required.

## 10.3 Schema Update Policies

Each change classified as:

1. `SCHEMA_UPDATE_POLICY_INITIAL_CREATION`
2. `SCHEMA_UPDATE_POLICY_ADDITIVE`
3. `SCHEMA_UPDATE_POLICY_BACKWARD_COMPATIBLE_REBUILD`
4. `SCHEMA_UPDATE_POLICY_BREAKING_REBUILD`

Query safety rules:

1. page-token chains cannot cross schema generation.
2. generation drift -> `SF_VERSION_CONFLICT`.
3. changing doc-id parse contract (`decimal-string -> uint64`) or `DocIDMapping.source_field` requires rebuild-class change; additive apply is forbidden.
4. changing typed schema semantics that reinterpret indexed values (for example `type`, text-analysis/vector semantics, ANN/fulltext tuning) requires rebuild-class change; additive apply is forbidden.

## 10.4 Control-Plane Idempotent Retry

`PutCollectionSchema` retry rules:

1. `RequestMeta.idempotency_key` must be non-empty; empty idempotency key is malformed (`SF_BAD_REQUEST`).
2. replay-safe schema apply still requires canonical control scope (`scope.collection`, `scope.ddl_commit_ts`, `schema_version`, `fence.schema_generation_token`, `update_policy`).
3. first schema creation on a collection with no prior lineage uses the 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`); accepted apply must replace the sentinel with a real active generation token.
4. business idempotency key:

```text
(namespace_id, collection_id, schema_version, schema_generation_token)
```

4. network dedupe key:

```text
(server-authenticated caller identity, client_id, rpc_method, namespace_id, collection_id, idempotency_key)
```

5. same business key + semantically equivalent payload is replay-safe success.
6. same business key + conflicting payload -> `SF_IDEMPOTENCY_CONFLICT` or `SF_VERSION_CONFLICT` by precedence.

`DropCollection` retry rules:

1. `RequestMeta.idempotency_key` must be non-empty; empty idempotency key is malformed (`SF_BAD_REQUEST`).
2. network dedupe key:

```text
(server-authenticated caller identity, client_id, rpc_method, namespace_id, collection_id, idempotency_key)
```

3. same dedupe key replay after successful drop returns replay-safe `DropCollectionSuccessResponse.status.already_dropped` semantics.
4. same dedupe key replay with conflicting `fence` payload -> `SF_IDEMPOTENCY_CONFLICT` or `SF_VERSION_CONFLICT` by precedence.

## 10.5 Control Admission Order (Deterministic)

Admission check order:

1. access scope and caller authority
2. request-level network dedupe (`RequestMeta.idempotency_key` short-window replay check)
3. schema-update-policy and static-policy support
4. fencing preconditions
5. ownership/doc-id/typed-schema contract validation
6. concurrency/admission limits
7. apply state transition

If multiple violations exist, this order defines evaluation flow only. Final emitted code must follow canonical precedence (`docs/specs/core/error-codes.md`).

Control-plane overload handling:

1. overload must fail with retryable `SF_RATE_LIMITED`.
2. error details must include `retry_after_ms`.

---

## 11. CDC Upstream Adapter Contract

The emulator MAY host multiple internal CDC upstream adapters, but all of them MUST terminate in the same canonical emulator state paths. Embedded Mini-CDC is the first concrete implementation; it is no longer the only design shape assumed by this document.

## 11.1 Purpose and Boundary

1. Upstream adapters own source-specific pull/decode/schema/lifecycle logic.
2. The canonical emulator core owns replay identity, MVCC ordering, schema fencing, readiness derivation, token semantics, and error precedence.
3. Adding a new upstream MUST NOT create a second write-state machine, a second progress model, or a second schema-apply path.
4. Upstream adapters are internal emulator components only; they do not expose an independent service contract.

## 11.2 Canonical Event Classes

Every admitted CDC upstream adapter must lower its source traffic into exactly these emulator-side semantic classes:

1. row change -> canonical `ROW_MUTATION` ingest under one `MutationBatch`
2. schema change -> canonical `PutCollectionSchema` control request
3. checkpoint/progress change -> canonical progress input from which collection-level `applied_ts` and `safe_ts` are derived

Rules:

1. adapters must bind each event to resolved collection scope plus retry-stable source-partition identity before the event crosses into emulator core packages;
2. adapters must not write `safe_ts` directly or bypass canonical `singlewriter` / `control` / readiness logic;
3. adapter-private payloads (for example TiDB `TableInfo`, decoded row chunks, or other source-native schema objects) remain outside the canonical core and must be treated according to adapter-specific ownership rules.

## 11.3 Upstream Transaction-Version Contract

This emulator design now requires every admitted CDC upstream adapter to use the same canonical `uint64` transaction-version domain already used by emulator ingest, control, and readiness contracts.

Rules:

1. an eligible upstream must express row commit version, schema-change commit version, and checkpoint/resolved version in one consistent `uint64` domain;
2. adapters MUST preserve that `uint64` domain directly when calling emulator core state paths; source-private signed-version reinterpretation or conversion layers are out of scope;
3. version equality and ordering across row/schema/checkpoint events must remain meaningful without auxiliary remapping; if an upstream cannot preserve that property, it is out of scope for this abstraction;
4. when canonical downstream contracts require positive values (`commit_ts`, resolved progress, DDL commit ts), the adapter MUST reject zero-valued source versions before emission rather than relying on later malformed-request handling.

## 11.4 Source Partition, Epoch, and Sequence Rules

1. every adapter instance must derive one retry-stable canonical `source_partition` string per ordered upstream partition/table/shard path;
2. `source_epoch` is owned by the adapter lifecycle and MUST bump whenever that adapter resets per-partition sequence continuity (restart, resubscribe, failover lineage reset, or equivalent source-specific reset);
3. if the upstream already provides sealed per-partition order, the adapter MAY map that order directly;
4. if the upstream callback order is only observational and not yet sealed, the adapter MUST use a reorder buffer and assign canonical `source_seq` only after seal;
5. canonical emitted `MutationBatch` remains single-partition, so rows from different `source_partition` values MUST be split before admission.

## 11.5 Adapter Capability Profiles

Two adapter capability profiles are sufficient for the current emulator design:

1. `data_only`
   - adapter emits row changes plus checkpoints only;
   - collection schema lineage is managed elsewhere, and row lowering must still resolve a valid active `schema_generation_token` before admission;
   - this profile is suitable for fixed-schema replay or sources that do not own authoritative DDL timeline.
2. `schema_aware`
   - adapter emits row changes, checkpoints, and schema changes;
   - schema changes must lower into full canonical `PutCollectionSchema` requests with replay-stable idempotency identity and canonical fence semantics.

The profile only constrains what the adapter may emit. It does not create separate emulator cores.

## 11.6 Shared Failure Policy

All CDC upstream adapters are fail-fast on:

1. callback/bridge errors that prevent canonical lowering,
2. source-version conversion violations,
3. ordering/cursor/checkpoint regressions,
4. schema-lineage resolution failures for the chosen adapter profile,
5. bounded-buffer overflow or other evidence that deterministic canonical order can no longer be guaranteed.

## 11.7 Embedded Mini-CDC Specialization

Embedded Mini-CDC is the TiDB/TiCDC/TiKV-specific `schema_aware` adapter in the current revision.

When Mini-CDC mode is enabled, emulator MUST additionally follow the source-verified constraints from `docs/specs/core/embedded-mini-cdc.md`. Those TiDB-specific rules include:

1. synchronous consume callback only (`return false` path),
2. borrowed/read-only ownership rules for source payloads such as `TableInfo` and DDL objects,
3. comparable table-record spans and `schemaTs = commitTs - 1` lookup discipline,
4. per-table reorder buffering, same-ts DML-before-DDL, and DDL fence by minimum resolved ts across the fixed table set,
5. fixed-table-set startup, explicit `RegisterKeyspace`, long-lived `runCtx`, and single-active-instance process invariants,
6. fail-fast handling for callback errors, startup dependency/config validation failures, schema-readiness deadline violations, and detected DDL consistency anomalies.

---

## 12. Schema Source and Key/Decode Pipeline

## 12.1 Code-Grounded Constraints

| Constraint | Evidence | Requirement |
|---|---|---|
| `/schema` and `/db-table/{id}` behavior and partition fallback exist | `pkg/server/http_status.go:238-245,263`; `pkg/server/handler/tikvhandler/tikv_handler.go:950-1045,1836-1850` | Use `/db-table` for first physical-ID resolution, `/schema?table_ids` for batched logical refresh. |
| `/schema?table_ids` partial-miss tolerant | `pkg/server/handler/tikvhandler/tikv_handler.go:1004-1021`; tests `http_handler_test.go:930-954` | Per-table miss handling; no whole-batch fatal on partial misses. |
| `IsRecordKey` is positional byte check | `pkg/tablecodec/tablecodec.go:1125-1128` | Normalize key before record-key checks. |
| `RemoveKeyspacePrefix` runtime-conditional | `pkg/util/rowcodec/common.go:356-376` | Multi-branch normalization required. |
| API-v2 decode support exists at table-ID path | `pkg/tablecodec/tablecodec.go:305-323` | Add API-v2 fallback branch in key normalization. |
| New-row decode helper allocates if used naively | `pkg/tablecodec/tablecodec.go:462-485` | Cache decoders/plans per descriptor. |
| table handle helpers available | `pkg/meta/model/table.go:126-133,406-414`; `pkg/table/tables/tables.go:675-683` | `doc_id` extraction must be schema-driven. |

## 12.2 Key Normalization Algorithm

`NormalizeRecordKey(rawKey)`:

1. if `IsRecordKey(rawKey)` -> use as-is
2. try `RemoveKeyspacePrefix(rawKey)` then re-check
3. try API-v2 decode path and re-check
4. still non-record-key -> ignore as non-row key

## 12.3 Decode Pipeline

1. resolve descriptor by physical table id
2. decode record key to handle
3. decode value (new/old row format branch)
4. fill handle columns if required
5. resolve `doc_id`
6. emit canonical row mutation

Hot-path allocation controls:

1. pooled datum maps
2. cached row decoders per descriptor epoch
3. decode required columns only
4. avoid row-key copy unless debug/export path needs it

---

## 13. `doc_id` Contract

The emulator MUST follow the canonical schema-driven contract:

1. `DocIDMapping` is authoritative for document identity source, type, immutability, uniqueness, and active generation policy.
2. Active-profile canonical admission allows only `DocIDMapping.source_field_type=SCALAR_FIELD_TYPE_UINT64`.
3. `DocIDMapping.generation_policy=DOC_ID_GENERATION_POLICY_CALLER_REQUIRED` means every write route remains explicit-identity-only and `CollectionSettings.allocator_admission_policy` must be absent.
4. `DocIDMapping.generation_policy=DOC_ID_GENERATION_POLICY_ALLOCATOR_OPTIONAL` is legal only on `COLLECTION_TRUTH_MODE_SEARCH_FABRIC` collections and means admitted create/insert routes may use allocator authority only when `CollectionSettings.allocator_admission_policy.caller_profile=ALLOCATOR_CALLER_PROFILE_ES_WRITE_GATEWAY_ONLY` and the policy publishes positive `max_plan_items_per_request` / positive `max_forward_reservation_jump`; patch/delete/update-style routes still remain explicit-identity-only.
5. Record decode MUST resolve `doc_id` from the configured `DocIDMapping.source_field`; decode logic MUST NOT infer canonical identity from column names or from handle shape once canonical schema is available.
6. Source values that cannot be normalized losslessly into canonical `DocumentID.id_value.u64` are rejected by canonical precedence (`SF_DOC_ID_CONTRACT_VIOLATION`, `SF_SCHEMA_VIOLATION`, or `SF_INVALID_DOC_ID` depending on violation class).

Test-only fixture helpers MAY offer pre-canonical convenience inference before a schema is materialized, but that behavior is outside the canonical emulator contract and MUST NOT change emulated runtime semantics after schema admission.

---

## 14. MVCC Storage and Retention

## 14.1 Apply Semantics

Per `(namespace_id, collection_id, doc_id)` maintain versions sorted by:

```text
(commit_ts, source_partition, source_epoch, source_seq)
```

Visibility at query time:

- choose latest version with `commit_ts <= effective_read_ts`
- tombstone hides document

## 14.2 Low-Allocation In-Memory Layout

```go
type VersionRef struct {
    CommitTS        uint64
    SourceEpoch     uint64
    SourceSeq       uint64
    SourcePartition uint32 // interned partition id
    Flags           uint8
    PayloadRef      uint32
}
```

Rules:

1. append-only per-doc timeline
2. binary search by deterministic ordering key
3. payload arena/slab reuse

## 14.3 Retention Modes

Mode `snapshot_strict` (default):

1. never drop history only because `safe_ts` advanced
2. structural dedup only
3. if memory budget exceeded -> backpressure with `SF_RATE_LIMITED`

Mode `bounded_history` (optional):

1. requires explicit monotonic `gc_cutoff_ts`
2. prune versions `< gc_cutoff_ts` while keeping one anchor below cutoff
3. snapshot guarantee only for `read_ts >= gc_cutoff_ts`

## 14.4 Quantitative Performance Gates

For TiDB integration parity lanes, emulator should enforce/validate:

1. aux-index `search_payload.global_candidate_topk <= 5000`
2. aux-index response payload `<= 4 MiB`
3. aux decode path `allocs/op <= 1.10x` baseline
4. copy amplification `<= 1.25`
5. added p95 query latency budget `<= 2 ms` (integration benchmark profile)

---

## 15. Static Policy Emulation

Emulator must provide deterministic static policy configuration for three planes.

## 15.1 Query Policy

Must define:

1. limits:
   - `max_query_payload_bytes`
   - `max_in_list_elements`
   - `max_predicate_ast_nodes`
   - `max_topk`
   - `max_page_depth`
2. feature flags:
   - `supported_aggregates`
   - `supports_vector_projection`
3. governance:
   - `default_page_token_ttl_ms`
   - `default_search_context_ttl_ms`
   - `token_signature_algorithm`

## 15.2 Ingest Policy

Must define:

1. limits:
   - `max_batch_bytes`
   - `max_entries_per_batch`
   - `max_inflight_batches_per_stream`
   - `max_chunk_bytes`
   - `max_inflight_snapshot_chunks`
2. governance:
   - `default_mutation_resume_token_ttl_ms`
   - `default_snapshot_upload_resume_token_ttl_ms`
   - `mutation_idempotency_retention_ms`
   - `snapshot_resume_retention_ms`
   - `token_signature_algorithm`
   - reconnect with non-zero cursor requires `resume_token`
3. snapshot formats:
   - `supported_payload_encodings`
   - `supported_compression_codecs`

## 15.3 Control Policy

Must define:

1. schema support:
   - `supported_schema_update_policies`
   - `requires_strict_drop_fencing`
   - `supports_index_sort_spec`
   - `supports_max_result_window`
   - `supports_lifecycle_policy`
   - `supports_canonical_schema_semantics`
2. limits:
   - `max_concurrent_schema_ops_per_table`
   - `max_concurrent_schema_ops_per_namespace`
3. analyzer support:
   - `supported_builtin_analyzers`
   - `default_builtin_analyzer`
   - `supports_analyzer_overrides`
   - `supported_analyzer_override_keys`
   - `analyzer_override_value_grammar`

Control-plane readback must remain authoritative:

1. `GetCollectionSchema` must return resolved `effective_max_result_window` (explicit schema value or control-plane default).
2. `GetSchemaSyncStatus` must echo the same resolved `effective_max_result_window` for convergence/status polling.
3. emulator tests must assert the resolved effective value rather than inferring from raw optional `CollectionSettings.max_result_window`.

If request includes `IndexSortSpec` while `supports_index_sort_spec=false`, fail closed with `SF_UNSUPPORTED_OPTION`.
If request includes `max_result_window` while `supports_max_result_window=false`, fail closed with `SF_UNSUPPORTED_OPTION`.
If request includes `lifecycle_policy` while `supports_lifecycle_policy=false`, fail closed with `SF_UNSUPPORTED_OPTION`.
If request includes typed canonical schema semantic fields while `supports_canonical_schema_semantics=false`, fail closed with `SF_UNSUPPORTED_OPTION`.

---

## 16. Error Model and Precedence

## 16.1 Canonical Error Source

`docs/specs/core/error-codes.md` is authoritative. Emulator emits only canonical `SF_*` codes.
Every emitted `ErrorStatus` must carry non-empty `correlation_id`; when caller supplied `RequestMeta.correlation_id`, emulator must echo that value on the error envelope, and otherwise it must generate a service correlation ID.

## 16.2 Deterministic Precedence

When multiple violations coexist, pick one by tier order:

1. access (`SF_UNAUTHORIZED`, `SF_FORBIDDEN`)
2. profile/scope/ownership
3. contract/resource
4. limits/admission
5. runtime/dependency/conflict
6. generic fallback

If multiple candidates in same tier, pick first code in `docs/specs/core/error-codes.md` order.

## 16.3 Emulator-Specific Mapping Examples

1. out-of-scope test namespace/collection -> `SF_FORBIDDEN`
2. malformed framing / invalid token format -> `SF_BAD_REQUEST`
3. token principal/scope mismatch -> `SF_FORBIDDEN`
4. malformed token signature envelope or page-token key-id out of verify overlap -> `SF_BAD_REQUEST`
5. token/query mismatch or expired page token in pagination -> `SF_INVALID_QUERY`
6. strict stale snapshot on Search-Fabric-truth primary/shared query routes -> `SF_VISIBILITY_TIMEOUT`
7. reconnect payload mismatch / batch fingerprint mismatch -> `SF_IDEMPOTENCY_CONFLICT`
8. invalid `TextClause.mode_override` branch/options (`query_expansion` misuse) -> `SF_UNSUPPORTED_QUERY_FEATURE`
9. highlight required while request has no text clauses or on a route outside `COLLECTION_TRUTH_MODE_SEARCH_FABRIC` native-search applicability -> `SF_UNSUPPORTED_QUERY_FEATURE`
10. highlight `explicit_fields` contract mismatch (`require_highlighting=true` with empty list, or `require_highlighting=false` with non-empty list) -> `SF_BAD_REQUEST`
11. highlight `explicit_fields` containing unknown or non-text-searchable fields -> `SF_SCHEMA_VIOLATION`
12. highlight request with only one tag (`highlight_pre_tag`/`highlight_post_tag`) -> `SF_BAD_REQUEST`
13. unbounded `StreamQuery` (`fetch_spec.limit=0`) with explicit `fetch_spec.track_total_hits.enabled=true` -> `SF_BAD_REQUEST`

---

## 17. Test-Only API Contract (Detailed)

## 17.1 `POST /__test__/inject_writes`

Input fields:

- required `collection_ref`
- required `writes[]`
  - each write carries `doc_id`, `commit_ts`, `op_type`, `payload`
  - optional per-write `source_seq`
- optional `idempotency_key`
- optional `batch_id`
- optional `stream_identity` (`source_partition`, `source_epoch`)
- optional `schema_generation_token`
- optional `resume_cursor` (`source_partition`, `source_epoch`, `source_seq`, `commit_ts`, `resume.token`)
- optional `policy` (`durability`, `visibility`, `undeclared_field`, `continue_on_error`, `ingest_mode`, `schema_evolution_wait_ms`)

Behavior:

- normalize to canonical `MutationBatch` and admit through the same ingest machinery, but this facade remains a non-canonical synthetic setup mode rather than a production Flow-B contract.
- caller-supplied `commit_ts` is legal only on this test-only facade.
- omitted `batch_id` -> `blake3("batch:" + collection_ref + normalized writes)`.
- omitted `idempotency_key` -> `blake3("request:" + collection_ref + normalized writes)`; this derived request identity MUST stay distinct from the derived batch identity.
- omitted `stream_identity` -> `source_partition="__test__/direct/<batch_id>"`, `source_epoch=1`.
- omitted per-write `source_seq` -> 1-based array index.
- omitted `policy` -> emulator static ingest defaults (`durability=WAL_CHUNK_DURABLE`, `visibility=ASYNC`, `undeclared_field=REJECT`, `ingest_mode=STREAMING`); callers that need non-default policy branches SHOULD set them explicitly.
- omitted `schema_generation_token` is allowed only for synthetic single-generation convenience fixtures with no replay/idempotency/DDL-boundary correctness assertions.
- when this convenience omission is used, emulator MUST deterministically resolve the current active schema-generation token for `collection_ref` from the collection catalog snapshot at test-facade admission time and stamp that exact token onto the normalized canonical `MutationBatch`.
- if no active schema-generation token exists, or if the fixture is intended to validate replay/idempotency/DDL-boundary behavior where generation choice must be caller-controlled, the facade MUST reject instead of guessing.
- retry/idempotency-focused cases SHOULD provide explicit `batch_id` and `stream_identity` rather than relying on convenience derivation.

## 17.2 `POST /__test__/inject_changefeed_batch`

Supports:

1. canonical `entries[]`

Required identifiers:

1. `collection_ref` is required.
2. Canonical `entries[]` payload MUST provide explicit `batch_id`, `stream_identity`, and `schema_generation_token` for replay-safe idempotency tests, CDC replay, historical replay, and DDL-boundary validation.
3. When CDC-style partition identity matters, caller SHOULD provide either explicit `stream_identity` or `source_table{keyspace_id?,table_id}` so emulator can derive stable `source_partition = keyspace:<keyspace_id>/table:<table_id>` or `table:<table_id>`.
4. Retry/idempotency test cases must provide explicit `batch_id`; derived IDs are not a substitute for replay test control.
5. Golden fixtures for this endpoint MUST use canonical `entries[]` only.
6. Omitting `schema_generation_token` is valid only for synthetic active-generation convenience fixtures; emulator MUST deterministically synthesize it from the current active collection catalog snapshot at admission time, and the omission is not valid for historical replay correctness tests.

## 17.3 `POST /__test__/inject_progress`

1. update per-modality applied ts (monotonic)
2. recompute `safe_ts = min(enabled modality applied ts, including metadata_aux only when that optional modality is present)`
3. direct `safe_ts` override is forbidden on this endpoint; any deterministic fault injection MUST still operate through per-modality applied-ts overrides so `safe_ts` remains derived.

Non-monotonic updates -> `SF_VERSION_CONFLICT`.

## 17.4 `GET /__test__/readiness?namespace_id=&collection_id=`

Returns:

1. `safe_ts`
2. `applied_ts_metadata_search`
3. `applied_ts_semantic_search`
4. optional `applied_ts_metadata_aux` (field presence, not `0` sentinel, indicates metadata-aux modality is enabled)
5. active schema version/generation snapshot

Validation:

1. `namespace_id` and `collection_id` query parameters are mandatory.
2. Missing/invalid parameters -> `SF_BAD_REQUEST`.

## 17.5 Optional `POST /__test__/fault_profile`

Inject deterministic runtime branches:

1. `SF_RATE_LIMITED`
2. `SF_BACKEND_UNAVAILABLE`
3. `SF_QUERY_TIMEOUT`
4. Optional visibility override branch (test-only fault injection):
   - `force_applied_ts_metadata_search`: optional uint64 override for metadata-search applied progress on the target collection.
   - `force_applied_ts_semantic_search`: optional uint64 override for semantic-search applied progress on the target collection.
   - `force_applied_ts_metadata_aux`: optional uint64 override for metadata-aux applied progress when that modality exists.
   - overrides MUST be scoped by (`namespace_id`, `collection_id`) and MUST be explicitly clearable (`clear_forced_visibility=true`) to restore derived behavior.
   - `safe_ts` and `effective_read_ts` remain derived from the effective per-modality applied timestamps after override; emulator MUST NOT expose direct `safe_ts` or `effective_read_ts` overrides because those would create states the production contract cannot enter.
   - this branch is only for deterministic test fault scenarios and MUST NOT be enabled in production profile.

## 17.6 Optional `POST /__test__/set_gc_cutoff`

1. enabled only in `bounded_history`
2. cutoff monotonic per collection

---

## 18. Observability and Diagnostics

Required metrics:

1. `sim_ingest_batches_total{result}`
2. `sim_ingest_first_reject_total{code}`
3. `sim_decode_latency_ms`
4. `sim_apply_latency_ms`
5. `sim_schema_refresh_total{result}`
6. `sim_safe_ts_lag_ms{namespace_id,collection_id}`
7. `sim_mvcc_versions{namespace_id,collection_id}`
8. `sim_snapshot_sessions{state}`
9. `sim_token_validation_fail_total{reason}`
10. `sim_query_page_depth_reject_total`
11. `sim_guardrail_reject_total{reason}`

Debug endpoints:

1. `/debug/schema?namespace_id=...&collection_id=...`
2. `/debug/mvcc?namespace_id=...&collection_id=...&doc_id=...`
3. `/debug/watermark?namespace_id=...&collection_id=...`
4. `/debug/snapshot_session?id=...`
5. `/debug/policy?plane=...`

---

## 19. Validation Matrix

## 19.1 Ingest

1. framing rules (`ROW* -> PROGRESS? -> BARRIER?`) accept/reject deterministically.
2. stream scope invariants (`source_partition/source_epoch/source_seq`) enforcement.
3. entry idempotency same-key/same-payload replay.
4. entry idempotency same-key/different-payload conflict.
5. batch idempotency same `batch_id` + same fingerprint replay.
6. same `batch_id` + different fingerprint conflict.
7. first-error stop and `rejection_context.first_rejected_*` cursor accuracy.
8. reconnect token/cursor coherence and expiry behavior.
9. mandatory-token reconnect rule for non-zero cursor.
10. undeclared-field policy branch behavior (`REJECT`/`IGNORE`) and deterministic default (`UNSPECIFIED -> REJECT`).
11. `continue_on_error=true` enforces row-only framing (presence of `PROGRESS_MARK`/`BARRIER_MARK` -> `SF_BAD_REQUEST`).
12. mutating ingest idempotency-key presence (`MutationBatch.meta.idempotency_key` non-empty) enforcement.
13. ingest operation-budget rejection (`max_entries_per_batch`) emits `SF_TOO_MANY_OPERATIONS` with required details (`limit_scope=max_entries_per_batch`, `configured_limit`, `requested_value`).

## 19.2 Snapshot

1. start idempotency with `client_session_token`.
2. upload init first-frame requirement.
3. upload complete-frame contract: exactly one `SnapshotUploadComplete` EOF frame and no post-complete chunk frames.
4. `SnapshotUploadComplete.total_chunks_sent` must match acknowledged progress (`next_expected_seq - 1`).
5. `CommitSnapshotSync` before accepted upload EOF completion (`COMPLETED` ACK) must be rejected with conflict semantics.
6. chunk sequence monotonic and replay behavior.
7. checksum/root-hash format validation.
8. root-hash mismatch rejection semantics.
9. checksum computed over transmitted compressed payload bytes.
10. parallel upload stream rejection for one session.
11. lease expiry transition and post-expiry rejection.
12. mutating snapshot idempotency-key presence: `StartSnapshotSync.meta.idempotency_key`, `SnapshotUploadInit.meta.idempotency_key`, `CommitSnapshotSync.meta.idempotency_key`, and `AbortSnapshotSync.meta.idempotency_key` must be non-empty.
13. session-to-collection binding checks for upload/commit/abort/progress (`sync_session_id` bound collection must equal request `CollectionRef`).

## 19.3 Query

1. strict/bounded/best-effort mode-coupled parameter constraints plus readiness evaluation (`STRICT: effective_read_ts=min(read_ts,safe_ts), read_ts>0, max_staleness_ms=0`; `BOUNDED: effective_read_ts=min(read_ts,safe_ts), read_ts>0, max_staleness_ms>0`; `BEST_EFFORT: effective_read_ts=safe_ts, read_ts=0, max_staleness_ms=0`).
2. aux strict stale -> fallback signal.
3. projected-query strict/bounded stale -> fallback result on `ExecuteProjectedQueryOrFallback`, while primary/shared Search-Fabric-truth routes fail closed as `SF_VISIBILITY_TIMEOUT`.
4. deterministic ordering via normalized `fetch_spec.sort_keys` (default `score desc` + `doc_id asc`, forced trailing `doc_id asc` when omitted).
5. unified success payload consistency checks (`hits`, `aggregations`, and coexistence).
6. total-hits semantics (`fetch_spec.track_total_hits` omitted default `threshold=10000` for unary and bounded stream queries, `StreamQuery limit=0` special case `omitted => enabled=false`, explicit unbounded-stream `enabled=true` rejection, `enabled`/`threshold` branches, present-but-empty `TrackTotalHitsSpec{}` rejection, and `success.total_hits` relation/value contract).
7. projection-driven vector policy + projected vector-field enforcement.
8. continuation token binding, misuse/expiry rejection, strict first-page `search_context.token` rejection (no exception), and depth governance under `page_window` oneof (`first_page` / `continuation_page` / `composite_page`) contract.
9. `max_page_depth` rejection emits `SF_TOO_MANY_OPERATIONS` with required details (`limit_scope=max_page_depth`, `configured_limit`, `requested_value`).
10. `max_result_window` window-limit rejection emits `SF_TOO_MANY_OPERATIONS` with required details (`limit_scope=max_result_window`, `max_result_window`, `requested_offset`, `requested_limit`, `requested_window`).
11. dynamic vector payload/window budget rejection emits canonical details (`limit_scope=dynamic_payload_budget|dynamic_result_window`, `effective_limit`, `requested_bytes`/`requested_window`).
12. page-token malformed/key-rotation-out-of-window branch rejects with `SF_BAD_REQUEST`.
13. requested projected vector fields must reject any projected-hit path with missing/invalid vector payload as `SF_SCHEMA_VIOLATION` (no partial omission); policy-disabled projection support remains `SF_UNSUPPORTED_OPTION`.
14. `fetch_spec.vector_projections[*].target_dimensions` follows MRL truncation contract: `unset/0 -> schema dimensions`, `0<target_dimensions<=schema dimensions` deterministic truncation, `target_dimensions>schema dimensions` fail-closed (`SF_VECTOR_DIMENSION_MISMATCH`).
15. `page_window` oneof branch exclusivity (`first_page` / `continuation_page` / `composite_page`) and mode-coupled `limit` semantics (`ExecuteQuery limit=0` only when `search_payload.aggregations` is non-empty or `fetch_spec.track_total_hits` is explicitly configured via `enabled`/`threshold`, `StreamQuery` unbounded `limit=0`, `ExecuteQuery` continuation `limit>0`, `StreamQuery` continuation `limit>=0`) enforcement.
16. `StreamQuery` must support both first-page (offset-free `first_page`) and resume (`continuation_page.next_page_token` non-empty) modes, must re-emit dictionaries on each RPC attempt's first hit-bearing success frame, and the dedicated stream request shape must not expose unary-only search-context/PIT controls or aggregation carriers.
17. `StreamQuery` non-terminal success frames must carry hits and emit non-empty `success.pagination.next_page.token`; zero-hit success is reserved for the terminal no-more-hits completion frame, which keeps the token empty.
18. `StreamQuery` non-terminal success frames must keep `warnings[]` and `total_hits` empty, while only the terminal success frame may populate them.
19. `fetch_spec.text_options` highlighting admission, `explicit_fields` gating (`require_highlighting=true` => non-empty explicit text-field list, `require_highlighting=false` => empty list), default tags, and malformed one-tag-only rejection.
20. recursive aggregation-tree admission (sibling-name uniqueness, node-shape constraints, and deterministic canonical ordering).
21. bucket/top-hits execution semantics (`terms`/`histogram`/`date_histogram`/`range`/`composite` + per-bucket `top_hits`) remain deterministic across repeated runs.
22. composite pagination semantics verify source-order comparator, reader-side `after_key` pruning, broker-side k-way merge, and `BucketResult.composite.after_key.fields` emission from the last bucket when more pages exist.
23. composite continuation guardrails verify `after_key` key-set/type checks, `max_composite_sources` policy reject path, and mandatory bound `composite_page.next_page_token` with exact token-cursor equality for non-empty `after_key`.
24. PIT-equivalent search-context issuance/continuation semantics (`success.pagination.search_context.token`, keep-alive extension, committed-manifest pin, and token-snapshot coherence).
25. relational query-route split:
   - `ExecuteProjectedQueryOrFallback(ProjectedQueryRequest)` preserves projected-route fallback ownership, overlay admission rules, and projected-route observability (`relational_execution_digest`)
   - `ExecutePrimaryQuery(PrimaryQueryRequest)` remains success/error only
26. projected-route best-effort overlay exception uses `snapshot_read_ts` as authoritative execution snapshot and binds continuation/search-context tokens to canonical overlay digest plus `snapshot_read_ts`.
27. query-digest coverage includes routing/vector-projection/rescore/nested/inner-hits controls, while cursor payloads such as `CompositeAggregation.after_key` remain excluded and digest-stable across continuation pages.
28. search-context natural-expiry-only semantics (no public `CloseSearchContext` RPC), bounded keep-alive refresh, and canonical malformed/scope mismatch error mapping.

## 19.4 Control

1. apply fencing plus fenced/unfenced drop semantics (`expected_prev_schema_version`, token, monotonic flag when drop/apply carries `fence`).
2. schema-update-policy transitions and query safety.
3. control idempotent replay/conflict behavior.
4. ownership/profile enforcement before mutation.
5. default metadata/name-binding catalog coverage: `PutCollectionMetadata`, `ListCollections`, `GetCollectionMetadata`, `PutNamespaceMetadata`, `ListNamespaces`, and `GetNamespaceMetadata` are part of the default emulator scope, including caller-kind rules, name-binding resolution, descriptor completeness/lifecycle projection, and catalog page-token behavior.
6. control static-policy gates for `supports_index_sort_spec`, `supports_max_result_window`, `supports_lifecycle_policy`, and `supports_canonical_schema_semantics`.
7. `FieldSchema.config.field_definition` and typed-schema constraint validation.
8. `CollectionSettings.max_result_window` validation (`> 0` when present).
9. `CollectionSettings.lifecycle_policy` validation (time-field typing, transition monotonicity, retention/order invariants, and ownership applicability).
10. control overload branch returns retryable `SF_RATE_LIMITED` with `retry_after_ms`.
11. allocator replay precedence after successful admission: identical retry after later tombstone, allocator-policy removal, caller-profile change, or schema-generation rollover still returns the originally recorded `realized_doc_ids[]` plus the originally recorded `schema_generation_token` and an `allocator_receipt` bound to that same successful result tuple before any fresh-admission recheck, and conflicting replay payload still fails as `SF_IDEMPOTENCY_CONFLICT` even when fresh evaluation would now hit allocator watermark/conflict checks.
12. allocator replay conflict/failure separation: conflicting canonical payload on the same replay key fails as `SF_IDEMPOTENCY_CONFLICT`, while failed allocator attempts remain non-`OK` failures or audit records only and never become a second success-shaped replay-truth family.
13. allocator catalog-unavailable tests: existing collection identity with missing/incomplete authoritative allocator admission facts returns `SF_CATALOG_UNAVAILABLE`, and the no-active-schema branch emits `ErrorStatus.context.allocator.catalog_unavailable.schema_bootstrap.schema_bootstrap_state=no_active_schema_generation` plus `catalog_unavailable_reason=schema_bootstrap` / `schema_bootstrap_state=no_active_schema_generation` rather than `SF_RESOURCE_NOT_FOUND`.
14. allocator request-shape tests: mixed explicit/omitted create/insert inputs lower into one ordered `plan.items[]`, explicit plan items remain unique, allocator-optional collections expose `caller_profile` plus positive `max_plan_items_per_request` / positive `max_forward_reservation_jump`, schema admission rejects `COLLECTION_TRUTH_MODE_EXTERNAL + DOC_ID_GENERATION_POLICY_ALLOCATOR_OPTIONAL`, schema admission rejects any allocator caller profile other than `ALLOCATOR_CALLER_PROFILE_ES_WRITE_GATEWAY_ONLY` in the current revision, and over-limit plan cardinality fails as `SF_TOO_MANY_OPERATIONS` with `ErrorStatus.context.allocator.limit_rejection.limit_scope=ALLOCATOR_LIMIT_SCOPE_PLAN_ITEMS`.
15. allocator follow-on-receipt tests: successful `AllocateIds` emits `allocator_receipt`, identical replay re-emits a receipt bound to the same replay-authoritative result tuple, allocator-backed `MutationBatch` must carry that receipt, and missing/forged/mismatched receipts are rejected before ingest row admission.
16. allocator receipt-validation classification tests: caller-mismatched valid receipts fail as `SF_FORBIDDEN`, all other canonical receipt-validation reasons fail as `SF_BAD_REQUEST`, `ErrorStatus.context.allocator.receipt_validation.reason` is authoritative, and `diagnostics["receipt_validation_reason"]` remains only a compatibility mirror.
17. allocator dense-burn tests: high explicit reservation within the admitted forward-reservation window advances `next_doc_id`, later fresh reservation of any skipped lower `doc_id < next_doc_id` fails as `SF_VERSION_CONFLICT` with `ErrorStatus.context.allocator.reservation_conflict.conflict_reason=ALLOCATOR_RESERVATION_CONFLICT_REASON_BELOW_WATERMARK_BURNED_GAP`, over-window jumps fail as `SF_TOO_MANY_OPERATIONS` with `ErrorStatus.context.allocator.limit_rejection.limit_scope=ALLOCATOR_LIMIT_SCOPE_FORWARD_RESERVATION_WINDOW`, identity-space exhaustion fails as `SF_TOO_MANY_OPERATIONS` with `ErrorStatus.context.allocator.limit_rejection.limit_scope=ALLOCATOR_LIMIT_SCOPE_IDENTITY_SPACE_REMAINING`, and failed allocator admissions do not advance watermark.

## 19.5 Mini-CDC

1. DML/DDL same-ts ordering (DML before DDL).
2. fixed-table-set fence behavior.
3. checkpoint monotonicity.
4. startup validation and fail-fast behavior.
5. callback ownership contract (read-only/borrowed).

## 19.6 Error Determinism

1. multi-violation precedence selection follows `docs/specs/core/error-codes.md` order.
2. no non-canonical error code leaks.
3. unknown enum values fail closed by canonical precedence (`SF_BAD_REQUEST` or `SF_UNSUPPORTED_OPTION` by tier), with deterministic first-error selection.
4. minimal `DebugDigestProbe` emulation verifies deterministic branches: success returns digest payload; mismatch returns `409 SF_DEBUG_DIGEST_MISMATCH` with required detail keys (`gateway_normalized_digest`, `service_attested_digest`, `digest_algorithm`); `caller_canonical_payload` invalid-shape branch rejects with `SF_BAD_REQUEST`; success branch may include caller-vs-gateway `normalization_diffs[]`.
5. transport-deadline determinism verifies RPC deadline handling through transport metadata/policy rather than canonical `RequestMeta` payload fields.

---

## 20. Minimal Implementation Sequence

1. Align ingest ordering/idempotency model to include `source_partition/source_epoch` and `BLAKE3` fingerprint.
2. Add first-error cursor reporting and canonical ingest admission order.
3. Implement policy manager and token service (ingest/query/control).
4. Implement snapshot session manager with lease + init/resume rules.
5. Harden unified query success payload, projection-driven vector contract, and pagination token contract.
6. Harden control-plane fencing and schema-update-policy handling.
7. Introduce the CDC upstream adapter seam and keep all source implementations above the canonical core.
8. Integrate Embedded Mini-CDC as the TiDB-specific `schema_aware` adapter with strict runtime constraints and fail-fast policy.
9. Expand conformance test matrix and perf gates.

---

## 21. Example Configuration

```yaml
emulator:
  enable: true

  api:
    grpc_enable: true
    grpc_listen: "127.0.0.1:19090"

  schema:
    mode: status_api
    tidb_status_addr: "http://127.0.0.1:10080"
    db_table_first_lookup: true
    refresh_interval: "3s"

  ingest:
    max_batch_bytes: 4194304
    max_entries_per_batch: 8192
    requires_resume_token_for_reconnect: true
    mutation_token_ttl_ms: 300000
    policy_default_durability: "WAL_CHUNK_DURABLE"
    policy_default_visibility: "ASYNC"

  query:
    max_topk: 5000
    max_page_depth: 128
    supports_vector_projection: true
    page_token_ttl_ms: 120000

  snapshot:
    lease_ttl_ms: 60000
    max_chunk_bytes: 1048576
    checksum_algorithm: "CRC32C"
    root_hash_algorithm: "BLAKE3"

  retention:
    mode: "snapshot_strict"   # snapshot_strict | bounded_history
    gc_cutoff_ts: 0
    max_total_versions: 5000000
    max_versions_per_doc: 64

  mini_cdc:
    enable: false
    keyspace_id: 4
    ddl_poll_limit: 256
    fixed_table_ids: [42001, 42002]

  test_api:
    enable: true
    listen: "127.0.0.1:18090"
    allowed_namespace_ids: [1001]
    allowed_table_id_ranges: ["42000-42999"]

  fault_injection:
    enable: true
```

---

## 22. Summary

This revised design keeps the emulator small and operable for local/CI usage, while upgrading it to a true **contract emulator**:

1. ingest/query/control behavior is deterministic and canonical.
2. snapshot/reconnect/pagination/token semantics are explicit and testable.
3. TiDB integration fallback and consistency rules are encoded directly.
4. CDC upstream adapters share one canonical seam; Embedded Mini-CDC remains the TiDB-specific specialization with source-verified lifecycle/ordering invariants.
5. error behavior is canonical `SF_*` with precedence stability.

The result is suitable for production-grade integration validation without introducing production-system operational complexity.
