From 07a9e583d01be9d50349646b912f7369c8332c11 Mon Sep 17 00:00:00 2001 From: Shiv Rossi Date: Tue, 29 Sep 2026 15:48:48 -0600 Subject: [PATCH] docs: define storage-first retained history contract --- .../changes/add-retained-history/design.md | 367 ++++++++++++++++++ .../changes/add-retained-history/proposal.md | 125 ++++++ .../specs/retained-history/spec.md | 285 ++++++++++++++ .../changes/add-retained-history/tasks.md | 49 +++ 4 files changed, 826 insertions(+) create mode 100644 openspec/changes/add-retained-history/design.md create mode 100644 openspec/changes/add-retained-history/proposal.md create mode 100644 openspec/changes/add-retained-history/specs/retained-history/spec.md create mode 100644 openspec/changes/add-retained-history/tasks.md diff --git a/openspec/changes/add-retained-history/design.md b/openspec/changes/add-retained-history/design.md new file mode 100644 index 0000000..e024ef2 --- /dev/null +++ b/openspec/changes/add-retained-history/design.md @@ -0,0 +1,367 @@ +# Design: Retained History + +## Status and frozen-artifact rule + +This document is the successor contract for the first retained-history reader. +It is a specification gate and changes no runtime behavior. The four files in +this directory are the only intended diff for COD-544. + +The following approved artifacts remain frozen: + +- `add-ingested-inbox/proposal.md`, `design.md`, `tasks.md`, and its spec; +- `add-forward-poll-cursors/*`; +- `add-sse-replay-cursors/*`. + +This successor does not erase their already-accepted identity, atomic-ingest, +provider-boundary, or bounded-live-replay guarantees. It resolves only the +first retained reader and its page semantics. The runtime implementation must +reference this successor rather than choosing between incompatible frozen +paragraphs. + +## Product and architecture decision + +Iris has two different data paths: + +1. **Acquisition/producer path:** a provider, bridge, or accepted outbound send + produces normalized records and commits them to the configured durable + backend; and +2. **Retained query path:** a reader answers history questions from that backend + without asking a messaging provider for more data. + +The retained query path is authoritative for the data it reports. It is not a +`MessageProvider`, has no send capability, and does not run acquisition as a +query side effect. The first backend may be `LocalFsIngestStore`; the contract +is backend-neutral so a configured remote durable backend can implement the same +reader/page interface later. `iris-core` owns typed values and contracts only; +filesystem, locking, token persistence, and configuration selection stay in +storage/provider/server boundaries. + +## Explicit supersession and compatibility matrix + +The matrix is part of the contract. “Superseded” means the named behavior is +not the contract COD-492 may implement as its first retained reader; it does +not mean the frozen file is edited or that unrelated guarantees disappear. + +| Concern | Frozen/current behavior | Retained-history successor | Owner and transition | +| --- | --- | --- | --- | +| Existing `list_messages` | `GET /messages/{thread_id}` returns `Vec` from the owning provider; `before` is the existing provider/timestamp cursor. A provider owner may be discovered with upstream I/O. | **Preserved as v1 compatibility.** It remains provider-only and keeps its array/input/cursor contract until a separately reviewed deprecation. New history consumers use `list_messages_v2`. | COD-492 owns the v2 operation and a documented v1 deprecation/migration path. No silent behavior change. | +| Existing `list_threads` | `GET /threads` returns provider-backed `Vec` with the existing cursor/order. | **Preserved as v1 compatibility.** It does not begin returning retained records implicitly. `list_threads_v2` is the normal retained query. | COD-492 + generated-surface verification. | +| Existing `list_contacts` | `GET /contacts` returns provider-backed `Vec` with the existing cursor/order. | **Preserved as v1 compatibility.** `list_contacts_v2` is the retained query. | COD-492 + generated-surface verification. | +| First retained public reader | Frozen `add-ingested-inbox/design.md:25–31,142–166` describes a versioned unified provider-plus-inbox page, while `:333–351` describes store-scoped pages and defers global pagination. | **Superseded for the first reader:** v2 pages enumerate one durable-store snapshot only. They do not merge live provider results. The normal noun-based v2 operations make retained history a first-class query rather than an obscure `/inbox` surface. | COD-544 chooses the store-scoped model under the storage-first ruling; COD-492 implements it. | +| Frozen inbox listing paragraphs | `add-ingested-inbox/design.md:319–351` merges providers before limit and permits server-held in-memory snapshot tokens. | **Superseded only for v2 query execution:** one store snapshot is read and limited before return; the snapshot/token is durable and cross-process. Existing provider-only v1 behavior remains unchanged. | COD-492/storage implementation. | +| Identity/order/atomicity | Frozen ingested-inbox contract defines scope-qualified identity, exact tie keys, atomic writes, archives, and replay safety. | **Retained and strengthened for reader use:** the reader indexes those committed records and never body-deduplicates or exposes partial snapshots. Any incompatible identity change requires a new contract. | COD-493/COD-492/storage slices. | +| Inbound acquisition/backfill | Not a query guarantee; source mapping and spool work are separate. | **Explicitly separate:** a producer/sync operation may acquire history and mark coverage, but v2 never invokes it. | Future inbound-acquisition ticket, with COD-495–497 as related source work. | +| Known accepted outbound sends | COD-475 owns persistence, accepted prefixes, audit/persistence failures, and ambiguous external outcomes. | **Shared seam:** a committed COD-475 record becomes visible to the same v2 reader after the durable commit. The reader does not decide whether a send happened or retry it. | COD-475 producer; COD-492 reader. | +| Live publication | COD-494 owns durable committed-event publication. The SSE broker remains process-local and bounded. | **Unchanged ownership:** a retained page is not an SSE replay cursor. COD-494 may publish a committed record separately after resolving it through the reader. | COD-494; `add-sse-replay-cursors` remains frozen. | +| Forward polling | COD-463 owns provider cursor security/principal/pre-I/O requirements. | **Unchanged:** retained v2 cursors are not provider forward-poll cursors and never reinterpret COD-463. | COD-463. | +| Encryption | COD-543 is a non-blocking design follow-up. | **Unchanged:** this contract does not choose key custody or claim encryption. | COD-543. | + +The v2 operation names are intentionally noun-based and source-neutral. They +are a generated compatibility version, not a provider-specific route: + +| Operation | Generated HTTP path | Required inputs | Output | +| --- | --- | --- | --- | +| `list_messages_v2` | `GET /v2/messages/{thread_id}` | exact retained `thread_id`; optional `limit`, `cursor` | `RetainedPage` | +| `list_threads_v2` | `GET /v2/threads` | optional `limit`, `cursor` | `RetainedPage` | +| `list_contacts_v2` | `GET /v2/contacts` | optional `limit`, `cursor` | `RetainedPage` | + +The exact names, paths, schemas, descriptions, and structured errors are to be +declared once in `api/operations.yaml` by COD-492 and projected through Hydra +to HTTP, compiled CLI, and MCP. No handwritten `/inbox` route, source-shaped +`IngestedMessage` schema, or transport-specific cursor is permitted. If Hydra +cannot express this operation shape, the projection gap is a prerequisite to +runtime work; a bespoke adapter is not an acceptable workaround. + +## Retained page contract + +The public v2 envelope is conceptually: + +```text +RetainedPage { + items: Vec, + next_cursor: Option, + snapshot: { + schema_version: "retained-history/v1", + commit_generation: u64, + captured_at: RFC3339 timestamp + }, + coverage: CoverageSummary +} +``` + +`RetainedPage` is a new, noun-based page envelope declared once for the +three operations. It preserves the existing normalized `Message`, `Thread`, +and `Contact` item schemas. The envelope never contains a filesystem path, +provider cache object, source credential, or decoded token. + +`CoverageSummary` contains: + +- `status`: `complete`, `partial`, or `not_acquired`; +- `as_of_generation`: the durable generation represented by the snapshot; +- `oldest_available_at` and `newest_available_at`, when known; +- `attachment_content`: `complete`, `reference_only`, `mixed`, or + `not_applicable`; and +- a bounded machine-readable `reason` when status is not `complete`, such as + `not_yet_synchronized`, `source_limited`, `permission_limited`, or + `retention_limited`. + +`complete` means complete for the declared configured scope, permissions, and +retention boundary, not omniscient source history. A complete scope with no +records is a valid empty result. `not_acquired` or `partial` with no items is +not evidence that the conversation never existed. A store-unavailable error is +an explicit structured error, not a page with an invented empty result. + +For `list_messages_v2`, `thread_id` is an exact canonical retained ID. The +store's thread-owner index resolves it; a missing retained thread returns +`retained_thread_not_found` (or the existing access-denied error where +applicable) without probing providers. It is never converted into an upstream +owner-discovery request. Thread and contact pages enumerate all records visible +in the retained snapshot and preserve their configured `provider_instance` +values. + +The v2 defaults are deterministic: `limit` defaults to 50 and accepts only +1–200. A value of zero or above 200 is a structured invalid-query error before +page execution. The first request omits `cursor`; a later request sends exactly +the returned opaque `next_cursor`. There is no timestamp-only high-water mark. + +## Durable snapshot and cursor lifecycle + +The durable backend owns the snapshot, its ordering index, and its token +lifecycle. The core contract describes operations such as `open_snapshot` and +`read_page` without selecting an I/O mechanism. A first page does the following +atomically with respect to writers: + +1. read one complete committed store state at generation `G`; +2. materialize or pin the three deterministic indexes needed by the requested + operation and filter; +3. persist a snapshot record containing `G`, schema/order version, query/filter + binding, access-scope binding, creation time, expiry, and the page position; +4. return the first page and an opaque cursor for the next position. + +A later page resolves that durable record and reads the same immutable state. +A commit after `G` is not inserted into the open snapshot. A consumer that wants +new data starts a new snapshot; it does not splice current records into an old +page sequence. Each read observes either the complete old state or the complete +new state at a storage commit boundary. + +The v1 lifecycle is fixed: + +- **Token format:** opaque implementation bytes; it is never decoded by a + consumer and does not expose paths, IDs, timestamps, or credentials. The + token binds operation kind, exact filter, ordering version, authorized + access scope, backend identity, and snapshot record. Binding mismatch is a + structured `retained_cursor_invalid` error before a partial page is emitted. +- **Portability:** a valid token is usable by HTTP, CLI, or MCP processes that + share the same configured durable backend and authorized access scope. A + token stored only in a process heap is non-conforming. A one-shot CLI may + return a token that a later invocation reopens. +- **Absolute lifetime:** 15 minutes from snapshot creation. Reads do not extend + this lifetime. Expired, deleted, corrupt, or unavailable snapshot records + return `retained_cursor_expired` (HTTP 409 / equivalent structured CLI/MCP + error) and no items from a newer snapshot. +- **Capacity:** at most 1,024 active snapshot records per backend. When a new + snapshot would exceed that bound, the least-recently-used record is evicted. + Ties are resolved by oldest creation time, then opaque token digest. The + backend may garbage-collect expired records at any time. Capacity eviction + and expiry use the same explicit error; they never fall back to providers. +- **Restart:** the token record and immutable snapshot state survive a process + restart when the configured backend is available. If a backend cannot restore + the token after its own restart, the token is explicitly expired while the + durable message records remain readable through a new snapshot. No process + incarnation is used as a history cursor. +- **Errors:** malformed/binding-invalid cursors are `retained_cursor_invalid`; + expired/evicted/unrestorable cursors are `retained_cursor_expired`; a + missing/corrupt backend is `retained_store_unavailable`. None silently + downgrade to provider I/O or an empty page. + +Snapshot metadata may be compacted or indexed differently by a future backend, +but the externally visible capacity, lifetime, ordering, error, and +cross-process guarantees are the v1 contract. Durable conversation records are +not pruned merely because page tokens expire. + +## Ordering and identity + +### One ordering per operation + +The reader applies the same complete key to every page before slicing by +`limit`: + +- messages in a thread: ascending `(timestamp, message_id)`; +- threads: descending `(last_message_at, source, provider_instance, + thread_id)`; and +- contacts: ascending `(display_name with null before present, source, + provider_instance, source_id, contact_id)`. + +`provider_instance` is included even when it is not part of the old provider +sort, because two same-type configured instances may expose the same local +source ID. UUIDs are compared by their canonical byte order. The cursor stores +the full key and snapshot position, not only a timestamp or local source ID. + +### Retained identity and reconciliation + +The writer/mapper, not the reader, assigns stable identities. The retained +contract preserves the approved installation-scoped model: + +- scope is `(source, installation_id)` from an operator-declared registry; +- thread identity is derived from `(scope, session_id)`; +- message identity is derived from `(scope, session_id, occurrence_id)`; +- contact identity uses `(scope, installation-global actor_id)`; and +- the canonical tuple uses a fixed v1 namespace and length-prefixed fields, + never delimiter concatenation or body text. + +`provider_instance` and `source_id` remain opaque, installation-qualified +values. The retained index also stores a direct thread-owner mapping from the +canonical thread ID to its scope/instance. A query uses that index and the +existing access boundary; it does not call `list_threads` on every provider. + +A source echo or retry with the same scope/session/occurrence and equivalent +canonical payload resolves to the existing message. The same body in two +occurrences remains two messages. A stable identity with a different immutable +payload is a typed conflict and cannot partially update the snapshot. A thread +`last_message_at` is the maximum accepted message timestamp; an exact tie is +resolved by the maximum stable message ID, never arrival time. Archive state is +metadata, preserves messages, and does not make the thread sendable or silently +revive it. + +The following fixed fixture namespace makes the expected scenario IDs concrete +without assigning it as the production namespace: +`00000000-0000-0000-0000-000000000001`. For this fixture only, UUIDv5 names +use the literal ASCII form +`iris.retained-history/fixture/v1|kind|source|installation|key` with no spaces +around separators; `key` is `session` for a thread, `session|occurrence` for a +message, and `actor` for a contact. Production identity remains the +length-prefixed canonical tuple described above. Its expected IDs are: + +| Fixture value | Expected ID | +| --- | --- | +| thread `(telegram, install-a, chat-7)` | `e19600b3-8000-570d-96c2-ee3759283b99` | +| thread `(telegram, install-b, chat-7)` | `cc5e8d2c-008e-5db0-80df-1defa7bb61b0` | +| message `(telegram, install-a, chat-7, event-1)` | `864b5d78-0657-5421-b7d7-562625c5b63a` | +| message `(telegram, install-b, chat-7, event-1)` | `cf39a3b4-9863-5df8-aa28-820a91a51f78` | +| message `(telegram, install-a, chat-8, event-1)` | `8821a977-06d5-55dc-aa44-a7688c515f4f` | +| contact `(telegram, install-a, actor-42)` | `57bf99da-9912-556e-9da8-042d8a8eb276` | +| contact `(telegram, install-b, actor-42)` | `f753ea7f-689b-5ea9-94d4-35de247340ea` | + +The fixture uses body `same body` for the three messages above. The IDs prove +that equal text, repeated occurrence IDs across sessions, and colliding local +session IDs do not determine identity. + +## Attachments and content ownership + +The retained reader returns the existing `Message.attachments` values exactly +as committed. It does not invent an attachment schema or download provider +content while answering a page. + +- An `iris://attachment/{uuid}` reference is content-owned by the configured + durable `AttachmentStore`. The producer must make the bytes and metadata + durable before committing a message that promises `attachment_content: + complete`; the reader may return the reference without reading the bytes. +- A provider URL or legacy pseudo-URL is a reference only. It is preserved as + metadata when permitted by the source contract, but it is not evidence that + Iris retained the bytes. The page coverage reports `reference_only` or + `mixed` rather than claiming local content. +- A missing attachment body is not repaired by provider fetch during a retained + query. A future attachment acquisition/repair slice must be explicit and + must not change the message identity or resend a message. +- Attachment access follows the existing storage/access boundary. COD-544 does + not choose encryption, a new backend, URL authorization, or retention policy. + +## Producer/reader handoff + +### Minimum committed record + +Before a record is visible to v2, the producer commit must contain the +normalized public value and the fields needed for deterministic ownership: + +- canonical `Message`, `Thread`, and any installation-global `Contact` values; +- configured `provider_instance`, source/provenance metadata, and scope; +- message direction as a source-authoritative fact (`is_outbound` is never + inferred from assistant authorship, body, or destination); +- stable session/occurrence/replay identity and canonical hash; +- attachment references plus the declared content coverage; and +- a commit generation/visibility record atomically linked to the values. + +The reader observes only committed records. A reader opened concurrently with a +writer sees the previous complete generation or the next complete generation, +never half of a batch. A matching replay produces no duplicate message or +page-generation change; a conflict produces no reader-visible mutation. + +### COD-475 boundary + +For a known accepted outbound send, COD-475 records the provider-returned +message with its exact body, direction, timestamp, source identity, +provider-instance identity, thread, and supported attachment references. The +next retained snapshot can return it through `list_messages_v2` and update the +thread's `last_message_at`. For a multi-request send, an accepted prefix is +retained even if a later request fails, while the send result and audit/error +semantics remain COD-475's responsibility. + +If the external provider accepted a message but durable persistence fails, +COD-475 must surface that post-send persistence failure without pretending the +send did not happen and without automatically resending. If the provider +outcome is ambiguous, the producer must preserve the ambiguity and apply its +approved reconciliation path; the reader cannot fabricate success. COD-544 +neither implements nor changes those outcomes. + +### Inbound acquisition boundary + +Normal provider synchronization/backfill is a separate future producer slice. +It may write the same retained contract and update `CoverageSummary`, but a +retained page never invokes it. A source that has not acquired a range reports +`not_acquired`/`partial` and its boundary; it does not return a complete empty +archive. COD-495–497 and any later acquisition ticket must name their cursor, +permission, and fixture contracts separately. + +### Other boundaries + +- COD-493 owns source-scoped ingest authorization, registration, and cross-scope + mutation validation. Coverage is not authorization. +- COD-494 owns durable committed-event publication and any lease/claim needed + for the live broker. A page token is not an SSE cursor. +- COD-543 owns encryption-at-rest design. This contract assumes an authorized + backend and does not claim ciphertext. +- COD-463 owns provider forward-poll cursor binding and pre-I/O security. A + retained cursor is not a forward-poll cursor. + +## Concrete synthetic acceptance matrix + +All rows use fixed RFC3339 timestamps and the fixture IDs above. “Provider +calls” means calls made while answering the retained query; the required value +for every row is zero. + +| Scenario | Setup and expected retained values | Page/cursor/error/coverage outcome | +| --- | --- | --- | +| Independent-process read | Process A commits `M-A-1` body `same body` at generation 7. Process B opens the same configured backend after A exits. | First `list_messages_v2` page returns `M-A-1` with exact body/ID and `commit_generation=7`; provider calls `0`; coverage is the producer-declared status. | +| Restart/reopen | A valid cursor references generation 7 and process B restarts/reopens the same backend before 15 minutes. | B resumes the same snapshot and order. If the backend cannot restore its durable token, it returns `retained_cursor_expired` and a fresh snapshot can still return `M-A-1`; it never asks the provider. | +| Upstream unavailable/rate-limited | Retained generation 7 contains `M-A-1`; the configured provider would fail or rate-limit. | v2 still returns the retained item; no upstream call occurs; coverage is `partial`/`source_limited` unless the producer marked the retained scope complete. | +| Matching replay/source echo | The same scoped occurrence and canonical payload arrives twice, body `same body`. | One `M-A-1`, one commit generation/outbox effect; the second write is an idempotent no-op; a page never duplicates it. | +| Same-type instance collision | `install-a/chat-7/event-1` and `install-b/chat-7/event-1` are accepted. | Threads are `T-A` and `T-B`; messages are `M-A-1` and `M-B-1`; each `provider_instance` is distinct; no overwrite or alias. | +| Equal-body distinct events | `install-a/chat-7/event-1` and `install-a/chat-8/event-1` both have body `same body`. | IDs are `M-A-1` and `M-A-2`; both remain in their own thread; body equality does not deduplicate. | +| Atomic old-or-new read | Reader starts at generation 7 while a writer commits a two-message batch at generation 8. | The open snapshot returns all generation-7 values or all generation-8 values according to its creation seam, never one new message without its thread/contact. | +| Mutation during paging | Page 1 opens at generation 7. A new message at timestamp `2026-09-29T00:00:03Z` commits at generation 8 before page 2. | Page 2 on the old cursor excludes the new message and preserves the old complete order. A new snapshot includes it; no timestamp high-water shortcut is used. | +| Token capacity/expiry | Create 1,025 active tokens, or use one after 15 minutes. | The deterministic LRU/expired token returns `retained_cursor_expired` with no partial page and no provider call. Durable messages remain readable from a new snapshot. | +| Missing/not-yet-acquired history | The store has no acquired records for `chat-99` and the producer has not declared that scope complete. | A threads/messages result is empty only with `coverage.status=not_acquired` and `reason=not_yet_synchronized`; it is not a complete empty archive and no provider call is made. | +| Outbound-to-reader handoff | COD-475's accepted send commits `M-OUT` body `reply`, `is_outbound=true`, timestamp `2026-09-29T00:00:02Z`, and an `iris://attachment/...` reference. | A later v2 page returns exact `M-OUT`, thread timestamp, and reference. Attachment coverage is `complete` only if the configured store owns bytes; no send is retried. | +| Accepted prefix/persistence failure | A multi-request send accepts `M-OUT-1`, then fails on request 2; persistence of the accepted prefix reports an error. | COD-475 exposes the actual send/persistence outcome; no reader page fabricates request 2 or resends request 1. Any committed `M-OUT-1` is visible after its commit; otherwise the failure remains explicit. | + +These are specification fixtures, not claims that the runtime already passes +them. Runtime slices must implement the rows through generated boundaries, +not only through private storage assertions. + +## Review and migration gates + +The implementation plan must keep these gates explicit: + +- Do not start COD-492 runtime until COD-544 is reviewed and the v2 page scope + is accepted. +- Do not silently migrate existing v0 LocalFs state or change `IngestBatch` + authentication in this ticket. A storage schema migration, if required, + gets a named predecessor/acceptance gate owned by its implementation slice + and preserves atomic old-state recovery. +- Do not add a provider call, source-specific route, or separate archive to + make a scenario pass. +- Do not mark a page `complete` merely because the store returned zero rows. +- Do not call the bounded SSE replay broker history or reuse its cursor. +- Preserve the generated-surface law: define each v2 operation once in + `api/operations.yaml`, regenerate all committed artifacts, and test an + HTTP/CLI/MCP consumer path. diff --git a/openspec/changes/add-retained-history/proposal.md b/openspec/changes/add-retained-history/proposal.md new file mode 100644 index 0000000..8e39b33 --- /dev/null +++ b/openspec/changes/add-retained-history/proposal.md @@ -0,0 +1,125 @@ +# Proposal: Retained History + +## Status and authority + +This is a successor specification gate for COD-492. It is documentation only: it +adds no Rust implementation, generated artifact, migration, dependency, +configuration, deployment, release, credential, or live-provider behavior. + +The contract executes Shiv's September 28 storage-first ruling recorded on +COD-475 and COD-492: retained-history questions read Iris-managed durable +storage (the configured backend or Iris's default), not a messaging provider. +The existing `add-ingested-inbox`, `add-forward-poll-cursors`, and +`add-sse-replay-cursors` proposals/designs are frozen and remain byte-unchanged. +Where this successor changes the first public reader's scope, it names the +superseded paragraph and the compatibility boundary instead of silently +rewriting an approved artifact. + +## Problem + +Iris already has a durable `IngestStore::apply_batch` write boundary, but the +normalized objects are private to the storage snapshot. The core contract has +no reader, `LocalFsIngestStore` has no public retained-history query, and the +current `list_messages` path resolves a provider and calls upstream +`list_messages` for every read. A process restart or an independently running +CLI/MCP/HTTP process therefore cannot use the accepted durable data as normal +history. + +The frozen ingested-inbox design contains two incompatible descriptions of the +first reader: + +- its ownership/public-surface paragraphs describe a unified live-provider plus + inbox page and a global snapshot; and +- its later pagination paragraphs describe a store-scoped snapshot and defer + global provider-plus-inbox pagination. + +Both cannot be the first cursor contract. Implementing either without an +explicit successor would make a cursor's coverage, restart behavior, and +zero-upstream guarantee unknowable to an LLM consumer. + +## Decision + +The successor contract makes the durable retained store the single history +authority and chooses the following boundary: + +1. **Reader ownership.** A core-owned, transport-neutral retained reader reads + normalized `Message`, `Thread`, and `Contact` values from the configured + durable backend. A shared query service owns composition of retained data, + not any HTTP handler. A retained query never calls a messaging provider, + discovers a thread owner through provider I/O, or synchronizes/backfills as a + hidden side effect. +2. **Normal generated surfaces.** COD-492 adds versioned, noun-based generated + operations `list_messages_v2`, `list_threads_v2`, and `list_contacts_v2`. + They are the normal retained-history list operations across HTTP, compiled + CLI, and MCP, projected once through Hydra. They are not an `inbox` route or + source-shaped resource. Existing v1 `list_*` operations remain an explicit + provider-only compatibility surface until a separately reviewed deprecation + removes them; new consumers use the v2 retained operations. The transition + is additive and declared, never a silent array-to-envelope replacement. +3. **One page model.** Each v2 operation reads one immutable snapshot owned by + the durable backend. The snapshot token is opaque, query-bound, durable + enough to reopen from another supported process, and never backed only by an + exited CLI or an in-memory server cache. A token is valid for at most 15 + minutes and at most 1,024 active snapshots per backend; deterministic LRU + eviction returns a typed expiry error. Expiry never falls back to upstream. +4. **Honest coverage.** A page envelope carries the existing normalized values, + the opaque next cursor, snapshot generation, and a coverage summary. An + empty page is `complete` only when the producer has declared the relevant + retained scope complete; not-yet-acquired or source-limited history is + represented as `not_acquired` or `partial`, not as a complete empty archive. +5. **Identity and ownership.** Retained identities are qualified by the + configured `(source, installation_id)` scope and stable session/occurrence + IDs. Same-type installations and equal-body occurrences remain distinct; + source echo/retry is idempotent by stable identity and canonical payload, not + by body text. A durable thread-owner index routes retained reads without a + provider call. Retained threads are read-only and never become send targets. +6. **Producer handoff.** COD-475 owns accepted outbound persistence and send + outcomes; inbound acquisition/backfill is a separate follow-on. Both write + the same normalized retained records and committed visibility boundary. The + reader does not reimplement sending, infer direction from authorship, or + turn persistence failure into a resend. COD-493 owns ingest authorization, + COD-494 owns committed-event publication, and COD-543 remains non-blocking. + +## Scope + +In scope: + +- the storage-first retained reader contract and its ownership boundaries; +- the v1 compatibility matrix and explicit supersession of the frozen conflict; +- one durable, cross-process snapshot/page/cursor model; +- retained identity, deduplication, ordering, coverage, attachment-reference, + and producer/reader handoff rules; +- concrete synthetic scenarios and a dependency-ordered runtime task plan. + +Out of scope: + +- implementing `IngestedInboxReader`, storage snapshots, migrations, or routes; +- editing any frozen OpenSpec artifact; +- changing `IngestBatch` authentication or choosing a credential/key policy; +- implementing outbound capture, normal inbound synchronization, publication, + SSE replay, forward polling, or encryption at rest; +- deployment, release, tag, package publication, or live-provider validation. + +## Non-goals + +- The process-local SSE replay broker is not retained history and is not a + continuation token for v2 queries. +- A provider cache, timestamp window, source-local archive, or hidden provider + request is not a substitute for the durable reader. +- The v2 contract does not claim complete historical coverage when a source has + not synchronized that interval or permission/retention limits it. +- A retained `Message` with an attachment reference does not promise that Iris + owns bytes unless the reference resolves through the configured durable + `AttachmentStore`; Iris never scrapes a provider URL during a read. +- This proposal does not authorize a schema/data migration. Any migration + requirement is an explicit gate for the implementation slice that owns it. + +## Success criteria + +A reviewer can trace a generated consumer through discovery, first retained +page, next page, process restart, and another process reopening the same token, +while proving zero messaging-provider I/O. The reviewer can also identify the +exact compatibility behavior of current callers, the owner of every side + effect, the complete ordering/tie keys, token capacity/lifetime/expiry, honest +coverage state, attachment ownership, and the boundaries that remain in +COD-475, COD-493, COD-494, COD-543, and the separate inbound-acquisition work. diff --git a/openspec/changes/add-retained-history/specs/retained-history/spec.md b/openspec/changes/add-retained-history/specs/retained-history/spec.md new file mode 100644 index 0000000..cf1f298 --- /dev/null +++ b/openspec/changes/add-retained-history/specs/retained-history/spec.md @@ -0,0 +1,285 @@ +# Retained History Specification + +## ADDED Requirements + +### Requirement: Retained history has one durable, source-neutral reader + +Iris SHALL expose a core-owned, zero-I/O retained-reader contract over the +normalized `Message`, `Thread`, and `Contact` values. A configured durable +backend SHALL implement the reader beside the normalized ingest writer. The +reader SHALL NOT be a send-capable `MessageProvider`, SHALL NOT infer a source +or installation, and SHALL NOT call a messaging provider while answering a +retained query. + +A retained query SHALL read one complete committed storage generation. A +failed write, replay conflict, unavailable backend, or partial state SHALL NOT +be represented as a successful empty page. A matching replay SHALL preserve +its original durable state and SHALL NOT mint another logical message or +commit generation. + +#### Scenario: Independent process reads a committed message + +- **GIVEN** process A commits message `864b5d78-0657-5421-b7d7-562625c5b63a` with body `same body` at generation 7 +- **AND** process A exits +- **WHEN** process B opens the same configured durable backend and runs `list_messages_v2` +- **THEN** B returns the exact message ID and body from the committed generation +- **AND** B makes zero messaging-provider calls +- **AND** the retained thread is not eligible for `send_message` + +#### Scenario: Backend is unavailable + +- **GIVEN** a retained query cannot open its configured durable backend +- **WHEN** a consumer asks for a retained page +- **THEN** Iris returns a structured `retained_store_unavailable` error +- **AND** Iris does not return a fabricated empty page +- **AND** Iris does not fall back to a messaging-provider request + +### Requirement: Normal generated queries use an explicit retained compatibility version + +Iris SHALL define `list_messages_v2`, `list_threads_v2`, and +`list_contacts_v2` once in `api/operations.yaml` and project them through +Hydra to HTTP, compiled CLI, and MCP. The operations SHALL return one +`RetainedPage` envelope containing normalized existing model values, +`next_cursor`, snapshot metadata, and coverage. They SHALL be noun-based and +source-neutral; an `inbox` route, provider-named operation, or source-shaped +`Ingested*` public resource SHALL NOT be required. + +The existing v1 `list_messages`, `list_threads`, and `list_contacts` operations +SHALL retain their current provider-only input/output/cursor behavior until an +explicit, separately reviewed deprecation. They SHALL NOT silently begin +returning retained records, and a v2 array-to-page change SHALL NOT be hidden +behind the old operation name. + +`list_messages_v2` SHALL use the exact retained `thread_id` and SHALL resolve +ownership from durable retained indexes. A missing retained thread SHALL return +`retained_thread_not_found` or the existing access-denied error without probing +providers. + +#### Scenario: Existing v1 caller and new retained caller + +- **GIVEN** a provider-backed v1 caller uses `GET /messages/{thread_id}` with its existing `before` cursor +- **AND** the same durable store contains retained message `864b5d78-0657-5421-b7d7-562625c5b63a` +- **WHEN** the v1 and v2 operations are called +- **THEN** v1 keeps its provider-only array/cursor contract +- **AND** `list_messages_v2` returns a `RetainedPage` from durable storage +- **AND** neither operation silently changes the other's schema or cursor semantics + +#### Scenario: Retained owner lookup does not probe providers + +- **GIVEN** `thread_id` `e19600b3-8000-570d-96c2-ee3759283b99` exists only in retained storage +- **WHEN** `list_messages_v2` reads that thread +- **THEN** it returns the retained messages +- **AND** the retained owner index resolves the thread +- **AND** every messaging provider call count remains zero + +### Requirement: Retained pages have durable, cross-process snapshot cursors + +A first v2 request without `cursor` SHALL open one immutable snapshot of a +complete durable generation and SHALL return its generation in the page +metadata. A later request SHALL use the returned opaque cursor to continue the +same operation, exact filter, access scope, ordering version, backend, and +snapshot. A cursor SHALL NOT be a timestamp-only high-water mark, a provider +forward-poll cursor, an SSE replay cursor, a filesystem path, or a process-heap +pointer. + +The v1 lifecycle SHALL permit at most 1,024 active snapshot records per +backend. A snapshot SHALL expire 15 minutes after creation, without lifetime +extension on read. Capacity eviction SHALL select the least-recently-used +record, with creation time and opaque token digest as deterministic tie-breaks. +A valid token SHALL be reopenable by supported HTTP, CLI, and MCP processes +that share the configured backend and authorization scope, including after a +service restart when the backend restores its durable snapshot record. + +Malformed or binding-invalid tokens SHALL return `retained_cursor_invalid`. +Expired, evicted, deleted, corrupt, or unrestorable tokens SHALL return +`retained_cursor_expired`. Neither error SHALL return a partial page or fall +back to a provider. Durable conversation records SHALL remain readable through +a new snapshot after token expiry. + +#### Scenario: Mutation during paging + +- **GIVEN** page 1 opens at durable generation 7 +- **AND** a writer commits a new message at generation 8 before page 2 +- **WHEN** the consumer requests page 2 with the original cursor +- **THEN** page 2 uses the immutable generation-7 snapshot +- **AND** the new message is absent from that cursor's sequence +- **AND** a new first-page request can observe generation 8 +- **AND** no provider call or timestamp-only recovery shortcut occurs + +#### Scenario: Restart preserves or explicitly expires a cursor + +- **GIVEN** a consumer saves a v2 cursor before a service restart +- **WHEN** another supported process resumes it against the same backend +- **THEN** the cursor continues the same snapshot if its durable token is present and unexpired +- **OR** Iris returns `retained_cursor_expired` with no partial page if the backend could not restore that token +- **AND** a new snapshot can still read the durable message records +- **AND** Iris never asks an upstream provider to repair the cursor + +#### Scenario: Capacity eviction is explicit + +- **GIVEN** 1,024 active snapshot records exist +- **WHEN** a 1,025th snapshot is opened +- **THEN** the deterministic least-recently-used record is evicted +- **AND** a later request using its cursor returns `retained_cursor_expired` +- **AND** the response contains no newer partial items and makes no provider call + +### Requirement: Ordering and identity are deterministic and collision-isolated + +Retained readers SHALL apply one complete ordering key before applying a page +limit: messages by ascending `(timestamp, message_id)`, threads by descending +`(last_message_at, source, provider_instance, thread_id)`, and contacts by +ascending `(display_name with null first, source, provider_instance, source_id, +contact_id)`. A cursor SHALL carry the full position needed to resume these +keys; timestamps alone SHALL NOT determine continuation. + +The retained writer SHALL qualify identities by the configured +`(source, installation_id)` scope. Thread identity SHALL include the source +session ID; message identity SHALL include both session and occurrence IDs; +contact identity SHALL require an installation-global actor ID. Identity +encoding SHALL use the fixed v1 namespace and length-prefixed fields, not body +text or unescaped delimiter concatenation. Source echo/retry with equivalent +canonical payload SHALL be idempotent; an immutable identity with a different +payload SHALL be a typed conflict. Equal bodies in distinct occurrences SHALL +remain distinct. + +A thread's `last_message_at` SHALL be the maximum accepted message timestamp, +with a stable message-ID tie-break. Archive metadata SHALL preserve history +and SHALL NOT silently revive a thread or grant send capability. A reader SHALL +return either a complete old generation or a complete new generation, never a +partial batch. + +#### Scenario: Two same-type installations collide locally + +- **GIVEN** installation A and installation B are both `telegram`, both use local session `chat-7`, and both use occurrence `event-1` +- **WHEN** their batches are retained +- **THEN** A uses thread `e19600b3-8000-570d-96c2-ee3759283b99` and message `864b5d78-0657-5421-b7d7-562625c5b63a` +- **AND** B uses thread `cc5e8d2c-008e-5db0-80df-1defa7bb61b0` and message `cf39a3b4-9863-5df8-aa28-820a91a51f78` +- **AND** their provider instances, contacts, and records do not overwrite one another + +#### Scenario: Equal body and repeated occurrence IDs remain distinct + +- **GIVEN** installation A retains body `same body` for `(chat-7,event-1)` and `(chat-8,event-1)` +- **WHEN** the reader lists both threads +- **THEN** their message IDs are `864b5d78-0657-5421-b7d7-562625c5b63a` and `8821a977-06d5-55dc-aa44-a7688c515f4f` +- **AND** both messages remain visible in their respective threads +- **AND** equal text and a repeated occurrence ID do not cause body deduplication + +#### Scenario: Timestamp tie has stable order + +- **GIVEN** two retained messages have the same timestamp +- **WHEN** a page is generated twice from the same snapshot +- **THEN** the messages have the same ascending message-ID tie order both times +- **AND** insertion/arrival order does not alter the page or cursor + +### Requirement: Coverage is honest and attachments have explicit ownership + +Every retained page SHALL carry a `CoverageSummary` with `status` `complete`, +`partial`, or `not_acquired`, the represented generation, known time bounds, +and attachment-content status. `complete` SHALL mean complete for the +configured scope, access boundary, and declared retention/acquisition range; +it SHALL NOT mean that the upstream source was queried during this request. +A producer that has not acquired a range or is source/permission limited SHALL +mark it `not_acquired` or `partial`. An empty `not_acquired` page SHALL NOT be +interpreted as a complete empty archive. + +The existing `Message.attachments` values SHALL be returned as committed. +`iris://attachment/{uuid}` bytes and metadata SHALL be owned by the configured +durable `AttachmentStore`; provider URLs are references only. The reader SHALL +not dereference provider URLs, scrape unavailable content, or make attachment +availability look complete. Any repair/acquisition path SHALL be separate and +shall not change message identity or resend a message. + +#### Scenario: Missing history is not a false empty archive + +- **GIVEN** no retained record exists for a scope that has not yet synchronized +- **WHEN** a v2 query returns zero items +- **THEN** the page says `coverage.status=not_acquired` +- **AND** its reason identifies `not_yet_synchronized` +- **AND** the consumer cannot mistake the result for a complete empty history +- **AND** no provider request is made + +#### Scenario: Provider outage uses retained coverage + +- **GIVEN** generation 7 contains a retained message but its messaging provider is unavailable or rate-limited +- **WHEN** a v2 query runs +- **THEN** it returns the committed message without provider I/O +- **AND** coverage is `partial`/`source_limited` unless the producer declared the retained scope complete + +#### Scenario: Attachment reference does not promise bytes + +- **GIVEN** a retained message carries an `iris://attachment/123` reference +- **AND** the configured attachment store owns its bytes and metadata +- **WHEN** a v2 page returns the message +- **THEN** it returns the exact reference and reports attachment content as complete +- **AND** the reader need not download the bytes +- **WHEN** a retained message instead carries a provider URL +- **THEN** the reference is preserved only where allowed and coverage reports `reference_only` or `mixed` +- **AND** the reader does not call the provider to fetch it + +### Requirement: Producer and reader handoff preserves accepted outcomes + +Before a record becomes visible to a retained page, its producer commit SHALL +atomically link the normalized message/thread/contact values, configured +provider-instance and provenance metadata, source-authoritative direction, +stable scope/session/occurrence/replay identity, attachment references, and a +commit generation. A matching replay SHALL be a no-op; a conflict SHALL expose +no new reader state. + +COD-475 SHALL own persistence of known accepted outbound sends, including +accepted prefixes from multi-request sends, exact external outcomes, +auditing, persistence failures, and ambiguous-send handling. A committed +accepted outbound message SHALL be readable through the same v2 query; the +reader SHALL NOT reimplement sending, infer `is_outbound` from assistant +authorship/body/destination, interpret persistence failure as permission to +resend, or fabricate an outcome. Normal inbound acquisition/backfill SHALL be +a separate producer slice and SHALL never be hidden inside a query. + +COD-493 SHALL own ingest authorization and scope registration, COD-494 SHALL +own committed-event publication, and COD-543 SHALL remain a non-blocking +at-rest-encryption design. A v2 page cursor SHALL NOT be reused as an SSE +replay or COD-463 forward-poll cursor. + +#### Scenario: Accepted outbound send becomes retained history + +- **GIVEN** COD-475's provider accepts outbound body `reply` at `2026-09-29T00:00:02Z` +- **AND** it commits the normalized message with `is_outbound=true` and an `iris://attachment/...` reference +- **WHEN** a later v2 message page is opened +- **THEN** the exact body, direction, timestamp, thread timestamp, and reference are returned +- **AND** the page is produced from durable storage with zero provider calls +- **AND** the reader does not resend or reinterpret the send + +#### Scenario: Accepted prefix and persistence failure remain explicit + +- **GIVEN** a multi-request send accepts request 1 and fails request 2 +- **AND** persistence of the accepted prefix fails after the external acceptance +- **WHEN** COD-475 reports the operation +- **THEN** it reports the actual send/persistence outcome without claiming request 1 was never dispatched +- **AND** it does not automatically resend request 1 +- **AND** no uncommitted record appears in a retained page +- **OR**, if the prefix committed before the failure was reported, that exact prefix is visible and no duplicate is created + +### Requirement: Runtime work is dependency-ordered and public-boundary verifiable + +The successor SHALL keep core/storage reader, shared query composition, generated +boundary, deterministic consumer acceptance, outbound producer, inbound +acquisition, and committed publication as separate implementation slices with +named predecessors and owners. It SHALL distinguish COD-475, COD-493, COD-494, +COD-543, COD-463, and COD-495–497 from new reader work. A runtime slice SHALL +not silently perform a persisted-state migration or choose an authorization, +credential, encryption, or retention policy that this contract leaves to its +owner. + +Each generated operation SHALL be exercised through the real HTTP, compiled +CLI, and MCP boundaries where supported. Runtime acceptance SHALL include +build/test/strict-Clippy/fmt/codegen-freshness/diff gates, independent-process +read, restart/reopen, zero-provider-call evidence, exact IDs/bodies/order, +coverage states, cursor expiry, and old-or-new atomic visibility. Structural +OpenSpec validation is the only validation applicable to this docs-only change. + +#### Scenario: Generated consumer follows a retained cursor + +- **GIVEN** an agent discovers `list_messages_v2` through generated HTTP, CLI, or MCP metadata +- **WHEN** it requests page 1, passes the returned cursor unchanged, and reopens from another supported process +- **THEN** it receives the same deterministic retained page sequence until the cursor expires +- **AND** it can interpret coverage and structured expiry errors without provider-specific knowledge +- **AND** the implementation evidence includes the real generated boundary rather than only private storage tests diff --git a/openspec/changes/add-retained-history/tasks.md b/openspec/changes/add-retained-history/tasks.md new file mode 100644 index 0000000..62ac31e --- /dev/null +++ b/openspec/changes/add-retained-history/tasks.md @@ -0,0 +1,49 @@ +# Tasks: Retained History + +This file is the living execution plan for the successor contract. Every task +below is unchecked because COD-544 is docs-only. The listed owners and +predecessors are binding planning boundaries; they do not authorize runtime, +migration, credential, release, deployment, or live-provider work from this +specification PR. + +## Contract gate + +- [ ] T1 — **Owner: Iris PM / COD-544; predecessor: Shiv's storage-first ruling.** Review and approve `proposal.md`, `design.md`, and the retained-history spec. Confirm the explicit supersession/compatibility table, the store-scoped v2 page choice, the 15-minute/1,024-token policy, the coverage states, and the synthetic matrix. Leave all frozen sibling OpenSpec files byte-unchanged. + +## Core and durable-reader foundation + +- [ ] T2 — **Owner: COD-492 implementation slice; predecessor: T1 and COD-493's approved scope/identity admission.** In `crates/iris-core/src/` (new retained-history module plus `lib.rs` exports), define the zero-I/O typed contracts for `RetainedPage`, `CoverageSummary`, snapshot metadata, query/filter bindings, opaque cursor errors, deterministic ordering keys, and the `IngestedInboxReader`/durable retained-store seam. Reuse the approved normalized `Message`, `Thread`, `Contact`, and attachment-reference values; do not create a send-capable provider or infer identity from body text. + +- [ ] T3 — **Owner: COD-492 storage slice; predecessor: T2 and an explicit migration decision.** In `crates/iris-storage/src/` and exports, expose complete-generation retained reads beside `IngestStore`, a direct retained thread-owner index, scope-qualified identity/reconciliation, archive/last-message semantics, and durable snapshot records. Implement atomic old-or-new reads and token persistence with 1,024 active records, 15-minute expiry, deterministic LRU eviction, binding checks, and structured expiry/unavailable errors. If current state requires a v0→v1 schema migration, create a separate reviewed migration gate before writing any migration code; do not smuggle it into the reader. + +- [ ] T4 — **Owner: COD-492 query-composition slice; predecessor: T2/T3.** In `crates/iris-providers/src/` (shared query service/module and exports), compose retained records without provider I/O, apply the complete operation-specific sort/limit keys, preserve configured `provider_instance`, resolve retained thread ownership, and return honest coverage. Keep acquisition/backfill out of this service. Add a deterministic zero-provider-call test seam. + +## Generated public reader and consumer evidence + +- [ ] T5 — **Owner: COD-492 public-boundary slice; predecessor: T3/T4 and approved v2 operation names.** Declare `list_messages_v2`, `list_threads_v2`, and `list_contacts_v2` once in `api/operations.yaml` with `RetainedPage` output, `limit`/opaque `cursor` inputs, coverage/snapshot schemas, and structured errors. Regenerate the committed HTTP/CLI/MCP artifacts and wire all three surfaces to the same query service. Keep v1 provider-only operations unchanged and do not add an `/inbox` route. + +- [ ] T6 — **Owner: COD-492 acceptance slice; predecessor: T5.** Add synthetic real-boundary tests through generated HTTP, compiled CLI, and MCP for discovery, first page, continuation, exact normalized values, query-bound cursor binding, zero provider calls, v1 compatibility, malformed/expired cursors, and retained-thread-not-found behavior. Verify `cargo build --all-targets`, `cargo test --all-targets`, strict Clippy, fmt, codegen freshness, and diff checks. + +- [ ] T7 — **Owner: COD-492 consumer/restart slice; predecessor: T6 and T3 durable tokens.** Exercise a writer process followed by an independent HTTP/CLI/MCP reader, close/reopen the process, restart the backend/service, and page a mutation-stable snapshot. Prove old-or-new atomic visibility, token capacity/expiry, no provider fallback, and honest `complete`/`partial`/`not_acquired` coverage through the generated surfaces. + +## Producer and downstream handoff boundaries + +- [ ] T8 — **Owner: COD-475; predecessor: the reviewed retained record seam from T2/T3.** Adapt accepted outbound results to the shared retained writer: exact provider-returned message/thread/contact identity, `is_outbound` authority, accepted prefixes, source-echo idempotency, attachments, thread timestamps, persistence/audit failures, and ambiguous outcomes. Do not reimplement send behavior in COD-492 and never turn persistence failure into an automatic resend. + +- [ ] T9 — **Owner: separate inbound-acquisition ticket (COD-495–497 or an explicitly created successor); predecessor: T2/T3 and source-specific permission/fixture decisions.** Define and implement normal synchronization/backfill as a producer that writes the shared retained contract and updates coverage. It must not be invoked by a retained query and must distinguish complete, partial, not-acquired, and permission-limited history. This ticket is intentionally not bundled into COD-544 or COD-492's reader. + +- [ ] T10 — **Owner: COD-494; predecessor: T2/T3 and the retained commit record.** Implement committed-event scanning/publication and any multi-process lease/claim needed for live delivery. Resolve records through the retained reader before handoff. Keep the bounded process-local SSE broker and its frozen cursor semantics separate from durable history; no v2 page token is an SSE cursor. + +## Security, attachments, and migration gates + +- [ ] T11 — **Owner: COD-493; predecessor: T2's typed scope/access inputs.** Implement the approved source-scoped authorization and cross-scope mutation checks. Bind retained cursor access to the existing authorization scope without inventing a new principal/key policy. Prove unauthorized reads/writes fail before data/provider I/O and keep secret/message content out of diagnostics. + +- [ ] T12 — **Owner: storage/attachment owner; predecessor: the approved retained record contract and COD-543's independent design.** Verify `iris://attachment/{uuid}` references against the configured durable `AttachmentStore`, preserve reference-only behavior for provider URLs, and define any content repair separately. Do not add a backend, encryption scheme, scraping path, or message resend here. + +- [ ] T13 — **Owner: the implementation slice that owns persisted-state evolution; predecessor: explicit schema review.** If a state/index/token schema migration is needed, specify versioning, interruption recovery, old-state readability, operator reconciliation, and secret-safe errors in a separate reviewed change. COD-544 authorizes no migration execution. + +## Verification and release boundary + +- [ ] T14 — **Owner: each implementation PR; predecessor: its own code slice.** Run `cargo build --all-targets`, `cargo test --all-targets`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --all -- --check`, `cargo run --locked -p iris-codegen --bin iris-codegen -- check`, and `git diff --check`. Exercise the nearest generated public boundary and record exact outputs; private storage tests alone are insufficient. + +- [ ] T15 — **Owner: COD-544 reviewer; predecessor: all four artifacts.** Run `openspec validate add-retained-history --strict` if the CLI is available. If unavailable, structurally verify the four required files, every requirement/scenario, the supersession matrix, and `git diff --check`. No runtime test, deployment, release, live-provider, or encryption claim is implied by this docs-only gate.