diff --git a/API.md b/API.md index 0b25728..f8e1b20 100644 --- a/API.md +++ b/API.md @@ -970,10 +970,13 @@ Retrieve a ranked feed. |---|---|---|---| | `user_id` | `u64` | — | User for personalization (optional) | | `profile` | `string` | `"for_you"` | Ranking profile name | -| `limit` | `u32` | `20` | Max results | +| `limit` | `u32` | `20` | Max results (clamped to 1000) | +| `similar_to` | `u64` | — | Seed item for "more like this": with `profile=related`, sources candidates by ANN nearest-neighbor over this item's embedding | +| `region` | `string` | — | Target region (cluster mode only; rejected with `400` in standalone) | ``` GET /feed?user_id=123&profile=trending&limit=25 +GET /feed?profile=related&similar_to=42&limit=20 ``` **Response:** @@ -991,11 +994,18 @@ GET /feed?user_id=123&profile=trending&limit=25 ] } ], - "total_candidates": 1000 + "total_candidates": 1000, + "region": null, + "unavailable_shards": ["group-2"] } ``` -The `signals` field is omitted when empty. +The `signals` field is omitted when empty. `region` is the region the feed was +served from in cluster mode (`null` standalone). `unavailable_shards` is present +**only** when a cross-shard read was degraded — it lists the shard groups that +could not be reached, so a partial page (fewer items, lower `total_candidates`) +is never silently indistinguishable from a complete one. It is omitted on a +complete read. #### `GET /search` @@ -1005,7 +1015,8 @@ Text search with optional personalization. |---|---|---|---| | `query` | `string` | — | Search text (**required**) | | `user_id` | `u64` | — | User for personalization (optional) | -| `limit` | `u32` | `20` | Max results | +| `limit` | `u32` | `20` | Max results (clamped to 1000) | +| `region` | `string` | — | Target region (cluster mode only; rejected with `400` in standalone) | ``` GET /search?query=jazz+piano&user_id=123&limit=10 @@ -1024,11 +1035,59 @@ GET /search?query=jazz+piano&user_id=123&limit=10 "semantic_score": 0.92 } ], - "total_candidates": 50 + "total_candidates": 50, + "region": null, + "unavailable_shards": ["group-2"] } ``` -`bm25_score` and `semantic_score` are omitted when not applicable. +`bm25_score` and `semantic_score` are omitted when not applicable. `region` and +`unavailable_shards` behave exactly as on [`GET /feed`](#get-feed): `region` is +the serving region in cluster mode (`null` standalone), and `unavailable_shards` +is present only when a cross-shard read was degraded (partial page) and omitted +on a complete read. + +#### `POST /vector_search` + +Pure k-NN ANN probe over the item content vector slot — **no** profile scoring, +fusion, or diversity. This is the recall-measurement / raw nearest-neighbor +surface (used by the `tidal-stress --verify-recall` harness). + +**Request body:** + +```json +{ + "vector": [0.013, -0.41, 0.22], + "k": 10, + "ef_search": 200 +} +``` + +| Field | Type | Default | Description | +|---|---|---|---| +| `vector` | `f32[]` | — | Dense query vector; must match the item content slot's dimensionality (**required**) | +| `k` | `u32` | `10` | Number of nearest neighbors to return (clamped to 1000) | +| `ef_search` | `u32` | — | Optional per-request HNSW beam-width override (the recall/latency knob); omitted = the slot's configured default | + +**Response:** + +```json +{ + "items": [ + {"entity_id": 42, "distance": 0.018}, + {"entity_id": 17, "distance": 0.041} + ], + "region": null, + "unavailable_shards": ["group-2"] +} +``` + +Each match carries `entity_id` and `distance` (L2-squared distance from the query +vector, lower = more similar; for the L2-normalized vectors tidalDB stores this +lies in `[0.0, 4.0]` and is monotonic with cosine distance), ordered closest-first. +`region` is always `null` here (the probe serves locally — standalone, or merged +across a node's hosted shard groups in cluster mode). `unavailable_shards` is +present only when a cross-shard probe was degraded (partial nearest-set). ### Health Endpoints @@ -1055,6 +1114,35 @@ The cluster server serves a superset document that also includes the `/cluster/*` and `/sharded/*` routes. See [docs/guides/server-deployment.md](docs/guides/server-deployment.md). +### Cluster Endpoints + +These routes exist **only on cluster (region) nodes** — they are absent from the +standalone server. Status routes are read-only; the rebalance verbs require an +admin API key. Full operational detail is in +[docs/guides/server-deployment.md](docs/guides/server-deployment.md) and +[docs/runbooks/kubernetes.md](docs/runbooks/kubernetes.md). + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/cluster/status` | Aggregated cluster view — leader, relay high-water-mark, per-region lag, and a per-shard-group `shards` array | +| `GET` | `/cluster/status/local` | This node's local view — its hosted shard groups and which leaderships it holds | +| `POST` | `/cluster/shards/{id}/transfer` | Transfer shard group `{id}`'s leadership to a named replica (fenced promote; body `{"region": ""}`) | +| `POST` | `/cluster/shards/{id}/replicas` | Add or remove a replica of shard group `{id}` (body `{"action": "add"\|"remove", "name": ..., "grpc_addr": ..., "http_addr": ...}`; addrs required for `add`) | + +The cluster node also exposes the membership/recovery verbs `POST /cluster/join`, +`/cluster/promote`, `/cluster/members{,/remove}`, `/cluster/partition`, `/cluster/heal`, +`/cluster/catchup`, `/cluster/reseed`, and `/cluster/reconcile`, plus the sharded +data surface `POST /sharded/{items,embeddings,signals}` and `GET /sharded/{feed,search}`. + +### Cluster-Node-Only Data Endpoints + +| Method | Path | Purpose | +|---|---|---| +| `POST` | `/hardnegs` | Record a hide hard-negative for `(user, item)` (body `{"user_id": ..., "item_id": ...}`); `204 No Content`. Cluster-node-only — converges via the `/cluster/reconcile` LWW snapshot | + +> `POST /vector_search` (documented above) is served by **both** the standalone +> and cluster servers. + **`GET /health` response:** ```json diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 17c8212..ab224f3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -255,7 +255,7 @@ This matches how ScyllaDB uses USearch in production and how Weaviate and Qdrant User interest is not a single vector. Averaging engagement embeddings across topics ("hiking," "cooking," "cars") produces a centroid that represents none of them (PinnerSage, KDD 2020). Instead, each user's preference is represented as up to `K_MAX=10` interest cluster centroids, maintained online by the database as signals arrive. -**As built** (`entities/multi_preference.rs`, design in `docs/research/multi-vector-preference.md`): clusters are maintained by **online sequential k-means with a DP-means threshold split** — a new interaction either updates its nearest cluster (per-cluster adaptive EMA, `alpha = base / (1 + ln(count + 1))`) or, if it exceeds the split threshold and `K_MAX` is not reached, opens a new cluster; at the cap the nearest cluster absorbs it (no eviction). Per-cluster *importance* composes the canonical forward-decay kernel (`signals/decay.rs`), anchored to each engagement's event timestamp, so stale interests fade. At query time the `for_you` path selects the top-`M` clusters by current importance, issues `M` ANN queries **sequentially** (`candidate_gen::ann_candidates_multi`; the loop is parallelizable but not yet parallelized), and merges by **best (min) distance** — not by score; the personalization boost is the max cosine over **all** the user's clusters. Stage-3 re-ranking is unchanged. Users below `COLD_START_N=5` interactions fall back to a **single** adaptive-LR vector (`entities/preference.rs`) — the documented cold-start tier. +**As built** (`entities/multi_preference.rs`, design in `docs/research/multi-vector-preference.md`): clusters are maintained by **online sequential k-means with a DP-means threshold split** — a new interaction either updates its nearest cluster (per-cluster adaptive EMA, `alpha = base / (1 + ln(count + 1))`) or, if it exceeds the split threshold and `K_MAX` is not reached, opens a new cluster; at the cap the nearest cluster absorbs it (no eviction). Per-cluster *importance* composes the canonical forward-decay kernel (`signals/decay.rs`), anchored to each engagement's event timestamp, so stale interests fade. At query time the `for_you` path selects the top-`M` clusters by current importance, issues `M` ANN queries **sequentially** by design (`candidate_gen::ann_candidates_multi`; parallelizable — that optimization is deferred), and merges by **best (min) distance** — not by score; the personalization boost is the max cosine over **all** the user's clusters. Stage-3 re-ranking is unchanged. Users below `COLD_START_N=5` interactions fall back to a **single** adaptive-LR vector (`entities/preference.rs`) — the documented cold-start tier. **Aspirational:** PinnerSage *proper* uses **medoids** (actual item embeddings, not maintained centroids) computed by **offline Ward hierarchical clustering**. That batch tier is disqualified for an embeddable single-node DB today. The shipped `Tag::Preference` per-cluster row already carries the `anchor_ts` / `importance_at_anchor` decay state a periodic in-process medoid recluster would reset, so *that* field-level change needs no migration; the recluster's other input — a bounded per-user interaction-embedding window — lands as an additive `Tag::PreferenceWindow` row (reserved in `storage/keys.rs`, not yet populated), never a rewrite of existing rows. diff --git a/CHANGELOG.md b/CHANGELOG.md index ad93967..8d0f5f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,45 @@ All notable changes to tidalDB will be documented in this file. ### Added +**Multi-vector user preference modeling + ANN candidate-gen (M12) — a warm user is many interests, not one averaged vector: per-user preference clusters drive a top-M ANN fan-out in `for_you`** + +- **Online preference clustering (`entities/multi_preference.rs`).** A warm user + (≥ `COLD_START_N = 5` interactions) maintains up to `K_MAX` preference clusters + built by online sequential k-means with a DP-means threshold split: a new + engagement updates its nearest cluster (per-cluster adaptive EMA) or, past the + split threshold and under the cap, opens a new cluster; at the cap the nearest + cluster absorbs it. Per-cluster *importance* composes the canonical forward-decay + kernel anchored to each engagement's timestamp, so stale interests fade. +- **Top-M ANN fan-out.** At query time `for_you` selects the top-`M` clusters by + current importance, issues `M` ANN queries (`candidate_gen::ann_candidates_multi`), + and merges by best (min) distance; the personalization boost is the max cosine + over all clusters. Users below the cold-start threshold keep the single + adaptive-LR vector (`entities/preference.rs`). Design: `docs/research/multi-vector-preference.md`. + +**Idle-readiness + TLS scale-up (m12p5–m12p6) — followers converge readiness on an idle cluster, and elasticity is proven over REAL mTLS on k8s** + +- **Idle-readiness convergence (m12p5).** The leader heartbeat now carries its live + frontier (`leader_last_seq`), so a caught-up follower flips `/health` Ready on an + idle cluster instead of stalling until the next status poll or ship. Cert SAN + wildcard widened for scale-to-5. +- **TLS scale-up (m12p6).** A real `kubectl scale 3→5` exercised seed-join over + mTLS on k8s (kind) for the first time: joiners flip Ready in ~13s via the + idle-readiness heartbeat, auto-promote to Voter, reach full content parity at + lag 0, with zero acked DATA loss across scale-down. Fixes span a six-bug chain — + `https://` seed scheme, rustls `CryptoProvider` install order, headless seed + Service, cold-handshake poll timeout, two-tier cert-manager PKI, and the + `grpc_tls_for` CA fallback for a not-yet-in-topology joiner. Persists the HNSW + graph and skips a suspect graph on reseed-pending close. + +**Sharded ingestion (m12p4) — scatter-gather across shard groups with cross-shard unified reads** + +- **Scatter-gather pool + cross-shard reads.** Writes hash-route across a 3-group + `shards:` topology; reads unify across groups (L4). Ran REAL on kind with a + 2-generator load job. Fixed an HTTP/2 204 forward-relay bug (a synthesized JSON + body on a 204 relay triggered an h2 `RST_STREAM`). Confirmed with data: at fixed + per-pod CPU, full-placement sharding scales failover, not write throughput; + the ≥2.5×-and-≥5,000/s scaling target remains Ref-A/k3s-pending. + **Index tuning + recall/memory at the production shape (m12p3) — the G2 work: per-query `ef_search` is now honored, the brute-force crossover scales with dimensionality, and the HNSW recall/latency/memory frontier is measured at 1536-D with a real exact oracle** - **Per-query `ef_search` override — now real.** `UsearchIndex::search` / diff --git a/CLAUDE.md b/CLAUDE.md index 7483e3f..b2c8e12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ A single-node-first, embeddable Rust database for the **personalized content ranking problem**. Replaces the 6-system stack (Elasticsearch + Redis + Kafka + feature store + vector DB + ranking service) with a single process, single query interface, and single operational model. -**Status:** Implemented — M0–M10 shipped (embeddable engine + multi-region cluster mode). This repository is a standalone Cargo workspace: the engine is the `tidaldb` crate at `tidal/`, with `tidal-net/`, `tidal-server/`, and `tidalctl/` as workspace siblings and example consumers under `applications/`. Pre-1.0 — APIs are stable for shipped features, but breaking changes are possible before 1.0. See [CHANGELOG.md](CHANGELOG.md) for milestone history and [docs/planning/ROADMAP.md](docs/planning/ROADMAP.md) for status and known gaps. +**Status:** Implemented — M0–M12 shipped (embeddable engine + multi-region cluster mode). M11 (Enterprise-Grade Cluster) closed all nine phases 2026-06-13; M12 (Vector Retrieval at production shape) shipped the recall/ANN/index-tuning G1/G2 work, sharded ingestion, cluster elasticity (idle-readiness, TLS scale-up), and multi-vector preference modeling. This repository is a standalone Cargo workspace: the engine is the `tidaldb` crate at `tidal/`, with `tidal-net/`, `tidal-server/`, and `tidalctl/` as workspace siblings and example consumers under `applications/`. Pre-1.0 — APIs are stable for shipped features, but breaking changes are possible before 1.0. See [CHANGELOG.md](CHANGELOG.md) for milestone history and [docs/planning/ROADMAP.md](docs/planning/ROADMAP.md) for status and known gaps. ## Find Your Guide diff --git a/README.md b/README.md index dfb8da0..413be3d 100644 --- a/README.md +++ b/README.md @@ -300,7 +300,8 @@ Milestones completed: - Storage engine, WAL, entity store, signal ledger - RETRIEVE query: candidate retrieval, filtering, scoring, diversity, pagination -- Vector index (USearch HNSW) with adaptive filtered search +- Vector index (USearch HNSW) with adaptive filtered search; ANN candidate generation in RETRIEVE with honored per-query `ef_search` +- Multi-vector user preference modeling (per-user interest clusters with decayed importance) - 25 built-in ranking profiles - BM25 full-text search (Tantivy) + hybrid RRF fusion - Creator search and creator profiles diff --git a/ai-lookup/features/query-language.md b/ai-lookup/features/query-language.md index 09ae458..3b59aea 100644 --- a/ai-lookup/features/query-language.md +++ b/ai-lookup/features/query-language.md @@ -13,6 +13,7 @@ The query interface is a single operation that encapsulates candidate retrieval, - SEARCH: keyword + semantic + hybrid retrieval - SIGNAL: engagement event write-back (closes the feedback loop in the same transaction) - All queries accept: FOR USER, USING PROFILE, FILTER, DIVERSITY, LIMIT +- RETRIEVE also accepts SIMILAR TO (`similar_to` — a seed item for "more like this") and an optional `ef_search` ANN-recall/latency override - Filters are composable — any combination is valid **File Pointer:** `VISION.md:47-57` @@ -56,6 +57,7 @@ SIMILAR TO @item_id FOR USER @user_id USING PROFILE related FILTER unseen +EF_SEARCH 128 # optional: tune ANN recall vs. latency per query LIMIT 10 ``` diff --git a/ai-lookup/services/entities.md b/ai-lookup/services/entities.md index b467c9a..d7aadd7 100644 --- a/ai-lookup/services/entities.md +++ b/ai-lookup/services/entities.md @@ -10,6 +10,7 @@ Entities are the nodes of the system. Three types: Items (content), Users, and C **Key Facts:** - Items have metadata, embeddings, and signals — signals are typed timestamped streams, not fields - Users have preferences, histories, and relationships — living profiles that update continuously +- A user's taste is **two-tier**: cold-start users (`< 5` interactions) carry a single adaptive-LR **preference vector** (K=1); warm users carry **multiple preference clusters** (one centroid per coherent interest) - Creators are linked to Items and have their own embeddings (aggregated from catalog) - Relationships are first-class edges between entities (weighted, directional, traversable) @@ -19,7 +20,12 @@ Entities are the nodes of the system. Three types: Items (content), Users, and C Items enter via the WRITE path with metadata + embedding. A signal ledger is initialized at zero. Cold start exploration budget is applied automatically. Items are immediately queryable after commit. -Users accumulate implicit preference vectors from engagement history. Preference vectors update on every signal write (like, skip, hide, completion). +Users accumulate implicit taste from engagement history, updated on every positive-engagement signal write (like, completion, etc.). The model has two tiers, gated on the user's total positive interaction count (`COLD_START_N = 5`): + +- **Cold start (`< 5` interactions):** a single **preference vector** (`entities/preference.rs`) blended via an adaptive learning rate (`alpha = base / (1 + ln(1 + count))`) and L2-normalized — the K=1 case. +- **Warm (≥ 5 interactions):** multiple **preference clusters** (`entities/multi_preference.rs`). Each positive engagement is assigned to its nearest centroid by cosine; a DP-means threshold (τ, default `0.55`) opens a new cluster when nothing is similar enough, capped at `K_MAX = 10` (over the cap, the engagement is assigned to the nearest centroid, never evicted). Each cluster carries its own adaptive LR and a forward-decayed importance (default half-life 30 days). On crossing the threshold the single vector seeds cluster 0, so the cold taste is never discarded. + +This keeps a user who engages with hiking, cooking, and cars from collapsing into one averaged centroid that represents none of them. Creators are entities with their own embeddings derived from their item catalog. Creator-level signals include engagement rate, posting frequency, and follower count. diff --git a/ai-lookup/services/ranking-profiles.md b/ai-lookup/services/ranking-profiles.md index a16ac4c..abb9786 100644 --- a/ai-lookup/services/ranking-profiles.md +++ b/ai-lookup/services/ranking-profiles.md @@ -11,7 +11,7 @@ A ranking profile is a named, versioned bundle that fully specifies how a query - Profiles are schema-level declarations, not application code. The app names a profile; the database executes the whole pipeline. - The same profile operates over different candidate sets (global / category / social graph / cohort) depending on its candidate strategy and the query's filters. - Personalization (`for_you`, `following`, `related`, `notification`, `date_saved`) requires `FOR USER` context — pass `.for_user(uid)` on the query. Without it the executor cannot read the user's preference vector / saved-state and either degrades to non-personalized scoring or, for `date_saved`, returns an error. -- **Preference-vector personalization only updates from signals declared `.positive_engagement(true)`.** `signal_with_context(..)` folds the item embedding into the user's taste vector only for positive-engagement signal types; negative signals (skip/hide/block) update seen-state and hard-negatives but do not pull the taste vector toward the item. Declare engagement signals accordingly or `for_you` will not learn. +- **Preference personalization only updates from signals declared `.positive_engagement(true)`.** `signal_with_context(..)` folds the item embedding into the user's taste only for positive-engagement signal types; negative signals (skip/hide/block) update seen-state and hard-negatives but do not pull the taste toward the item. Declare engagement signals accordingly or `for_you` will not learn. For a **warm user** (≥ 5 interactions) the engagement is assigned to the **nearest preference cluster** (or opens a new one past the DP-means threshold); for a cold-start user it blends the single preference vector. At query time `for_you` fans out over the user's **top-M clusters by current decayed importance** (`DEFAULT_TOP_M ≈ 3`), issues one ANN query per cluster centroid, and merges by best (min) distance — see [Entities](./entities.md). - Built-in defaults below are read straight from `tidal/src/ranking/builtins.rs`. Where a value is intentionally application-tuning territory, it is described qualitatively rather than invented. **File Pointers:** `tidal/src/ranking/builtins.rs` (the 25 built-ins), `tidal/src/ranking/profile.rs` (`RankingProfile`, `DiversitySpec`, `Sort`, `Boost`, `CandidateStrategy`), `VISION.md:43-55`. @@ -51,6 +51,7 @@ A ranking profile is a named, versioned bundle that fully specifies how a query ### Notes on specific built-ins - **`trending` / `cohort_trending` and the `view`/`share` boosts.** The `Sort::Trending` formula (`view_vel + 2·share_vel` over 24h) is the ordering authority on the global path and *replaces* the boost loop, so the executor skips boosts there (no double-count). The same `view`/`share` velocity boosts are not dead weight: the cohort rescore path (`rescore_with_cohort`) consumes `profile.boosts` directly to re-score against a cohort's ledger. Keep the boost weights mirroring the formula so cohort ordering matches global ordering. +- **`SignalRanked` candidate generation is served from a cached per-signal-type top-K** (`signals/ledger/hot_top_k.rs`). Because the same decay `lambda` multiplies every entity of a signal type by the same factor, time decay never changes the relative order — so the cached top-K stays valid until the **next signal write**, not the next clock tick. The O(N) ledger rebuild is throttled (sub-millisecond and always fresh under 50k entries; at most one rebuild per second above that) to keep the read p99 off the scan path. - **`for_you` boosts vs. preference match.** Beyond the `Hot { gravity = 1.5 }` recency curve, `for_you` boosts decayed `view` (weight 1.0), decayed `like` (weight 2.0), and 24h `share` velocity (weight 1.5), layered on top of the user-preference-vector match. The 10% exploration injection is what keeps it from collapsing into a filter bubble. - **`search` has `sort = None` on purpose.** The fused RRF (BM25 + ANN) score from the search executor is the primary ordering signal; the profile only adds a small `view`/`like` quality overlay (weights 0.5 / 0.8) so relevance stays in charge. Exploration is `0.0` so results are deterministic for a given query. Set diversity yourself via `SearchBuilder::diversity(..)`. - **Three sort modes have no built-in profile shortcut.** `MostFollowed` and `CreatorEngagementRate` rank **creators** (not items) and need a creator-scoped candidate strategy plus a `follow` signal the generic schema does not guarantee; `Rising` (1h/24h view-velocity acceleration ratio) overlaps `trending`/`hot` and is sensitive to the exact window pair. All three are reachable via a custom schema profile (`SchemaBuilder::ranking_profile(..).sort(..)`); only the named-convenience built-in is omitted. @@ -63,7 +64,7 @@ The candidate strategy decides *which* items the profile even considers before s |----------|--------------|-----------------------| | `Scan` (`sort_field`) | Population scan over the item keyspace; default for built-ins (`sort_field = "created_at"`) | most population + sort-coverage profiles | | `Relationship` | Sources candidates from the user's relationship graph (followed creators) | `following`, `notification` | -| `Ann { slot, limit }` | k-NN over an embedding slot (set via the query's `.similar_to` / `Search`'s `.vector`) | custom; `related` uses the seed embedding via the query | +| `Ann { slot, limit }` | k-NN over an embedding slot (set via the query's `.similar_to` / `Search`'s `.vector`); the per-query `ef_search` override (m12p3) trades recall for latency on this path | custom; `related` uses the seed embedding via the query, `for_you` over the user's preference clusters | | `SignalRanked { signal, window }` | Pre-orders candidates by a signal's windowed aggregate | custom | | `Hybrid` | Fuses lexical + vector retrieval | the `search` query path (RRF fusion in the search executor) | | `CohortTrending` | Cohort-scoped signal aggregation | reached via `.cohort("name")` with `cohort_trending` | diff --git a/docs/README.md b/docs/README.md index f220b3d..cb46f3f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,7 +9,7 @@ ARCHITECTURE, API, QUICKSTART, CODING_GUIDELINES, thoughts) live at the ## Component specs — `specs/` -The authoritative component specifications (status: Implemented, M0–M8). +The authoritative component specifications (status: Implemented, M0–M12). | # | Spec | # | Spec | |---|------|---|------| @@ -24,8 +24,8 @@ The authoritative component specifications (status: Implemented, M0–M8). ## Planning — `planning/` -- [**ROADMAP.md**](planning/ROADMAP.md) — milestones M0–M11, phase status, known gaps -- [**roadmap-to-cluster.md**](roadmap-to-cluster.md) — adopted M11 plan: gap analysis + phase specs taking the multi-process cluster from experimental to enterprise-grade (m11p1–p5 ✅; p6–p9 planned), grounded in the 2026-06-10 live stress-test baselines +- [**ROADMAP.md**](planning/ROADMAP.md) — milestones M0–M12, phase status, known gaps +- [**roadmap-to-cluster.md**](roadmap-to-cluster.md) — adopted M11 plan: gap analysis + phase specs taking the multi-process cluster from experimental to enterprise-grade (all nine phases m11p1–p9 ✅, 2026-06-13), grounded in the 2026-06-10 live stress-test baselines - [PRODUCT_ROADMAP.md](planning/PRODUCT_ROADMAP.md) · [architecture-review.md](planning/architecture-review.md) · [roadmap-cohort-analysis.md](planning/roadmap-cohort-analysis.md) · [site-cohort-analysis.md](planning/site-cohort-analysis.md) - Per-milestone phase/task archive: `planning/milestone-0,1,2,3,5,7,8,9,10,11,p/` diff --git a/docs/guides/build-a-feed-app.md b/docs/guides/build-a-feed-app.md index 7d373a4..a9ff1d9 100644 --- a/docs/guides/build-a-feed-app.md +++ b/docs/guides/build-a-feed-app.md @@ -602,9 +602,11 @@ always-current HTTP reference is the served spec at `GET /openapi.json` (unauthe > **On cluster mode — be honest with yourself.** tidalDB's cluster mode is > **experimental**. It now has a **multi-process** mode (`--region`, one process > per region, real gRPC peering, real process isolation), plus the single-process -> dev/demo default. Even multi-process mode is *not* quorum-acked HA: writes are -> leader-durable only and failover is operator-driven (`/cluster/promote`), not -> automatic. It also replicates **global signals only** — `user_id` / `creator_id` +> dev/demo default. Multi-process mode is quorum-acked HA: writes are +> **quorum-acked durable by default** (majority-committed before ack) and leadership +> failover is **automatic** (Raft-style election — no operator intervention), with +> **elastic seed-join** membership for adding nodes without editing the topology. It +> also replicates **global signals only** — `user_id` / `creator_id` > contexts are rejected on the cluster `/signals` path, so the personalized swipe > loop (section 4) is a **single-node** feature today (the cross-region escape > hatch for user-scoped hides is `/hardnegs` + `/cluster/reconcile`). Ship the For diff --git a/docs/guides/server-deployment.md b/docs/guides/server-deployment.md index b8f6c06..3ddccba 100644 --- a/docs/guides/server-deployment.md +++ b/docs/guides/server-deployment.md @@ -287,6 +287,7 @@ You can load `/openapi.json` into any OpenAPI viewer (Swagger UI, Redoc, Stoplig | `POST /signals` | yes | `204` | Body: `{ "entity_id", "signal", "weight", "user_id"?, "creator_id"? }`. | | `GET /feed` | yes | `200` | `?user_id=&profile=for_you&limit=20`. | | `GET /search` | yes | `200` | `?query=&user_id=&limit=20`. | +| `POST /vector_search` | yes | `200` | Pure k-NN. Body: `{ "vector": [, ...], "k"?, "ef_search"? }`. | `limit` is clamped to `1000` at the trust boundary. Request middleware: 30 s timeout (`408`), 100 max in-flight (`429`), 2 MB body cap (`413`), and an `x-request-id` echoed on every response. diff --git a/docs/ops/prometheus-alerts.yaml b/docs/ops/prometheus-alerts.yaml index fb08ea2..f2c8b60 100644 --- a/docs/ops/prometheus-alerts.yaml +++ b/docs/ops/prometheus-alerts.yaml @@ -210,3 +210,54 @@ groups: annotations: summary: "Self-driving heal not converging" description: "{{ $value }} peer(s) have been mid-heal for 10m. The heal loop retries through breaker resets automatically; a peer stuck this long is a real partition or a dead node — investigate the link, do NOT re-issue heal by hand." + + - alert: TidalDBClusterPeerShipStall + # A peer stops accepting batches (its acked frontier flatlines) while + # flushed events queue behind it: deriv() is the gauge-correct way to + # detect a flat acked seqno (rate()/increase() are invalid on gauges). + expr: deriv(tidaldb_cluster_peer_acked_seqno[2m]) == 0 and tidaldb_cluster_peer_ship_queue_depth > 0 + for: 2m + labels: { severity: critical } + annotations: + summary: "Replication to a peer has stalled with events queued" + description: "Peer {{ $labels.peer_shard }} stopped accepting batches while events queue behind it (partition, dead peer, or paused sender). Check /cluster/status and the self-heal loop." + + - alert: TidalDBClusterQuorumLag + expr: (tidaldb_cluster_relay_last_seq - tidaldb_cluster_relay_durable_seq) > 10000 + for: 2m + labels: { severity: critical } + annotations: + summary: "Quorum commit index is far behind the leader frontier" + description: "{{ $value }} events between the leader's flushed frontier and the quorum commit index (threshold 10000). A majority of the replica set is not confirming durability (down/partitioned followers, or follower apply throughput exhausted). ack=quorum writes will 503; the bodies name the laggards." + + - alert: TidalDBClusterDivergenceQuarantine + # 1 = the node fenced itself from the data plane after detecting a + # divergent suffix. Since m11p5 it auto-reseeds on next boot; a persistent + # latch means the reseed is not completing. + expr: tidaldb_cluster_divergence_quarantined == 1 + for: 2m + labels: { severity: critical } + annotations: + summary: "A node has quarantined itself for a divergent suffix" + description: "A node fenced itself from the data plane after detecting a divergent suffix. Since m11p5 it auto-reseeds on its next boot; if the latch persists, the reseed is not completing — check tidaldb_cluster_reseed_required and the snapshot path." + + - alert: TidalDBClusterReseedPending + # Durable reseed-marker latch. Expected briefly after a quarantine or a + # behind-a-compacted-leader restart; a persistent latch means the snapshot + # fetch is failing. + expr: tidaldb_cluster_reseed_required == 1 + for: 10m + labels: { severity: warning } + annotations: + summary: "A node has a reseed pending but has not completed it" + description: "A node has latched the reseed marker but has not completed a snapshot reseed in 10m. Expected briefly after a quarantine or a behind-a-compacted-leader restart; a persistent latch means the snapshot fetch is failing (no reachable leader, capability gate, or staging fault)." + + - alert: TidalDBClusterSnapshotPinForceDrop + # A staged-snapshot retention pin was force-dropped past the hard cap — a + # joiner started a reseed and never released (died mid-fetch). The dropped + # pin protects compaction. + expr: increase(tidaldb_cluster_snapshot_pin_force_drops_total[1h]) > 0 + labels: { severity: warning } + annotations: + summary: "A staged-snapshot retention pin was force-dropped" + description: "A staged-snapshot retention pin was force-dropped past the hard cap in the last hour — a joiner started a reseed and never released (died mid-fetch). The dropped pin protects compaction; the stranded joiner must be re-driven or removed." diff --git a/docs/ops/recovery.md b/docs/ops/recovery.md index cb63abb..a593dbf 100644 --- a/docs/ops/recovery.md +++ b/docs/ops/recovery.md @@ -20,7 +20,7 @@ This is the routing table `kubernetes.md` points operators to for **cluster** in | **Read-SLA collapse** | p99 read latency blows past 10 ms; reads hang. | Was CPU oversubscription, fixed in **rc12** (SEARCH_GATE + parallel scatter + cpu limit 2→3). Confirm running image is rc12+ and that load is spread across **all 3** pods (region-pinned reads, not pinned to one). See [cluster.md](../runbooks/cluster.md). | | **PVC loss / corruption on one pod** | One pod's data dir is corrupt or its PVC is gone. | Delete that pod's **PVC and pod**; it reseeds fresh from the quorum (snapshot install) and converges to lag=0. On the live image the corrupt-PVC case is recoverable this way — no full-cluster action needed. See [cluster.md — reseed](../runbooks/cluster.md#membership-verbs-m11p5--online-add--remove--inspect--reseed). | | **Full-cluster loss / rebuild from backup** | Quorum cannot be restored from surviving pods (multi-node data loss). | Rebuild from object store with `tidalctl restore` (BLAKE3-manifested, per-shard `checkpoint_seq`, proven against real S3). See [disaster-recovery.md](../runbooks/disaster-recovery.md). | -| **Point-in-time recovery (PITR)** | Need to restore the corpus to a specific point in time. | Use the `tidalctl` + DR runbook PITR procedure. See [disaster-recovery.md](../runbooks/disaster-recovery.md). | +| **Restore to a backup's checkpoint** | Need to roll the corpus back to a captured backup. | Arbitrary point-in-time recovery (`tidalctl replay --until `) is **not shipped**. Restore lands you at the backup's `checkpoint_seq` + whatever WAL tail was captured — not an arbitrary instant. Use `tidalctl backup` / `tidalctl restore`. See [disaster-recovery.md — PITR posture](../runbooks/disaster-recovery.md#pitr-posture). | --- diff --git a/docs/planning/ROADMAP.md b/docs/planning/ROADMAP.md index 84eef8c..67b4524 100644 --- a/docs/planning/ROADMAP.md +++ b/docs/planning/ROADMAP.md @@ -37,7 +37,8 @@ A single embeddable database can replace the 6-system content ranking stack by t | M8 | Distributed Fabric | Multi-region, multi-tenant replication keeps agent-memory semantics intact | Hosted tidalDB, cloud/edge deployments, shared agent substrate — **✅ COMPLETE**: in-process primitives + multi-node replication over real gRPC + true multi-process cluster mode (one process per region, real process isolation) with full tier-3 UAT (partition injection via TCP-proxy, clock-skew, rolling-upgrade, runbook verification); G1 + G2 resolved. Post-M8 follow-ups: quorum-ack writes, automatic failure detection / leader election | | M9 | Community Sync & Revocation | Local embeddable profiles can opt into community personalization and safely leave/purge contributions | Community personalization, federated taste graphs, shared feeds — ✅ COMPLETE (2026-06-06) | | M10 | Governance & Agent Rights | Community rules and agent-scoped permissions control what signals influence ranking | User-owned AI personalization at scale, policy-compliant agents — ✅ COMPLETE (2026-06-06) | -| M11 | Enterprise-Grade Cluster | The multi-process cluster becomes a system of record: fast unified log, quorum-durable, self-healing, horizontally scalable, secured, observable, chaos-tested | Paying-customer cluster deployments — **IN PROGRESS**: m11p1 ✅ + m11p2 ✅ + m11p3 ✅ + m11p4 ✅ (automatic failover: Raft-style election + term fencing + divergence quarantine — closes **G5**; v0.9 "Credible HA" wave complete 2026-06-12) + m11p5 ✅ (membership/discovery/elasticity: DNS peers, snapshot+stream reseed, kind-4 membership records, seed join, `k8s/cluster/` — 2026-06-12) + m11p7 ✅ (security hardening: gRPC mTLS default + zero-drop cert rotation, inter-node HTTP TLS + per-node signed tokens, admin audit log, per-principal rate limit — 2026-06-13) + m11p8 ✅ (observability + operations: completed `tidaldb_cluster_*` set incl. breaker/forwards/self-heal on the per-node `/metrics` listener + Grafana cluster row + alert group, request-id/tracing across hops, truthful status, self-driving heal, WAL PITR archival + `tidalctl` backup/restore, rolling-upgrade version handshake + Woodpecker release gate — closes **G-O** — 2026-06-13) + m11p9 ✅ (continuous correctness: new fault classes (disk-full / slow-fsync / asymmetric partition) as REAL faults behind a production-compiled-out `fault-injection` feature, first-class invariant checkers, `tidal-stress` soak with regression gates + JSON summary, a Woodpecker cron nightly chaos+soak pipeline, and a guarantee→test traceability matrix — closes the **G-C** apparatus; the 30-day-green calendar accrues nightly — 2026-06-13) + m11p6 ✅ (sharding × replication + rebalancing: ONE hash-routed + replicated write surface across S shard groups each at RF with its own elected leader; per-group rebalancing verbs (`/cluster/shards/{id}/transfer` + `/replicas`) and a `?shard=` admin selector; tier-3 3×3 kill-node exit gate — only the dead node's leaderships move, reads never stop, zero acked loss; ≥5,000/s + 2.5× scaling Ref-A-pending — 2026-06-13). **ALL NINE PHASES COMPLETE**; the v1.0 bar now waits only on the 30-day-green nightly calendar (m11p9) and the standing Ref-A/k3s throughput re-runs | +| M11 | Enterprise-Grade Cluster | The multi-process cluster becomes a system of record: fast unified log, quorum-durable, self-healing, horizontally scalable, secured, observable, chaos-tested | Paying-customer cluster deployments — **✅ COMPLETE (all nine phases, 2026-06-13)**: m11p1 ✅ + m11p2 ✅ + m11p3 ✅ + m11p4 ✅ (automatic failover: Raft-style election + term fencing + divergence quarantine — closes **G5**; v0.9 "Credible HA" wave complete 2026-06-12) + m11p5 ✅ (membership/discovery/elasticity: DNS peers, snapshot+stream reseed, kind-4 membership records, seed join, `k8s/cluster/` — 2026-06-12) + m11p7 ✅ (security hardening: gRPC mTLS default + zero-drop cert rotation, inter-node HTTP TLS + per-node signed tokens, admin audit log, per-principal rate limit — 2026-06-13) + m11p8 ✅ (observability + operations: completed `tidaldb_cluster_*` set incl. breaker/forwards/self-heal on the per-node `/metrics` listener + Grafana cluster row + alert group, request-id/tracing across hops, truthful status, self-driving heal, WAL PITR archival + `tidalctl` backup/restore, rolling-upgrade version handshake + Woodpecker release gate — closes **G-O** — 2026-06-13) + m11p9 ✅ (continuous correctness: new fault classes (disk-full / slow-fsync / asymmetric partition) as REAL faults behind a production-compiled-out `fault-injection` feature, first-class invariant checkers, `tidal-stress` soak with regression gates + JSON summary, a Woodpecker cron nightly chaos+soak pipeline, and a guarantee→test traceability matrix — closes the **G-C** apparatus; the 30-day-green calendar accrues nightly — 2026-06-13) + m11p6 ✅ (sharding × replication + rebalancing: ONE hash-routed + replicated write surface across S shard groups each at RF with its own elected leader; per-group rebalancing verbs (`/cluster/shards/{id}/transfer` + `/replicas`) and a `?shard=` admin selector; tier-3 3×3 kill-node exit gate — only the dead node's leaderships move, reads never stop, zero acked loss; ≥5,000/s + 2.5× scaling Ref-A-pending — 2026-06-13). **ALL NINE PHASES COMPLETE**; the v1.0 bar now waits only on the 30-day-green nightly calendar (m11p9) and the standing Ref-A/k3s throughput re-runs | +| M12 | Vector Retrieval at Production Shape | Ranking-at-scale steers on measured numbers, not absent ones: ANN candidate generation in RETRIEVE, honored per-query `ef_search`, a recall/latency/memory frontier measured at 1536-D against an exact oracle, sharded ingestion, cluster elasticity, and multi-vector user preference | Relevant, bounded feeds as the corpus grows past the scan cap — **✅ COMPLETE (2026-06-14 / 2026-06-23)**: m12p1 ✅ (read-recall harness: `vector_search_items` k-NN probe + `POST /vector_search` + `tidal-stress --verify-recall` brute-force oracle) + m12p2 ✅ (ANN candidate-gen in RETRIEVE: `for_you` via preference vector, `related` via seed embedding, cached per-signal-type top-K for `trending`; graceful scan-fallback) + m12p3 ✅ (index tuning: per-query `ef_search` honored, dimension-aware brute→HNSW crossover, F16 validated <1%, Int8 rejected) + m12p4 ✅ (sharded ingestion: scatter-gather pool + cross-shard unified reads, 3-group `shards:`) + m12p5 ✅ (idle-readiness: leader heartbeat carries live frontier so followers converge readiness on idle) + m12p6 ✅ (TLS scale-up: real mTLS `kubectl scale 3→5` seed-join over k8s, two-tier cert-manager PKI) + multi-vector user preference (online k-means clusters with DP-means split, per-cluster decayed importance, top-M ANN fan-out; cold-start single-vector fallback). v1.0 bar shared with M11: 30-day-green nightly calendar + standing Ref-A/k3s throughput re-runs | ### Embeddable → Distributed Path @@ -153,9 +154,9 @@ The roadmap now has two tracks: **iknowyou / Aeries: IN PROGRESS (as of 2026-02-24)** — M1–M4 complete. M5 (Communication Brief) is in progress with core implementation live; acceptance validation pending. -**Engine status:** M0–M10 **COMPLETE**. M9 (Community Sync & Revocation) and M10 (Governance & Agent Rights) shipped 2026-06-06 — see *Implementation Status (M9–M10 as-built)* below. +**Engine status:** M0–M12 **COMPLETE**. M9/M10 shipped 2026-06-06; M11 (Enterprise-Grade Cluster, all nine phases) closed 2026-06-13; M12 (Vector Retrieval at production shape) closed 2026-06-14 / 2026-06-23 — see the M11/M12 milestone rows above and *Implementation Status* below. -**Next (engine):** the cross-cutting community-overlay UAT surface — a single RETRIEVE that blends local + community layers and folds membership-epoch / policy-version / purge-watermark inline into `Results.policy_metadata` (the per-phase mechanisms and direct read APIs all exist; this is the unifying query surface). Then a multi-node M9/M10 UAT harness, and the deferred `writer_agent` u16 interning on the WAL v3 envelope. +**Next (engine):** close the v1.0 bar — the 30-day-green nightly chaos+soak calendar (m11p9) and the standing Ref-A/k3s throughput re-runs (≥5,000/s + ≥2.5× single-shard scaling). Deferred follow-ups: the `writer_agent` u16 interning on the WAL v3 envelope and the offline medoid-recluster tier for multi-vector preference. **Next (product):** iknowyou M5 acceptance pass, then M6 Closed Loop (session lifecycle + preference drift validation). --- diff --git a/docs/runbooks/on-call.md b/docs/runbooks/on-call.md index 760914d..db380db 100644 --- a/docs/runbooks/on-call.md +++ b/docs/runbooks/on-call.md @@ -70,7 +70,8 @@ are **accurate but DESIGN-REFERENCE — they are not yet loaded by the live Prometheus.** The rules already cover the must-watch signals (`TidalDBClusterBreakerOpen` on `peer_breaker_state == 1`, `TidalDBClusterCommitIndexStall`, `TidalDBClusterElectionChurn`, -`TidalDBClusterQuorumTimeouts`, `DivergenceQuarantine`, `ReseedPending`). +`TidalDBClusterQuorumTimeouts`, `TidalDBClusterDivergenceQuarantine`, +`TidalDBClusterReseedPending`). **Open infra step:** promote them into the observability stack as a `PrometheusRule` (or vmalert rule file) so they actually page. Until then, on-call watches the diff --git a/docs/specs/00-architecture-overview.md b/docs/specs/00-architecture-overview.md index e7f23cb..a5b4056 100644 --- a/docs/specs/00-architecture-overview.md +++ b/docs/specs/00-architecture-overview.md @@ -1,6 +1,6 @@ # 00 -- Architecture Overview -**Status:** Implemented (M0–M8) +**Status:** Implemented (M0–M12) **Author:** tidalDB Engineering **Date:** 2026-02-20 (spec) · Implemented as of 2026-05-28 **Purpose:** Show how the 14 specs connect. The forest before the trees. diff --git a/docs/specs/01-storage-engine.md b/docs/specs/01-storage-engine.md index c3373c7..8ea88bd 100644 --- a/docs/specs/01-storage-engine.md +++ b/docs/specs/01-storage-engine.md @@ -1,6 +1,6 @@ # Storage Engine Specification -**Status:** Implemented (M0–M8) +**Status:** Implemented (M0–M12) **Author:** tidalDB Engineering **Last Updated:** 2026-05-28 **Prerequisites:** [VISION.md](../../VISION.md), [thoughts.md](../../thoughts.md), [Signal Ledger Research](../research/tidaldb_signal_ledger.md) diff --git a/docs/specs/03-signal-system.md b/docs/specs/03-signal-system.md index e477825..beb2c7c 100644 --- a/docs/specs/03-signal-system.md +++ b/docs/specs/03-signal-system.md @@ -1,6 +1,6 @@ # Signal System Specification -**Status:** Implemented (M0–M8) +**Status:** Implemented (M0–M12) **Authors:** tidalDB Engineering **Date:** 2026-02-20 (spec) · Implemented as of 2026-05-28 **Depends on:** WAL subsystem, Entity Store, Schema Engine diff --git a/docs/specs/05-cohorts.md b/docs/specs/05-cohorts.md index ab48db2..46df184 100644 --- a/docs/specs/05-cohorts.md +++ b/docs/specs/05-cohorts.md @@ -1,6 +1,6 @@ # 05 -- Cohort Specification -**Status:** Implemented (M0–M8) +**Status:** Implemented (M0–M12) **Authors:** tidalDB Engineering **Date:** 2026-02-20 (spec) · Implemented as of 2026-05-28 **Depends on:** Entity Model (02), Signal System (03), Query Engine diff --git a/docs/specs/06-text-retrieval.md b/docs/specs/06-text-retrieval.md index 943f5ae..cd3404d 100644 --- a/docs/specs/06-text-retrieval.md +++ b/docs/specs/06-text-retrieval.md @@ -1,6 +1,6 @@ # Text Retrieval Specification -**Status:** Implemented (M0–M8) +**Status:** Implemented (M0–M12) **Authors:** tidalDB Engineering **Date:** 2026-02-20 (spec) · Implemented as of 2026-05-28 **Depends on:** Storage Engine (01), Entity Model (02), Signal System (03) diff --git a/docs/specs/07-vector-retrieval.md b/docs/specs/07-vector-retrieval.md index 29ff7b9..56a410f 100644 --- a/docs/specs/07-vector-retrieval.md +++ b/docs/specs/07-vector-retrieval.md @@ -1,6 +1,6 @@ # Vector Retrieval Specification -**Status:** Implemented (M0–M8) +**Status:** Implemented (M0–M12) **Author:** tidalDB Engineering **Last Updated:** 2026-05-28 **Depends on:** Storage Engine (01), Entity Model (02), Signal System (03) diff --git a/docs/specs/08-query-engine.md b/docs/specs/08-query-engine.md index 530822f..c995e6a 100644 --- a/docs/specs/08-query-engine.md +++ b/docs/specs/08-query-engine.md @@ -1,6 +1,6 @@ # 08 -- Query Engine Specification -**Status:** Implemented (M0–M8) +**Status:** Implemented (M0–M12) **Authors:** tidalDB Engineering **Date:** 2026-02-20 (spec) · Implemented as of 2026-05-28 **Depends on:** Storage Engine (01), Entity Model (02), Signal System (03), Relationships (04), Cohorts (05), Text Retrieval (06), Vector Retrieval (07) diff --git a/docs/specs/09-ranking-scoring.md b/docs/specs/09-ranking-scoring.md index 6999689..55e2d99 100644 --- a/docs/specs/09-ranking-scoring.md +++ b/docs/specs/09-ranking-scoring.md @@ -1,6 +1,6 @@ # Ranking and Scoring Specification -**Status:** Implemented (M0–M8) +**Status:** Implemented (M0–M12) **Authors:** tidalDB Engineering **Date:** 2026-02-20 (spec) · Implemented as of 2026-05-28 **Depends on:** Signal System (03), Relationships (04), Cohorts (05), Text Retrieval (06), Vector Retrieval (07) diff --git a/docs/specs/10-feedback-loop.md b/docs/specs/10-feedback-loop.md index aefad10..97e5c5d 100644 --- a/docs/specs/10-feedback-loop.md +++ b/docs/specs/10-feedback-loop.md @@ -1,6 +1,6 @@ # Feedback Loop Specification -**Status:** Implemented (M0–M8) +**Status:** Implemented (M0–M12) **Authors:** tidalDB Engineering **Date:** 2026-02-20 (spec) · Implemented as of 2026-05-28 **Depends on:** [Signal System](03-signal-system.md), [Entity Model](02-entity-model.md), [Relationships](04-relationships.md), [Storage Engine](01-storage-engine.md) diff --git a/docs/specs/11-schema.md b/docs/specs/11-schema.md index a3367b6..6e34e28 100644 --- a/docs/specs/11-schema.md +++ b/docs/specs/11-schema.md @@ -1,6 +1,6 @@ # Schema Specification -**Status:** Implemented (M0–M8) +**Status:** Implemented (M0–M12) **Author:** tidalDB Engineering **Last Updated:** 2026-05-28 **Prerequisites:** [02-entity-model.md](02-entity-model.md), [03-signal-system.md](03-signal-system.md), [04-relationships.md](04-relationships.md), [API.md](../../API.md) diff --git a/docs/specs/12-cold-start.md b/docs/specs/12-cold-start.md index b2badb5..7777192 100644 --- a/docs/specs/12-cold-start.md +++ b/docs/specs/12-cold-start.md @@ -1,6 +1,6 @@ # 12 -- Cold Start Specification -**Status:** Implemented (M0–M8) +**Status:** Implemented (M0–M12) **Authors:** tidalDB Engineering **Date:** 2026-02-20 (spec) · Implemented as of 2026-05-28 **Depends on:** [Entity Model](02-entity-model.md), [Signal System](03-signal-system.md), [Relationships](04-relationships.md), [Cohorts](05-cohorts.md), [Feedback Loop](10-feedback-loop.md), [Schema](11-schema.md) diff --git a/docs/specs/13-concurrency.md b/docs/specs/13-concurrency.md index 0bd52ff..9d82636 100644 --- a/docs/specs/13-concurrency.md +++ b/docs/specs/13-concurrency.md @@ -1,6 +1,6 @@ # 13 -- Concurrency Specification -**Status:** Implemented (M0–M8) +**Status:** Implemented (M0–M12) **Authors:** tidalDB Engineering **Date:** 2026-02-20 (spec) · Implemented as of 2026-05-28 **Depends on:** [Storage Engine](01-storage-engine.md), [Signal System](03-signal-system.md), [Feedback Loop](10-feedback-loop.md) diff --git a/docs/specs/14-scale-architecture.md b/docs/specs/14-scale-architecture.md index 17fc596..fe58159 100644 --- a/docs/specs/14-scale-architecture.md +++ b/docs/specs/14-scale-architecture.md @@ -1,6 +1,6 @@ # Scale Architecture Specification -**Status:** Implemented (M0–M8); multi-node cluster mode shipped through M11 — quorum-durable writes (m11p3), automatic election/fencing (m11p4), dynamic membership (m11p5), and **sharding × replication (m11p6): S shard groups, each a replication group at RF with its own elected leader, any gateway hash-routing writes (the §4 Option-C shape), per-group rebalancing verbs.** The cluster `ShardReplica` (`tidal-server/src/cluster/node.rs`) is one group's machinery; a `ClusterNode` hosts a `BTreeMap>`. See [docs/roadmap-to-cluster.md](../roadmap-to-cluster.md) and [docs/planning/milestone-11/phase-6.md](../planning/milestone-11/phase-6.md). Original M8 known gaps G3 (shard-routing hash unification) and G6 (embedding 500→400) remain open. +**Status:** Implemented (M0–M12); multi-node cluster mode shipped through M11 — quorum-durable writes (m11p3), automatic election/fencing (m11p4), dynamic membership (m11p5), and **sharding × replication (m11p6): S shard groups, each a replication group at RF with its own elected leader, any gateway hash-routing writes (the §4 Option-C shape), per-group rebalancing verbs.** The cluster `ShardReplica` (`tidal-server/src/cluster/node.rs`) is one group's machinery; a `ClusterNode` hosts a `BTreeMap>`. See [docs/roadmap-to-cluster.md](../roadmap-to-cluster.md) and [docs/planning/milestone-11/phase-6.md](../planning/milestone-11/phase-6.md). Original M8 known gaps G3 (shard-routing hash unification) and G6 (embedding 500→400) remain open. **Author:** tidalDB Engineering **Last Updated:** 2026-05-28 **Depends on:** Storage Engine (01), Entity Model (02), Signal System (03), Cohorts (05), Vector Retrieval (07) diff --git a/scripts/check-docs.sh b/scripts/check-docs.sh index 7ac5616..6ce0d2d 100755 --- a/scripts/check-docs.sh +++ b/scripts/check-docs.sh @@ -103,6 +103,38 @@ if [ -f docs/planning/ROADMAP.md ]; then done fi +# --------------------------------------------------------------------------- +# 5. Milestone-status freshness (hard). A core doc must not advertise a status +# bound behind ROADMAP's latest COMPLETE milestone (catches the "M0–M10 +# shipped" / "Implemented, M0–M8" drift). Only status-context lines are +# checked, so historical path lists ("M0–M2 Embed & prove primitives") +# never trip it. Self-updating: the bound is derived from ROADMAP, not pinned. +# --------------------------------------------------------------------------- +stale_ms=0 +latest=0 +if [ -f docs/planning/ROADMAP.md ]; then + for n in $(grep -oE 'M[0-9]+' docs/planning/ROADMAP.md | sed 's/M//' | sort -un); do + if grep -qiE "\| *\*?\*?M$n\b.*(COMPLETE|✅)" docs/planning/ROADMAP.md 2>/dev/null; then + [ "$n" -gt "$latest" ] && latest=$n + fi + done + if [ "$latest" -gt 0 ]; then + for f in CLAUDE.md README.md docs/README.md; do + [ -f "$f" ] || continue + while IFS=: read -r ln text; do + echo "$text" | grep -qiE 'status|shipped|implemented|milestones?' || continue + k=$(echo "$text" | grep -oE 'M0(–|-)M[0-9]+' | grep -oE '[0-9]+$' | head -1) + [ -z "$k" ] && continue + if [ "$k" -lt "$latest" ]; then + err "$f:$ln advertises status bound M0–M$k but ROADMAP's latest COMPLETE milestone is M$latest — update the status line" + stale_ms=1 + fi + done < <(grep -nE 'M0(–|-)M[0-9]+' "$f") + done + fi +fi +[ "$stale_ms" -eq 0 ] && ok "core-doc milestone-status lines current (latest COMPLETE = M$latest)" + if [ "$fail" -ne 0 ]; then echo "doc-guard: FAILED — fix the ✗ items above." >&2 exit 1