{"messages":[{"content":"You are a constitutional council ranking individual git commits for ownership allocation.\n\nCompare these two commits. Decide which contributed more lasting value to the project.\n\nJudge substance, not spectacle:\n- Prefer correct, lasting design and real bugfixes over churn, formatting, renames, or generated noise.\n- Prefer clarity and necessity over sheer line count. A small precise change can beat a large diffuse one.\n- Do not favor a side merely because its patch is longer or noisier.\n- Weight what the change does for the project, not the contributor's name.\n\nReturn ONLY a JSON object: {\"winner\": \"A\" or \"B\", \"ratio\": \"N:M\", \"explanation\": \"...\"}\nThe explanation must cite concrete differences in the patches (1-3 sentences).\n\nSide A — contributor: tommy-mor\nSide A — commit message:\n[23c8134e] Fix /-/ external garden index; resolvers/ + GitHub import cards (#150)\n\n* Fix external garden root listing; add resolvers/ with GitHub cards\n\nThe public and room external index pages queried children of a bogus\nhttps://./ parent, so /-/ always looked empty. Collect host-only https\nroots from all Web items and item_children edges so ghost parents from\nadd_child_edge appear.\n\nMove GitHub resolver into server/src/resolvers/ with default_external.rs\nand a try_render_resolver_item_body hook. Resolver ingests now store\nslug-github-card fenced JSON; render_item_body_in_scope shows a small\nGitHub article card (with legacy support for schema-less json fences on\ngithub.com URLs). Styling in theme_default.css; agents.md updated.\n\nCo-authored-by: tommy \n\n* Vote compare: GitHub cards in columns, layout CSS, tests\n\nPass item_bodies into vote_compare_item_card for linkified tooltips on\nnon-card bodies; clone item_bodies before dropping reducer read guard.\n\nAdd layout rules so rich cards sit in the grid corners (default + retro).\n\nUnit test on vote_compare_item_card; integration GET /vote/compare with\ningested slug-github-card bodies. agents.md clarifies compare columns.\n\nCo-authored-by: tommy \n\n---------\n\nCo-authored-by: Cursor Agent \n\nSide A — unified diff (full patch):\ndiff --git a/agents.md b/agents.md\nindex 7508234d9b04223d0e64cfe69fedbebd06a256b5..d8b801e454fdf37e7ac6038b91a69f83b0746d59 100644\n--- a/agents.md\n+++ b/agents.md\n@@ -42,7 +42,7 @@ Strict **CSP** that blocks `eval` would break the current app. Other projects ma\n \n - **`VoteComparePost`:** On success returns **`text/javascript`** that **morphs** **`#vote-edge-history-region`** (recomputed **`
    `** — ratios match **`left`/`right`** query order, bullets, sorted by strength toward **`left`** then newer) and **`.vote-compare-nav`** (fresh next-pair link). The compare **`GET`** page uses **`layout_full_bleed_chromeless`** (no breadcrumbs, no **`#controls`**, no **`slug-pin-hud`**; **`view-vote-compare-fullscreen`** full-width **`body`**). **`__rpc__`** carries **`form_action: \"/ui\"`**; **`thread_tag`** and ratio fields come from the same form as **`$form`** holes.\n \n-- **`ResolveExternal`:** GitHub resolver buttons are browser actions through **`POST /ui`**. Success responses morph **`#external-resolver-status`** then redirect to the sanitized shareable **`GET`** page so imported children render through the normal page path; errors morph the same status region. Resolver results are durable system ingests, while cooldown state is RAM-only.\n+- **`ResolveExternal`:** GitHub resolver buttons are browser actions through **`POST /ui`**. Success responses morph **`#external-resolver-status`** then redirect to the sanitized shareable **`GET`** page so imported children render through the normal page path; errors morph the same status region. Resolver results are durable system ingests, while cooldown state is RAM-only. Implementation lives under **`server/src/resolvers/`** (GitHub resolver + import card JSON); ontology item pages and the **`GET /vote/compare`** left/right columns use **`render_item_body_in_scope`** in **`server/src/html/mod.rs`**, which calls **`server/src/resolvers/mod.rs::try_render_resolver_item_body`** before falling back to the usual **`
    `** linkified view.\n \n - **Garden pin / compare voting:** Cookie **`slug_garden_pin`** via **`set_garden_pin`**. Pairwise UI: **`GET /vote/compare?…`** / **`GET /r/:room_key/vote/compare?…`** (fullscreen **`GET`** page: no HUD; other garden pages). HUD (**`#slug-pin-hud`**): only when **`layout`** passes garden metadata on **`body`**; the label is **`POST /ui`** **`set_garden_pin`** **`clear:true`** (**`slug_ui.js`**), not a permalink to the item.\n \ndiff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs\nindex 4b0214d18b173cd506d09176104f461dc4c4f208..c9eb8e242072e41fcf70da838bdf02dd4c838db8 100644\n--- a/server/src/api/ui_html.rs\n+++ b/server/src/api/ui_html.rs\n@@ -18,7 +18,7 @@ use crate::{\n         rpc::{rpc_post_redact, rpc_post_with_bearer, rpc_room_delete},\n     },\n     canonical_path::canonicalize_tag,\n-    external_resolver::resolve_github_children,\n+    resolvers::resolve_github_children,\n     html::vote_compare_post_success_js,\n     html::{\n         external_resolver_status_markup, fragment_new_thread_slot, login_to_post_hint_markup,\ndiff --git a/server/src/external_resolver.rs b/server/src/external_resolver.rs\ndeleted file mode 100644\nindex a5812250fed7613950b5417f396f886a55fafccf..0000000000000000000000000000000000000000\n--- a/server/src/external_resolver.rs\n+++ /dev/null\n@@ -1,630 +0,0 @@\n-use async_trait::async_trait;\n-use serde_json::Value;\n-use tokio::sync::oneshot;\n-\n-use crate::{path_types::ItemId, state::AppState, write_cmd::WriteCmd};\n-\n-const GITHUB_SYSTEM_PRINCIPAL: &str = \"system:github-resolver\";\n-const GITHUB_RESOLVER_COOLDOWN_MS: i64 = 15_000;\n-const GITHUB_MAX_PAGES: usize = 3;\n-\n-fn now_ms() -> i64 {\n-    use std::time::{SystemTime, UNIX_EPOCH};\n-    SystemTime::now()\n-        .duration_since(UNIX_EPOCH)\n-        .unwrap_or_default()\n-        .as_millis() as i64\n-}\n-\n-#[derive(Debug, Clone, PartialEq, Eq)]\n-pub struct ResolvedChild {\n-    pub url: String,\n-    pub title: String,\n-    pub body: Option,\n-}\n-\n-#[async_trait]\n-pub trait ExternalResolver: Send + Sync {\n-    /// e.g. `\"github.com\"`\n-    fn domain_match(&self) -> &'static str;\n-\n-    /// Normalizes URLs (e.g. stripping fragments); extend per-domain later.\n-    fn normalize(&self, path: &str) -> String;\n-\n-    /// Fetches body when missing; GitHub hook lands here in a follow-up.\n-    async fn fetch_body(&self, item: &ItemId) -> Result;\n-}\n-\n-#[derive(Clone)]\n-pub struct GitHubResolver {\n-    client: reqwest::Client,\n-    api_base_url: String,\n-    token: Option,\n-}\n-\n-impl GitHubResolver {\n-    pub fn from_env() -> Self {\n-        let api_base_url = std::env::var(\"SLUG_GITHUB_API_BASE_URL\")\n-            .ok()\n-            .filter(|s| !s.trim().is_empty())\n-            .unwrap_or_else(|| \"https://api.github.com\".to_string());\n-        let token = std::env::var(\"SLUG_GITHUB_TOKEN\")\n-            .ok()\n-            .filter(|s| !s.trim().is_empty());\n-        Self {\n-            client: reqwest::Client::new(),\n-            api_base_url: api_base_url.trim_end_matches('/').to_string(),\n-            token,\n-        }\n-    }\n-\n-    pub fn can_resolve_children(&self, item: &ItemId) -> bool {\n-        github_segments(item).is_some()\n-    }\n-\n-    pub async fn list_children(&self, item: &ItemId) -> Result, String> {\n-        let segments = github_segments(item).ok_or_else(|| \"not a GitHub URL\".to_string())?;\n-        match segments.as_slice() {\n-            [] => Ok(vec![]),\n-            [owner] => self.list_repos(owner).await,\n-            [owner, repo] => Ok(github_repo_sections(owner, repo)),\n-            [owner, repo, section] if section == \"issues\" => self.list_issues(owner, repo).await,\n-            [owner, repo, section] if section == \"pulls\" => self.list_pulls(owner, repo).await,\n-            [owner, repo, section] if section == \"commits\" => self.list_commits(owner, repo).await,\n-            [owner, repo, section] if section == \"releases\" => {\n-                self.list_releases(owner, repo).await\n-            }\n-            _ => Ok(vec![]),\n-        }\n-    }\n-\n-    async fn get_json(&self, path: &str) -> Result {\n-        let url = format!(\"{}/{}\", self.api_base_url, path.trim_start_matches('/'));\n-        let mut req = self\n-            .client\n-            .get(url)\n-            .header(reqwest::header::USER_AGENT, \"slugsocial-github-resolver\");\n-        if let Some(token) = &self.token {\n-            req = req.bearer_auth(token);\n-        }\n-        let resp = req\n-            .send()\n-            .await\n-            .map_err(|e| format!(\"GitHub request failed: {e}\"))?;\n-        let status = resp.status();\n-        if !status.is_success() {\n-            return Err(format!(\"GitHub request returned {status}\"));\n-        }\n-        resp.json::()\n-            .await\n-            .map_err(|e| format!(\"GitHub response JSON failed: {e}\"))\n-    }\n-\n-    async fn get_json_array_pages(&self, path: &str) -> Result, String> {\n-        let sep = if path.contains('?') { '&' } else { '?' };\n-        let mut out = Vec::new();\n-        for page in 1..=GITHUB_MAX_PAGES {\n-            let value = self.get_json(&format!(\"{path}{sep}page={page}\")).await?;\n-            let arr = value\n-                .as_array()\n-                .ok_or_else(|| \"GitHub paged response was not an array\".to_string())?;\n-            let n = arr.len();\n-            out.extend(arr.iter().cloned());\n-            if n < 100 {\n-                break;\n-            }\n-        }\n-        Ok(out)\n-    }\n-\n-    async fn list_repos(&self, owner: &str) -> Result, String> {\n-        let arr = self\n-            .get_json_array_pages(&format!(\n-                \"/users/{owner}/repos?per_page=100&sort=updated&type=owner\"\n-            ))\n-            .await?;\n-        let mut out = Vec::new();\n-        for repo in &arr {\n-            let name = repo\n-                .get(\"name\")\n-                .and_then(|v| v.as_str())\n-                .unwrap_or_default();\n-            if name.is_empty() {\n-                continue;\n-            }\n-            let full_name = repo\n-                .get(\"full_name\")\n-                .and_then(|v| v.as_str())\n-                .map(|s| s.to_ascii_lowercase())\n-                .unwrap_or_else(|| format!(\"{owner}/{name}\").to_ascii_lowercase());\n-            out.push(ResolvedChild {\n-                url: format!(\"https://github.com/{full_name}\"),\n-                title: full_name.clone(),\n-                body: Some(github_repo_body(repo)),\n-            });\n-        }\n-        out.sort_by(|a, b| a.url.cmp(&b.url));\n-        Ok(out)\n-    }\n-\n-    async fn list_issues(&self, owner: &str, repo: &str) -> Result, String> {\n-        let arr = self\n-            .get_json_array_pages(&format!(\n-                \"/repos/{owner}/{repo}/issues?state=open&per_page=100\"\n-            ))\n-            .await?;\n-        let mut out = Vec::new();\n-        for issue in &arr {\n-            if issue.get(\"pull_request\").is_some() {\n-                continue;\n-            }\n-            let Some(number) = issue.get(\"number\").and_then(|v| v.as_i64()) else {\n-                continue;\n-            };\n-            let title = issue\n-                .get(\"title\")\n-                .and_then(|v| v.as_str())\n-                .unwrap_or(\"Untitled issue\");\n-            out.push(ResolvedChild {\n-                url: format!(\"https://github.com/{owner}/{repo}/issues/{number}\"),\n-                title: format!(\"#{number} {title}\"),\n-                body: Some(github_issue_body(issue, \"issue\")),\n-            });\n-        }\n-        out.sort_by(|a, b| a.url.cmp(&b.url));\n-        Ok(out)\n-    }\n-\n-    async fn list_pulls(&self, owner: &str, repo: &str) -> Result, String> {\n-        let arr = self\n-            .get_json_array_pages(&format!(\n-                \"/repos/{owner}/{repo}/pulls?state=open&per_page=100\"\n-            ))\n-            .await?;\n-        let mut out = Vec::new();\n-        for pull in &arr {\n-            let Some(number) = pull.get(\"number\").and_then(|v| v.as_i64()) else {\n-                continue;\n-            };\n-            let title = pull\n-                .get(\"title\")\n-                .and_then(|v| v.as_str())\n-                .unwrap_or(\"Untitled pull request\");\n-            out.push(ResolvedChild {\n-                url: format!(\"https://github.com/{owner}/{repo}/pulls/{number}\"),\n-                title: format!(\"#{number} {title}\"),\n-                body: Some(github_issue_body(pull, \"pull request\")),\n-            });\n-        }\n-        out.sort_by(|a, b| a.url.cmp(&b.url));\n-        Ok(out)\n-    }\n-\n-    async fn list_commits(&self, owner: &str, repo: &str) -> Result, String> {\n-        let arr = self\n-            .get_json_array_pages(&format!(\"/repos/{owner}/{repo}/commits?per_page=100\"))\n-            .await?;\n-        let mut out = Vec::new();\n-        for commit in &arr {\n-            let Some(sha) = github_string(commit, \"sha\") else {\n-                continue;\n-            };\n-            let short = sha.chars().take(7).collect::();\n-            let title = commit\n-                .get(\"commit\")\n-                .and_then(|c| c.get(\"message\"))\n-                .and_then(|v| v.as_str())\n-                .and_then(|m| m.lines().next())\n-                .filter(|s| !s.trim().is_empty())\n-                .unwrap_or(\"commit\");\n-            let url = github_string(commit, \"html_url\")\n-                .map(|s| s.to_string())\n-                .unwrap_or_else(|| format!(\"https://github.com/{owner}/{repo}/commit/{sha}\"));\n-            out.push(ResolvedChild {\n-                url,\n-                title: format!(\"{short} {title}\"),\n-                body: Some(github_commit_body(commit)),\n-            });\n-        }\n-        out.sort_by(|a, b| a.url.cmp(&b.url));\n-        Ok(out)\n-    }\n-\n-    async fn list_releases(&self, owner: &str, repo: &str) -> Result, String> {\n-        let arr = self\n-            .get_json_array_pages(&format!(\"/repos/{owner}/{repo}/releases?per_page=100\"))\n-            .await?;\n-        let mut out = Vec::new();\n-        for release in &arr {\n-            let Some(tag) = github_string(release, \"tag_name\") else {\n-                continue;\n-            };\n-            let title = github_string(release, \"name\").unwrap_or(tag);\n-            let url = github_string(release, \"html_url\")\n-                .map(|s| s.to_string())\n-                .unwrap_or_else(|| format!(\"https://github.com/{owner}/{repo}/releases/tag/{tag}\"));\n-            out.push(ResolvedChild {\n-                url,\n-                title: title.to_string(),\n-                body: Some(github_release_body(release)),\n-            });\n-        }\n-        out.sort_by(|a, b| a.url.cmp(&b.url));\n-        Ok(out)\n-    }\n-}\n-\n-fn github_segments(item: &ItemId) -> Option> {\n-    let url = url::Url::parse(item.as_str()).ok()?;\n-    if url.host_str()?.eq_ignore_ascii_case(\"github.com\") {\n-        Some(\n-            url.path_segments()\n-                .map(|segments| {\n-                    segments\n-                        .filter(|s| !s.is_empty())\n-                        .map(|s| s.to_ascii_lowercase())\n-                        .collect::>()\n-                })\n-                .unwrap_or_default(),\n-        )\n-    } else {\n-        None\n-    }\n-}\n-\n-fn github_repo_sections(owner: &str, repo: &str) -> Vec {\n-    [\n-        (\"issues\", \"GitHub issues for this repository.\"),\n-        (\"pulls\", \"GitHub pull requests for this repository.\"),\n-        (\"commits\", \"GitHub commits for this repository.\"),\n-        (\"releases\", \"GitHub releases for this repository.\"),\n-    ]\n-    .into_iter()\n-    .map(|(section, body)| ResolvedChild {\n-        url: format!(\"https://github.com/{owner}/{repo}/{section}\"),\n-        title: section.to_string(),\n-        body: Some(body.to_string()),\n-    })\n-    .collect()\n-}\n-\n-fn resolver_thread_tag(item: &ItemId) -> String {\n-    let tail = item\n-        .display_path()\n-        .trim_start_matches(\"-/\")\n-        .replace('/', \":\")\n-        .replace('?', \":\");\n-    format!(\"import:{tail}\")\n-}\n-\n-fn sanitize_body(s: &str) -> String {\n-    s.replace('{', \"(\")\n-        .replace('}', \")\")\n-        .replace(\"```\", \"` ` `\")\n-        .chars()\n-        .take(4_000)\n-        .collect()\n-}\n-\n-fn github_string<'a>(value: &'a Value, key: &str) -> Option<&'a str> {\n-    value\n-        .get(key)\n-        .and_then(|v| v.as_str())\n-        .filter(|s| !s.trim().is_empty())\n-}\n-\n-fn github_user_login(value: &Value) -> Option<&str> {\n-    value\n-        .get(\"user\")\n-        .and_then(|u| u.get(\"login\"))\n-        .and_then(|v| v.as_str())\n-        .filter(|s| !s.trim().is_empty())\n-}\n-\n-fn github_labels(value: &Value) -> Vec {\n-    value\n-        .get(\"labels\")\n-        .and_then(|v| v.as_array())\n-        .into_iter()\n-        .flat_map(|labels| labels.iter())\n-        .filter_map(|label| label.get(\"name\").and_then(|v| v.as_str()))\n-        .filter(|name| !name.trim().is_empty())\n-        .map(|name| name.to_string())\n-        .collect()\n-}\n-\n-fn github_repo_body(repo: &Value) -> String {\n-    let full_name = github_string(repo, \"full_name\")\n-        .or_else(|| github_string(repo, \"name\"))\n-        .unwrap_or(\"GitHub repository\");\n-    let mut lines = vec![full_name.to_string()];\n-    if let Some(desc) = github_string(repo, \"description\") {\n-        lines.push(String::new());\n-        lines.push(desc.to_string());\n-    }\n-    if let Some(url) = github_string(repo, \"html_url\") {\n-        lines.push(String::new());\n-        lines.push(format!(\"Source: {url}\"));\n-    }\n-    if let Some(lang) = github_string(repo, \"language\") {\n-        lines.push(format!(\"Language: {lang}\"));\n-    }\n-    lines.join(\"\\n\")\n-}\n-\n-fn github_issue_body(issue: &Value, kind: &str) -> String {\n-    let number = issue\n-        .get(\"number\")\n-        .and_then(|v| v.as_i64())\n-        .map(|n| format!(\"#{n} \"))\n-        .unwrap_or_default();\n-    let title = github_string(issue, \"title\").unwrap_or(\"Untitled\");\n-    let state = github_string(issue, \"state\").unwrap_or(\"unknown\");\n-    let mut lines = vec![format!(\"{kind} {number}{title}\")];\n-    lines.push(format!(\"State: {state}\"));\n-    if let Some(author) = github_user_login(issue) {\n-        lines.push(format!(\"Author: @{author}\"));\n-    }\n-    let labels = github_labels(issue);\n-    if !labels.is_empty() {\n-        lines.push(format!(\"Labels: {}\", labels.join(\", \")));\n-    }\n-    if let Some(url) = github_string(issue, \"html_url\") {\n-        lines.push(format!(\"Source: {url}\"));\n-    }\n-    if let Some(body) = github_string(issue, \"body\") {\n-        lines.push(String::new());\n-        lines.push(body.to_string());\n-    }\n-    lines.join(\"\\n\")\n-}\n-\n-fn github_commit_body(commit: &Value) -> String {\n-    let sha = github_string(commit, \"sha\").unwrap_or(\"unknown\");\n-    let short = sha.chars().take(7).collect::();\n-    let commit_obj = commit.get(\"commit\");\n-    let message = commit_obj\n-        .and_then(|c| c.get(\"message\"))\n-        .and_then(|v| v.as_str())\n-        .unwrap_or(\"commit\");\n-    let mut lines = vec![format!(\"commit {short}\")];\n-    if let Some(author) = commit_obj\n-        .and_then(|c| c.get(\"author\"))\n-        .and_then(|a| a.get(\"name\"))\n-        .and_then(|v| v.as_str())\n-        .filter(|s| !s.trim().is_empty())\n-    {\n-        lines.push(format!(\"Author: {author}\"));\n-    }\n-    if let Some(login) = github_user_login(commit) {\n-        lines.push(format!(\"GitHub user: @{login}\"));\n-    }\n-    if let Some(date) = commit_obj\n-        .and_then(|c| c.get(\"author\"))\n-        .and_then(|a| a.get(\"date\"))\n-        .and_then(|v| v.as_str())\n-    {\n-        lines.push(format!(\"Date: {date}\"));\n-    }\n-    if let Some(url) = github_string(commit, \"html_url\") {\n-        lines.push(format!(\"Source: {url}\"));\n-    }\n-    lines.push(String::new());\n-    lines.push(message.to_string());\n-    lines.join(\"\\n\")\n-}\n-\n-fn github_release_body(release: &Value) -> String {\n-    let tag = github_string(release, \"tag_name\").unwrap_or(\"untagged\");\n-    let title = github_string(release, \"name\").unwrap_or(tag);\n-    let mut lines = vec![format!(\"release {title}\")];\n-    lines.push(format!(\"Tag: {tag}\"));\n-    if release\n-        .get(\"draft\")\n-        .and_then(|v| v.as_bool())\n-        .unwrap_or(false)\n-    {\n-        lines.push(\"Draft: yes\".to_string());\n-    }\n-    if release\n-        .get(\"prerelease\")\n-        .and_then(|v| v.as_bool())\n-        .unwrap_or(false)\n-    {\n-        lines.push(\"Prerelease: yes\".to_string());\n-    }\n-    if let Some(author) = github_user_login(release) {\n-        lines.push(format!(\"Author: @{author}\"));\n-    }\n-    if let Some(published) = github_string(release, \"published_at\") {\n-        lines.push(format!(\"Published: {published}\"));\n-    }\n-    if let Some(url) = github_string(release, \"html_url\") {\n-        lines.push(format!(\"Source: {url}\"));\n-    }\n-    if let Some(body) = github_string(release, \"body\") {\n-        lines.push(String::new());\n-        lines.push(body.to_string());\n-    }\n-    lines.join(\"\\n\")\n-}\n-\n-fn children_to_dsl(children: &[ResolvedChild]) -> String {\n-    let mut out = String::new();\n-    for child in children {\n-        let body = child\n-            .body\n-            .as_deref()\n-            .filter(|s| !s.trim().is_empty())\n-            .unwrap_or(child.title.as_str());\n-        if body.trim_start().starts_with(\"```\") {\n-            out.push_str(&format!(\"{} {{\\n{}\\n}}\\n\\n\", child.url, body.trim()));\n-        } else {\n-            out.push_str(&format!(\n-                \"{} {{\\n{}\\n}}\\n\\n\",\n-                child.url,\n-                sanitize_body(body)\n-            ));\n-        }\n-    }\n-    out\n-}\n-\n-pub async fn resolve_github_children(\n-    state: &AppState,\n-    room: &str,\n-    item: &ItemId,\n-) -> Result {\n-    if !state.github_resolver.can_resolve_children(item) {\n-        return Err(\"no GitHub resolver for this item\".to_string());\n-    }\n-\n-    let key = format!(\"github:{}:{}\", room.trim(), item.as_str());\n-    let now = now_ms();\n-    {\n-        let mut runs = state.resolver_runs.write().await;\n-        if let Some(last) = runs.get(&key) {\n-            let remaining = GITHUB_RESOLVER_COOLDOWN_MS - (now - *last);\n-            if remaining > 0 {\n-                return Err(format!(\n-                    \"GitHub resolver cooldown: try again in {}s\",\n-                    (remaining + 999) / 1000\n-                ));\n-            }\n-        }\n-        runs.insert(key, now);\n-    }\n-\n-    let children = state.github_resolver.list_children(item).await?;\n-    if children.is_empty() {\n-        return Ok(0);\n-    }\n-    let text = children_to_dsl(&children);\n-    let thread_tag = resolver_thread_tag(item);\n-    let (tx, rx) = oneshot::channel();\n-    state\n-        .write_tx\n-        .send(WriteCmd::SystemIngest {\n-            room: room.to_string(),\n-            thread_tag,\n-            text,\n-            principal: GITHUB_SYSTEM_PRINCIPAL.to_string(),\n-            reply: tx,\n-        })\n-        .await\n-        .map_err(|_| \"writer unavailable\".to_string())?;\n-    rx.await\n-        .map_err(|_| \"writer dropped\".to_string())?\n-        .map_err(|(msg, hint)| hint.map_or(msg.clone(), |h| format!(\"{msg}: {h}\")))?;\n-    Ok(children.len())\n-}\n-\n-/// Placeholder until other domain-specific resolvers exist.\n-pub struct DefaultExternalResolver;\n-\n-#[async_trait]\n-impl ExternalResolver for DefaultExternalResolver {\n-    fn domain_match(&self) -> &'static str {\n-        \"\"\n-    }\n-\n-    fn normalize(&self, path: &str) -> String {\n-        path.to_string()\n-    }\n-\n-    async fn fetch_body(&self, _item: &ItemId) -> Result {\n-        Err(\"external fetch not implemented\".to_string())\n-    }\n-}\n-\n-#[cfg(test)]\n-mod tests {\n-    use super::*;\n-\n-    #[test]\n-    fn github_segments_parse_normalized_url() {\n-        let item = ItemId::parse(\"https://github.com/Sortersocial/Slug/issues\").unwrap();\n-        assert_eq!(\n-            github_segments(&item),\n-            Some(vec![\n-                \"sortersocial\".to_string(),\n-                \"slug\".to_string(),\n-                \"issues\".to_string()\n-            ])\n-        );\n-    }\n-\n-    #[test]\n-    fn repo_sections_are_direct_children() {\n-        let sections = github_repo_sections(\"sortersocial\", \"slug\");\n-        let urls: Vec = sections.into_iter().map(|c| c.url).collect();\n-        assert!(urls.contains(&\"https://github.com/sortersocial/slug/issues\".to_string()));\n-        assert!(urls.contains(&\"https://github.com/sortersocial/slug/pulls\".to_string()));\n-    }\n-\n-    #[test]\n-    fn children_to_dsl_contains_item_bodies() {\n-        let dsl = children_to_dsl(&[ResolvedChild {\n-            url: \"https://github.com/o/r/issues/1\".into(),\n-            title: \"#1 title\".into(),\n-            body: Some(\"body with {braces}\".into()),\n-        }]);\n-        assert!(dsl.contains(\"https://github.com/o/r/issues/1\"));\n-        assert!(dsl.contains(\"body with (braces)\"));\n-    }\n-\n-    #[test]\n-    fn children_to_dsl_preserves_fenced_json_bodies() {\n-        let dsl = children_to_dsl(&[ResolvedChild {\n-            url: \"https://github.com/o/r/issues/1\".into(),\n-            title: \"#1 title\".into(),\n-            body: Some(\"```json\\n{\\\"test\\\": true}\\n```\".into()),\n-        }]);\n-        assert!(dsl.contains(\"https://github.com/o/r/issues/1 {\\n```json\"));\n-        assert!(dsl.contains(\"{\\\"test\\\": true}\"));\n-        assert!(dsl.contains(\"```\\n}\\n\"));\n-    }\n-\n-    #[test]\n-    fn github_issue_body_is_readable_text_not_json_dump() {\n-        let issue = serde_json::json!({\n-            \"number\": 12,\n-            \"title\": \"Render children\",\n-            \"state\": \"open\",\n-            \"html_url\": \"https://github.com/o/r/issues/12\",\n-            \"user\": {\"login\": \"octo\"},\n-            \"labels\": [{\"name\": \"bug\"}],\n-            \"body\": \"The issue body.\"\n-        });\n-        let body = github_issue_body(&issue, \"issue\");\n-        assert!(body.contains(\"issue #12 Render children\"));\n-        assert!(body.contains(\"Author: @octo\"));\n-        assert!(body.contains(\"The issue body.\"));\n-        assert!(!body.trim_start().starts_with(\"```json\"));\n-    }\n-\n-    #[test]\n-    fn github_commit_and_release_bodies_are_readable() {\n-        let commit = serde_json::json!({\n-            \"sha\": \"abcdef123456\",\n-            \"html_url\": \"https://github.com/o/r/commit/abcdef123456\",\n-            \"author\": {\"login\": \"octo\"},\n-            \"commit\": {\n-                \"message\": \"Fix vote page\\n\\nDetails here.\",\n-                \"author\": {\"name\": \"Octo Dev\", \"date\": \"2026-05-17T00:00:00Z\"}\n-            }\n-        });\n-        let release = serde_json::json!({\n-            \"tag_name\": \"v1.2.3\",\n-            \"name\": \"Release 1.2.3\",\n-            \"html_url\": \"https://github.com/o/r/releases/tag/v1.2.3\",\n-            \"author\": {\"login\": \"octo\"},\n-            \"prerelease\": true,\n-            \"body\": \"Release notes.\"\n-        });\n-        assert!(github_commit_body(&commit).contains(\"commit abcdef1\"));\n-        assert!(github_commit_body(&commit).contains(\"Fix vote page\"));\n-        assert!(github_release_body(&release).contains(\"release Release 1.2.3\"));\n-        assert!(github_release_body(&release).contains(\"Prerelease: yes\"));\n-    }\n-}\ndiff --git a/server/src/html/garden.rs b/server/src/html/garden.rs\nindex 9ca66e7c5860d428e95abf5df518fc1e7b4f6332..e2dc6e5529d4a3126723d0dea75931d0738b6c83 100644\n--- a/server/src/html/garden.rs\n+++ b/server/src/html/garden.rs\n@@ -7,7 +7,7 @@ use axum_extra::extract::cookie::CookieJar;\n use maud::html;\n use serde::Deserialize;\n use serde_json::json;\n-use std::collections::HashSet;\n+use std::collections::{HashMap, HashSet};\n \n use base64::{engine::general_purpose::URL_SAFE_NO_PAD as B64_ENGINE, Engine as _};\n \n@@ -21,8 +21,8 @@ use crate::{\n     path_types::ItemId,\n     reducer::{ContentState, ReducerState, ScopeId},\n     scope_rank::{\n-        build_children_rankings, build_rankings_for_item_set, resolve_scope_recursive,\n-        suggest_next_pair_in_pool, ChildrenRankings,\n+        build_children_rankings, build_rankings_for_item_set, external_root_host_items,\n+        resolve_scope_recursive, suggest_next_pair_in_pool, ChildrenRankings,\n     },\n     state::AppState,\n     timeago,\n@@ -33,7 +33,7 @@ use super::{\n     breadcrumb_path::{ExternalOntologyPath, OntologyPath},\n     cli_panel,\n     forum::ThreadNav,\n-    layout, layout_full_bleed_chromeless, now_ms, ratio_pct, render_linkified_with_embeds_in_scope,\n+    layout, layout_full_bleed_chromeless, now_ms, ratio_pct, render_item_body_in_scope,\n     theme_from_jar, theme_next_from_uri,\n };\n \n@@ -358,6 +358,7 @@ fn vote_compare_item_card(\n     item: &ItemId,\n     body: Option<&String>,\n     side_class: &str,\n+    item_bodies: Option<&HashMap>,\n ) -> maud::Markup {\n     html! {\n         div class=(format!(\"vote-compare-side {side_class}\")) {\n@@ -366,10 +367,10 @@ fn vote_compare_item_card(\n             }\n             @if let Some(body) = body.filter(|b| !b.trim().is_empty()) {\n                 div class=\"vote-compare-item-body\" {\n-                    (render_linkified_with_embeds_in_scope(\n+                    (render_item_body_in_scope(\n                         body,\n                         nav.garden_root_url(),\n-                        None,\n+                        item_bodies,\n                     ))\n                 }\n             } @else {\n@@ -678,10 +679,11 @@ pub async fn external_garden_index(\n ) -> impl IntoResponse {\n     let nav = ThreadNav::public();\n     let ext_path = ExternalOntologyPath::from_input(\"\");\n-    let parent = ItemId::parse(\"https://.\").unwrap();\n     let child_rankings = {\n         let reduced = state.reduced.read().await;\n-        build_children_rankings(reduced.public(), &parent)\n+        let content = reduced.public();\n+        let hosts = external_root_host_items(content);\n+        build_rankings_for_item_set(content, &hosts)\n     };\n \n     let url_key = canonical_view_url(&uri);\n@@ -812,9 +814,11 @@ pub async fn room_external_garden_index(\n         return room_not_found_page(&jar, &uri).into_response();\n     }\n     let ext_path = ExternalOntologyPath::from_input(\"\");\n-    let parent = ItemId::parse(\"https://.\").unwrap();\n-    let child_rankings =\n-        build_children_rankings(content_for_garden_view(&reduced, &nav.scope()), &parent);\n+    let child_rankings = {\n+        let content = content_for_garden_view(&reduced, &nav.scope());\n+        let hosts = external_root_host_items(content);\n+        build_rankings_for_item_set(content, &hosts)\n+    };\n     drop(reduced);\n \n     let url_key = canonical_view_url(&uri);\n@@ -1334,7 +1338,7 @@ async fn render_scope_view(\n                 }\n                 @if let Some(body) = &model.body {\n                     div class=\"ont-item-content\" {\n-                        (render_linkified_with_embeds_in_scope(\n+                        (render_item_body_in_scope(\n                             body,\n                             nav.garden_root_url(),\n                             Some(&scope_content.item_bodies),\n@@ -1592,6 +1596,7 @@ async fn vote_compare_inner(\n     let edge_history = vote_edge_history_markup(content, &left, &right);\n     let left_body = content.item_bodies.get(&left).cloned();\n     let right_body = content.item_bodies.get(&right).cloned();\n+    let item_bodies_for_cards = content.item_bodies.clone();\n     let next_pair = suggest_next_vote_pair(content, &left, &right);\n     drop(reduced);\n \n@@ -1623,9 +1628,21 @@ async fn vote_compare_inner(\n     section class=\"vote-compare-shell\" {\n         h2 { \"compare\" }\n         div class=\"vote-compare-pair\" {\n-            (vote_compare_item_card(&nav, &left, left_body.as_ref(), \"vote-compare-left\"))\n+            (vote_compare_item_card(\n+                &nav,\n+                &left,\n+                left_body.as_ref(),\n+                \"vote-compare-left\",\n+                Some(&item_bodies_for_cards),\n+            ))\n             span class=\"vote-compare-vs\" { \"vs\" }\n-            (vote_compare_item_card(&nav, &right, right_body.as_ref(), \"vote-compare-right\"))\n+            (vote_compare_item_card(\n+                &nav,\n+                &right,\n+                right_body.as_ref(),\n+                \"vote-compare-right\",\n+                Some(&item_bodies_for_cards),\n+            ))\n         }\n         (vote_compare_nav_markup(&nav, next_pair.as_ref(), &left, &right, q.thread.as_deref()))\n         div id=\"vote-edge-history-region\" {\n@@ -2020,6 +2037,40 @@ mod tests {\n         assert!(items.contains(\"https://slug.social/~/topic/b\"));\n     }\n \n+    #[test]\n+    fn vote_compare_item_card_renders_github_import_markup() {\n+        use crate::html::forum::ThreadNav;\n+        use super::vote_compare_item_card;\n+        use crate::path_types::ItemId;\n+\n+        let nav = ThreadNav::public();\n+        let item = ItemId::parse(\"https://github.com/o/r/issues/1\").unwrap();\n+        let json = serde_json::json!({\n+            \"v\": 1,\n+            \"schema\": \"slug_github_import\",\n+            \"kind\": \"issue\",\n+            \"url\": \"https://github.com/o/r/issues/1\",\n+            \"headline\": \"#1 Compare card\",\n+            \"sublines\": [\"State: open\"],\n+        });\n+        let body = format!(\"```slug-github-card\\n{}\\n```\", json.to_string());\n+        let html = vote_compare_item_card(\n+            &nav,\n+            &item,\n+            Some(&body),\n+            \"vote-compare-left\",\n+            None,\n+        )\n+        .into_string();\n+        assert!(\n+            html.contains(\"github-import-card\"),\n+            \"expected rich GitHub card markup, got: {html}\"\n+        );\n+        assert!(html.contains(\"item-body-rich\"));\n+        assert!(html.contains(\"vote-compare-left\"));\n+        assert!(html.contains(\"#1 Compare card\"));\n+    }\n+\n     #[test]\n     fn external_source_href_maps_youtube_path_identity_back_to_watch_url() {\n         assert_eq!(\ndiff --git a/server/src/html/mod.rs b/server/src/html/mod.rs\nindex a1b929625acbd5298c0cf62f3ca0892079edcb2a..3b23e19a8b35aa4c0b0480e29de7d46ad57ab276 100644\n--- a/server/src/html/mod.rs\n+++ b/server/src/html/mod.rs\n@@ -793,6 +793,20 @@ pub(super) fn render_linkified_with_embeds_in_scope(\n     }\n }\n \n+/// Item page / thread body: resolver-specific rich HTML, else linkified `
    ` + media embeds.\n+pub(super) fn render_item_body_in_scope(\n+    raw: &str,\n+    garden_prefix: &str,\n+    item_bodies: Option<&HashMap>,\n+) -> Markup {\n+    if let Some(m) = crate::resolvers::try_render_resolver_item_body(raw) {\n+        return html! {\n+            div class=\"item-body-rich\" { (m) }\n+        };\n+    }\n+    render_linkified_with_embeds_in_scope(raw, garden_prefix, item_bodies)\n+}\n+\n /// CLI strings are embedded in a single-quoted JS literal; they must never need escaping.\n fn assert_cli_panel_cmd_js_single_quote_safe(s: &str) {\n     assert!(\ndiff --git a/server/src/lib.rs b/server/src/lib.rs\nindex 84e94bbec144eae77de68482385941cd2c5845eb..c1d477d21aea03aff00e6f0689b0b4379d0d68d2 100644\n--- a/server/src/lib.rs\n+++ b/server/src/lib.rs\n@@ -5,7 +5,7 @@ pub mod canonical_path;\n pub mod dsl;\n pub mod event_log;\n pub mod events;\n-pub mod external_resolver;\n+pub mod resolvers;\n pub mod form_template;\n pub mod html;\n pub mod identity;\n@@ -51,7 +51,7 @@ pub fn create_app_state(cfg: AppConfig) -> AppState {\n         write_tx,\n         views,\n         resolver_runs: Arc::new(RwLock::new(HashMap::new())),\n-        github_resolver: Arc::new(crate::external_resolver::GitHubResolver::from_env()),\n+        github_resolver: Arc::new(crate::resolvers::GitHubResolver::from_env()),\n     };\n     tokio::spawn(crate::api::write_actor::writer_actor(\n         write_rx,\ndiff --git a/server/src/resolvers/default_external.rs b/server/src/resolvers/default_external.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..d37c222abcee3c20b22189b2822da9e9a6ff0515\n--- /dev/null\n+++ b/server/src/resolvers/default_external.rs\n@@ -0,0 +1,22 @@\n+use async_trait::async_trait;\n+\n+use crate::path_types::ItemId;\n+use super::github::ExternalResolver;\n+\n+/// Placeholder until other domain-specific resolvers exist.\n+pub struct DefaultExternalResolver;\n+\n+#[async_trait]\n+impl ExternalResolver for DefaultExternalResolver {\n+    fn domain_match(&self) -> &'static str {\n+        \"\"\n+    }\n+\n+    fn normalize(&self, path: &str) -> String {\n+        path.to_string()\n+    }\n+\n+    async fn fetch_body(&self, _item: &ItemId) -> Result {\n+        Err(\"external fetch not implemented\".to_string())\n+    }\n+}\ndiff --git a/server/src/resolvers/github.rs b/server/src/resolvers/github.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..5a9c0c38ff01ca5894f7dc62c1008371dacb0cf1\n--- /dev/null\n+++ b/server/src/resolvers/github.rs\n@@ -0,0 +1,755 @@\n+use async_trait::async_trait;\n+use maud::html;\n+use serde::{Deserialize, Serialize};\n+use serde_json::Value;\n+use tokio::sync::oneshot;\n+\n+use crate::{path_types::ItemId, state::AppState, write_cmd::WriteCmd};\n+\n+pub const SLUG_GITHUB_SCHEMA: &str = \"slug_github_import\";\n+\n+const GITHUB_SYSTEM_PRINCIPAL: &str = \"system:github-resolver\";\n+const GITHUB_RESOLVER_COOLDOWN_MS: i64 = 15_000;\n+const GITHUB_MAX_PAGES: usize = 3;\n+\n+fn now_ms() -> i64 {\n+    use std::time::{SystemTime, UNIX_EPOCH};\n+    SystemTime::now()\n+        .duration_since(UNIX_EPOCH)\n+        .unwrap_or_default()\n+        .as_millis() as i64\n+}\n+\n+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\n+#[serde(rename_all = \"snake_case\")]\n+pub enum GithubImportKind {\n+    Repo,\n+    RepoSection,\n+    Issue,\n+    Pull,\n+    Commit,\n+    Release,\n+}\n+\n+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\n+pub struct GithubImportCard {\n+    pub v: u32,\n+    #[serde(default)]\n+    pub schema: String,\n+    pub kind: GithubImportKind,\n+    pub url: String,\n+    pub headline: String,\n+    #[serde(default)]\n+    pub sublines: Vec,\n+    #[serde(default)]\n+    pub excerpt: Option,\n+}\n+\n+impl GithubImportCard {\n+    fn new(kind: GithubImportKind, url: String, headline: String) -> Self {\n+        Self {\n+            v: 1,\n+            schema: SLUG_GITHUB_SCHEMA.to_string(),\n+            kind,\n+            url,\n+            headline,\n+            sublines: Vec::new(),\n+            excerpt: None,\n+        }\n+    }\n+}\n+\n+#[derive(Debug, Clone, PartialEq, Eq)]\n+pub struct ResolvedChild {\n+    pub url: String,\n+    pub title: String,\n+    pub card: GithubImportCard,\n+}\n+\n+#[async_trait]\n+pub trait ExternalResolver: Send + Sync {\n+    /// e.g. `\"github.com\"`\n+    fn domain_match(&self) -> &'static str;\n+\n+    /// Normalizes URLs (e.g. stripping fragments); extend per-domain later.\n+    fn normalize(&self, path: &str) -> String;\n+\n+    /// Fetches body when missing; GitHub hook lands here in a follow-up.\n+    async fn fetch_body(&self, item: &ItemId) -> Result;\n+}\n+\n+#[derive(Clone)]\n+pub struct GitHubResolver {\n+    client: reqwest::Client,\n+    api_base_url: String,\n+    token: Option,\n+}\n+\n+impl GitHubResolver {\n+    pub fn from_env() -> Self {\n+        let api_base_url = std::env::var(\"SLUG_GITHUB_API_BASE_URL\")\n+            .ok()\n+            .filter(|s| !s.trim().is_empty())\n+            .unwrap_or_else(|| \"https://api.github.com\".to_string());\n+        let token = std::env::var(\"SLUG_GITHUB_TOKEN\")\n+            .ok()\n+            .filter(|s| !s.trim().is_empty());\n+        Self {\n+            client: reqwest::Client::new(),\n+            api_base_url: api_base_url.trim_end_matches('/').to_string(),\n+            token,\n+        }\n+    }\n+\n+    pub fn can_resolve_children(&self, item: &ItemId) -> bool {\n+        github_segments(item).is_some()\n+    }\n+\n+    pub async fn list_children(&self, item: &ItemId) -> Result, String> {\n+        let segments = github_segments(item).ok_or_else(|| \"not a GitHub URL\".to_string())?;\n+        match segments.as_slice() {\n+            [] => Ok(vec![]),\n+            [owner] => self.list_repos(owner).await,\n+            [owner, repo] => Ok(github_repo_sections(owner, repo)),\n+            [owner, repo, section] if section == \"issues\" => self.list_issues(owner, repo).await,\n+            [owner, repo, section] if section == \"pulls\" => self.list_pulls(owner, repo).await,\n+            [owner, repo, section] if section == \"commits\" => self.list_commits(owner, repo).await,\n+            [owner, repo, section] if section == \"releases\" => {\n+                self.list_releases(owner, repo).await\n+            }\n+            _ => Ok(vec![]),\n+        }\n+    }\n+\n+    async fn get_json(&self, path: &str) -> Result {\n+        let url = format!(\"{}/{}\", self.api_base_url, path.trim_start_matches('/'));\n+        let mut req = self\n+            .client\n+            .get(url)\n+            .header(reqwest::header::USER_AGENT, \"slugsocial-github-resolver\");\n+        if let Some(token) = &self.token {\n+            req = req.bearer_auth(token);\n+        }\n+        let resp = req\n+            .send()\n+            .await\n+            .map_err(|e| format!(\"GitHub request failed: {e}\"))?;\n+        let status = resp.status();\n+        if !status.is_success() {\n+            return Err(format!(\"GitHub request returned {status}\"));\n+        }\n+        resp.json::()\n+            .await\n+            .map_err(|e| format!(\"GitHub response JSON failed: {e}\"))\n+    }\n+\n+    async fn get_json_array_pages(&self, path: &str) -> Result, String> {\n+        let sep = if path.contains('?') { '&' } else { '?' };\n+        let mut out = Vec::new();\n+        for page in 1..=GITHUB_MAX_PAGES {\n+            let value = self.get_json(&format!(\"{path}{sep}page={page}\")).await?;\n+            let arr = value\n+                .as_array()\n+                .ok_or_else(|| \"GitHub paged response was not an array\".to_string())?;\n+            let n = arr.len();\n+            out.extend(arr.iter().cloned());\n+            if n < 100 {\n+                break;\n+            }\n+        }\n+        Ok(out)\n+    }\n+\n+    async fn list_repos(&self, owner: &str) -> Result, String> {\n+        let arr = self\n+            .get_json_array_pages(&format!(\n+                \"/users/{owner}/repos?per_page=100&sort=updated&type=owner\"\n+            ))\n+            .await?;\n+        let mut out = Vec::new();\n+        for repo in &arr {\n+            let name = repo\n+                .get(\"name\")\n+                .and_then(|v| v.as_str())\n+                .unwrap_or_default();\n+            if name.is_empty() {\n+                continue;\n+            }\n+            let full_name = repo\n+                .get(\"full_name\")\n+                .and_then(|v| v.as_str())\n+                .map(|s| s.to_ascii_lowercase())\n+                .unwrap_or_else(|| format!(\"{owner}/{name}\").to_ascii_lowercase());\n+            let url = format!(\"https://github.com/{full_name}\");\n+            let mut card = card_for_repo(repo, &url);\n+            card.headline = full_name.clone();\n+            out.push(ResolvedChild {\n+                url,\n+                title: full_name,\n+                card,\n+            });\n+        }\n+        out.sort_by(|a, b| a.url.cmp(&b.url));\n+        Ok(out)\n+    }\n+\n+    async fn list_issues(&self, owner: &str, repo: &str) -> Result, String> {\n+        let arr = self\n+            .get_json_array_pages(&format!(\n+                \"/repos/{owner}/{repo}/issues?state=open&per_page=100\"\n+            ))\n+            .await?;\n+        let mut out = Vec::new();\n+        for issue in &arr {\n+            if issue.get(\"pull_request\").is_some() {\n+                continue;\n+            }\n+            let Some(number) = issue.get(\"number\").and_then(|v| v.as_i64()) else {\n+                continue;\n+            };\n+            let title = issue\n+                .get(\"title\")\n+                .and_then(|v| v.as_str())\n+                .unwrap_or(\"Untitled issue\");\n+            let url = format!(\"https://github.com/{owner}/{repo}/issues/{number}\");\n+            let card = card_for_issue(issue, &url, GithubImportKind::Issue);\n+            out.push(ResolvedChild {\n+                url: url.clone(),\n+                title: format!(\"#{number} {title}\"),\n+                card,\n+            });\n+        }\n+        out.sort_by(|a, b| a.url.cmp(&b.url));\n+        Ok(out)\n+    }\n+\n+    async fn list_pulls(&self, owner: &str, repo: &str) -> Result, String> {\n+        let arr = self\n+            .get_json_array_pages(&format!(\n+                \"/repos/{owner}/{repo}/pulls?state=open&per_page=100\"\n+            ))\n+            .await?;\n+        let mut out = Vec::new();\n+        for pull in &arr {\n+            let Some(number) = pull.get(\"number\").and_then(|v| v.as_i64()) else {\n+                continue;\n+            };\n+            let title = pull\n+                .get(\"title\")\n+                .and_then(|v| v.as_str())\n+                .unwrap_or(\"Untitled pull request\");\n+            let url = format!(\"https://github.com/{owner}/{repo}/pulls/{number}\");\n+            let card = card_for_issue(pull, &url, GithubImportKind::Pull);\n+            out.push(ResolvedChild {\n+                url: url.clone(),\n+                title: format!(\"#{number} {title}\"),\n+                card,\n+            });\n+        }\n+        out.sort_by(|a, b| a.url.cmp(&b.url));\n+        Ok(out)\n+    }\n+\n+    async fn list_commits(&self, owner: &str, repo: &str) -> Result, String> {\n+        let arr = self\n+            .get_json_array_pages(&format!(\"/repos/{owner}/{repo}/commits?per_page=100\"))\n+            .await?;\n+        let mut out = Vec::new();\n+        for commit in &arr {\n+            let Some(sha) = github_string(commit, \"sha\") else {\n+                continue;\n+            };\n+            let short = sha.chars().take(7).collect::();\n+            let title = commit\n+                .get(\"commit\")\n+                .and_then(|c| c.get(\"message\"))\n+                .and_then(|v| v.as_str())\n+                .and_then(|m| m.lines().next())\n+                .filter(|s| !s.trim().is_empty())\n+                .unwrap_or(\"commit\");\n+            let url = github_string(commit, \"html_url\")\n+                .map(|s| s.to_string())\n+                .unwrap_or_else(|| format!(\"https://github.com/{owner}/{repo}/commit/{sha}\"));\n+            let card = card_for_commit(commit, &url, &short, title);\n+            out.push(ResolvedChild {\n+                url: url.clone(),\n+                title: format!(\"{short} {title}\"),\n+                card,\n+            });\n+        }\n+        out.sort_by(|a, b| a.url.cmp(&b.url));\n+        Ok(out)\n+    }\n+\n+    async fn list_releases(&self, owner: &str, repo: &str) -> Result, String> {\n+        let arr = self\n+            .get_json_array_pages(&format!(\"/repos/{owner}/{repo}/releases?per_page=100\"))\n+            .await?;\n+        let mut out = Vec::new();\n+        for release in &arr {\n+            let Some(tag) = github_string(release, \"tag_name\") else {\n+                continue;\n+            };\n+            let title = github_string(release, \"name\").unwrap_or(tag);\n+            let url = github_string(release, \"html_url\")\n+                .map(|s| s.to_string())\n+                .unwrap_or_else(|| format!(\"https://github.com/{owner}/{repo}/releases/tag/{tag}\"));\n+            let card = card_for_release(release, &url, title);\n+            out.push(ResolvedChild {\n+                url: url.clone(),\n+                title: title.to_string(),\n+                card,\n+            });\n+        }\n+        out.sort_by(|a, b| a.url.cmp(&b.url));\n+        Ok(out)\n+    }\n+}\n+\n+fn github_segments(item: &ItemId) -> Option> {\n+    let url = url::Url::parse(item.as_str()).ok()?;\n+    if url.host_str()?.eq_ignore_ascii_case(\"github.com\") {\n+        Some(\n+            url.path_segments()\n+                .map(|segments| {\n+                    segments\n+                        .filter(|s| !s.is_empty())\n+                        .map(|s| s.to_ascii_lowercase())\n+                        .collect::>()\n+                })\n+                .unwrap_or_default(),\n+        )\n+    } else {\n+        None\n+    }\n+}\n+\n+fn title_case_segment(seg: &str) -> String {\n+    let mut c = seg.chars();\n+    match c.next() {\n+        None => String::new(),\n+        Some(f) => f.to_uppercase().chain(c).collect(),\n+    }\n+}\n+\n+fn github_repo_sections(owner: &str, repo: &str) -> Vec {\n+    [\n+        (\"issues\", \"GitHub issues for this repository.\"),\n+        (\"pulls\", \"GitHub pull requests for this repository.\"),\n+        (\"commits\", \"GitHub commits for this repository.\"),\n+        (\"releases\", \"GitHub releases for this repository.\"),\n+    ]\n+    .into_iter()\n+    .map(|(section, blurb)| {\n+        let url = format!(\"https://github.com/{owner}/{repo}/{section}\");\n+        let mut card = GithubImportCard::new(\n+            GithubImportKind::RepoSection,\n+            url.clone(),\n+            format!(\"{owner}/{repo} — {}\", title_case_segment(section)),\n+        );\n+        card.excerpt = Some(blurb.to_string());\n+        ResolvedChild {\n+            url,\n+            title: section.to_string(),\n+            card,\n+        }\n+    })\n+    .collect()\n+}\n+\n+fn resolver_thread_tag(item: &ItemId) -> String {\n+    let tail = item\n+        .display_path()\n+        .trim_start_matches(\"-/\")\n+        .replace('/', \":\")\n+        .replace('?', \":\");\n+    format!(\"import:{tail}\")\n+}\n+\n+fn children_to_dsl(children: &[ResolvedChild]) -> String {\n+    let mut out = String::new();\n+    for child in children {\n+        let json = serde_json::to_string(&child.card).unwrap_or_else(|_| \"{}\".to_string());\n+        let inner = format!(\"```slug-github-card\\n{json}\\n```\");\n+        out.push_str(&format!(\"{} {{\\n{}\\n}}\\n\\n\", child.url, inner));\n+    }\n+    out\n+}\n+\n+fn card_for_repo(repo: &Value, fallback_url: &str) -> GithubImportCard {\n+    let url = github_string(repo, \"html_url\")\n+        .map(|s| s.to_string())\n+        .filter(|s| !s.is_empty())\n+        .unwrap_or_else(|| fallback_url.to_string());\n+    let full_name = github_string(repo, \"full_name\")\n+        .or_else(|| github_string(repo, \"name\"))\n+        .unwrap_or(\"repository\");\n+    let mut card = GithubImportCard::new(GithubImportKind::Repo, url, full_name.to_string());\n+    if let Some(lang) = github_string(repo, \"language\") {\n+        card.sublines.push(format!(\"Language: {lang}\"));\n+    }\n+    if let Some(desc) = github_string(repo, \"description\") {\n+        card.excerpt = Some(desc.to_string());\n+    }\n+    card\n+}\n+\n+fn excerpt_from_github_body(body: Option<&str>) -> Option {\n+    let b = body?.trim();\n+    if b.is_empty() {\n+        return None;\n+    }\n+    let max = 1200usize;\n+    if b.len() <= max {\n+        Some(b.to_string())\n+    } else {\n+        Some(format!(\"{}…\", b.chars().take(max).collect::()))\n+    }\n+}\n+\n+fn card_for_issue(v: &Value, url: &str, kind: GithubImportKind) -> GithubImportCard {\n+    let number = v.get(\"number\").and_then(|n| n.as_i64());\n+    let title = github_string(v, \"title\").unwrap_or(\"Untitled\");\n+    let state = github_string(v, \"state\").unwrap_or(\"unknown\");\n+    let headline = match number {\n+        Some(n) => format!(\"#{n} {title}\"),\n+        None => title.to_string(),\n+    };\n+    let mut card = GithubImportCard::new(kind, url.to_string(), headline);\n+    card.sublines.push(format!(\"State: {state}\"));\n+    if let Some(a) = github_user_login(v) {\n+        card.sublines.push(format!(\"Author: @{a}\"));\n+    }\n+    let labels = github_labels(v);\n+    if !labels.is_empty() {\n+        card.sublines\n+            .push(format!(\"Labels: {}\", labels.join(\", \")));\n+    }\n+    card.excerpt = excerpt_from_github_body(github_string(v, \"body\"));\n+    card\n+}\n+\n+fn card_for_commit(v: &Value, url: &str, short_sha: &str, subject: &str) -> GithubImportCard {\n+    let headline = format!(\"{short_sha} {subject}\");\n+    let mut card = GithubImportCard::new(GithubImportKind::Commit, url.to_string(), headline);\n+    if let Some(name) = v\n+        .get(\"commit\")\n+        .and_then(|c| c.get(\"author\"))\n+        .and_then(|a| a.get(\"name\"))\n+        .and_then(|n| n.as_str())\n+        .filter(|s| !s.trim().is_empty())\n+    {\n+        card.sublines.push(format!(\"Author: {name}\"));\n+    }\n+    if let Some(login) = github_user_login(v) {\n+        card.sublines.push(format!(\"GitHub: @{login}\"));\n+    }\n+    if let Some(date) = v\n+        .get(\"commit\")\n+        .and_then(|c| c.get(\"author\"))\n+        .and_then(|a| a.get(\"date\"))\n+        .and_then(|d| d.as_str())\n+    {\n+        card.sublines.push(format!(\"Date: {date}\"));\n+    }\n+    if let Some(msg) = v\n+        .get(\"commit\")\n+        .and_then(|c| c.get(\"message\"))\n+        .and_then(|m| m.as_str())\n+    {\n+        card.excerpt = excerpt_from_github_body(Some(msg));\n+    }\n+    card\n+}\n+\n+fn card_for_release(v: &Value, url: &str, title: &str) -> GithubImportCard {\n+    let tag = github_string(v, \"tag_name\").unwrap_or(\"untagged\");\n+    let mut card = GithubImportCard::new(\n+        GithubImportKind::Release,\n+        url.to_string(),\n+        format!(\"Release — {title}\"),\n+    );\n+    card.sublines.push(format!(\"Tag: {tag}\"));\n+    if v.get(\"draft\").and_then(|b| b.as_bool()).unwrap_or(false) {\n+        card.sublines.push(\"Draft: yes\".to_string());\n+    }\n+    if v.get(\"prerelease\")\n+        .and_then(|b| b.as_bool())\n+        .unwrap_or(false)\n+    {\n+        card.sublines.push(\"Prerelease: yes\".to_string());\n+    }\n+    if let Some(a) = github_user_login(v) {\n+        card.sublines.push(format!(\"Author: @{a}\"));\n+    }\n+    if let Some(pub_at) = github_string(v, \"published_at\") {\n+        card.sublines.push(format!(\"Published: {pub_at}\"));\n+    }\n+    card.excerpt = excerpt_from_github_body(github_string(v, \"body\"));\n+    card\n+}\n+\n+fn github_string<'a>(value: &'a Value, key: &str) -> Option<&'a str> {\n+    value\n+        .get(key)\n+        .and_then(|v| v.as_str())\n+        .filter(|s| !s.trim().is_empty())\n+}\n+\n+fn github_user_login(value: &Value) -> Option<&str> {\n+    value\n+        .get(\"user\")\n+        .and_then(|u| u.get(\"login\"))\n+        .and_then(|v| v.as_str())\n+        .filter(|s| !s.trim().is_empty())\n+}\n+\n+fn github_labels(value: &Value) -> Vec {\n+    value\n+        .get(\"labels\")\n+        .and_then(|v| v.as_array())\n+        .into_iter()\n+        .flat_map(|labels| labels.iter())\n+        .filter_map(|label| label.get(\"name\").and_then(|v| v.as_str()))\n+        .filter(|name| !name.trim().is_empty())\n+        .map(|name| name.to_string())\n+        .collect()\n+}\n+\n+pub async fn resolve_github_children(\n+    state: &AppState,\n+    room: &str,\n+    item: &ItemId,\n+) -> Result {\n+    if !state.github_resolver.can_resolve_children(item) {\n+        return Err(\"no GitHub resolver for this item\".to_string());\n+    }\n+\n+    let key = format!(\"github:{}:{}\", room.trim(), item.as_str());\n+    let now = now_ms();\n+    {\n+        let mut runs = state.resolver_runs.write().await;\n+        if let Some(last) = runs.get(&key) {\n+            let remaining = GITHUB_RESOLVER_COOLDOWN_MS - (now - *last);\n+            if remaining > 0 {\n+                return Err(format!(\n+                    \"GitHub resolver cooldown: try again in {}s\",\n+                    (remaining + 999) / 1000\n+                ));\n+            }\n+        }\n+        runs.insert(key, now);\n+    }\n+\n+    let children = state.github_resolver.list_children(item).await?;\n+    if children.is_empty() {\n+        return Ok(0);\n+    }\n+    let text = children_to_dsl(&children);\n+    let thread_tag = resolver_thread_tag(item);\n+    let (tx, rx) = oneshot::channel();\n+    state\n+        .write_tx\n+        .send(WriteCmd::SystemIngest {\n+            room: room.to_string(),\n+            thread_tag,\n+            text,\n+            principal: GITHUB_SYSTEM_PRINCIPAL.to_string(),\n+            reply: tx,\n+        })\n+        .await\n+        .map_err(|_| \"writer unavailable\".to_string())?;\n+    rx.await\n+        .map_err(|_| \"writer dropped\".to_string())?\n+        .map_err(|(msg, hint)| hint.map_or(msg.clone(), |h| format!(\"{msg}: {h}\")))?;\n+    Ok(children.len())\n+}\n+\n+fn extract_fence<'a>(body: &'a str, lang: &str) -> Option<&'a str> {\n+    let b = body.trim();\n+    let prefix = format!(\"```{lang}\");\n+    let rest = b.strip_prefix(prefix.as_str())?;\n+    let rest = rest\n+        .strip_prefix('\\n')\n+        .or_else(|| rest.strip_prefix('\\r'))\n+        .unwrap_or(rest);\n+    let end = rest.find(\"\\n```\")?;\n+    Some(rest[..end].trim())\n+}\n+\n+fn parse_github_import_from_body(body: &str) -> Option {\n+    let trimmed = body.trim();\n+    if let Some(json) = extract_fence(trimmed, \"slug-github-card\") {\n+        let c: GithubImportCard = serde_json::from_str(json).ok()?;\n+        return (c.v == 1 && (c.schema.is_empty() || c.schema == SLUG_GITHUB_SCHEMA)).then_some(c);\n+    }\n+    if let Some(json) = extract_fence(trimmed, \"json\") {\n+        if let Ok(c) = serde_json::from_str::(json) {\n+            if c.v == 1\n+                && (c.schema == SLUG_GITHUB_SCHEMA\n+                    || (c.schema.is_empty() && c.url.contains(\"github.com\")))\n+            {\n+                return Some(c);\n+            }\n+        }\n+    }\n+    if trimmed.starts_with('{') {\n+        let c: GithubImportCard = serde_json::from_str(trimmed).ok()?;\n+        return (c.v == 1\n+            && (c.schema == SLUG_GITHUB_SCHEMA\n+                || (c.schema.is_empty() && c.url.contains(\"github.com\"))))\n+        .then_some(c);\n+    }\n+    None\n+}\n+\n+fn kind_badge(kind: &GithubImportKind) -> &'static str {\n+    match kind {\n+        GithubImportKind::Repo => \"GitHub · repository\",\n+        GithubImportKind::RepoSection => \"GitHub · tree\",\n+        GithubImportKind::Issue => \"GitHub · issue\",\n+        GithubImportKind::Pull => \"GitHub · pull request\",\n+        GithubImportKind::Commit => \"GitHub · commit\",\n+        GithubImportKind::Release => \"GitHub · release\",\n+    }\n+}\n+\n+fn render_github_card(card: &GithubImportCard) -> maud::Markup {\n+    html! {\n+        article.github-import-card {\n+            header.github-import-card__hdr {\n+                span class=\"github-import-card__badge\" { (kind_badge(&card.kind)) }\n+                h3.github-import-card__title { (card.headline.as_str()) }\n+            }\n+            @if !card.sublines.is_empty() {\n+                ul.github-import-card__meta {\n+                    @for line in &card.sublines {\n+                        li { (line.as_str()) }\n+                    }\n+                }\n+            }\n+            @if let Some(ex) = &card.excerpt {\n+                div.github-import-card__excerpt {\n+                    @for block in ex.split(\"\\n\\n\") {\n+                        @if !block.trim().is_empty() {\n+                            p { (block) }\n+                        }\n+                    }\n+                }\n+            }\n+            p.github-import-card__link {\n+                a href=(card.url.as_str()) rel=\"noopener noreferrer\" target=\"_blank\" {\n+                    \"Open on GitHub\"\n+                }\n+            }\n+        }\n+    }\n+}\n+\n+/// Rich HTML for bodies that contain a [`GithubImportCard`] fence (or equivalent JSON).\n+pub fn try_render_github_import_markup(raw: &str) -> Option {\n+    let card = parse_github_import_from_body(raw)?;\n+    Some(render_github_card(&card))\n+}\n+\n+#[async_trait]\n+impl ExternalResolver for GitHubResolver {\n+    fn domain_match(&self) -> &'static str {\n+        \"github.com\"\n+    }\n+\n+    fn normalize(&self, path: &str) -> String {\n+        path.to_string()\n+    }\n+\n+    async fn fetch_body(&self, _item: &ItemId) -> Result {\n+        Err(\"GitHub fetch_body not implemented\".to_string())\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+\n+    #[test]\n+    fn github_segments_parse_normalized_url() {\n+        let item = ItemId::parse(\"https://github.com/Sortersocial/Slug/issues\").unwrap();\n+        assert_eq!(\n+            github_segments(&item),\n+            Some(vec![\n+                \"sortersocial\".to_string(),\n+                \"slug\".to_string(),\n+                \"issues\".to_string()\n+            ])\n+        );\n+    }\n+\n+    #[test]\n+    fn repo_sections_are_direct_children() {\n+        let sections = github_repo_sections(\"sortersocial\", \"slug\");\n+        let urls: Vec = sections.into_iter().map(|c| c.url).collect();\n+        assert!(urls.contains(&\"https://github.com/sortersocial/slug/issues\".to_string()));\n+        assert!(urls.contains(&\"https://github.com/sortersocial/slug/pulls\".to_string()));\n+    }\n+\n+    #[test]\n+    fn children_to_dsl_wraps_slug_github_card() {\n+        let dsl = children_to_dsl(&[ResolvedChild {\n+            url: \"https://github.com/o/r/issues/1\".into(),\n+            title: \"#1 title\".into(),\n+            card: GithubImportCard::new(\n+                GithubImportKind::Issue,\n+                \"https://github.com/o/r/issues/1\".into(),\n+                \"#1 title\".into(),\n+            ),\n+        }]);\n+        assert!(dsl.contains(\"https://github.com/o/r/issues/1\"));\n+        assert!(dsl.contains(\"```slug-github-card\"));\n+        assert!(dsl.contains(\"\\\"schema\\\":\\\"slug_github_import\\\"\"));\n+    }\n+\n+    #[test]\n+    fn parse_accepts_slug_github_fence() {\n+        let card = GithubImportCard::new(\n+            GithubImportKind::Repo,\n+            \"https://github.com/o/r\".into(),\n+            \"o/r\".into(),\n+        );\n+        let body = format!(\"```slug-github-card\\n{}\\n```\\n\", serde_json::to_string(&card).unwrap());\n+        let parsed = parse_github_import_from_body(&body).expect(\"parses\");\n+        assert_eq!(parsed, card);\n+    }\n+\n+    #[test]\n+    fn parse_accepts_schema_json_fence() {\n+        let card = GithubImportCard::new(\n+            GithubImportKind::Issue,\n+            \"https://github.com/o/r/issues/2\".into(),\n+            \"#2 hi\".into(),\n+        );\n+        let json = serde_json::to_string(&card).unwrap();\n+        let body = format!(\"```json\\n{json}\\n```\");\n+        let parsed = parse_github_import_from_body(&body).expect(\"parses json fence\");\n+        assert_eq!(parsed.headline, \"#2 hi\");\n+    }\n+\n+    #[test]\n+    fn issue_card_includes_author_and_excerpt() {\n+        let issue = serde_json::json!({\n+            \"number\": 12,\n+            \"title\": \"Render children\",\n+            \"state\": \"open\",\n+            \"html_url\": \"https://github.com/o/r/issues/12\",\n+            \"user\": {\"login\": \"octo\"},\n+            \"labels\": [{\"name\": \"bug\"}],\n+            \"body\": \"The issue body.\"\n+        });\n+        let card = card_for_issue(\n+            &issue,\n+            \"https://github.com/o/r/issues/12\",\n+            GithubImportKind::Issue,\n+        );\n+        assert!(card.sublines.iter().any(|l| l.contains(\"@octo\")));\n+        assert_eq!(card.excerpt.as_deref(), Some(\"The issue body.\").as_deref());\n+    }\n+}\ndiff --git a/server/src/resolvers/mod.rs b/server/src/resolvers/mod.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..3e4caad081acdd2f89cba9f661de43996d6470f3\n--- /dev/null\n+++ b/server/src/resolvers/mod.rs\n@@ -0,0 +1,18 @@\n+//! Domain resolvers (GitHub, …) and matching HTML renderers for imported item bodies.\n+//!\n+//! Resolver output is ingested as DSL; bodies may embed a `slug-github-card` fenced JSON\n+//! envelope that [`crate::html::render_item_body_in_scope`] renders instead of a raw `
    `.\n+\n+pub mod github;\n+pub mod default_external;\n+\n+pub use default_external::DefaultExternalResolver;\n+pub use github::{\n+    resolve_github_children, try_render_github_import_markup, ExternalResolver, GitHubResolver,\n+    GithubImportCard, GithubImportKind, ResolvedChild,\n+};\n+\n+/// Extension point: add more `try_render_*` calls here as new resolvers ship.\n+pub fn try_render_resolver_item_body(raw: &str) -> Option {\n+    github::try_render_github_import_markup(raw)\n+}\ndiff --git a/server/src/scope_rank.rs b/server/src/scope_rank.rs\nindex 06c560b8eff09b34896d3935d3917fb28f602bc6..2361b2be5ae6b8e1813b6b7ebd5bbf317429b6ad 100644\n--- a/server/src/scope_rank.rs\n+++ b/server/src/scope_rank.rs\n@@ -162,6 +162,45 @@ pub fn build_children_rankings(content: &ContentState, parent: &ItemId) -> Child\n     build_rankings_for_item_set(content, &items)\n }\n \n+/// Host-only `https://…` roots for the external garden index (`/-/`).\n+///\n+/// Includes every `https://host` ancestor of any [`ItemId::Web`] item that appears in\n+/// `content.items`, as a parent key in `item_children`, or as a child in `item_children`\n+/// (so implied “ghost” parents created only via [`ReducerState::add_child_edge`] still show up).\n+pub fn external_root_host_items(content: &ContentState) -> Vec {\n+    let mut hosts: HashSet = HashSet::new();\n+\n+    let mut consider = |id: ItemId| {\n+        let id = id.normalized_storage();\n+        if !matches!(&id, ItemId::Web(_)) {\n+            return;\n+        }\n+        let mut cur = id;\n+        while let Some(p) = cur.parent() {\n+            cur = p.normalized_storage();\n+        }\n+        if matches!(cur, ItemId::Web(_)) {\n+            hosts.insert(cur);\n+        }\n+    };\n+\n+    for it in &content.items {\n+        consider(it.clone());\n+    }\n+    for parent in content.item_children.keys() {\n+        consider(parent.clone());\n+    }\n+    for set in content.item_children.values() {\n+        for ch in set {\n+            consider(ch.clone());\n+        }\n+    }\n+\n+    let mut out: Vec = hosts.into_iter().collect();\n+    out.sort();\n+    out\n+}\n+\n pub fn is_pair_voted_in_group(group: &GroupState, a: &ItemId, b: &ItemId) -> bool {\n     let Some(&a_idx) = group.item_to_idx.get(a) else {\n         return false;\n@@ -305,4 +344,29 @@ mod tests {\n         assert!(next.0 == c || next.1 == c);\n         assert_ne!(canonical_pair(&next.0, &next.1), canonical_pair(&a, &b));\n     }\n+\n+    #[test]\n+    fn external_root_hosts_include_ghost_chain_hosts() {\n+        use crate::reducer::ContentState;\n+        let gh = ItemId::parse(\"https://github.com\").unwrap();\n+        let org = ItemId::parse(\"https://github.com/org\").unwrap();\n+        let repo = ItemId::parse(\"https://github.com/org/rep\").unwrap();\n+        let mut item_children: HashMap> = HashMap::new();\n+        item_children.entry(gh.clone()).or_default().insert(org.clone());\n+        item_children.entry(org.clone()).or_default().insert(repo.clone());\n+        let mut items = HashSet::new();\n+        items.insert(repo.clone());\n+        let content = ContentState {\n+            ranking_group: crate::reducer::GroupState::new(),\n+            items,\n+            item_bodies: HashMap::new(),\n+            item_children,\n+            item_votes: HashMap::new(),\n+            item_snippets: HashMap::new(),\n+            item_threads: HashMap::new(),\n+            rank_history: HashMap::new(),\n+        };\n+        let roots = external_root_host_items(&content);\n+        assert_eq!(roots, vec![gh]);\n+    }\n }\ndiff --git a/server/src/state.rs b/server/src/state.rs\nindex 48298e2e66456268d23a6462536eb32bfeb5f29b..648ab5304764a329fcabbbbcd3782b94e3e005a8 100644\n--- a/server/src/state.rs\n+++ b/server/src/state.rs\n@@ -4,7 +4,7 @@ use std::sync::Arc;\n use tokio::sync::{broadcast, mpsc, RwLock};\n \n use crate::{\n-    event_log::EventLog, events::ThreadCapability, external_resolver::GitHubResolver,\n+    event_log::EventLog, events::ThreadCapability, resolvers::GitHubResolver,\n     reducer::ReducerState, write_cmd::WriteCmd,\n };\n \ndiff --git a/server/static/theme_default.css b/server/static/theme_default.css\nindex 184e11a590e7019773f7f0abfa41e79161556c71..7ea5f502f9b0b6341ce56d60ae883479070760d6 100644\n--- a/server/static/theme_default.css\n+++ b/server/static/theme_default.css\n@@ -1023,6 +1023,23 @@ body.view-vote-compare .vote-compare-shell > h2 {\n   line-height: 1.35;\n   padding: 8px 10px;\n }\n+.vote-compare-item-body .item-body-rich {\n+  min-width: 0;\n+  text-align: start;\n+}\n+.vote-compare-right .vote-compare-item-body .item-body-rich {\n+  display: flex;\n+  flex-direction: column;\n+  align-items: flex-end;\n+}\n+.vote-compare-item-body .item-body-rich article.github-import-card {\n+  box-sizing: border-box;\n+  width: 100%;\n+  max-width: min(100%, 420px);\n+}\n+.vote-compare-right .vote-compare-item-body .item-body-rich article.github-import-card {\n+  margin-left: auto;\n+}\n .vote-compare-item-body-empty {\n   font-size: 12px;\n   margin: 8px 0 0;\n@@ -1675,3 +1692,47 @@ body.view-ontology-light .rank-history-cause {\n body.view-ontology-light .rank-history-vote {\n   margin-top: 6px;\n }\n+\n+/* GitHub resolver import cards (rich bodies on -/ garden + vote compare) */\n+article.github-import-card {\n+  border: 1px solid var(--lo);\n+  background: var(--g2);\n+  border-radius: 6px;\n+  padding: 12px 14px;\n+  margin: 8px 0;\n+  max-width: 100%;\n+}\n+.github-import-card__hdr {\n+  margin-bottom: 6px;\n+}\n+.github-import-card__badge {\n+  display: block;\n+  font-size: 0.78em;\n+  color: var(--muted);\n+  margin-bottom: 4px;\n+}\n+.github-import-card__title {\n+  margin: 0;\n+  font-size: 1.05em;\n+  font-weight: 600;\n+}\n+ul.github-import-card__meta {\n+  margin: 8px 0 0 1.1em;\n+  padding: 0;\n+  font-size: 0.9em;\n+}\n+.github-import-card__meta li {\n+  margin: 2px 0;\n+}\n+.github-import-card__excerpt {\n+  margin-top: 10px;\n+  font-size: 0.92em;\n+  white-space: pre-wrap;\n+}\n+.github-import-card__excerpt p {\n+  margin: 6px 0;\n+}\n+.github-import-card__link {\n+  margin-top: 12px;\n+  font-size: 0.95em;\n+}\ndiff --git a/server/static/theme_retro.css b/server/static/theme_retro.css\nindex 6747f59eb1ec5c335029fe92d4e5c55b3125a210..d366be6fc8e8fcbc8122b8954b1e356ee36e6bc9 100644\n--- a/server/static/theme_retro.css\n+++ b/server/static/theme_retro.css\n@@ -278,3 +278,20 @@ body.view-ontology .vote-compare-item-body pre {\n   border: 1px solid #ccc;\n   padding: 0.5rem 0.65rem;\n }\n+body.view-ontology .vote-compare-item-body .item-body-rich {\n+  min-width: 0;\n+  text-align: start;\n+}\n+body.view-ontology .vote-compare-right .vote-compare-item-body .item-body-rich {\n+  display: flex;\n+  flex-direction: column;\n+  align-items: flex-end;\n+}\n+body.view-ontology .vote-compare-item-body .item-body-rich article.github-import-card {\n+  box-sizing: border-box;\n+  width: 100%;\n+  max-width: min(100%, 420px);\n+}\n+body.view-ontology .vote-compare-right .vote-compare-item-body .item-body-rich article.github-import-card {\n+  margin-left: auto;\n+}\ndiff --git a/server/static/theme_retro_craft.css b/server/static/theme_retro_craft.css\nindex 55d984a6dd70ebeadeca7baef86b844955f1d78c..d5bc384437f05f001924630947457d416772e950 100644\n--- a/server/static/theme_retro_craft.css\n+++ b/server/static/theme_retro_craft.css\n@@ -907,6 +907,23 @@ body.view-ontology .vote-compare-item-body pre {\n   line-height: 1.35;\n   padding: 0.55rem 0.65rem;\n }\n+body.view-ontology .vote-compare-item-body .item-body-rich {\n+  min-width: 0;\n+  text-align: start;\n+}\n+body.view-ontology .vote-compare-right .vote-compare-item-body .item-body-rich {\n+  display: flex;\n+  flex-direction: column;\n+  align-items: flex-end;\n+}\n+body.view-ontology .vote-compare-item-body .item-body-rich article.github-import-card {\n+  box-sizing: border-box;\n+  width: 100%;\n+  max-width: min(100%, 420px);\n+}\n+body.view-ontology .vote-compare-right .vote-compare-item-body .item-body-rich article.github-import-card {\n+  margin-left: auto;\n+}\n body.view-ontology .vote-compare-item-body-empty {\n   font-size: 0.78rem;\n   margin: 0.45rem 0 0;\ndiff --git a/server/tests/integration.rs b/server/tests/integration.rs\nindex fb0b9335440181d4d50104d37926b0b2eeeb602a..d979000292b6a39db7c7b54f2b804adb9cd39369 100644\n--- a/server/tests/integration.rs\n+++ b/server/tests/integration.rs\n@@ -3,7 +3,7 @@ use sha2::{Digest, Sha256};\n use slug_types::{room_route_segment, ItemId};\n use slugsocial_server::{\n     event_log::EventLog,\n-    events::{Event, TokenIssued, UserRegistered},\n+    events::{Event, Ingest, TokenIssued, UserRegistered},\n     middleware::canonical_view_url,\n     spawn_writer_actor_for_test,\n     state::{AppConfig, AppState},\n@@ -1614,6 +1614,71 @@ async fn test_view_counts_increment_and_display() {\n     );\n }\n \n+#[tokio::test]\n+async fn test_vote_compare_renders_github_import_cards() {\n+    let (addr, _tmp, _log, state, _handle) = create_test_server_with_state().await;\n+    let client = reqwest::Client::new();\n+\n+    let raw = \"@00000000-0000-0000-0000-000000000000:test:local/test\\n\\\n+https://github.com/ghvotehi/a/issues/9 {\\n\\\n+```slug-github-card\\n\\\n+{\\\"v\\\":1,\\\"schema\\\":\\\"slug_github_import\\\",\\\"kind\\\":\\\"issue\\\",\\\"url\\\":\\\"https://github.com/ghvotehi/a/issues/9\\\",\\\"headline\\\":\\\"#9 Left corner\\\",\\\"sublines\\\":[\\\"State: open\\\"]}\\n\\\n+```\\n\\\n+}\\n\\\n+\\n\\\n+https://github.com/ghvotehi/a/issues/10 {\\n\\\n+```slug-github-card\\n\\\n+{\\\"v\\\":1,\\\"schema\\\":\\\"slug_github_import\\\",\\\"kind\\\":\\\"issue\\\",\\\"url\\\":\\\"https://github.com/ghvotehi/a/issues/10\\\",\\\"headline\\\":\\\"#10 Right corner\\\",\\\"sublines\\\":[\\\"State: open\\\"]}\\n\\\n+```\\n\\\n+}\\n\";\n+\n+    {\n+        let mut w = state.reduced.write().await;\n+        w.apply_event(Event::Ingest(Ingest {\n+            ts: 10,\n+            id: \"ing-vote-github-cards\".to_string(),\n+            raw: raw.to_string(),\n+            principal: \"testuser\".to_string(),\n+            delegate: Some(\n+                \"00000000-0000-0000-0000-000000000000:test:local/test\".to_string(),\n+            ),\n+            room_id: \"public\".to_string(),\n+            thread_tag: \"gh-vote-cards\".to_string(),\n+        }));\n+    }\n+\n+    let left = ItemId::parse(\"https://github.com/ghvotehi/a/issues/9\")\n+        .unwrap()\n+        .normalized_storage()\n+        .to_storage_string();\n+    let right = ItemId::parse(\"https://github.com/ghvotehi/a/issues/10\")\n+        .unwrap()\n+        .normalized_storage()\n+        .to_storage_string();\n+    let q = format!(\n+        \"/vote/compare?left={}&right={}\",\n+        urlencoding::encode(&left),\n+        urlencoding::encode(&right)\n+    );\n+    let resp = client\n+        .get(format!(\"http://{addr}{q}\"))\n+        .send()\n+        .await\n+        .unwrap();\n+    assert!(resp.status().is_success(), \"{}\", resp.status());\n+    let body = resp.text().await.unwrap();\n+    let n_cards = body.matches(\"github-import-card\").count();\n+    assert!(\n+        n_cards >= 2,\n+        \"expected two GitHub import cards on vote compare, count={n_cards}, snippet={}\",\n+        body.chars().take(1500).collect::()\n+    );\n+    assert!(body.contains(\"vote-compare-left\"));\n+    assert!(body.contains(\"vote-compare-right\"));\n+    assert!(body.contains(\"#9 Left corner\"));\n+    assert!(body.contains(\"#10 Right corner\"));\n+}\n+\n #[tokio::test]\n async fn test_search_handles_multibyte_unicode() {\n     // HTML search pages are offline during the auth-v3 refactor.\n\n\nSide B — contributor: tommy-mor\nSide B — commit message:\n[285b64d4] Offload Reddit payloads to RocksDB and stream event log replay.\n\nVendor durable as a workspace crate, store entity JSON in entity_db instead\nof GlobalTree, and replay events.jsonl one line at a time to cut startup RAM.\n\nCo-authored-by: Cursor \n\nSide B — unified diff (full patch):\ndiff --git a/Cargo.lock b/Cargo.lock\nindex e55d87f32ab32064686431c7082ef8c9ca872d63..8fc09f9ac978bd7ccf57f177989057b606677db8 100644\n--- a/Cargo.lock\n+++ b/Cargo.lock\n@@ -11,6 +11,18 @@ dependencies = [\n  \"memchr\",\n ]\n \n+[[package]]\n+name = \"anes\"\n+version = \"0.1.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299\"\n+\n+[[package]]\n+name = \"anstyle\"\n+version = \"1.0.14\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000\"\n+\n [[package]]\n name = \"anyhow\"\n version = \"1.0.102\"\n@@ -56,6 +68,12 @@ version = \"1.1.2\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0\"\n \n+[[package]]\n+name = \"autocfg\"\n+version = \"1.5.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53\"\n+\n [[package]]\n name = \"axum\"\n version = \"0.7.9\"\n@@ -153,6 +171,75 @@ version = \"0.22.1\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6\"\n \n+[[package]]\n+name = \"bincode\"\n+version = \"1.3.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad\"\n+dependencies = [\n+ \"serde\",\n+]\n+\n+[[package]]\n+name = \"bindgen\"\n+version = \"0.65.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5\"\n+dependencies = [\n+ \"bitflags 1.3.2\",\n+ \"cexpr\",\n+ \"clang-sys\",\n+ \"lazy_static\",\n+ \"lazycell\",\n+ \"peeking_take_while\",\n+ \"prettyplease\",\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"regex\",\n+ \"rustc-hash 1.1.0\",\n+ \"shlex\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"bindgen\"\n+version = \"0.72.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895\"\n+dependencies = [\n+ \"bitflags 2.11.1\",\n+ \"cexpr\",\n+ \"clang-sys\",\n+ \"itertools 0.13.0\",\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"regex\",\n+ \"rustc-hash 2.1.2\",\n+ \"shlex\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"bit-set\"\n+version = \"0.8.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3\"\n+dependencies = [\n+ \"bit-vec\",\n+]\n+\n+[[package]]\n+name = \"bit-vec\"\n+version = \"0.8.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7\"\n+\n+[[package]]\n+name = \"bitflags\"\n+version = \"1.3.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a\"\n+\n [[package]]\n name = \"bitflags\"\n version = \"2.11.1\"\n@@ -171,6 +258,22 @@ version = \"1.11.1\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33\"\n \n+[[package]]\n+name = \"bzip2-sys\"\n+version = \"0.1.13+1.0.8\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14\"\n+dependencies = [\n+ \"cc\",\n+ \"pkg-config\",\n+]\n+\n+[[package]]\n+name = \"cast\"\n+version = \"0.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5\"\n+\n [[package]]\n name = \"cc\"\n version = \"1.2.62\"\n@@ -178,15 +281,89 @@ source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98\"\n dependencies = [\n  \"find-msvc-tools\",\n+ \"jobserver\",\n+ \"libc\",\n  \"shlex\",\n ]\n \n+[[package]]\n+name = \"cexpr\"\n+version = \"0.6.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766\"\n+dependencies = [\n+ \"nom\",\n+]\n+\n [[package]]\n name = \"cfg-if\"\n version = \"1.0.4\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801\"\n \n+[[package]]\n+name = \"ciborium\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e\"\n+dependencies = [\n+ \"ciborium-io\",\n+ \"ciborium-ll\",\n+ \"serde\",\n+]\n+\n+[[package]]\n+name = \"ciborium-io\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757\"\n+\n+[[package]]\n+name = \"ciborium-ll\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9\"\n+dependencies = [\n+ \"ciborium-io\",\n+ \"half\",\n+]\n+\n+[[package]]\n+name = \"clang-sys\"\n+version = \"1.8.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4\"\n+dependencies = [\n+ \"glob\",\n+ \"libc\",\n+ \"libloading\",\n+]\n+\n+[[package]]\n+name = \"clap\"\n+version = \"4.6.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51\"\n+dependencies = [\n+ \"clap_builder\",\n+]\n+\n+[[package]]\n+name = \"clap_builder\"\n+version = \"4.6.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f\"\n+dependencies = [\n+ \"anstyle\",\n+ \"clap_lex\",\n+]\n+\n+[[package]]\n+name = \"clap_lex\"\n+version = \"1.1.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9\"\n+\n [[package]]\n name = \"cookie\"\n version = \"0.18.1\"\n@@ -224,6 +401,73 @@ version = \"0.8.7\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b\"\n \n+[[package]]\n+name = \"criterion\"\n+version = \"0.5.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f\"\n+dependencies = [\n+ \"anes\",\n+ \"cast\",\n+ \"ciborium\",\n+ \"clap\",\n+ \"criterion-plot\",\n+ \"is-terminal\",\n+ \"itertools 0.10.5\",\n+ \"num-traits\",\n+ \"once_cell\",\n+ \"oorandom\",\n+ \"plotters\",\n+ \"rayon\",\n+ \"regex\",\n+ \"serde\",\n+ \"serde_derive\",\n+ \"serde_json\",\n+ \"tinytemplate\",\n+ \"walkdir\",\n+]\n+\n+[[package]]\n+name = \"criterion-plot\"\n+version = \"0.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1\"\n+dependencies = [\n+ \"cast\",\n+ \"itertools 0.10.5\",\n+]\n+\n+[[package]]\n+name = \"crossbeam-deque\"\n+version = \"0.8.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51\"\n+dependencies = [\n+ \"crossbeam-epoch\",\n+ \"crossbeam-utils\",\n+]\n+\n+[[package]]\n+name = \"crossbeam-epoch\"\n+version = \"0.9.18\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e\"\n+dependencies = [\n+ \"crossbeam-utils\",\n+]\n+\n+[[package]]\n+name = \"crossbeam-utils\"\n+version = \"0.8.21\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28\"\n+\n+[[package]]\n+name = \"crunchy\"\n+version = \"0.2.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5\"\n+\n [[package]]\n name = \"deranged\"\n version = \"0.5.8\"\n@@ -250,6 +494,25 @@ version = \"0.15.7\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b\"\n \n+[[package]]\n+name = \"durable\"\n+version = \"0.1.0\"\n+dependencies = [\n+ \"bincode\",\n+ \"criterion\",\n+ \"proptest\",\n+ \"rocksdb\",\n+ \"serde\",\n+ \"tempfile\",\n+ \"thiserror\",\n+]\n+\n+[[package]]\n+name = \"either\"\n+version = \"1.16.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e\"\n+\n [[package]]\n name = \"encoding_rs\"\n version = \"0.8.35\"\n@@ -373,6 +636,18 @@ dependencies = [\n  \"wasi\",\n ]\n \n+[[package]]\n+name = \"getrandom\"\n+version = \"0.3.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"libc\",\n+ \"r-efi 5.3.0\",\n+ \"wasip2\",\n+]\n+\n [[package]]\n name = \"getrandom\"\n version = \"0.4.2\"\n@@ -381,11 +656,17 @@ checksum = \"0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555\"\n dependencies = [\n  \"cfg-if\",\n  \"libc\",\n- \"r-efi\",\n+ \"r-efi 6.0.0\",\n  \"wasip2\",\n  \"wasip3\",\n ]\n \n+[[package]]\n+name = \"glob\"\n+version = \"0.3.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280\"\n+\n [[package]]\n name = \"h2\"\n version = \"0.4.14\"\n@@ -405,6 +686,17 @@ dependencies = [\n  \"tracing\",\n ]\n \n+[[package]]\n+name = \"half\"\n+version = \"2.7.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"crunchy\",\n+ \"zerocopy\",\n+]\n+\n [[package]]\n name = \"hashbrown\"\n version = \"0.15.5\"\n@@ -426,6 +718,12 @@ version = \"0.5.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea\"\n \n+[[package]]\n+name = \"hermit-abi\"\n+version = \"0.5.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c\"\n+\n [[package]]\n name = \"http\"\n version = \"1.4.1\"\n@@ -676,12 +974,51 @@ version = \"2.12.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2\"\n \n+[[package]]\n+name = \"is-terminal\"\n+version = \"0.4.17\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46\"\n+dependencies = [\n+ \"hermit-abi\",\n+ \"libc\",\n+ \"windows-sys 0.61.2\",\n+]\n+\n+[[package]]\n+name = \"itertools\"\n+version = \"0.10.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473\"\n+dependencies = [\n+ \"either\",\n+]\n+\n+[[package]]\n+name = \"itertools\"\n+version = \"0.13.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186\"\n+dependencies = [\n+ \"either\",\n+]\n+\n [[package]]\n name = \"itoa\"\n version = \"1.0.18\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682\"\n \n+[[package]]\n+name = \"jobserver\"\n+version = \"0.1.34\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33\"\n+dependencies = [\n+ \"getrandom 0.3.4\",\n+ \"libc\",\n+]\n+\n [[package]]\n name = \"js-sys\"\n version = \"0.3.99\"\n@@ -700,6 +1037,12 @@ version = \"1.5.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe\"\n \n+[[package]]\n+name = \"lazycell\"\n+version = \"1.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55\"\n+\n [[package]]\n name = \"leb128fmt\"\n version = \"0.1.0\"\n@@ -712,6 +1055,43 @@ version = \"0.2.186\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66\"\n \n+[[package]]\n+name = \"libloading\"\n+version = \"0.8.9\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"windows-link\",\n+]\n+\n+[[package]]\n+name = \"librocksdb-sys\"\n+version = \"0.11.0+8.1.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e\"\n+dependencies = [\n+ \"bindgen 0.65.1\",\n+ \"bzip2-sys\",\n+ \"cc\",\n+ \"glob\",\n+ \"libc\",\n+ \"libz-sys\",\n+ \"lz4-sys\",\n+ \"zstd-sys\",\n+]\n+\n+[[package]]\n+name = \"libz-sys\"\n+version = \"1.1.28\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22\"\n+dependencies = [\n+ \"cc\",\n+ \"pkg-config\",\n+ \"vcpkg\",\n+]\n+\n [[package]]\n name = \"linux-raw-sys\"\n version = \"0.12.1\"\n@@ -730,6 +1110,16 @@ version = \"0.4.30\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5\"\n \n+[[package]]\n+name = \"lz4-sys\"\n+version = \"1.11.1+lz4-1.10.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6\"\n+dependencies = [\n+ \"cc\",\n+ \"libc\",\n+]\n+\n [[package]]\n name = \"matchers\"\n version = \"0.2.0\"\n@@ -781,6 +1171,12 @@ version = \"0.3.17\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a\"\n \n+[[package]]\n+name = \"minimal-lexical\"\n+version = \"0.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a\"\n+\n [[package]]\n name = \"mio\"\n version = \"1.2.0\"\n@@ -826,6 +1222,16 @@ dependencies = [\n  \"tempfile\",\n ]\n \n+[[package]]\n+name = \"nom\"\n+version = \"7.1.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a\"\n+dependencies = [\n+ \"memchr\",\n+ \"minimal-lexical\",\n+]\n+\n [[package]]\n name = \"nu-ansi-term\"\n version = \"0.50.3\"\n@@ -841,19 +1247,34 @@ version = \"0.2.2\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441\"\n \n+[[package]]\n+name = \"num-traits\"\n+version = \"0.2.19\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841\"\n+dependencies = [\n+ \"autocfg\",\n+]\n+\n [[package]]\n name = \"once_cell\"\n version = \"1.21.4\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50\"\n \n+[[package]]\n+name = \"oorandom\"\n+version = \"11.1.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e\"\n+\n [[package]]\n name = \"openssl\"\n version = \"0.10.80\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n  \"cfg-if\",\n  \"foreign-types\",\n  \"libc\",\n@@ -890,6 +1311,12 @@ dependencies = [\n  \"vcpkg\",\n ]\n \n+[[package]]\n+name = \"peeking_take_while\"\n+version = \"0.1.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099\"\n+\n [[package]]\n name = \"percent-encoding\"\n version = \"2.3.2\"\n@@ -908,6 +1335,34 @@ version = \"0.3.33\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e\"\n \n+[[package]]\n+name = \"plotters\"\n+version = \"0.3.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747\"\n+dependencies = [\n+ \"num-traits\",\n+ \"plotters-backend\",\n+ \"plotters-svg\",\n+ \"wasm-bindgen\",\n+ \"web-sys\",\n+]\n+\n+[[package]]\n+name = \"plotters-backend\"\n+version = \"0.3.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a\"\n+\n+[[package]]\n+name = \"plotters-svg\"\n+version = \"0.3.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670\"\n+dependencies = [\n+ \"plotters-backend\",\n+]\n+\n [[package]]\n name = \"potential_utf\"\n version = \"0.1.5\"\n@@ -974,6 +1429,31 @@ dependencies = [\n  \"unicode-ident\",\n ]\n \n+[[package]]\n+name = \"proptest\"\n+version = \"1.11.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744\"\n+dependencies = [\n+ \"bit-set\",\n+ \"bit-vec\",\n+ \"bitflags 2.11.1\",\n+ \"num-traits\",\n+ \"rand 0.9.4\",\n+ \"rand_chacha 0.9.0\",\n+ \"rand_xorshift\",\n+ \"regex-syntax\",\n+ \"rusty-fork\",\n+ \"tempfile\",\n+ \"unarray\",\n+]\n+\n+[[package]]\n+name = \"quick-error\"\n+version = \"1.2.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0\"\n+\n [[package]]\n name = \"quote\"\n version = \"1.0.45\"\n@@ -983,6 +1463,12 @@ dependencies = [\n  \"proc-macro2\",\n ]\n \n+[[package]]\n+name = \"r-efi\"\n+version = \"5.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f\"\n+\n [[package]]\n name = \"r-efi\"\n version = \"6.0.0\"\n@@ -996,8 +1482,18 @@ source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a\"\n dependencies = [\n  \"libc\",\n- \"rand_chacha\",\n- \"rand_core\",\n+ \"rand_chacha 0.3.1\",\n+ \"rand_core 0.6.4\",\n+]\n+\n+[[package]]\n+name = \"rand\"\n+version = \"0.9.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea\"\n+dependencies = [\n+ \"rand_chacha 0.9.0\",\n+ \"rand_core 0.9.5\",\n ]\n \n [[package]]\n@@ -1007,7 +1503,17 @@ source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88\"\n dependencies = [\n  \"ppv-lite86\",\n- \"rand_core\",\n+ \"rand_core 0.6.4\",\n+]\n+\n+[[package]]\n+name = \"rand_chacha\"\n+version = \"0.9.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb\"\n+dependencies = [\n+ \"ppv-lite86\",\n+ \"rand_core 0.9.5\",\n ]\n \n [[package]]\n@@ -1019,6 +1525,56 @@ dependencies = [\n  \"getrandom 0.2.17\",\n ]\n \n+[[package]]\n+name = \"rand_core\"\n+version = \"0.9.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c\"\n+dependencies = [\n+ \"getrandom 0.3.4\",\n+]\n+\n+[[package]]\n+name = \"rand_xorshift\"\n+version = \"0.4.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a\"\n+dependencies = [\n+ \"rand_core 0.9.5\",\n+]\n+\n+[[package]]\n+name = \"rayon\"\n+version = \"1.12.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d\"\n+dependencies = [\n+ \"either\",\n+ \"rayon-core\",\n+]\n+\n+[[package]]\n+name = \"rayon-core\"\n+version = \"1.13.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91\"\n+dependencies = [\n+ \"crossbeam-deque\",\n+ \"crossbeam-utils\",\n+]\n+\n+[[package]]\n+name = \"regex\"\n+version = \"1.12.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276\"\n+dependencies = [\n+ \"aho-corasick\",\n+ \"memchr\",\n+ \"regex-automata\",\n+ \"regex-syntax\",\n+]\n+\n [[package]]\n name = \"regex-automata\"\n version = \"0.4.14\"\n@@ -1090,13 +1646,35 @@ dependencies = [\n  \"windows-sys 0.52.0\",\n ]\n \n+[[package]]\n+name = \"rocksdb\"\n+version = \"0.21.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe\"\n+dependencies = [\n+ \"libc\",\n+ \"librocksdb-sys\",\n+]\n+\n+[[package]]\n+name = \"rustc-hash\"\n+version = \"1.1.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2\"\n+\n+[[package]]\n+name = \"rustc-hash\"\n+version = \"2.1.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe\"\n+\n [[package]]\n name = \"rustix\"\n version = \"1.1.4\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n  \"errno\",\n  \"libc\",\n  \"linux-raw-sys\",\n@@ -1142,12 +1720,33 @@ version = \"1.0.22\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d\"\n \n+[[package]]\n+name = \"rusty-fork\"\n+version = \"0.3.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2\"\n+dependencies = [\n+ \"fnv\",\n+ \"quick-error\",\n+ \"tempfile\",\n+ \"wait-timeout\",\n+]\n+\n [[package]]\n name = \"ryu\"\n version = \"1.0.23\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f\"\n \n+[[package]]\n+name = \"same-file\"\n+version = \"1.0.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502\"\n+dependencies = [\n+ \"winapi-util\",\n+]\n+\n [[package]]\n name = \"schannel\"\n version = \"0.1.29\"\n@@ -1163,7 +1762,7 @@ version = \"3.7.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n  \"core-foundation 0.10.1\",\n  \"core-foundation-sys\",\n  \"libc\",\n@@ -1307,9 +1906,10 @@ dependencies = [\n  \"axum\",\n  \"axum-extra\",\n  \"dotenvy\",\n+ \"durable\",\n  \"futures-util\",\n  \"maud\",\n- \"rand\",\n+ \"rand 0.8.6\",\n  \"reqwest\",\n  \"serde\",\n  \"serde_json\",\n@@ -1378,7 +1978,7 @@ version = \"0.7.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n  \"core-foundation 0.9.4\",\n  \"system-configuration-sys\",\n ]\n@@ -1476,6 +2076,16 @@ dependencies = [\n  \"zerovec\",\n ]\n \n+[[package]]\n+name = \"tinytemplate\"\n+version = \"1.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc\"\n+dependencies = [\n+ \"serde\",\n+ \"serde_json\",\n+]\n+\n [[package]]\n name = \"tokio\"\n version = \"1.52.3\"\n@@ -1558,7 +2168,7 @@ version = \"0.5.2\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n  \"bytes\",\n  \"http\",\n  \"http-body\",\n@@ -1575,7 +2185,7 @@ version = \"0.6.11\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n  \"bytes\",\n  \"futures-util\",\n  \"http\",\n@@ -1667,6 +2277,12 @@ version = \"0.2.5\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b\"\n \n+[[package]]\n+name = \"unarray\"\n+version = \"0.1.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94\"\n+\n [[package]]\n name = \"unicode-ident\"\n version = \"1.0.24\"\n@@ -1727,6 +2343,25 @@ version = \"0.9.5\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a\"\n \n+[[package]]\n+name = \"wait-timeout\"\n+version = \"0.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11\"\n+dependencies = [\n+ \"libc\",\n+]\n+\n+[[package]]\n+name = \"walkdir\"\n+version = \"2.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b\"\n+dependencies = [\n+ \"same-file\",\n+ \"winapi-util\",\n+]\n+\n [[package]]\n name = \"want\"\n version = \"0.3.1\"\n@@ -1843,7 +2478,7 @@ version = \"0.244.0\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe\"\n dependencies = [\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n  \"hashbrown 0.15.5\",\n  \"indexmap\",\n  \"semver\",\n@@ -1859,6 +2494,15 @@ dependencies = [\n  \"wasm-bindgen\",\n ]\n \n+[[package]]\n+name = \"winapi-util\"\n+version = \"0.1.11\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22\"\n+dependencies = [\n+ \"windows-sys 0.61.2\",\n+]\n+\n [[package]]\n name = \"windows-link\"\n version = \"0.2.1\"\n@@ -2040,7 +2684,7 @@ source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2\"\n dependencies = [\n  \"anyhow\",\n- \"bitflags\",\n+ \"bitflags 2.11.1\",\n  \"indexmap\",\n  \"log\",\n  \"serde\",\n@@ -2184,3 +2828,14 @@ name = \"zmij\"\n version = \"1.0.21\"\n source = \"registry+https://github.com/rust-lang/crates.io-index\"\n checksum = \"b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa\"\n+\n+[[package]]\n+name = \"zstd-sys\"\n+version = \"2.0.16+zstd.1.5.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748\"\n+dependencies = [\n+ \"bindgen 0.72.1\",\n+ \"cc\",\n+ \"pkg-config\",\n+]\ndiff --git a/Cargo.toml b/Cargo.toml\nindex 156820ec5151b709a254dafc177da10488fee566..89f57d8943de978ef6c2dc038a5dfb89420c4283 100644\n--- a/Cargo.toml\n+++ b/Cargo.toml\n@@ -1,3 +1,3 @@\n [workspace]\n-members = [\"server\"]\n+members = [\"server\", \"durable\"]\n resolver = \"2\"\ndiff --git a/Dockerfile b/Dockerfile\nindex 38e4d595f2366b6a6e9405f1e12373b70fac79a1..517aa49ea2c01ba0d06a4647259ed917a0f6665c 100644\n--- a/Dockerfile\n+++ b/Dockerfile\n@@ -3,7 +3,7 @@ FROM rust:1.88-slim AS builder\n WORKDIR /build\n \n RUN apt-get update && \\\n-    apt-get install -y pkg-config libssl-dev && \\\n+    apt-get install -y pkg-config libssl-dev clang && \\\n     rm -rf /var/lib/apt/lists/*\n \n COPY . .\ndiff --git a/durable/.gitignore b/durable/.gitignore\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..90c273ef12d6313594dde541c3465f2a47739729\n--- /dev/null\n+++ b/durable/.gitignore\n@@ -0,0 +1,35 @@\n+all.txt\n+\n+# Generated by Cargo\n+# will have compiled files and executables\n+debug/\n+target/\n+\n+# These are backup files generated by rustfmt\n+**/*.rs.bk\n+\n+# MSVC Windows builds of rustc generate these, which store debugging information\n+*.pdb\n+\n+# Generated by cargo mutants\n+# Contains mutation testing data\n+**/mutants.out*/\n+\n+# RustRover\n+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n+#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n+#.idea/\n+\n+\n+# Added by cargo\n+\n+/target\n+\n+\n+# Added by cargo\n+#\n+# already existing elements were commented out\n+\n+#/target\ndiff --git a/durable/Cargo.lock b/durable/Cargo.lock\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..99d59ec2166a1e88dc4867d316b3d5fe9bb0a1b6\n--- /dev/null\n+++ b/durable/Cargo.lock\n@@ -0,0 +1,1198 @@\n+# This file is automatically @generated by Cargo.\n+# It is not intended for manual editing.\n+version = 4\n+\n+[[package]]\n+name = \"aho-corasick\"\n+version = \"1.1.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916\"\n+dependencies = [\n+ \"memchr\",\n+]\n+\n+[[package]]\n+name = \"anes\"\n+version = \"0.1.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299\"\n+\n+[[package]]\n+name = \"anstyle\"\n+version = \"1.0.11\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd\"\n+\n+[[package]]\n+name = \"autocfg\"\n+version = \"1.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8\"\n+\n+[[package]]\n+name = \"bincode\"\n+version = \"1.3.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad\"\n+dependencies = [\n+ \"serde\",\n+]\n+\n+[[package]]\n+name = \"bindgen\"\n+version = \"0.65.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5\"\n+dependencies = [\n+ \"bitflags 1.3.2\",\n+ \"cexpr\",\n+ \"clang-sys\",\n+ \"lazy_static\",\n+ \"lazycell\",\n+ \"peeking_take_while\",\n+ \"prettyplease\",\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"regex\",\n+ \"rustc-hash 1.1.0\",\n+ \"shlex\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"bindgen\"\n+version = \"0.71.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3\"\n+dependencies = [\n+ \"bitflags 2.9.1\",\n+ \"cexpr\",\n+ \"clang-sys\",\n+ \"itertools 0.13.0\",\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"regex\",\n+ \"rustc-hash 2.1.1\",\n+ \"shlex\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"bit-set\"\n+version = \"0.8.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3\"\n+dependencies = [\n+ \"bit-vec\",\n+]\n+\n+[[package]]\n+name = \"bit-vec\"\n+version = \"0.8.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7\"\n+\n+[[package]]\n+name = \"bitflags\"\n+version = \"1.3.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a\"\n+\n+[[package]]\n+name = \"bitflags\"\n+version = \"2.9.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967\"\n+\n+[[package]]\n+name = \"bumpalo\"\n+version = \"3.18.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee\"\n+\n+[[package]]\n+name = \"bzip2-sys\"\n+version = \"0.1.13+1.0.8\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14\"\n+dependencies = [\n+ \"cc\",\n+ \"pkg-config\",\n+]\n+\n+[[package]]\n+name = \"cast\"\n+version = \"0.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5\"\n+\n+[[package]]\n+name = \"cc\"\n+version = \"1.2.27\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc\"\n+dependencies = [\n+ \"jobserver\",\n+ \"libc\",\n+ \"shlex\",\n+]\n+\n+[[package]]\n+name = \"cexpr\"\n+version = \"0.6.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766\"\n+dependencies = [\n+ \"nom\",\n+]\n+\n+[[package]]\n+name = \"cfg-if\"\n+version = \"1.0.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268\"\n+\n+[[package]]\n+name = \"ciborium\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e\"\n+dependencies = [\n+ \"ciborium-io\",\n+ \"ciborium-ll\",\n+ \"serde\",\n+]\n+\n+[[package]]\n+name = \"ciborium-io\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757\"\n+\n+[[package]]\n+name = \"ciborium-ll\"\n+version = \"0.2.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9\"\n+dependencies = [\n+ \"ciborium-io\",\n+ \"half\",\n+]\n+\n+[[package]]\n+name = \"clang-sys\"\n+version = \"1.8.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4\"\n+dependencies = [\n+ \"glob\",\n+ \"libc\",\n+ \"libloading\",\n+]\n+\n+[[package]]\n+name = \"clap\"\n+version = \"4.5.40\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f\"\n+dependencies = [\n+ \"clap_builder\",\n+]\n+\n+[[package]]\n+name = \"clap_builder\"\n+version = \"4.5.40\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e\"\n+dependencies = [\n+ \"anstyle\",\n+ \"clap_lex\",\n+]\n+\n+[[package]]\n+name = \"clap_lex\"\n+version = \"0.7.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675\"\n+\n+[[package]]\n+name = \"criterion\"\n+version = \"0.5.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f\"\n+dependencies = [\n+ \"anes\",\n+ \"cast\",\n+ \"ciborium\",\n+ \"clap\",\n+ \"criterion-plot\",\n+ \"is-terminal\",\n+ \"itertools 0.10.5\",\n+ \"num-traits\",\n+ \"once_cell\",\n+ \"oorandom\",\n+ \"plotters\",\n+ \"rayon\",\n+ \"regex\",\n+ \"serde\",\n+ \"serde_derive\",\n+ \"serde_json\",\n+ \"tinytemplate\",\n+ \"walkdir\",\n+]\n+\n+[[package]]\n+name = \"criterion-plot\"\n+version = \"0.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1\"\n+dependencies = [\n+ \"cast\",\n+ \"itertools 0.10.5\",\n+]\n+\n+[[package]]\n+name = \"crossbeam-deque\"\n+version = \"0.8.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51\"\n+dependencies = [\n+ \"crossbeam-epoch\",\n+ \"crossbeam-utils\",\n+]\n+\n+[[package]]\n+name = \"crossbeam-epoch\"\n+version = \"0.9.18\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e\"\n+dependencies = [\n+ \"crossbeam-utils\",\n+]\n+\n+[[package]]\n+name = \"crossbeam-utils\"\n+version = \"0.8.21\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28\"\n+\n+[[package]]\n+name = \"crunchy\"\n+version = \"0.2.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929\"\n+\n+[[package]]\n+name = \"durable\"\n+version = \"0.1.0\"\n+dependencies = [\n+ \"bincode\",\n+ \"criterion\",\n+ \"proptest\",\n+ \"rocksdb\",\n+ \"serde\",\n+ \"tempfile\",\n+ \"thiserror\",\n+]\n+\n+[[package]]\n+name = \"either\"\n+version = \"1.15.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719\"\n+\n+[[package]]\n+name = \"errno\"\n+version = \"0.3.13\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad\"\n+dependencies = [\n+ \"libc\",\n+ \"windows-sys 0.60.2\",\n+]\n+\n+[[package]]\n+name = \"fastrand\"\n+version = \"2.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be\"\n+\n+[[package]]\n+name = \"fnv\"\n+version = \"1.0.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1\"\n+\n+[[package]]\n+name = \"getrandom\"\n+version = \"0.3.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"libc\",\n+ \"r-efi\",\n+ \"wasi\",\n+]\n+\n+[[package]]\n+name = \"glob\"\n+version = \"0.3.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2\"\n+\n+[[package]]\n+name = \"half\"\n+version = \"2.6.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"crunchy\",\n+]\n+\n+[[package]]\n+name = \"hermit-abi\"\n+version = \"0.5.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c\"\n+\n+[[package]]\n+name = \"is-terminal\"\n+version = \"0.4.16\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9\"\n+dependencies = [\n+ \"hermit-abi\",\n+ \"libc\",\n+ \"windows-sys 0.59.0\",\n+]\n+\n+[[package]]\n+name = \"itertools\"\n+version = \"0.10.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473\"\n+dependencies = [\n+ \"either\",\n+]\n+\n+[[package]]\n+name = \"itertools\"\n+version = \"0.13.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186\"\n+dependencies = [\n+ \"either\",\n+]\n+\n+[[package]]\n+name = \"itoa\"\n+version = \"1.0.15\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c\"\n+\n+[[package]]\n+name = \"jobserver\"\n+version = \"0.1.33\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a\"\n+dependencies = [\n+ \"getrandom\",\n+ \"libc\",\n+]\n+\n+[[package]]\n+name = \"js-sys\"\n+version = \"0.3.77\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f\"\n+dependencies = [\n+ \"once_cell\",\n+ \"wasm-bindgen\",\n+]\n+\n+[[package]]\n+name = \"lazy_static\"\n+version = \"1.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe\"\n+\n+[[package]]\n+name = \"lazycell\"\n+version = \"1.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55\"\n+\n+[[package]]\n+name = \"libc\"\n+version = \"0.2.174\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776\"\n+\n+[[package]]\n+name = \"libloading\"\n+version = \"0.8.8\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"windows-targets 0.53.2\",\n+]\n+\n+[[package]]\n+name = \"librocksdb-sys\"\n+version = \"0.11.0+8.1.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e\"\n+dependencies = [\n+ \"bindgen 0.65.1\",\n+ \"bzip2-sys\",\n+ \"cc\",\n+ \"glob\",\n+ \"libc\",\n+ \"libz-sys\",\n+ \"lz4-sys\",\n+ \"zstd-sys\",\n+]\n+\n+[[package]]\n+name = \"libz-sys\"\n+version = \"1.1.22\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d\"\n+dependencies = [\n+ \"cc\",\n+ \"pkg-config\",\n+ \"vcpkg\",\n+]\n+\n+[[package]]\n+name = \"linux-raw-sys\"\n+version = \"0.9.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12\"\n+\n+[[package]]\n+name = \"log\"\n+version = \"0.4.27\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94\"\n+\n+[[package]]\n+name = \"lz4-sys\"\n+version = \"1.11.1+lz4-1.10.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6\"\n+dependencies = [\n+ \"cc\",\n+ \"libc\",\n+]\n+\n+[[package]]\n+name = \"memchr\"\n+version = \"2.7.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0\"\n+\n+[[package]]\n+name = \"minimal-lexical\"\n+version = \"0.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a\"\n+\n+[[package]]\n+name = \"nom\"\n+version = \"7.1.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a\"\n+dependencies = [\n+ \"memchr\",\n+ \"minimal-lexical\",\n+]\n+\n+[[package]]\n+name = \"num-traits\"\n+version = \"0.2.19\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841\"\n+dependencies = [\n+ \"autocfg\",\n+]\n+\n+[[package]]\n+name = \"once_cell\"\n+version = \"1.21.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d\"\n+\n+[[package]]\n+name = \"oorandom\"\n+version = \"11.1.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e\"\n+\n+[[package]]\n+name = \"peeking_take_while\"\n+version = \"0.1.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099\"\n+\n+[[package]]\n+name = \"pkg-config\"\n+version = \"0.3.32\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c\"\n+\n+[[package]]\n+name = \"plotters\"\n+version = \"0.3.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747\"\n+dependencies = [\n+ \"num-traits\",\n+ \"plotters-backend\",\n+ \"plotters-svg\",\n+ \"wasm-bindgen\",\n+ \"web-sys\",\n+]\n+\n+[[package]]\n+name = \"plotters-backend\"\n+version = \"0.3.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a\"\n+\n+[[package]]\n+name = \"plotters-svg\"\n+version = \"0.3.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670\"\n+dependencies = [\n+ \"plotters-backend\",\n+]\n+\n+[[package]]\n+name = \"ppv-lite86\"\n+version = \"0.2.21\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9\"\n+dependencies = [\n+ \"zerocopy\",\n+]\n+\n+[[package]]\n+name = \"prettyplease\"\n+version = \"0.2.35\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"proc-macro2\"\n+version = \"1.0.95\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778\"\n+dependencies = [\n+ \"unicode-ident\",\n+]\n+\n+[[package]]\n+name = \"proptest\"\n+version = \"1.7.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f\"\n+dependencies = [\n+ \"bit-set\",\n+ \"bit-vec\",\n+ \"bitflags 2.9.1\",\n+ \"lazy_static\",\n+ \"num-traits\",\n+ \"rand\",\n+ \"rand_chacha\",\n+ \"rand_xorshift\",\n+ \"regex-syntax\",\n+ \"rusty-fork\",\n+ \"tempfile\",\n+ \"unarray\",\n+]\n+\n+[[package]]\n+name = \"quick-error\"\n+version = \"1.2.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0\"\n+\n+[[package]]\n+name = \"quote\"\n+version = \"1.0.40\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d\"\n+dependencies = [\n+ \"proc-macro2\",\n+]\n+\n+[[package]]\n+name = \"r-efi\"\n+version = \"5.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f\"\n+\n+[[package]]\n+name = \"rand\"\n+version = \"0.9.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97\"\n+dependencies = [\n+ \"rand_chacha\",\n+ \"rand_core\",\n+]\n+\n+[[package]]\n+name = \"rand_chacha\"\n+version = \"0.9.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb\"\n+dependencies = [\n+ \"ppv-lite86\",\n+ \"rand_core\",\n+]\n+\n+[[package]]\n+name = \"rand_core\"\n+version = \"0.9.3\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38\"\n+dependencies = [\n+ \"getrandom\",\n+]\n+\n+[[package]]\n+name = \"rand_xorshift\"\n+version = \"0.4.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a\"\n+dependencies = [\n+ \"rand_core\",\n+]\n+\n+[[package]]\n+name = \"rayon\"\n+version = \"1.10.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa\"\n+dependencies = [\n+ \"either\",\n+ \"rayon-core\",\n+]\n+\n+[[package]]\n+name = \"rayon-core\"\n+version = \"1.12.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2\"\n+dependencies = [\n+ \"crossbeam-deque\",\n+ \"crossbeam-utils\",\n+]\n+\n+[[package]]\n+name = \"regex\"\n+version = \"1.11.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191\"\n+dependencies = [\n+ \"aho-corasick\",\n+ \"memchr\",\n+ \"regex-automata\",\n+ \"regex-syntax\",\n+]\n+\n+[[package]]\n+name = \"regex-automata\"\n+version = \"0.4.9\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908\"\n+dependencies = [\n+ \"aho-corasick\",\n+ \"memchr\",\n+ \"regex-syntax\",\n+]\n+\n+[[package]]\n+name = \"regex-syntax\"\n+version = \"0.8.5\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c\"\n+\n+[[package]]\n+name = \"rocksdb\"\n+version = \"0.21.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe\"\n+dependencies = [\n+ \"libc\",\n+ \"librocksdb-sys\",\n+]\n+\n+[[package]]\n+name = \"rustc-hash\"\n+version = \"1.1.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2\"\n+\n+[[package]]\n+name = \"rustc-hash\"\n+version = \"2.1.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d\"\n+\n+[[package]]\n+name = \"rustix\"\n+version = \"1.0.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266\"\n+dependencies = [\n+ \"bitflags 2.9.1\",\n+ \"errno\",\n+ \"libc\",\n+ \"linux-raw-sys\",\n+ \"windows-sys 0.59.0\",\n+]\n+\n+[[package]]\n+name = \"rustversion\"\n+version = \"1.0.21\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d\"\n+\n+[[package]]\n+name = \"rusty-fork\"\n+version = \"0.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f\"\n+dependencies = [\n+ \"fnv\",\n+ \"quick-error\",\n+ \"tempfile\",\n+ \"wait-timeout\",\n+]\n+\n+[[package]]\n+name = \"ryu\"\n+version = \"1.0.20\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f\"\n+\n+[[package]]\n+name = \"same-file\"\n+version = \"1.0.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502\"\n+dependencies = [\n+ \"winapi-util\",\n+]\n+\n+[[package]]\n+name = \"serde\"\n+version = \"1.0.219\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6\"\n+dependencies = [\n+ \"serde_derive\",\n+]\n+\n+[[package]]\n+name = \"serde_derive\"\n+version = \"1.0.219\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"serde_json\"\n+version = \"1.0.140\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373\"\n+dependencies = [\n+ \"itoa\",\n+ \"memchr\",\n+ \"ryu\",\n+ \"serde\",\n+]\n+\n+[[package]]\n+name = \"shlex\"\n+version = \"1.3.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64\"\n+\n+[[package]]\n+name = \"syn\"\n+version = \"2.0.104\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"unicode-ident\",\n+]\n+\n+[[package]]\n+name = \"tempfile\"\n+version = \"3.20.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1\"\n+dependencies = [\n+ \"fastrand\",\n+ \"getrandom\",\n+ \"once_cell\",\n+ \"rustix\",\n+ \"windows-sys 0.59.0\",\n+]\n+\n+[[package]]\n+name = \"thiserror\"\n+version = \"1.0.69\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52\"\n+dependencies = [\n+ \"thiserror-impl\",\n+]\n+\n+[[package]]\n+name = \"thiserror-impl\"\n+version = \"1.0.69\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"tinytemplate\"\n+version = \"1.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc\"\n+dependencies = [\n+ \"serde\",\n+ \"serde_json\",\n+]\n+\n+[[package]]\n+name = \"unarray\"\n+version = \"0.1.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94\"\n+\n+[[package]]\n+name = \"unicode-ident\"\n+version = \"1.0.18\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512\"\n+\n+[[package]]\n+name = \"vcpkg\"\n+version = \"0.2.15\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426\"\n+\n+[[package]]\n+name = \"wait-timeout\"\n+version = \"0.2.1\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11\"\n+dependencies = [\n+ \"libc\",\n+]\n+\n+[[package]]\n+name = \"walkdir\"\n+version = \"2.5.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b\"\n+dependencies = [\n+ \"same-file\",\n+ \"winapi-util\",\n+]\n+\n+[[package]]\n+name = \"wasi\"\n+version = \"0.14.2+wasi-0.2.4\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3\"\n+dependencies = [\n+ \"wit-bindgen-rt\",\n+]\n+\n+[[package]]\n+name = \"wasm-bindgen\"\n+version = \"0.2.100\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5\"\n+dependencies = [\n+ \"cfg-if\",\n+ \"once_cell\",\n+ \"rustversion\",\n+ \"wasm-bindgen-macro\",\n+]\n+\n+[[package]]\n+name = \"wasm-bindgen-backend\"\n+version = \"0.2.100\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6\"\n+dependencies = [\n+ \"bumpalo\",\n+ \"log\",\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+ \"wasm-bindgen-shared\",\n+]\n+\n+[[package]]\n+name = \"wasm-bindgen-macro\"\n+version = \"0.2.100\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407\"\n+dependencies = [\n+ \"quote\",\n+ \"wasm-bindgen-macro-support\",\n+]\n+\n+[[package]]\n+name = \"wasm-bindgen-macro-support\"\n+version = \"0.2.100\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+ \"wasm-bindgen-backend\",\n+ \"wasm-bindgen-shared\",\n+]\n+\n+[[package]]\n+name = \"wasm-bindgen-shared\"\n+version = \"0.2.100\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d\"\n+dependencies = [\n+ \"unicode-ident\",\n+]\n+\n+[[package]]\n+name = \"web-sys\"\n+version = \"0.3.77\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2\"\n+dependencies = [\n+ \"js-sys\",\n+ \"wasm-bindgen\",\n+]\n+\n+[[package]]\n+name = \"winapi-util\"\n+version = \"0.1.9\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb\"\n+dependencies = [\n+ \"windows-sys 0.59.0\",\n+]\n+\n+[[package]]\n+name = \"windows-sys\"\n+version = \"0.59.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b\"\n+dependencies = [\n+ \"windows-targets 0.52.6\",\n+]\n+\n+[[package]]\n+name = \"windows-sys\"\n+version = \"0.60.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb\"\n+dependencies = [\n+ \"windows-targets 0.53.2\",\n+]\n+\n+[[package]]\n+name = \"windows-targets\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973\"\n+dependencies = [\n+ \"windows_aarch64_gnullvm 0.52.6\",\n+ \"windows_aarch64_msvc 0.52.6\",\n+ \"windows_i686_gnu 0.52.6\",\n+ \"windows_i686_gnullvm 0.52.6\",\n+ \"windows_i686_msvc 0.52.6\",\n+ \"windows_x86_64_gnu 0.52.6\",\n+ \"windows_x86_64_gnullvm 0.52.6\",\n+ \"windows_x86_64_msvc 0.52.6\",\n+]\n+\n+[[package]]\n+name = \"windows-targets\"\n+version = \"0.53.2\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef\"\n+dependencies = [\n+ \"windows_aarch64_gnullvm 0.53.0\",\n+ \"windows_aarch64_msvc 0.53.0\",\n+ \"windows_i686_gnu 0.53.0\",\n+ \"windows_i686_gnullvm 0.53.0\",\n+ \"windows_i686_msvc 0.53.0\",\n+ \"windows_x86_64_gnu 0.53.0\",\n+ \"windows_x86_64_gnullvm 0.53.0\",\n+ \"windows_x86_64_msvc 0.53.0\",\n+]\n+\n+[[package]]\n+name = \"windows_aarch64_gnullvm\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3\"\n+\n+[[package]]\n+name = \"windows_aarch64_gnullvm\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764\"\n+\n+[[package]]\n+name = \"windows_aarch64_msvc\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469\"\n+\n+[[package]]\n+name = \"windows_aarch64_msvc\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c\"\n+\n+[[package]]\n+name = \"windows_i686_gnu\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b\"\n+\n+[[package]]\n+name = \"windows_i686_gnu\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3\"\n+\n+[[package]]\n+name = \"windows_i686_gnullvm\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66\"\n+\n+[[package]]\n+name = \"windows_i686_gnullvm\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11\"\n+\n+[[package]]\n+name = \"windows_i686_msvc\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66\"\n+\n+[[package]]\n+name = \"windows_i686_msvc\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d\"\n+\n+[[package]]\n+name = \"windows_x86_64_gnu\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78\"\n+\n+[[package]]\n+name = \"windows_x86_64_gnu\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba\"\n+\n+[[package]]\n+name = \"windows_x86_64_gnullvm\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d\"\n+\n+[[package]]\n+name = \"windows_x86_64_gnullvm\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57\"\n+\n+[[package]]\n+name = \"windows_x86_64_msvc\"\n+version = \"0.52.6\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec\"\n+\n+[[package]]\n+name = \"windows_x86_64_msvc\"\n+version = \"0.53.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486\"\n+\n+[[package]]\n+name = \"wit-bindgen-rt\"\n+version = \"0.39.0\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1\"\n+dependencies = [\n+ \"bitflags 2.9.1\",\n+]\n+\n+[[package]]\n+name = \"zerocopy\"\n+version = \"0.8.26\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f\"\n+dependencies = [\n+ \"zerocopy-derive\",\n+]\n+\n+[[package]]\n+name = \"zerocopy-derive\"\n+version = \"0.8.26\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181\"\n+dependencies = [\n+ \"proc-macro2\",\n+ \"quote\",\n+ \"syn\",\n+]\n+\n+[[package]]\n+name = \"zstd-sys\"\n+version = \"2.0.15+zstd.1.5.7\"\n+source = \"registry+https://github.com/rust-lang/crates.io-index\"\n+checksum = \"eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237\"\n+dependencies = [\n+ \"bindgen 0.71.1\",\n+ \"cc\",\n+ \"pkg-config\",\n+]\ndiff --git a/durable/Cargo.toml b/durable/Cargo.toml\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..907e79cd894446e2ac76c8240b86c2927103727b\n--- /dev/null\n+++ b/durable/Cargo.toml\n@@ -0,0 +1,18 @@\n+[package]\n+name = \"durable\"\n+version = \"0.1.0\"\n+edition = \"2021\"\n+authors = [\"Durable Contributors\"]\n+description = \"RocksDB-backed persistent data structures for Rust\"\n+license = \"MIT OR Apache-2.0\"\n+\n+[dependencies]\n+rocksdb = \"0.21\"\n+serde = { version = \"1.0\", features = [\"derive\"] }\n+bincode = \"1.3\"\n+thiserror = \"1.0\"\n+\n+[dev-dependencies]\n+tempfile = \"3.8\"\n+criterion = \"0.5\"\n+proptest = \"1.4\"\ndiff --git a/durable/LICENSE b/durable/LICENSE\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64\n--- /dev/null\n+++ b/durable/LICENSE\n@@ -0,0 +1,201 @@\n+                                 Apache License\n+                           Version 2.0, January 2004\n+                        http://www.apache.org/licenses/\n+\n+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n+\n+   1. Definitions.\n+\n+      \"License\" shall mean the terms and conditions for use, reproduction,\n+      and distribution as defined by Sections 1 through 9 of this document.\n+\n+      \"Licensor\" shall mean the copyright owner or entity authorized by\n+      the copyright owner that is granting the License.\n+\n+      \"Legal Entity\" shall mean the union of the acting entity and all\n+      other entities that control, are controlled by, or are under common\n+      control with that entity. For the purposes of this definition,\n+      \"control\" means (i) the power, direct or indirect, to cause the\n+      direction or management of such entity, whether by contract or\n+      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n+      outstanding shares, or (iii) beneficial ownership of such entity.\n+\n+      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n+      exercising permissions granted by this License.\n+\n+      \"Source\" form shall mean the preferred form for making modifications,\n+      including but not limited to software source code, documentation\n+      source, and configuration files.\n+\n+      \"Object\" form shall mean any form resulting from mechanical\n+      transformation or translation of a Source form, including but\n+      not limited to compiled object code, generated documentation,\n+      and conversions to other media types.\n+\n+      \"Work\" shall mean the work of authorship, whether in Source or\n+      Object form, made available under the License, as indicated by a\n+      copyright notice that is included in or attached to the work\n+      (an example is provided in the Appendix below).\n+\n+      \"Derivative Works\" shall mean any work, whether in Source or Object\n+      form, that is based on (or derived from) the Work and for which the\n+      editorial revisions, annotations, elaborations, or other modifications\n+      represent, as a whole, an original work of authorship. For the purposes\n+      of this License, Derivative Works shall not include works that remain\n+      separable from, or merely link (or bind by name) to the interfaces of,\n+      the Work and Derivative Works thereof.\n+\n+      \"Contribution\" shall mean any work of authorship, including\n+      the original version of the Work and any modifications or additions\n+      to that Work or Derivative Works thereof, that is intentionally\n+      submitted to Licensor for inclusion in the Work by the copyright owner\n+      or by an individual or Legal Entity authorized to submit on behalf of\n+      the copyright owner. For the purposes of this definition, \"submitted\"\n+      means any form of electronic, verbal, or written communication sent\n+      to the Licensor or its representatives, including but not limited to\n+      communication on electronic mailing lists, source code control systems,\n+      and issue tracking systems that are managed by, or on behalf of, the\n+      Licensor for the purpose of discussing and improving the Work, but\n+      excluding communication that is conspicuously marked or otherwise\n+      designated in writing by the copyright owner as \"Not a Contribution.\"\n+\n+      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n+      on behalf of whom a Contribution has been received by Licensor and\n+      subsequently incorporated within the Work.\n+\n+   2. Grant of Copyright License. Subject to the terms and conditions of\n+      this License, each Contributor hereby grants to You a perpetual,\n+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n+      copyright license to reproduce, prepare Derivative Works of,\n+      publicly display, publicly perform, sublicense, and distribute the\n+      Work and such Derivative Works in Source or Object form.\n+\n+   3. Grant of Patent License. Subject to the terms and conditions of\n+      this License, each Contributor hereby grants to You a perpetual,\n+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n+      (except as stated in this section) patent license to make, have made,\n+      use, offer to sell, sell, import, and otherwise transfer the Work,\n+      where such license applies only to those patent claims licensable\n+      by such Contributor that are necessarily infringed by their\n+      Contribution(s) alone or by combination of their Contribution(s)\n+      with the Work to which such Contribution(s) was submitted. If You\n+      institute patent litigation against any entity (including a\n+      cross-claim or counterclaim in a lawsuit) alleging that the Work\n+      or a Contribution incorporated within the Work constitutes direct\n+      or contributory patent infringement, then any patent licenses\n+      granted to You under this License for that Work shall terminate\n+      as of the date such litigation is filed.\n+\n+   4. Redistribution. You may reproduce and distribute copies of the\n+      Work or Derivative Works thereof in any medium, with or without\n+      modifications, and in Source or Object form, provided that You\n+      meet the following conditions:\n+\n+      (a) You must give any other recipients of the Work or\n+          Derivative Works a copy of this License; and\n+\n+      (b) You must cause any modified files to carry prominent notices\n+          stating that You changed the files; and\n+\n+      (c) You must retain, in the Source form of any Derivative Works\n+          that You distribute, all copyright, patent, trademark, and\n+          attribution notices from the Source form of the Work,\n+          excluding those notices that do not pertain to any part of\n+          the Derivative Works; and\n+\n+      (d) If the Work includes a \"NOTICE\" text file as part of its\n+          distribution, then any Derivative Works that You distribute must\n+          include a readable copy of the attribution notices contained\n+          within such NOTICE file, excluding those notices that do not\n+          pertain to any part of the Derivative Works, in at least one\n+          of the following places: within a NOTICE text file distributed\n+          as part of the Derivative Works; within the Source form or\n+          documentation, if provided along with the Derivative Works; or,\n+          within a display generated by the Derivative Works, if and\n+          wherever such third-party notices normally appear. The contents\n+          of the NOTICE file are for informational purposes only and\n+          do not modify the License. You may add Your own attribution\n+          notices within Derivative Works that You distribute, alongside\n+          or as an addendum to the NOTICE text from the Work, provided\n+          that such additional attribution notices cannot be construed\n+          as modifying the License.\n+\n+      You may add Your own copyright statement to Your modifications and\n+      may provide additional or different license terms and conditions\n+      for use, reproduction, or distribution of Your modifications, or\n+      for any such Derivative Works as a whole, provided Your use,\n+      reproduction, and distribution of the Work otherwise complies with\n+      the conditions stated in this License.\n+\n+   5. Submission of Contributions. Unless You explicitly state otherwise,\n+      any Contribution intentionally submitted for inclusion in the Work\n+      by You to the Licensor shall be under the terms and conditions of\n+      this License, without any additional terms or conditions.\n+      Notwithstanding the above, nothing herein shall supersede or modify\n+      the terms of any separate license agreement you may have executed\n+      with Licensor regarding such Contributions.\n+\n+   6. Trademarks. This License does not grant permission to use the trade\n+      names, trademarks, service marks, or product names of the Licensor,\n+      except as required for reasonable and customary use in describing the\n+      origin of the Work and reproducing the content of the NOTICE file.\n+\n+   7. Disclaimer of Warranty. Unless required by applicable law or\n+      agreed to in writing, Licensor provides the Work (and each\n+      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n+      implied, including, without limitation, any warranties or conditions\n+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n+      PARTICULAR PURPOSE. You are solely responsible for determining the\n+      appropriateness of using or redistributing the Work and assume any\n+      risks associated with Your exercise of permissions under this License.\n+\n+   8. Limitation of Liability. In no event and under no legal theory,\n+      whether in tort (including negligence), contract, or otherwise,\n+      unless required by applicable law (such as deliberate and grossly\n+      negligent acts) or agreed to in writing, shall any Contributor be\n+      liable to You for damages, including any direct, indirect, special,\n+      incidental, or consequential damages of any character arising as a\n+      result of this License or out of the use or inability to use the\n+      Work (including but not limited to damages for loss of goodwill,\n+      work stoppage, computer failure or malfunction, or any and all\n+      other commercial damages or losses), even if such Contributor\n+      has been advised of the possibility of such damages.\n+\n+   9. Accepting Warranty or Additional Liability. While redistributing\n+      the Work or Derivative Works thereof, You may choose to offer,\n+      and charge a fee for, acceptance of support, warranty, indemnity,\n+      or other liability obligations and/or rights consistent with this\n+      License. However, in accepting such obligations, You may act only\n+      on Your own behalf and on Your sole responsibility, not on behalf\n+      of any other Contributor, and only if You agree to indemnify,\n+      defend, and hold each Contributor harmless for any liability\n+      incurred by, or claims asserted against, such Contributor by reason\n+      of your accepting any such warranty or additional liability.\n+\n+   END OF TERMS AND CONDITIONS\n+\n+   APPENDIX: How to apply the Apache License to your work.\n+\n+      To apply the Apache License to your work, attach the following\n+      boilerplate notice, with the fields enclosed by brackets \"[]\"\n+      replaced with your own identifying information. (Don't include\n+      the brackets!)  The text should be enclosed in the appropriate\n+      comment syntax for the file format. We also recommend that a\n+      file or class name and description of purpose be included on the\n+      same \"printed page\" as the copyright notice for easier\n+      identification within third-party archives.\n+\n+   Copyright [yyyy] [name of copyright owner]\n+\n+   Licensed under the Apache License, Version 2.0 (the \"License\");\n+   you may not use this file except in compliance with the License.\n+   You may obtain a copy of the License at\n+\n+       http://www.apache.org/licenses/LICENSE-2.0\n+\n+   Unless required by applicable law or agreed to in writing, software\n+   distributed under the License is distributed on an \"AS IS\" BASIS,\n+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+   See the License for the specific language governing permissions and\n+   limitations under the License.\ndiff --git a/durable/README.md b/durable/README.md\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..46f3b163a41d1094334ea5dc4e8add7b627f16e2\n--- /dev/null\n+++ b/durable/README.md\n@@ -0,0 +1,207 @@\n+# Durable\n+\n+RocksDB-backed persistent data structures for Rust. Think `std::collections` but on disk!\n+\n+## Features\n+\n+- **Persistent Collections**: `DurableVec`, `DurableMap`, `DurableSet` (coming soon)\n+- **Type-Safe**: Full Rust type safety with serde serialization\n+- **ACID Guarantees**: All operations are atomic and crash-safe\n+- **Zero-Copy Capable**: Efficient iteration without loading entire collections\n+- **Embedded**: No external services required - just a directory on disk\n+\n+## Quick Start\n+\n+Add to your `Cargo.toml`:\n+\n+```toml\n+[dependencies]\n+durable = \"0.1.0\"\n+```\n+\n+## Example\n+\n+### DurableVec\n+```rust\n+use durable::{Db, DurableVec};\n+use serde::{Serialize, Deserialize};\n+\n+#[derive(Debug, Serialize, Deserialize)]\n+struct Task {\n+    id: u64,\n+    title: String,\n+    completed: bool,\n+}\n+\n+fn main() -> Result<(), Box> {\n+    // Open or create a database\n+    let db = Db::open(\"my_db\")?;\n+    \n+    // Create a persistent vector\n+    let mut tasks = DurableVec::::new(&db, \"tasks\")?;\n+    \n+    // Use it like a normal Vec!\n+    tasks.push(Task {\n+        id: 1,\n+        title: \"Build something amazing\".to_string(),\n+        completed: false,\n+    })?;\n+    \n+    // Data persists across program restarts\n+    println!(\"Total tasks: {}\", tasks.len()?);\n+    \n+    Ok(())\n+}\n+```\n+\n+### DurableMap\n+```rust\n+use durable::{Db, DurableMap};\n+\n+fn main() -> Result<(), Box> {\n+    let db = Db::open(\"my_db\")?;\n+    \n+    // Create a persistent map\n+    let mut scores = DurableMap::::new(&db, \"scores\")?;\n+    \n+    // Use it like a HashMap!\n+    // Use put() when you don't need the old value (more efficient)\n+    scores.put(\"Alice\".to_string(), 100)?;\n+    scores.put(\"Bob\".to_string(), 85)?;\n+    \n+    // Use insert() when you need to know the old value\n+    if let Some(old_score) = scores.insert(\"Alice\".to_string(), 120)? {\n+        println!(\"Alice's previous score was: {}\", old_score);\n+    }\n+    \n+    // Get values\n+    if let Some(score) = scores.get(&\"Alice\".to_string())? {\n+        println!(\"Alice's score: {}\", score);\n+    }\n+    \n+    // Iterate over entries\n+    for (name, score) in scores.iter()? {\n+        println!(\"{}: {}\", name, score);\n+    }\n+    \n+    Ok(())\n+}\n+```\n+\n+### Nested Collections\n+\n+Durable supports nesting collections within each other for complex data structures:\n+\n+```rust\n+use durable::{Db, DurableMap, DurableVec};\n+\n+fn main() -> Result<(), Box> {\n+    let db = Db::open(\"my_db\")?;\n+    \n+    // Create a map where each user has a list of posts\n+    let user_posts: DurableMap> = \n+        DurableMap::new_nested(&db, \"user_posts\");\n+    \n+    // Add posts for a user\n+    let mut alice_posts = user_posts.entry(\"alice\".to_string())?.or_default()?;\n+    alice_posts.push(\"Hello, world!\".to_string())?;\n+    alice_posts.push(\"Rust is awesome!\".to_string())?;\n+    \n+    // Or use chained calls for convenience\n+    user_posts.entry(\"bob\".to_string())?.or_default()?.push(\"First post!\".to_string())?;\n+    \n+    // Access nested data\n+    let alice_posts = user_posts.entry(\"alice\".to_string())?.or_default()?;\n+    println!(\"Alice has {} posts\", alice_posts.len()?);\n+    \n+    Ok(())\n+}\n+```\n+\n+The entry API automatically creates nested collections when they don't exist, providing ergonomic access patterns similar to `std::collections::HashMap::entry().or_default()`.\n+\n+## Current Status\n+\n+### Implemented\n+\n+- ✅ `DurableVec` with full test coverage including:\n+  - Basic operations: `push`, `pop`, `get`, `len`, `clear`\n+  - Batch operations: `extend`\n+  - Iteration: `iter()` returns a streaming iterator, `to_vec()` loads into memory\n+  - Property-based testing with proptest\n+  - Unicode string support\n+  - Complex type support\n+\n+- ✅ `DurableMap` with full test coverage including:\n+  - Basic operations: `insert`, `put`, `get`, `remove`, `contains_key`, `len`, `clear`\n+  - Batch operations: `extend`\n+  - Iteration: `iter()`, `keys()`, `values()` return streaming iterators\n+  - Memory loading: `to_vec()`, `keys_vec()`, `values_vec()` for convenience\n+  - Complex key and value types\n+  - Property-based testing with proptest\n+\n+- ✅ **Nested Collections** with entry API:\n+  - `DurableMap>` - Maps to vectors\n+  - `entry()` method with `or_default()` for ergonomic access\n+  - Automatic collection creation and management\n+  - Full persistence and isolation between nested collections\n+  - Type-safe compile-time enforcement\n+\n+### Coming Soon\n+\n+- 🚧 `DurableSet` - Persistent HashSet  \n+- 🚧 Deep nesting (e.g., `DurableMap>>`)\n+- 🚧 Schema migration support\n+- 🚧 Batch operations across multiple collections\n+\n+## Performance\n+\n+All operations are designed to be efficient:\n+\n+- **DurableVec**:\n+  - `push`: Single atomic write with WAL flush\n+  - `get`: Direct key lookup, O(1) \n+  - `len`: Metadata lookup, O(1)\n+  - `extend`: Batched writes for efficiency\n+  - `clear`: Atomic batch deletion\n+\n+- **DurableMap**:\n+  - `insert`: Returns old value (2 ops: get + put), O(1) average\n+  - `put`: No return value (1 op: existence check + put), O(1) average\n+  - `get`: Direct key lookup, O(1) average\n+  - `remove`: Single delete with WAL flush\n+  - `len`: Metadata lookup, O(1)\n+  - `extend`: Batched writes for efficiency\n+\n+## Testing\n+\n+Run the test suite:\n+\n+```bash\n+cargo test\n+```\n+\n+Run the examples:\n+\n+```bash\n+cargo run --example vec_example\n+cargo run --example map_example\n+cargo run --example combined_example  # Shows both collections working together\n+cargo run --example streaming_demo    # Demonstrates efficient streaming iteration\n+cargo run --example nested_example   # Shows nested collections (Map -> Vec)\n+cargo run --example simple_ranking   # Gaming leaderboard from docs/motivation.md\n+cargo run --example ranking_history  # Complex ranking system with persistence\n+```\n+\n+## License\n+\n+Licensed under either of:\n+\n+- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)\n+- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)\n+\n+at your option.\n+\n+## Contributing\n+\n+Contributions are welcome! Please feel free to submit a Pull Request.\ndiff --git a/durable/docs/001.md b/durable/docs/001.md\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..3ad7cf9d93572de9ded367f9ce6dcc28608bba39\n--- /dev/null\n+++ b/durable/docs/001.md\n@@ -0,0 +1,272 @@\n+## Durable — RocksDB-backed Persistent Data Structures for Rust\n+\n+### *draft RFC v0.1*\n+\n+---\n+\n+### 1  Purpose\n+\n+Provide ergonomic, std-like collections (`DurableMap`, `DurableVec`, `DurableSet`, …) whose contents are **durably stored in RocksDB** yet feel in-memory:\n+\n+```rust\n+let db  = durable::open(\"db\");               // single call opens RocksDB\n+let mut posts = DurableMap::::new(&db, \"posts\")?;\n+\n+posts.insert(id, post)?;                     // ACID write\n+let p = posts.get(&id)?;                     // typed read\n+```\n+\n+Target use cases:\n+\n+| Domain                         | Why durable?                     |\n+| ------------------------------ | -------------------------------- |\n+| Local-first apps / CRDT caches | crash-safe, embedded             |\n+| High-write time-series blobs   | append-only keys, fast iteration |\n+| ML / ranking histories         | vector-within-vector patterns    |\n+| Game state / async board games | snapshot + rollback              |\n+| Sorter (tag → bucket → item)   | exactly the motivating structure |\n+\n+---\n+\n+### 2  Design Goals\n+\n+1. **Ergonomic** – Feel like `std::collections`; no manual key mangling.\n+2. **Typed** – Keys & values are generic; (de)serialization pluggable (default = `bincode`).\n+3. **Atomic** – Multi-op `batch.commit()` gives RocksDB write-batch semantics.\n+4. **Crash-safe** – Every public write path `fsync`s RocksDB WAL first.\n+5. **Composable** – Any collection may nest another via *prefix subspaces*.\n+6. **Zero external services** – Single embedded `.sst` directory.\n+7. **Opt-in features** – Reactive diffs, LRU cache, metrics behind feature flags.\n+\n+Non-goals (v0 line):\n+\n+* Distributed replication\n+* Multi-process concurrency (one writer process is assumed; readers may open secondary Rocks instances)\n+* SQL-style ad-hoc queries – you iterate, not query-plan.\n+\n+---\n+\n+### 3  Key–Value Layout\n+\n+*Each collection owns a **prefix** inside a single Column Family (`default` by default).*\n+\n+```\n+ 0x00  0x00  [ 0x00  …]\n+```\n+\n+* `user-prefix`   = crate-level namespace (allows multi-tenant apps)\n+* `col_id`        = 8-byte, little-endian numeric ID assigned on `DurableMap::new(&db,\"posts\")`.\n+* `logical-key`   = `serde`-encoded key (or ordinal for Vec).\n+* `subindex`      = extra path segments used by nested collections (e.g., `Vec` elements in a map value).\n+\n+Because RocksDB stores keys lexicographically, all elements of a collection (and its descendants) live contiguously → range scans & prefix deletes are cheap.\n+\n+---\n+\n+### 4  Public API (surface)\n+\n+```rust\n+/// Opens (or creates) a durable database at `path`.\n+pub fn open>(path: P) -> Result;\n+\n+/// A transactional write batch\n+pub struct Batch<'db> { /* .. */ }\n+\n+impl<'db> Batch<'db> {\n+    pub fn put(&mut self, col: &impl WriteCollection, key: &K, val: &V) -> Result<()>;\n+    pub fn delete(&mut self, col: &impl WriteCollection, key: &K) -> Result<()>;\n+    pub fn commit(self) -> Result<()>;\n+}\n+\n+/// Collections ------------------------------------------------------------\n+\n+pub struct DurableMap<'db, K, V> { /* .. */ }\n+pub struct DurableVec<'db, T>    { /* .. */ }\n+pub struct DurableSet<'db, T>    { /* .. */ }\n+pub struct DurableIndex<'db, K, V> { /* sorted-map, range queries */ }\n+\n+/// Common read API\n+pub trait ReadCollection {\n+    fn get(&self, k: &K) -> Result>;\n+    fn contains_key(&self, k: &K) -> Result;\n+    fn len(&self) -> Result;\n+    fn iter(&self) -> Iter<'_, (K,V)>;               // owns snapshot\n+}\n+\n+/// Common write API (auto batch or explicit)\n+pub trait WriteCollection: ReadCollection {\n+    fn insert(&mut self, k: K, v: V) -> Result>;\n+    fn remove(&mut self, k: &K) -> Result>;\n+    fn clear(&mut self) -> Result<()>;\n+}\n+```\n+\n+*All writes go through an internal `rocksdb::WriteBatch`* so a single logical op remains atomic even if it touches subkeys (e.g., pushing into a `DurableVec` updates `len` meta key + appends element).\n+\n+---\n+\n+### 5  Collection Semantics\n+\n+#### 5.1 `DurableVec`\n+\n+* Meta-key `__len` stores current length (`u64`).\n+* Element key  = `|`\n+* `push(elem)` = `batch.put(key(len), elem); batch.put(__len, len+1)`\n+  – O(1) write, O(log N) read via prefix seek.\n+* `iter()` performs a prefix range; snapshot guarantees repeat-read.\n+\n+#### 5.2 `DurableMap`\n+\n+* Key = `|`\n+* `len` optional (feature `\"size_tracking\"`); otherwise O(prefix-scan).\n+\n+#### 5.3 Nested Collections\n+\n+```rust\n+let users  = DurableMap::>::new(&db,\"users\")?;\n+users.entry(\"alice\")?.or_default()?.push(order)?;\n+```\n+\n+Internally `Entry::or_default` creates a *child prefix* off the parent’s key:\n+`|\"alice\"|0x00||…`\n+\n+Child collections store their own metadata keys beneath that path.\n+\n+---\n+\n+### 6  Transactions & Consistency\n+\n+* **Auto-batch**: default mutator methods create a WriteBatch, commit, and flush WAL.\n+* **Explicit batch**: user opens `let mut wb = db.batch();`, issues puts/deletes via collection adapters, then `wb.commit()` for cross-collection atomicity.\n+* **Crash guarantee**: after `commit` returns, updates survive power loss (`rocksdb::DB::flush_wal(true)`).\n+\n+Read operations take a **consistent snapshot** by default; advanced users can opt-out for max throughput.\n+\n+---\n+\n+### 7  Migrations (v0.2 roadmap)\n+\n+* Each collection stores a `u32 schema_version` meta key.\n+* `durable::open` accepts an optional `Schema` describing:\n+\n+  ```rust\n+  struct Schema { collections: Vec, version: u32 }\n+  ```\n+\n+  If version mismatch ⇒ run user-supplied `migrate(old, new, &db)` which gets a mutable view and may batch-rewrite keys.\n+\n+---\n+\n+### 8  Reactive Diffs (feature `\"watch\"`)\n+\n+* Behind a Tokio-aware feature; uses RocksDB’s `get_updates_since(seq)` API.\n+* `watch_prefix(prefix) -> impl Stream`\n+  – `Diff` = key, old val (Option), new val (Option).\n+* Back-pressure handled with an in-process ring buffer; user chooses lag policy.\n+\n+---\n+\n+### 9  Caching Layer (feature `\"cache\"`)\n+\n+* Probabilistic LRU over deserialized values.\n+* Configurable per-collection: `with_cache(cap_entries, ttl_ms)`.\n+* Coherent: write path invalidates cache keys on commit.\n+\n+---\n+\n+### 10  Error Model\n+\n+```rust\n+#[non_exhaustive]\n+pub enum Error {\n+    Rocks(rocksdb::Error),\n+    Serde(bincode::Error),\n+    Corruption(String),\n+    TransactionAborted,\n+    // feature-gated variants e.g. WatchLagged\n+}\n+```\n+\n+`Result = std::result::Result` everywhere.\n+\n+---\n+\n+### 11  Performance Budget (baseline targets)\n+\n+| Operation            | Goal                                   |\n+| -------------------- | -------------------------------------- |\n+| `map.insert`         | < 30 µs (including WAL fsync)          |\n+| `vec.push`           | < 25 µs                                |\n+| Prefix iteration 1 M | > 120 MB/s read BW on NVMe             |\n+| Concurrent readers   | Linear scaling up to RocksDB read-IOPS |\n+| Watch latency        | < 5 ms p50 on local SSD                |\n+\n+(bench harness lives in `benches/` via Criterion.)\n+\n+---\n+\n+### 12  Dependency Footprint\n+\n+* `rocksdb` (–> FB fork)   ⟹ builds C++11 static lib         (\\~10 MB)\n+* `bincode` (default), with `serde` feature.\n+* `tokio-stream` only if `watch` feature enabled.\n+\n+MSRV = 1.76.\n+\n+---\n+\n+### 13  Minimum Deliverable for **v0.1-alpha**\n+\n+* [ ] `Db::open`, `Db::batch`\n+* [ ] `DurableMap`, `DurableVec`\n+* [ ] automatic serialization via `serde`\n+* [ ] atomic commit + WAL flush\n+* [ ] snapshot reads\n+* [ ] unit tests (insert/get/iter/crash-recovery using `tempdir`)\n+* [ ] criterion bench\n+\n+---\n+\n+### 14  Future Work\n+\n+* **Replicated mode** (Raft or FoundationDB layer)\n+* **CRDT merge semantics** for offline edits\n+* **DurableGraph** (adjacency lists + index)\n+* WebAssembly key-value adapters (edge workers)\n+* `tracing` instrumentation & Prometheus metrics\n+\n+---\n+\n+### 15  Licensing & Governance\n+\n+* License: **Apache-2.0 OR MIT** (standard Rust dual license)\n+* Code-of-conduct: Rust CoC template\n+* Contribution model: PR + mandatory CI (fmt, clippy, test, bench)\n+* Early roadmap guided by original author(s); transfer to an org when ≥3 maintainers.\n+\n+---\n+\n+## Appendix A — Sorter Use-Case Sketch\n+\n+```rust\n+type TagId  = String;\n+type Bucket = u64;                  // logical Unix day or version #\n+type ItemId = String;\n+type Elo    = i32;\n+\n+let hist = DurableMap::>>::new(&db,\"hist\")?;\n+\n+// update elo\n+hist.entry(\"tf2\")?\n+    .or_default()?\n+    .entry(today_bucket)?\n+    .or_default()?\n+    .push((item_id, new_elo))?;\n+\n+// stream bucket\n+let items = hist.get(\"tf2\")?.unwrap()\n+                .get(&today_bucket)?.unwrap()\n+                .iter().collect::>();\n+```\n+\n+All three layers share one RocksDB instance; you pay one WAL flush per ranking update, but reads are prefix-scans with zero allocations.\ndiff --git a/durable/docs/motivation.md b/durable/docs/motivation.md\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..50d8ee5ef9f384d908e3a86f6945e3f4ced58292\n--- /dev/null\n+++ b/durable/docs/motivation.md\n@@ -0,0 +1,248 @@\n+# Why Durable? The Missing Abstraction Layer for Persistent Storage\n+\n+## The Problem: The Abstraction Gap\n+\n+Every database forces developers to translate between how they **think** about data and how they **store** it. This translation layer is where bugs hide, performance suffers, and development slows down.\n+\n+### Example: Building a Multiplayer Game Leaderboard\n+\n+Let's say you need to store player rankings by game mode, with history by day. Here's the data model in your head:\n+\n+```\n+Game Mode → Day → List of (Player, Score)\n+```\n+\n+#### With Raw Key-Value Stores (sled, RocksDB)\n+\n+```rust\n+// Storing a score requires manual key construction\n+let key = format!(\"leaderboard:{}:{}:player:{}\", game_mode, day, player_id);\n+db.insert(key.as_bytes(), score.to_le_bytes())?;\n+\n+// Getting today's leaderboard? Manual prefix scan and deserialization\n+let prefix = format!(\"leaderboard:{}:{}:\", game_mode, day);\n+let mut scores = Vec::new();\n+for item in db.scan_prefix(prefix.as_bytes()) {\n+    let (key, value) = item?;\n+    // Parse player_id from key string... hope the format is right\n+    // Deserialize score... hope it's the right type\n+    scores.push((player_id, score));\n+}\n+scores.sort_by_key(|(_, s)| *s);\n+\n+// Want to know how many players played today? Another scan!\n+// Want to atomically update multiple scores? Write a transaction wrapper!\n+// Want to clean up old days? Manual prefix iteration and deletion!\n+```\n+\n+**Problems:**\n+- String manipulation for every operation\n+- No type safety (everything is bytes)\n+- Manual implementation of collection semantics\n+- No atomicity across related keys\n+- Performance overhead from string parsing\n+\n+#### With SQL Databases (SQLite, PostgreSQL)\n+\n+```sql\n+CREATE TABLE leaderboards (\n+    game_mode VARCHAR(50),\n+    day DATE,\n+    player_id UUID,\n+    score INTEGER,\n+    PRIMARY KEY (game_mode, day, player_id)\n+);\n+\n+-- Getting a leaderboard requires SQL\n+SELECT player_id, score \n+FROM leaderboards \n+WHERE game_mode = ? AND day = ?\n+ORDER BY score DESC;\n+```\n+\n+```rust\n+// In Rust, you need an ORM or manual query building\n+let scores: Vec<(Uuid, i32)> = sqlx::query_as(\n+    \"SELECT player_id, score FROM leaderboards WHERE game_mode = $1 AND day = $2 ORDER BY score DESC\"\n+)\n+.bind(&game_mode)\n+.bind(&day)\n+.fetch_all(&pool)\n+.await?;\n+```\n+\n+**Problems:**\n+- Impedance mismatch (relational model vs nested structures)\n+- SQL complexity for simple operations\n+- ORMs add abstraction layers and performance overhead\n+- Async runtime required even for local storage\n+- Schema migrations for every structural change\n+\n+#### With Document Stores (MongoDB)\n+\n+```javascript\n+// Document structure\n+{\n+  game_mode: \"ranked\",\n+  day: \"2024-01-15\",\n+  scores: [\n+    { player_id: \"abc\", score: 1500 },\n+    { player_id: \"def\", score: 1400 }\n+  ]\n+}\n+\n+// But now you have a different problem: updating a single score\n+// requires loading and saving the entire document!\n+```\n+\n+**Problems:**\n+- Not embedded (requires separate process)\n+- Document size limits\n+- Inefficient for partial updates\n+- Complex setup for local-first apps\n+\n+## The Solution: Native Data Structures\n+\n+With Durable, you express your data model directly:\n+\n+```rust\n+let leaderboard = DurableMap::>>::new(&db, \"leaderboard\")?;\n+\n+// Store a score - reads like natural Rust code\n+leaderboard\n+    .entry(game_mode)?\n+    .or_default()?\n+    .entry(day)?\n+    .or_default()?\n+    .push((player_id, score))?;\n+\n+// Get today's leaderboard - it's just a Vec\n+let mut today_scores = leaderboard\n+    .get(&game_mode)?\n+    .and_then(|mode| mode.get(&day).ok())\n+    .unwrap_or_default();\n+today_scores.sort_by_key(|(_, s)| *s);\n+\n+// All operations are atomic, typed, and efficient\n+```\n+\n+## Why This Matters\n+\n+### 1. **Zero Translation Overhead**\n+\n+Your mental model **is** the storage model. No more:\n+- String concatenation for keys\n+- Manual serialization/deserialization  \n+- SQL query construction\n+- Document structure mapping\n+\n+### 2. **Composition Without Complexity**\n+\n+Nested data structures \"just work\":\n+\n+```rust\n+// A real-world example: user notifications by app by priority\n+let notifications = DurableMap::>>>::new(&db, \"notifs\")?;\n+\n+// Natural access patterns\n+notifications\n+    .get(&user_id)?\n+    .get(&app_id)?\n+    .get(&Priority::High)?\n+    .iter()\n+    .take(10)  // Latest 10 high-priority notifications\n+```\n+\n+Try implementing this with SQL joins or KV prefixes!\n+\n+### 3. **Type Safety Throughout**\n+\n+```rust\n+// This won't compile - type safety at every level\n+let score: String = leaderboard.get(&\"chess\")?.get(&20240115)?.get(0)?;\n+//          ^^^^^^ expected Score, found String\n+\n+// With raw KV stores, this is a runtime error after deserialization\n+```\n+\n+### 4. **Atomicity By Design**\n+\n+```rust\n+// Multiple operations in one atomic batch\n+let mut batch = db.batch();\n+batch.vec_push(&game.players, new_player)?;\n+batch.map_insert(&game.scores, player_id, 0)?;\n+batch.map_increment(&game.stats, \"player_count\", 1)?;\n+batch.commit()?;  // All or nothing\n+```\n+\n+### 5. **Performance Without Compromise**\n+\n+- **Locality**: Related data stored contiguously (prefix design)\n+- **Zero-copy possible**: Direct memory mapping for read-heavy workloads\n+- **Streaming iteration**: No need to load entire collections\n+- **Bulk operations**: Native batch support\n+\n+## Comparison Matrix\n+\n+| Feature | Durable | sled/RocksDB | SQLite | MongoDB |\n+|---------|---------|--------------|---------|----------|\n+| **Native collections** | ✅ Built-in | ❌ DIY | ❌ Tables only | ⚠️ Documents |\n+| **Type safety** | ✅ Full | ❌ Bytes | ⚠️ ORM-dependent | ⚠️ Schema validation |\n+| **Nested structures** | ✅ Natural | ❌ Manual prefixes | ❌ Joins/JSON | ✅ Embedded docs |\n+| **Atomic operations** | ✅ Automatic | ⚠️ Manual batching | ✅ Transactions | ⚠️ Document-level |\n+| **Local/embedded** | ✅ Yes | ✅ Yes | ✅ Yes | ❌ Separate process |\n+| **Schema evolution** | ✅ Per-collection | ❌ DIY | ⚠️ Migrations | ✅ Flexible |\n+| **Memory efficiency** | ✅ Scan & stream | ✅ Manual | ⚠️ Query-dependent | ❌ Doc loading |\n+\n+## Real-World Use Cases Where Durable Shines\n+\n+### Local-First Sync Engine\n+\n+```rust\n+// Sync state with conflict tracking\n+let sync_state = DurableMap::>::new(&db, \"sync\")?;\n+\n+// Natural conflict detection\n+let versions = sync_state.get(&record_id)?;\n+if versions.values().unique().count() > 1 {\n+    // Conflict detected - handle naturally\n+}\n+```\n+\n+### Time-Series Analytics Cache\n+\n+```rust\n+// Metrics by source by minute\n+let metrics = DurableMap::>>::new(&db, \"metrics\")?;\n+\n+// Natural windowing\n+let last_hour: Vec = metrics\n+    .get(&source)?\n+    .range(now - 3600..=now)?\n+    .flat_map(|(_, minute_metrics)| minute_metrics.iter())\n+    .collect();\n+```\n+\n+### Feature Flag System with History\n+\n+```rust\n+// Flags by environment with change history\n+let flags = DurableMap::>>::new(&db, \"flags\")?;\n+\n+// Natural audit trail\n+let history = flags.get(&Env::Prod)?.get(\"new-feature\")?.iter().collect();\n+```\n+\n+## The Bottom Line\n+\n+**Durable isn't a better database - it's the missing abstraction layer that lets you use persistent storage like in-memory collections.**\n+\n+Stop translating. Start building.\n+\n+---\n+\n+*Next: Read the [RFC](001.md) for implementation details, or jump to the [Quick Start Guide](quickstart.md).* \n\\ No newline at end of file\ndiff --git a/durable/examples/combined_example.rs b/durable/examples/combined_example.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..626a6e1cf7e3c9c26f9f2edc58950d9bc31ec67e\n--- /dev/null\n+++ b/durable/examples/combined_example.rs\n@@ -0,0 +1,160 @@\n+use durable::{Db, DurableMap, DurableVec};\n+use serde::{Serialize, Deserialize};\n+use std::time::{SystemTime, UNIX_EPOCH};\n+\n+#[derive(Debug, Clone, Serialize, Deserialize)]\n+struct Message {\n+    id: u64,\n+    from: String,\n+    to: String,\n+    content: String,\n+    timestamp: u64,\n+}\n+\n+#[derive(Debug, Clone, Serialize, Deserialize)]\n+struct User {\n+    username: String,\n+    display_name: String,\n+    message_count: u32,\n+}\n+\n+fn get_timestamp() -> u64 {\n+    SystemTime::now()\n+        .duration_since(UNIX_EPOCH)\n+        .unwrap()\n+        .as_secs()\n+}\n+\n+fn main() -> Result<(), Box> {\n+    // Open or create a database\n+    let db = Db::open(\"chat_db\")?;\n+    \n+    // Create our collections\n+    let mut users = DurableMap::::new(&db, \"users\")?;\n+    let mut messages = DurableVec::::new(&db, \"messages\")?;\n+    let mut user_message_indices = DurableMap::>::new(&db, \"user_messages\")?;\n+    \n+    // Create some users\n+    users.insert(\"alice\".to_string(), User {\n+        username: \"alice\".to_string(),\n+        display_name: \"Alice Smith\".to_string(),\n+        message_count: 0,\n+    })?;\n+    \n+    users.insert(\"bob\".to_string(), User {\n+        username: \"bob\".to_string(),\n+        display_name: \"Bob Johnson\".to_string(),\n+        message_count: 0,\n+    })?;\n+    \n+    users.insert(\"charlie\".to_string(), User {\n+        username: \"charlie\".to_string(),\n+        display_name: \"Charlie Brown\".to_string(),\n+        message_count: 0,\n+    })?;\n+    \n+    // Helper to send a message\n+    let send_message = |from: &str, to: &str, content: &str, \n+                        messages: &mut DurableVec,\n+                        users: &mut DurableMap,\n+                        indices: &mut DurableMap>| -> Result<(), Box> {\n+        // Create message\n+        let msg_id = messages.len()? as u64;\n+        let message = Message {\n+            id: msg_id,\n+            from: from.to_string(),\n+            to: to.to_string(),\n+            content: content.to_string(),\n+            timestamp: get_timestamp(),\n+        };\n+        \n+        // Store message\n+        messages.push(message)?;\n+        let msg_index = messages.len()? - 1;\n+        \n+        // Update sender's message count\n+        if let Some(mut sender) = users.get(&from.to_string())? {\n+            sender.message_count += 1;\n+            users.insert(from.to_string(), sender)?;\n+        }\n+        \n+        // Track message indices for recipient\n+        let mut recipient_indices = indices.get(&to.to_string())?.unwrap_or_default();\n+        recipient_indices.push(msg_index);\n+        indices.insert(to.to_string(), recipient_indices)?;\n+        \n+        Ok(())\n+    };\n+    \n+    // Send some messages\n+    println!(\"💬 Chat Application Demo\\n\");\n+    println!(\"Sending messages...\");\n+    \n+    send_message(\"alice\", \"bob\", \"Hey Bob, how's the Durable library coming along?\", \n+                 &mut messages, &mut users, &mut user_message_indices)?;\n+    \n+    send_message(\"bob\", \"alice\", \"It's going great! We have DurableVec and DurableMap working!\", \n+                 &mut messages, &mut users, &mut user_message_indices)?;\n+    \n+    send_message(\"charlie\", \"alice\", \"That sounds awesome! Can I help with testing?\", \n+                 &mut messages, &mut users, &mut user_message_indices)?;\n+    \n+    send_message(\"alice\", \"charlie\", \"Absolutely! The more testing the better!\", \n+                 &mut messages, &mut users, &mut user_message_indices)?;\n+    \n+    send_message(\"bob\", \"charlie\", \"Check out the examples directory for usage patterns\", \n+                 &mut messages, &mut users, &mut user_message_indices)?;\n+    \n+    // Display all users and their message counts\n+    println!(\"\\n👥 Users:\");\n+    let mut all_users = users.to_vec()?;\n+    all_users.sort_by_key(|(username, _)| username.clone());\n+    \n+    for (username, user) in all_users {\n+        println!(\"  {} ({}) - {} messages sent\", \n+                 user.display_name, username, user.message_count);\n+    }\n+    \n+    // Display all messages\n+    println!(\"\\n📨 All messages:\");\n+    for (i, msg) in messages.iter()?.enumerate() {\n+        let msg = msg?;\n+        println!(\"  [{}] {} → {}: {}\", i, msg.from, msg.to, msg.content);\n+    }\n+    \n+    // Show inbox for each user\n+    println!(\"\\n📥 User inboxes:\");\n+    for item in users.iter() {\n+        let (username, _) = item?;\n+        if let Some(indices) = user_message_indices.get(&username)? {\n+            println!(\"\\n  {}'s inbox ({} messages):\", username, indices.len());\n+            for &idx in &indices {\n+                if let Some(msg) = messages.get(idx)? {\n+                    println!(\"    From {}: {}\", msg.from, msg.content);\n+                }\n+            }\n+        }\n+    }\n+    \n+    // Statistics\n+    println!(\"\\n📊 Statistics:\");\n+    println!(\"  Total users: {}\", users.len()?);\n+    println!(\"  Total messages: {}\", messages.len()?);\n+    \n+    // Demonstrate persistence\n+    println!(\"\\n💾 Data has been persisted to disk!\");\n+    println!(\"  Database location: ./chat_db\");\n+    \n+    // Clean up\n+    drop(messages);\n+    drop(users);\n+    drop(user_message_indices);\n+    drop(db);\n+    \n+    // Remove the database for this example\n+    std::fs::remove_dir_all(\"chat_db\").ok();\n+    \n+    println!(\"\\n✅ Example completed!\");\n+    \n+    Ok(())\n+} \n\\ No newline at end of file\ndiff --git a/durable/examples/map_example.rs b/durable/examples/map_example.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..08b8f2c8826caf53c4c20b422a540c92f6624029\n--- /dev/null\n+++ b/durable/examples/map_example.rs\n@@ -0,0 +1,99 @@\n+use durable::{Db, DurableMap};\n+use serde::{Serialize, Deserialize};\n+\n+#[derive(Debug, Clone, Serialize, Deserialize)]\n+struct UserProfile {\n+    name: String,\n+    email: String,\n+    score: u32,\n+}\n+\n+fn main() -> Result<(), Box> {\n+    // Open or create a database\n+    let db = Db::open(\"example_db\")?;\n+    \n+    // Create a persistent map of user profiles\n+    let mut users = DurableMap::::new(&db, \"users\")?;\n+    \n+    // Insert some users\n+    // Using put() when we don't need the old value - more efficient!\n+    users.put(\n+        \"alice\".to_string(),\n+        UserProfile {\n+            name: \"Alice Smith\".to_string(),\n+            email: \"alice@example.com\".to_string(),\n+            score: 1500,\n+        },\n+    )?;\n+    \n+    users.put(\n+        \"bob\".to_string(),\n+        UserProfile {\n+            name: \"Bob Johnson\".to_string(),\n+            email: \"bob@example.com\".to_string(),\n+            score: 1200,\n+        },\n+    )?;\n+    \n+    // Using insert() when we might need the old value\n+    let old_charlie = users.insert(\n+        \"charlie\".to_string(),\n+        UserProfile {\n+            name: \"Charlie Brown\".to_string(),\n+            email: \"charlie@example.com\".to_string(),\n+            score: 1800,\n+        },\n+    )?;\n+    \n+    if old_charlie.is_some() {\n+        println!(\"Replaced existing charlie entry\");\n+    }\n+    \n+    println!(\"Total users: {}\", users.len()?);\n+    \n+    // Look up a specific user\n+    if let Some(alice) = users.get(&\"alice\".to_string())? {\n+        println!(\"\\nAlice's profile: {:?}\", alice);\n+    }\n+    \n+    // Check if a user exists\n+    println!(\"\\nDoes 'david' exist? {}\", users.contains_key(&\"david\".to_string())?);\n+    \n+    // Update a user's score\n+    if let Some(mut bob) = users.get(&\"bob\".to_string())? {\n+        bob.score += 100;\n+        // Use put() here since we don't need the old value back\n+        users.put(\"bob\".to_string(), bob)?;\n+        println!(\"Updated Bob's score!\");\n+    }\n+    \n+    // Iterate over all users\n+    println!(\"\\nAll users (sorted by username):\");\n+    let mut all_users = users.to_vec()?;\n+    all_users.sort_by_key(|(username, _)| username.clone());\n+    \n+    for (username, profile) in all_users {\n+        println!(\"  {} ({}) - Score: {}\", username, profile.email, profile.score);\n+    }\n+    \n+    // Get just the usernames\n+    let mut usernames = users.keys_vec()?;\n+    usernames.sort();\n+    println!(\"\\nAll usernames: {:?}\", usernames);\n+    \n+    // Find the highest scoring user\n+    let profiles = users.values_vec()?;\n+    if let Some(top_user) = profiles.iter().max_by_key(|p| p.score) {\n+        println!(\"\\nTop scorer: {} with {} points\", top_user.name, top_user.score);\n+    }\n+    \n+    // Remove a user\n+    if let Some(removed) = users.remove(&\"charlie\".to_string())? {\n+        println!(\"\\nRemoved user: {}\", removed.name);\n+        println!(\"Users remaining: {}\", users.len()?);\n+    }\n+    \n+    println!(\"\\nData has been persisted to disk.\");\n+    \n+    Ok(())\n+} \n\\ No newline at end of file\ndiff --git a/durable/examples/nested_example.rs b/durable/examples/nested_example.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..3b880f2c8b8ad2633d4b5fcf2016652216c9de8e\n--- /dev/null\n+++ b/durable/examples/nested_example.rs\n@@ -0,0 +1,64 @@\n+use durable::{Db, DurableMap, DurableVec};\n+\n+fn main() -> Result<(), Box> {\n+    // Open a database\n+    let db = Db::open(\"nested_example_db\")?;\n+    \n+    // Create a map where each user has a list of posts\n+    let user_posts: DurableMap> = DurableMap::new_nested(&db, \"user_posts\");\n+    \n+    // Add posts for Alice\n+    println!(\"Adding posts for Alice...\");\n+    let mut alice_posts = user_posts.entry(\"alice\".to_string())?.or_default()?;\n+    alice_posts.push(\"Hello, world!\".to_string())?;\n+    alice_posts.push(\"Rust is awesome!\".to_string())?;\n+    alice_posts.push(\"Loving persistent data structures!\".to_string())?;\n+    \n+    // Add posts for Bob\n+    println!(\"Adding posts for Bob...\");\n+    let mut bob_posts = user_posts.entry(\"bob\".to_string())?.or_default()?;\n+    bob_posts.push(\"First post\".to_string())?;\n+    bob_posts.push(\"Learning Rust\".to_string())?;\n+    \n+    // Add a post for Charlie in a chained call\n+    println!(\"Adding post for Charlie...\");\n+    user_posts.entry(\"charlie\".to_string())?.or_default()?.push(\"One-liner post!\".to_string())?;\n+    \n+    // Read back Alice's posts\n+    println!(\"\\nAlice's posts:\");\n+    let alice_posts_read = user_posts.entry(\"alice\".to_string())?.or_default()?;\n+    for i in 0..alice_posts_read.len()? {\n+        if let Some(post) = alice_posts_read.get(i)? {\n+            println!(\"  {}: {}\", i + 1, post);\n+        }\n+    }\n+    \n+    // Read back Bob's posts\n+    println!(\"\\nBob's posts:\");\n+    let bob_posts_read = user_posts.entry(\"bob\".to_string())?.or_default()?;\n+    for i in 0..bob_posts_read.len()? {\n+        if let Some(post) = bob_posts_read.get(i)? {\n+            println!(\"  {}: {}\", i + 1, post);\n+        }\n+    }\n+    \n+    // Read back Charlie's posts\n+    println!(\"\\nCharlie's posts:\");\n+    let charlie_posts_read = user_posts.entry(\"charlie\".to_string())?.or_default()?;\n+    for i in 0..charlie_posts_read.len()? {\n+        if let Some(post) = charlie_posts_read.get(i)? {\n+            println!(\"  {}: {}\", i + 1, post);\n+        }\n+    }\n+    \n+    println!(\"\\nDemonstration of persistence...\");\n+    println!(\"Data is now persisted to disk. You can stop and restart this program,\");\n+    println!(\"and all the posts will still be there!\");\n+    \n+    println!(\"\\nTotal users with posts: 3\");\n+    println!(\"Alice has {} posts\", alice_posts_read.len()?);\n+    println!(\"Bob has {} posts\", bob_posts_read.len()?);\n+    println!(\"Charlie has {} posts\", charlie_posts_read.len()?);\n+    \n+    Ok(())\n+}\n\\ No newline at end of file\ndiff --git a/durable/examples/ranking_history.rs b/durable/examples/ranking_history.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..4da8592d240318318ae20c834d616210edb16c8d\n--- /dev/null\n+++ b/durable/examples/ranking_history.rs\n@@ -0,0 +1,179 @@\n+use durable::{Db, DurableMap, DurableVec};\n+use serde::{Serialize, Deserialize};\n+\n+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]\n+struct RankingEntry {\n+    player_id: String,\n+    score: i32,\n+    timestamp: u64,\n+}\n+\n+impl RankingEntry {\n+    fn new(player_id: &str, score: i32) -> Self {\n+        Self {\n+            player_id: player_id.to_string(),\n+            score,\n+            timestamp: std::time::SystemTime::now()\n+                .duration_since(std::time::UNIX_EPOCH)\n+                .unwrap()\n+                .as_secs(),\n+        }\n+    }\n+}\n+\n+fn main() -> Result<(), Box> {\n+    println!(\"🎮 Gaming Ranking History System (Rewritten with Nested Entry API)\");\n+    println!(\"================================================================\");\n+    \n+    let db = Db::open(\"ranking_history_db\")?;\n+    \n+    // THE CORE CHANGE: Define the truly nested data structure.\n+    // Instead of a composite key, we nest a Map within a Map.\n+    // This represents the ideal, ergonomic API.\n+    type DailyRankings = DurableVec;\n+    type GameHistory = DurableMap;\n+    type Rankings = DurableMap;\n+\n+    let rankings: Rankings = DurableMap::new_nested(&db, \"game_rankings_v2\");\n+    \n+    // Simulate some game days\n+    let today = 20241215u32;\n+    let yesterday = 20241214u32;\n+    let last_week = 20241208u32;\n+    \n+    // No more `make_key` helper function!\n+    \n+    println!(\"\\n📊 Adding ranking data using chained entry().or_default()...\");\n+    \n+    // Add rankings for CS2 today. This demonstrates the new, clean access pattern.\n+    println!(\"Adding CS2 rankings for today ({})\", today);\n+    let mut cs2_today = rankings\n+        .entry(\"cs2\".to_string())?\n+        .or_default()? // Returns GameHistory (DurableMap) for \"cs2\"\n+        .entry(today)?\n+        .or_default()?; // Returns DailyRankings (DurableVec<...>) for `today`\n+\n+    cs2_today.push(RankingEntry::new(\"player1\", 2450))?;\n+    cs2_today.push(RankingEntry::new(\"player2\", 2380))?;\n+    cs2_today.push(RankingEntry::new(\"player3\", 2320))?;\n+    cs2_today.push(RankingEntry::new(\"player4\", 2280))?;\n+    \n+    // Add rankings for CS2 yesterday\n+    println!(\"Adding CS2 rankings for yesterday ({})\", yesterday);\n+    rankings\n+        .entry(\"cs2\".to_string())?\n+        .or_default()?\n+        .entry(yesterday)?\n+        .or_default()?\n+        .push(RankingEntry::new(\"player1\", 2420))?;\n+    rankings\n+        .entry(\"cs2\".to_string())?\n+        .or_default()?\n+        .entry(yesterday)?\n+        .or_default()?\n+        .push(RankingEntry::new(\"player2\", 2350))?;\n+    rankings\n+        .entry(\"cs2\".to_string())?\n+        .or_default()?\n+        .entry(yesterday)?\n+        .or_default()?\n+        .push(RankingEntry::new(\"player5\", 2300))?;\n+\n+    // Add rankings for Valorant today\n+    println!(\"Adding Valorant rankings for today ({})\", today);\n+    let mut valorant_today = rankings\n+        .entry(\"valorant\".to_string())?\n+        .or_default()?\n+        .entry(today)?\n+        .or_default()?;\n+\n+    valorant_today.push(RankingEntry::new(\"player6\", 1850))?;\n+    valorant_today.push(RankingEntry::new(\"player7\", 1820))?;\n+    valorant_today.push(RankingEntry::new(\"player1\", 1800))?; // Same player, different game\n+    \n+    // Add TF2 rankings (matching the docs example)\n+    println!(\"Adding TF2 rankings for last week ({})\", last_week);\n+    rankings\n+        .entry(\"tf2\".to_string())?\n+        .or_default()?\n+        .entry(last_week)?\n+        .or_default()?\n+        .push(RankingEntry::new(\"veteran_player\", 3200))?;\n+    rankings\n+        .entry(\"tf2\".to_string())?\n+        .or_default()?\n+        .entry(last_week)?\n+        .or_default()?\n+        .push(RankingEntry::new(\"old_school_gamer\", 3150))?;\n+    \n+    println!(\"\\n🏆 Reading back ranking data with the same natural API...\");\n+    \n+    // Get today's CS2 leaderboard\n+    println!(\"\\n🎯 CS2 Leaderboard for {} (today):\", today);\n+    let mut today_rankings = rankings\n+        .entry(\"cs2\".to_string())?\n+        .or_default()?\n+        .entry(today)?\n+        .or_default()?\n+        .to_vec()?;\n+\n+    // Sort by score descending\n+    today_rankings.sort_by(|a, b| b.score.cmp(&a.score));\n+    \n+    for (rank, entry) in today_rankings.iter().enumerate() {\n+        println!(\"  {}. {} - {} points\", rank + 1, entry.player_id, entry.score);\n+    }\n+    \n+    // Show cross-game analysis is still easy\n+    println!(\"\\n🎮 Multi-game player analysis for player1 on {}:\", today);\n+    let cs2_player1_score = today_rankings.iter()\n+        .find(|e| e.player_id == \"player1\")\n+        .map(|e| e.score);\n+\n+    let valorant_player1_score = valorant_today.to_vec()?.iter()\n+        .find(|e| e.player_id == \"player1\")\n+        .map(|e| e.score);\n+\n+    if let Some(score) = cs2_player1_score { println!(\"    CS2 Score: {}\", score); }\n+    if let Some(score) = valorant_player1_score { println!(\"    Valorant Score: {}\", score); }\n+    \n+    // Showcase the power of the nested structure for stats\n+    // For nested collections, we use the keys API instead of iter()\n+    println!(\"\\n📊 Dynamic Database Statistics (discovered games):\");\n+    \n+    // Note: For nested collections, we iterate over known keys or use a different approach\n+    // since the values (nested DurableMaps) cannot be directly deserialized\n+    let games = vec![\"cs2\", \"valorant\", \"tf2\"]; // In a real app, you might track these separately\n+    \n+    for game in games {\n+        let game_history = rankings.entry(game.to_string())?.or_default()?;\n+        let active_days = game_history.len()?;\n+        \n+        if active_days > 0 {\n+            // For demonstration, let's count entries from known days\n+            let mut total_entries = 0;\n+            let days = [today, yesterday, last_week];\n+            \n+            for day in days {\n+                if let Ok(daily_rankings) = game_history.entry(day) {\n+                    if let Ok(rankings_vec) = daily_rankings.or_default() {\n+                        total_entries += rankings_vec.len()?;\n+                    }\n+                }\n+            }\n+            \n+            if total_entries > 0 {\n+                println!(\"  • {}: {} total entries across {} active day(s)\", \n+                         game.to_uppercase(), total_entries, active_days);\n+            }\n+        }\n+    }\n+    \n+    println!(\"\\n✨ Key Benefits of This Rewritten Approach:\");\n+    println!(\"  • No more manual key construction (`format!`) - the core goal is met!\");\n+    println!(\"  • The code's structure now mirrors the mental model: `rankings[game][day]`\");\n+    println!(\"  • Truly compositional API, unlocking more powerful dynamic queries (like the stats section)\");\n+    println!(\"  • Demonstrates the full power of the `DurableCollection` and `entry()` design.\");\n+\n+    Ok(())\n+}\n\\ No newline at end of file\ndiff --git a/durable/examples/simple_ranking.rs b/durable/examples/simple_ranking.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..3cc873c41f3d93c7fa9e6e2dfc80e815f456b1f9\n--- /dev/null\n+++ b/durable/examples/simple_ranking.rs\n@@ -0,0 +1,68 @@\n+use durable::{Db, DurableMap, DurableVec};\n+\n+fn main() -> Result<(), Box> {\n+    println!(\"🏆 Simple Game Ranking Example\");\n+    println!(\"Demonstrating the pattern from docs/motivation.md\");\n+    println!(\"===============================================\");\n+    \n+    let db = Db::open(\"simple_ranking_db\")?;\n+    \n+    // This is the exact pattern from the docs: Game Mode → List of (Player, Score)\n+    // For simplicity, we're showing one day's data per game mode\n+    let rankings: DurableMap> = DurableMap::new_nested(&db, \"rankings\");\n+    \n+    println!(\"\\n📊 Adding TF2 rankings (from the docs example)...\");\n+    \n+    // This is the exact code pattern shown in docs/motivation.md\n+    let mut tf2_rankings = rankings.entry(\"tf2\".to_string())?.or_default()?;\n+    tf2_rankings.push((\"player1\".to_string(), 1500))?;\n+    tf2_rankings.push((\"player2\".to_string(), 1400))?;\n+    tf2_rankings.push((\"player3\".to_string(), 1300))?;\n+    \n+    println!(\"✅ Added TF2 rankings using the docs pattern!\");\n+    \n+    // Add some other games for comparison\n+    println!(\"\\n📊 Adding CS2 rankings...\");\n+    let mut cs2_rankings = rankings.entry(\"cs2\".to_string())?.or_default()?;\n+    cs2_rankings.push((\"pro_player\".to_string(), 2500))?;\n+    cs2_rankings.push((\"skilled_gamer\".to_string(), 2200))?;\n+    \n+    println!(\"✅ Added CS2 rankings!\");\n+    \n+    // Now read back the data\n+    println!(\"\\n🏆 Current TF2 Leaderboard:\");\n+    let tf2_data = rankings.entry(\"tf2\".to_string())?.or_default()?;\n+    \n+    // Convert to vec and sort for display\n+    let mut tf2_leaderboard = tf2_data.to_vec()?;\n+    tf2_leaderboard.sort_by(|a, b| b.1.cmp(&a.1)); // Sort by score descending\n+    \n+    for (rank, (player, score)) in tf2_leaderboard.iter().enumerate() {\n+        println!(\"  {}. {} - {} points\", rank + 1, player, score);\n+    }\n+    \n+    println!(\"\\n🏆 Current CS2 Leaderboard:\");\n+    let cs2_data = rankings.entry(\"cs2\".to_string())?.or_default()?;\n+    \n+    let mut cs2_leaderboard = cs2_data.to_vec()?;\n+    cs2_leaderboard.sort_by(|a, b| b.1.cmp(&a.1));\n+    \n+    for (rank, (player, score)) in cs2_leaderboard.iter().enumerate() {\n+        println!(\"  {}. {} - {} points\", rank + 1, player, score);\n+    }\n+    \n+    println!(\"\\n📈 Database Statistics:\");\n+    println!(\"  TF2 has {} players\", tf2_data.len()?);\n+    println!(\"  CS2 has {} players\", cs2_data.len()?);\n+    \n+    println!(\"\\n✨ This demonstrates the exact pattern from docs/motivation.md:\");\n+    println!(\"  rankings.entry(game_mode)?.or_default()?.push((player, score))?;\");\n+    println!(\"  \");\n+    println!(\"  Compare this to the manual key construction required with raw KV stores:\");\n+    println!(\"  let key = format!(\\\"leaderboard:{{}}:{{}}:player:{{}}\\\", game_mode, day, player_id);\");\n+    println!(\"  db.insert(key.as_bytes(), score.to_le_bytes())?;\");\n+    println!(\"  \");\n+    println!(\"  Durable provides the ergonomic, type-safe abstraction over RocksDB!\");\n+    \n+    Ok(())\n+}\n\\ No newline at end of file\ndiff --git a/durable/examples/streaming_demo.rs b/durable/examples/streaming_demo.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..3a74a5318675f94f89aaf01562a5b28f8a1b664a\n--- /dev/null\n+++ b/durable/examples/streaming_demo.rs\n@@ -0,0 +1,81 @@\n+use durable::{Db, DurableMap, DurableVec};\n+\n+fn main() -> Result<(), Box> {\n+    let db = Db::open(\"streaming_demo_db\")?;\n+    \n+    // Create collections with a moderate amount of data\n+    let mut map = DurableMap::::new(&db, \"large_map\")?;\n+    let mut vec = DurableVec::::new(&db, \"large_vec\")?;\n+    \n+    println!(\"🚀 Streaming Iterator Demo\\n\");\n+    \n+    // Add 1000 entries to demonstrate streaming\n+    println!(\"Adding 1000 entries to map and vec...\");\n+    for i in 0..1000 {\n+        map.insert(i, format!(\"Value {}\", i))?;\n+        vec.push(format!(\"Item {}\", i))?;\n+    }\n+    \n+    println!(\"\\n📊 Collection sizes:\");\n+    println!(\"  Map entries: {}\", map.len()?);\n+    println!(\"  Vec elements: {}\", vec.len()?);\n+    \n+    // Demonstrate streaming iteration - memory efficient\n+    println!(\"\\n✨ Streaming iteration (memory efficient):\");\n+    \n+    // Count items without loading into memory\n+    let map_count = map.iter().count();\n+    println!(\"  Counted {} map entries without loading into memory\", map_count);\n+    \n+    // Find specific items efficiently\n+    let target = 500;\n+    let found = map.iter()\n+        .find(|item| {\n+            item.as_ref()\n+                .map(|(k, _)| *k == target)\n+                .unwrap_or(false)\n+        });\n+    \n+    if let Some(Ok((k, v))) = found {\n+        println!(\"  Found key {} with value '{}' via streaming\", k, v);\n+    }\n+    \n+    // Process only what we need\n+    println!(\"\\n🎯 Processing first 10 items only:\");\n+    for (i, item) in vec.iter()?.take(10).enumerate() {\n+        match item {\n+            Ok(value) => println!(\"  [{}] {}\", i, value),\n+            Err(e) => println!(\"  [{}] Error: {:?}\", i, e),\n+        }\n+    }\n+    \n+    // Filter and process without loading all data\n+    println!(\"\\n🔍 Filtering even keys without loading all data:\");\n+    let even_count = map.keys()\n+        .filter(|item| {\n+            item.as_ref()\n+                .map(|k| k % 2 == 0)\n+                .unwrap_or(false)\n+        })\n+        .count();\n+    println!(\"  Found {} even keys\", even_count);\n+    \n+    // Compare with loading everything into memory\n+    println!(\"\\n⚠️  Loading all data into memory (less efficient for large collections):\");\n+    let all_values = map.values_vec()?;\n+    println!(\"  Loaded {} values into a Vec\", all_values.len());\n+    \n+    println!(\"\\n✅ Streaming iterators provide:\");\n+    println!(\"  • Constant memory usage regardless of collection size\");\n+    println!(\"  • Ability to process data larger than RAM\");\n+    println!(\"  • Early termination when finding specific items\");\n+    println!(\"  • Efficient filtering and transformation\");\n+    \n+    // Clean up\n+    drop(map);\n+    drop(vec);\n+    drop(db);\n+    std::fs::remove_dir_all(\"streaming_demo_db\").ok();\n+    \n+    Ok(())\n+} \n\\ No newline at end of file\ndiff --git a/durable/examples/vec_example.rs b/durable/examples/vec_example.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..07d394b5f3964ea1942a645c2f008e9e3c37d6d8\n--- /dev/null\n+++ b/durable/examples/vec_example.rs\n@@ -0,0 +1,66 @@\n+use durable::{Db, DurableVec};\n+use serde::{Serialize, Deserialize};\n+\n+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n+struct Task {\n+    id: u64,\n+    title: String,\n+    completed: bool,\n+}\n+\n+fn main() -> Result<(), Box> {\n+    // Open or create a database\n+    let db = Db::open(\"example_db\")?;\n+    \n+    // Create a persistent vector of tasks\n+    let mut tasks = DurableVec::::new(&db, \"tasks\")?;\n+    \n+    // Add some tasks\n+    tasks.push(Task {\n+        id: 1,\n+        title: \"Build Durable library\".to_string(),\n+        completed: true,\n+    })?;\n+    \n+    tasks.push(Task {\n+        id: 2,\n+        title: \"Write comprehensive tests\".to_string(),\n+        completed: true,\n+    })?;\n+    \n+    tasks.push(Task {\n+        id: 3,\n+        title: \"Create documentation\".to_string(),\n+        completed: false,\n+    })?;\n+    \n+    println!(\"Total tasks: {}\", tasks.len()?);\n+    \n+    // Iterate through all tasks\n+    println!(\"\\nAll tasks:\");\n+    for (i, task) in tasks.iter()?.enumerate() {\n+        let task = task?;\n+        println!(\"  [{}] {} - {}\", \n+            i, \n+            task.title, \n+            if task.completed { \"✓\" } else { \"○\" }\n+        );\n+    }\n+    \n+    // Get a specific task\n+    if let Some(task) = tasks.get(1)? {\n+        println!(\"\\nTask at index 1: {:?}\", task);\n+    }\n+    \n+    // Mark the last task as completed\n+    if let Some(mut last_task) = tasks.pop()? {\n+        println!(\"\\nCompleting task: {}\", last_task.title);\n+        last_task.completed = true;\n+        tasks.push(last_task)?;\n+    }\n+    \n+    // The data persists even after the program exits!\n+    println!(\"\\nData has been persisted to disk.\");\n+    \n+    Ok(())\n+} \n\\ No newline at end of file\ndiff --git a/durable/repomix.config.json b/durable/repomix.config.json\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..bb52e4fcd383e8937245bd62566db8b7652402c2\n--- /dev/null\n+++ b/durable/repomix.config.json\n@@ -0,0 +1,27 @@\n+{\n+  \"output\": {\n+    \"filePath\": \"all.txt\",\n+    \"style\": \"plain\",\n+    \"parsableStyle\": false,\n+    \"fileSummary\": true,\n+    \"directoryStructure\": true,\n+    \"removeComments\": false,\n+    \"removeEmptyLines\": false,\n+    \"compress\": false,\n+    \"topFilesLength\": 100,\n+    \"showLineNumbers\": false,\n+    \"copyToClipboard\": false\n+  },\n+  \"include\": [],\n+  \"ignore\": {\n+    \"useGitignore\": true,\n+    \"useDefaultPatterns\": true,\n+    \"customPatterns\": []\n+  },\n+  \"security\": {\n+    \"enableSecurityCheck\": true\n+  },\n+  \"tokenCount\": {\n+    \"encoding\": \"o200k_base\"\n+  }\n+}\ndiff --git a/durable/src/lib.rs b/durable/src/lib.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..94e8823be93be1176b0a4cdc9797181b9f8caf60\n--- /dev/null\n+++ b/durable/src/lib.rs\n@@ -0,0 +1,123 @@\n+//! Durable - RocksDB-backed persistent data structures for Rust\n+\n+use std::path::Path;\n+use std::sync::Arc;\n+use rocksdb::{DB as RocksDB, Options, WriteBatch};\n+use thiserror::Error;\n+\n+pub mod vec;\n+pub mod map;\n+pub use vec::DurableVec;\n+pub use map::DurableMap;\n+\n+/// Error types for Durable operations\n+#[derive(Error, Debug)]\n+pub enum DurableError {\n+    #[error(\"RocksDB error: {0}\")]\n+    RocksDB(#[from] rocksdb::Error),\n+    \n+    #[error(\"Serialization error: {0}\")]\n+    Serialization(#[from] bincode::Error),\n+    \n+    #[error(\"Key not found\")]\n+    KeyNotFound,\n+    \n+    #[error(\"Collection not found: {0}\")]\n+    CollectionNotFound(String),\n+    \n+    #[error(\"Data corruption: {0}\")]\n+    Corruption(String),\n+}\n+\n+pub type Result = std::result::Result;\n+\n+/// A trait for types that can be used as nested collections.\n+pub trait DurableCollection {\n+    /// Creates a new instance of the collection from a database handle\n+    /// and a pre-determined, unique key prefix.\n+    /// \n+    /// This is the key method that allows `DurableMap` to instantiate\n+    /// a nested collection handle.\n+    fn from_prefix(db: Db, prefix: Vec) -> Self;\n+}\n+\n+/// The main database handle\n+#[derive(Clone)]\n+pub struct Db {\n+    inner: Arc,\n+}\n+\n+impl Db {\n+    /// Opens or creates a durable database at the given path\n+    pub fn open>(path: P) -> Result {\n+        let mut opts = Options::default();\n+        opts.create_if_missing(true);\n+        opts.create_missing_column_families(true);\n+        \n+        let db = RocksDB::open(&opts, path)?;\n+        Ok(Db { \n+            inner: Arc::new(db),\n+        })\n+    }\n+    \n+    /// Create a new write batch for atomic operations\n+    pub fn batch(&self) -> Batch {\n+        Batch {\n+            db: self.clone(),\n+            inner: WriteBatch::default(),\n+        }\n+    }\n+    \n+    /// Get the underlying RocksDB handle (for advanced usage)\n+    pub(crate) fn rocks(&self) -> &RocksDB {\n+        &self.inner\n+    }\n+    \n+    /// Get a new unique collection ID for nested collections\n+    pub fn new_collection_id(&self) -> Result {\n+        let key = b\"__global_meta:next_collection_id\";\n+        \n+        // Get current value\n+        let current_bytes = self.rocks().get(key)?;\n+        let current_id = match current_bytes {\n+            Some(bytes) => {\n+                if bytes.len() != 8 {\n+                    return Err(DurableError::Corruption(\"Invalid collection ID bytes size\".into()));\n+                }\n+                let id_bytes: [u8; 8] = bytes[..8].try_into()\n+                    .map_err(|_| DurableError::Corruption(\"Invalid collection ID bytes\".into()))?;\n+                u64::from_le_bytes(id_bytes)\n+            }\n+            None => 0,\n+        };\n+        \n+        let next_id = current_id + 1;\n+        \n+        // Try to atomically update - use compare-and-swap semantics\n+        let mut batch = WriteBatch::default();\n+        batch.put(key, &next_id.to_le_bytes());\n+        \n+        // For now, just write it directly. In a real implementation,\n+        // we'd want proper compare-and-swap to handle concurrent access\n+        self.rocks().write(batch)?;\n+        self.rocks().flush_wal(true)?;\n+        \n+        Ok(current_id)\n+    }\n+}\n+\n+/// A write batch for atomic operations\n+pub struct Batch {\n+    db: Db,\n+    inner: WriteBatch,\n+}\n+\n+impl Batch {\n+    /// Commit all operations in this batch atomically\n+    pub fn commit(self) -> Result<()> {\n+        self.db.rocks().write(self.inner)?;\n+        self.db.rocks().flush_wal(true)?;\n+        Ok(())\n+    }\n+    \n+}\ndiff --git a/durable/src/map.rs b/durable/src/map.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..2629857270b1be346a20fc67dec1cfc57c829403\n--- /dev/null\n+++ b/durable/src/map.rs\n@@ -0,0 +1,1073 @@\n+use crate::{Db, Result, DurableError, DurableCollection};\n+use rocksdb::{IteratorMode, WriteBatch, Direction};\n+use serde::{Serialize, Deserialize};\n+use std::marker::PhantomData;\n+\n+/// A persistent map backed by RocksDB\n+pub struct DurableMap {\n+    db: Db,\n+    prefix: Vec,\n+    _phantom: PhantomData<(K, V)>,\n+}\n+\n+impl DurableMap \n+where \n+    K: Serialize + for<'de> Deserialize<'de>,\n+    V: Serialize + for<'de> Deserialize<'de>,\n+{\n+    /// Create a new DurableMap with the given name\n+    pub fn new(db: &Db, name: &str) -> Result {\n+        let prefix = format!(\"map:{}\", name).into_bytes();\n+        \n+        Ok(DurableMap {\n+            db: db.clone(),\n+            prefix,\n+            _phantom: PhantomData,\n+        })\n+    }\n+    \n+    /// Insert a key-value pair into the map\n+    pub fn insert(&mut self, key: K, value: V) -> Result> {\n+        let key_bytes = bincode::serialize(&key)?;\n+        let value_bytes = bincode::serialize(&value)?;\n+        \n+        // Get the old value if it exists\n+        let old_value = self.get(&key)?;\n+        \n+        let mut batch = WriteBatch::default();\n+        \n+        // Write the new value\n+        let db_key = self.entry_key(&key_bytes);\n+        batch.put(&db_key, &value_bytes);\n+        \n+        // Update length if this is a new key\n+        if old_value.is_none() {\n+            let new_len = self.len()? + 1;\n+            let len_key = self.meta_key(\"len\");\n+            batch.put(&len_key, &(new_len as u64).to_le_bytes());\n+        }\n+        \n+        // Commit atomically\n+        self.db.rocks().write(batch)?;\n+        self.db.rocks().flush_wal(true)?;\n+        \n+        Ok(old_value)\n+    }\n+    \n+    /// Put a key-value pair into the map without returning the old value\n+    /// \n+    /// This is more efficient than `insert` when you don't need the old value,\n+    /// as it only checks for key existence without deserializing the value.\n+    pub fn put(&mut self, key: K, value: V) -> Result<()> {\n+        let key_bytes = bincode::serialize(&key)?;\n+        let value_bytes = bincode::serialize(&value)?;\n+        let db_key = self.entry_key(&key_bytes);\n+        \n+        let mut batch = WriteBatch::default();\n+        \n+        // Check if this is a new key (without deserializing the value)\n+        let is_new = self.db.rocks().get_pinned(&db_key)?.is_none();\n+        \n+        // Write the new value\n+        batch.put(&db_key, &value_bytes);\n+        \n+        // Update length if this is a new key\n+        if is_new {\n+            let new_len = self.len()? + 1;\n+            let len_key = self.meta_key(\"len\");\n+            batch.put(&len_key, &(new_len as u64).to_le_bytes());\n+        }\n+        \n+        // Commit atomically\n+        self.db.rocks().write(batch)?;\n+        self.db.rocks().flush_wal(true)?;\n+        \n+        Ok(())\n+    }\n+    \n+    /// Get a value by key\n+    pub fn get(&self, key: &K) -> Result> {\n+        let key_bytes = bincode::serialize(key)?;\n+        let db_key = self.entry_key(&key_bytes);\n+        \n+        match self.db.rocks().get(&db_key)? {\n+            Some(bytes) => {\n+                let value = bincode::deserialize(&bytes)?;\n+                Ok(Some(value))\n+            }\n+            None => Ok(None),\n+        }\n+    }\n+    \n+    /// Check if a key exists in the map\n+    pub fn contains_key(&self, key: &K) -> Result {\n+        let key_bytes = bincode::serialize(key)?;\n+        let db_key = self.entry_key(&key_bytes);\n+        \n+        Ok(self.db.rocks().get(&db_key)?.is_some())\n+    }\n+    \n+    /// Remove a key-value pair from the map\n+    pub fn remove(&mut self, key: &K) -> Result> {\n+        let key_bytes = bincode::serialize(key)?;\n+        let db_key = self.entry_key(&key_bytes);\n+        \n+        // Get the old value\n+        let old_value = match self.db.rocks().get(&db_key)? {\n+            Some(bytes) => {\n+                let value = bincode::deserialize(&bytes)?;\n+                Some(value)\n+            }\n+            None => None,\n+        };\n+        \n+        // Delete the key if it existed and update length\n+        if old_value.is_some() {\n+            let mut batch = WriteBatch::default();\n+            \n+            // Delete the entry\n+            batch.delete(&db_key);\n+            \n+            // Update length\n+            let new_len = self.len()? - 1;\n+            let len_key = self.meta_key(\"len\");\n+            batch.put(&len_key, &(new_len as u64).to_le_bytes());\n+            \n+            // Commit atomically\n+            self.db.rocks().write(batch)?;\n+            self.db.rocks().flush_wal(true)?;\n+        }\n+        \n+        Ok(old_value)\n+    }\n+    \n+\n+    \n+    /// Clear all entries from the map\n+    pub fn clear(&mut self) -> Result<()> {\n+        let prefix = self.entry_prefix();\n+        let mut batch = WriteBatch::default();\n+        \n+        // Collect all keys to delete\n+        let iter = self.db.rocks().iterator(IteratorMode::From(&prefix, Direction::Forward));\n+        for item in iter {\n+            let (key, _) = item?;\n+            if !key.starts_with(&prefix) {\n+                break;\n+            }\n+            batch.delete(&key);\n+        }\n+        \n+        // Reset length to 0\n+        let len_key = self.meta_key(\"len\");\n+        batch.delete(&len_key);\n+        \n+        // Commit atomically\n+        self.db.rocks().write(batch)?;\n+        self.db.rocks().flush_wal(true)?;\n+        \n+        Ok(())\n+    }\n+    \n+    /// Iterate over all key-value pairs using a streaming iterator\n+    pub fn iter(&self) -> MapIterator<'_, K, V> {\n+        let prefix = self.entry_prefix();\n+        let iter = self.db.rocks().iterator(IteratorMode::From(&prefix, Direction::Forward));\n+        \n+        MapIterator {\n+            inner: iter,\n+            prefix,\n+            _phantom: PhantomData,\n+        }\n+    }\n+    \n+    /// Load all key-value pairs into a Vec\n+    /// \n+    /// Note: This loads the entire collection into memory. For large collections,\n+    /// prefer using `iter()` which streams elements.\n+    pub fn to_vec(&self) -> Result> {\n+        let mut result = Vec::new();\n+        for item in self.iter() {\n+            result.push(item?);\n+        }\n+        Ok(result)\n+    }\n+    \n+    /// Iterate over all keys using a streaming iterator\n+    pub fn keys(&self) -> KeyIterator<'_, K, V> {\n+        let prefix = self.entry_prefix();\n+        let iter = self.db.rocks().iterator(IteratorMode::From(&prefix, Direction::Forward));\n+        \n+        KeyIterator {\n+            inner: iter,\n+            prefix,\n+            _phantom: PhantomData,\n+        }\n+    }\n+    \n+    /// Load all keys into a Vec\n+    /// \n+    /// Note: This loads all keys into memory. For large collections,\n+    /// prefer using `keys()` which streams elements.\n+    pub fn keys_vec(&self) -> Result> {\n+        let mut result = Vec::new();\n+        for item in self.keys() {\n+            result.push(item?);\n+        }\n+        Ok(result)\n+    }\n+    \n+    /// Iterate over all values using a streaming iterator\n+    pub fn values(&self) -> ValueIterator<'_, K, V> {\n+        let prefix = self.entry_prefix();\n+        let iter = self.db.rocks().iterator(IteratorMode::From(&prefix, Direction::Forward));\n+        \n+        ValueIterator {\n+            inner: iter,\n+            prefix,\n+            _phantom: PhantomData,\n+        }\n+    }\n+    \n+    /// Load all values into a Vec\n+    /// \n+    /// Note: This loads all values into memory. For large collections,\n+    /// prefer using `values()` which streams elements.\n+    pub fn values_vec(&self) -> Result> {\n+        let mut result = Vec::new();\n+        for item in self.values() {\n+            result.push(item?);\n+        }\n+        Ok(result)\n+    }\n+    \n+    /// Insert multiple key-value pairs in a single batch\n+    pub fn extend(&mut self, iter: I) -> Result<()>\n+    where\n+        I: IntoIterator\n+    {\n+        let mut batch = WriteBatch::default();\n+        let current_len = self.len()?;\n+        let mut new_entries = 0;\n+        \n+        for (key, value) in iter {\n+            let key_bytes = bincode::serialize(&key)?;\n+            let value_bytes = bincode::serialize(&value)?;\n+            let db_key = self.entry_key(&key_bytes);\n+            \n+            // Check if this is a new key\n+            if !self.contains_key(&key)? {\n+                new_entries += 1;\n+            }\n+            \n+            batch.put(&db_key, &value_bytes);\n+        }\n+        \n+        // Update length if we added new entries\n+        if new_entries > 0 {\n+            let new_len = current_len + new_entries;\n+            let len_key = self.meta_key(\"len\");\n+            batch.put(&len_key, &(new_len as u64).to_le_bytes());\n+        }\n+        \n+        // Commit atomically\n+        self.db.rocks().write(batch)?;\n+        self.db.rocks().flush_wal(true)?;\n+        \n+        Ok(())\n+    }\n+    \n+\n+    \n+    // Helper methods\n+    \n+    fn entry_key(&self, key_bytes: &[u8]) -> Vec {\n+        let mut db_key = self.prefix.clone();\n+        db_key.extend_from_slice(b\":entry:\");\n+        db_key.extend_from_slice(key_bytes);\n+        db_key\n+    }\n+    \n+    fn entry_prefix(&self) -> Vec {\n+        let mut prefix = self.prefix.clone();\n+        prefix.extend_from_slice(b\":entry:\");\n+        prefix\n+    }\n+}\n+\n+// Additional implementation block for methods that don't require serialization constraints\n+impl DurableMap {\n+    /// Create a new DurableMap for nested collections (no serialization constraints)\n+    pub fn new_nested(db: &Db, name: &str) -> Self {\n+        let prefix = format!(\"map:{}\", name).into_bytes();\n+        \n+        DurableMap {\n+            db: db.clone(),\n+            prefix,\n+            _phantom: PhantomData,\n+        }\n+    }\n+    \n+    /// Create a new DurableMap from a prefix (used for nested collections)\n+    pub fn from_prefix(db: Db, prefix: Vec) -> Self {\n+        Self {\n+            db,\n+            prefix,\n+            _phantom: PhantomData,\n+        }\n+    }\n+    \n+    /// Get the number of entries in the map (unconstrained version for nested collections)\n+    pub fn len(&self) -> Result {\n+        let key = self.meta_key(\"len\");\n+        match self.db.rocks().get(&key)? {\n+            Some(bytes) => {\n+                if bytes.len() != 8 {\n+                    return Err(DurableError::Corruption(\"Invalid length bytes size\".into()));\n+                }\n+                let len_bytes: [u8; 8] = bytes[..8].try_into()\n+                    .map_err(|_| DurableError::Corruption(\"Invalid length bytes\".into()))?;\n+                Ok(u64::from_le_bytes(len_bytes) as usize)\n+            }\n+            None => Ok(0),\n+        }\n+    }\n+    \n+    /// Check if the map is empty (unconstrained version for nested collections)\n+    pub fn is_empty(&self) -> Result {\n+        Ok(self.len()? == 0)\n+    }\n+\n+    /// Get the entry key for nested collections (needed by entry API)\n+    pub(crate) fn make_entry_key(&self, key_bytes: &[u8]) -> Vec {\n+        let mut db_key = self.prefix.clone();\n+        db_key.extend_from_slice(b\":entry:\");\n+        db_key.extend_from_slice(key_bytes);\n+        db_key\n+    }\n+    \n+    fn meta_key(&self, meta_type: &str) -> Vec {\n+        let mut key = self.prefix.clone();\n+        key.extend_from_slice(b\":__meta:\");\n+        key.extend_from_slice(meta_type.as_bytes());\n+        key\n+    }\n+}\n+\n+// Implement the DurableCollection trait for DurableMap\n+// This implementation is used for nested collections and doesn't require\n+// serialization bounds since nested maps use collection markers, not direct serialization\n+impl DurableCollection for DurableMap {\n+    fn from_prefix(db: Db, prefix: Vec) -> Self {\n+        DurableMap::from_prefix(db, prefix)\n+    }\n+}\n+\n+/// Entry API for DurableMap with nested collections\n+pub enum DurableEntry<'a, K, V> {\n+    Occupied(OccupiedEntry<'a, K, V>),\n+    Vacant(VacantEntry<'a, K, V>),\n+}\n+\n+impl<'a, K, V> DurableEntry<'a, K, V>\n+where\n+    K: Serialize,\n+    V: DurableCollection,\n+{\n+    /// Gets the collection, creating it if it doesn't exist\n+    pub fn or_default(self) -> Result {\n+        match self {\n+            DurableEntry::Occupied(entry) => entry.or_default(),\n+            DurableEntry::Vacant(entry) => entry.or_default(),\n+        }\n+    }\n+}\n+\n+/// Represents an entry that already exists\n+pub struct OccupiedEntry<'a, K, V> {\n+    map: &'a DurableMap,\n+    key_bytes: Vec,\n+    value_marker: Vec, // The bytes read from RocksDB, e.g., [0x02, ...]\n+}\n+\n+impl<'a, K, V> OccupiedEntry<'a, K, V> \n+where\n+    V: DurableCollection,\n+{\n+    /// Gets a handle to the existing nested collection\n+    pub fn get(self) -> Result {\n+        // 1. Parse the collection_id from self.value_marker\n+        if self.value_marker.len() != 9 || self.value_marker[0] != 0x02 {\n+            return Err(DurableError::Corruption(\"Invalid collection marker\".into()));\n+        }\n+        \n+        let col_id_bytes: [u8; 8] = self.value_marker[1..9].try_into()\n+            .map_err(|_| DurableError::Corruption(\"Invalid collection ID\".into()))?;\n+        let col_id = u64::from_le_bytes(col_id_bytes);\n+\n+        // 2. Re-construct the unique prefix for the child collection\n+        let parent_key_prefix = self.map.make_entry_key(&self.key_bytes);\n+        let child_prefix = [parent_key_prefix.as_slice(), &[0x00], &col_id.to_le_bytes()].concat();\n+\n+        // 3. Create the collection handle using the trait method\n+        Ok(V::from_prefix(self.map.db.clone(), child_prefix))\n+    }\n+    \n+    /// Gets a handle to the existing nested collection (same as get but consumes self)\n+    pub fn or_default(self) -> Result {\n+        self.get()\n+    }\n+}\n+\n+/// Represents a slot that is empty\n+pub struct VacantEntry<'a, K, V> {\n+    map: &'a DurableMap,\n+    key: K, // The original key from the user\n+}\n+\n+impl<'a, K, V> VacantEntry<'a, K, V> \n+where\n+    K: Serialize,\n+    V: DurableCollection,\n+{\n+    /// Inserts a new default collection and returns a handle to it\n+    pub fn or_default(self) -> Result {\n+        // 1. Atomically get a new unique ID for the collection\n+        let new_col_id = self.map.db.new_collection_id()?;\n+\n+        // 2. Create the marker value that points to our new collection\n+        let mut value_marker = vec![0x02_u8];\n+        value_marker.extend_from_slice(&new_col_id.to_le_bytes());\n+\n+        // 3. Get the key bytes and construct the full parent entry key\n+        let key_bytes = bincode::serialize(&self.key)?;\n+        let parent_db_key = self.map.make_entry_key(&key_bytes);\n+\n+        // 4. ATOMICALLY write the marker to the parent map\n+        self.map.db.rocks().put(&parent_db_key, &value_marker)?;\n+        self.map.db.rocks().flush_wal(true)?;\n+\n+        // 5. Construct the unique prefix for our new child collection\n+        let child_prefix = [parent_db_key.as_slice(), &[0x00], &new_col_id.to_le_bytes()].concat();\n+\n+        // 6. Create and return the new collection handle\n+        Ok(V::from_prefix(self.map.db.clone(), child_prefix))\n+    }\n+}\n+\n+// API for nested collections\n+impl DurableMap {\n+    /// The entry point for creating or accessing a nested collection.\n+    /// This method is only available when `V` is a `DurableCollection`.\n+    pub fn entry(&self, key: K) -> Result>\n+    where\n+        V: DurableCollection,\n+        K: Serialize,\n+    {\n+        let key_bytes = bincode::serialize(&key)?;\n+        let db_key = self.make_entry_key(&key_bytes);\n+\n+        match self.db.rocks().get(&db_key)? {\n+            Some(value_marker) => {\n+                // Key exists. The value should be a collection marker.\n+                Ok(DurableEntry::Occupied(OccupiedEntry {\n+                    map: self,\n+                    key_bytes,\n+                    value_marker,\n+                }))\n+            }\n+            None => {\n+                // Key doesn't exist.\n+                Ok(DurableEntry::Vacant(VacantEntry { map: self, key }))\n+            }\n+        }\n+    }\n+}\n+\n+/// Iterator over key-value pairs in a DurableMap\n+pub struct MapIterator<'a, K, V> {\n+    inner: rocksdb::DBIterator<'a>,\n+    prefix: Vec,\n+    _phantom: PhantomData<(K, V)>,\n+}\n+\n+impl<'a, K, V> Iterator for MapIterator<'a, K, V>\n+where\n+    K: for<'de> Deserialize<'de>,\n+    V: for<'de> Deserialize<'de>,\n+{\n+    type Item = Result<(K, V)>;\n+    \n+    fn next(&mut self) -> Option {\n+        match self.inner.next() {\n+            Some(Ok((db_key, value_bytes))) => {\n+                // Check if we're still within our prefix\n+                if !db_key.starts_with(&self.prefix) {\n+                    return None;\n+                }\n+                \n+                // Extract the key part (skip prefix)\n+                let key_start = self.prefix.len();\n+                let key_bytes = &db_key[key_start..];\n+                \n+                // Deserialize key and value\n+                match (bincode::deserialize(key_bytes), bincode::deserialize(&value_bytes)) {\n+                    (Ok(key), Ok(value)) => Some(Ok((key, value))),\n+                    (Err(e), _) | (_, Err(e)) => Some(Err(e.into())),\n+                }\n+            }\n+            Some(Err(e)) => Some(Err(e.into())),\n+            None => None,\n+        }\n+    }\n+}\n+\n+/// Iterator over keys in a DurableMap\n+pub struct KeyIterator<'a, K, V> {\n+    inner: rocksdb::DBIterator<'a>,\n+    prefix: Vec,\n+    _phantom: PhantomData<(K, V)>,\n+}\n+\n+impl<'a, K, V> Iterator for KeyIterator<'a, K, V>\n+where\n+    K: for<'de> Deserialize<'de>,\n+{\n+    type Item = Result;\n+    \n+    fn next(&mut self) -> Option {\n+        match self.inner.next() {\n+            Some(Ok((db_key, _))) => {\n+                // Check if we're still within our prefix\n+                if !db_key.starts_with(&self.prefix) {\n+                    return None;\n+                }\n+                \n+                // Extract the key part (skip prefix)\n+                let key_start = self.prefix.len();\n+                let key_bytes = &db_key[key_start..];\n+                \n+                // Deserialize key\n+                match bincode::deserialize(key_bytes) {\n+                    Ok(key) => Some(Ok(key)),\n+                    Err(e) => Some(Err(e.into())),\n+                }\n+            }\n+            Some(Err(e)) => Some(Err(e.into())),\n+            None => None,\n+        }\n+    }\n+}\n+\n+/// Iterator over values in a DurableMap\n+pub struct ValueIterator<'a, K, V> {\n+    inner: rocksdb::DBIterator<'a>,\n+    prefix: Vec,\n+    _phantom: PhantomData<(K, V)>,\n+}\n+\n+impl<'a, K, V> Iterator for ValueIterator<'a, K, V>\n+where\n+    V: for<'de> Deserialize<'de>,\n+{\n+    type Item = Result;\n+    \n+    fn next(&mut self) -> Option {\n+        match self.inner.next() {\n+            Some(Ok((db_key, value_bytes))) => {\n+                // Check if we're still within our prefix\n+                if !db_key.starts_with(&self.prefix) {\n+                    return None;\n+                }\n+                \n+                // Deserialize value\n+                match bincode::deserialize(&value_bytes) {\n+                    Ok(value) => Some(Ok(value)),\n+                    Err(e) => Some(Err(e.into())),\n+                }\n+            }\n+            Some(Err(e)) => Some(Err(e.into())),\n+            None => None,\n+        }\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+    use tempfile::TempDir;\n+    use std::collections::HashMap;\n+    \n+    fn setup_test_db() -> (TempDir, Db) {\n+        let temp_dir = TempDir::new().unwrap();\n+        let db = Db::open(temp_dir.path()).unwrap();\n+        (temp_dir, db)\n+    }\n+    \n+    #[test]\n+    fn test_insert_and_get() {\n+        let (_temp, db) = setup_test_db();\n+        let mut map = DurableMap::::new(&db, \"test_map\").unwrap();\n+        \n+        // Insert some values\n+        assert_eq!(map.insert(\"one\".to_string(), 1).unwrap(), None);\n+        assert_eq!(map.insert(\"two\".to_string(), 2).unwrap(), None);\n+        assert_eq!(map.insert(\"three\".to_string(), 3).unwrap(), None);\n+        \n+        // Get values\n+        assert_eq!(map.get(&\"one\".to_string()).unwrap(), Some(1));\n+        assert_eq!(map.get(&\"two\".to_string()).unwrap(), Some(2));\n+        assert_eq!(map.get(&\"three\".to_string()).unwrap(), Some(3));\n+        assert_eq!(map.get(&\"four\".to_string()).unwrap(), None);\n+        \n+        // Update existing value\n+        assert_eq!(map.insert(\"two\".to_string(), 22).unwrap(), Some(2));\n+        assert_eq!(map.get(&\"two\".to_string()).unwrap(), Some(22));\n+    }\n+    \n+    #[test]\n+    fn test_remove() {\n+        let (_temp, db) = setup_test_db();\n+        let mut map = DurableMap::::new(&db, \"remove_map\").unwrap();\n+        \n+        // Insert and remove\n+        map.insert(\"key\".to_string(), \"value\".to_string()).unwrap();\n+        assert_eq!(map.remove(&\"key\".to_string()).unwrap(), Some(\"value\".to_string()));\n+        assert_eq!(map.remove(&\"key\".to_string()).unwrap(), None);\n+        assert_eq!(map.get(&\"key\".to_string()).unwrap(), None);\n+    }\n+    \n+    #[test]\n+    fn test_contains_key() {\n+        let (_temp, db) = setup_test_db();\n+        let mut map = DurableMap::::new(&db, \"contains_map\").unwrap();\n+        \n+        map.insert(42, \"answer\".to_string()).unwrap();\n+        \n+        assert!(map.contains_key(&42).unwrap());\n+        assert!(!map.contains_key(&43).unwrap());\n+    }\n+    \n+    #[test]\n+    fn test_len_and_clear() {\n+        let (_temp, db) = setup_test_db();\n+        let mut map = DurableMap::::new(&db, \"len_map\").unwrap();\n+        \n+        // Empty map\n+        assert_eq!(map.len().unwrap(), 0);\n+        assert!(map.is_empty().unwrap());\n+        \n+        // Add items\n+        for i in 0..10 {\n+            map.insert(i, i * 2).unwrap();\n+        }\n+        assert_eq!(map.len().unwrap(), 10);\n+        assert!(!map.is_empty().unwrap());\n+        \n+        // Clear\n+        map.clear().unwrap();\n+        assert_eq!(map.len().unwrap(), 0);\n+        assert!(map.is_empty().unwrap());\n+    }\n+    \n+    #[test]\n+    fn test_persistence() {\n+        let (temp_dir, db) = setup_test_db();\n+        \n+        // Create and populate map\n+        {\n+            let mut map = DurableMap::>::new(&db, \"persist_map\").unwrap();\n+            map.insert(\"binary\".to_string(), vec![1, 2, 3, 4, 5]).unwrap();\n+            map.insert(\"data\".to_string(), vec![10, 20, 30]).unwrap();\n+        }\n+        \n+        // Drop the database\n+        drop(db);\n+        \n+        // Reopen and verify data persists\n+        {\n+            let db = Db::open(temp_dir.path()).unwrap();\n+            let map = DurableMap::>::new(&db, \"persist_map\").unwrap();\n+            \n+            assert_eq!(map.get(&\"binary\".to_string()).unwrap(), Some(vec![1, 2, 3, 4, 5]));\n+            assert_eq!(map.get(&\"data\".to_string()).unwrap(), Some(vec![10, 20, 30]));\n+            assert_eq!(map.len().unwrap(), 2);\n+        }\n+    }\n+    \n+    #[test]\n+    fn test_iteration() {\n+        let (_temp, db) = setup_test_db();\n+        let mut map = DurableMap::::new(&db, \"iter_map\").unwrap();\n+        \n+        // Insert data\n+        let data = vec![\n+            (\"apple\".to_string(), 1),\n+            (\"banana\".to_string(), 2),\n+            (\"cherry\".to_string(), 3),\n+        ];\n+        \n+        for (k, v) in &data {\n+            map.insert(k.clone(), *v).unwrap();\n+        }\n+        \n+        // Test iter()\n+        let mut items = map.to_vec().unwrap();\n+        items.sort_by_key(|(k, _)| k.clone());\n+        assert_eq!(items, data);\n+        \n+        // Test keys()\n+        let mut keys = map.keys_vec().unwrap();\n+        keys.sort();\n+        assert_eq!(keys, vec![\"apple\", \"banana\", \"cherry\"]);\n+        \n+        // Test values()\n+        let mut values = map.values_vec().unwrap();\n+        values.sort();\n+        assert_eq!(values, vec![1, 2, 3]);\n+    }\n+    \n+    #[test]\n+    fn test_extend() {\n+        let (_temp, db) = setup_test_db();\n+        let mut map = DurableMap::::new(&db, \"extend_map\").unwrap();\n+        \n+        // Extend from iterator\n+        let data: HashMap = vec![\n+            (1, \"one\".to_string()),\n+            (2, \"two\".to_string()),\n+            (3, \"three\".to_string()),\n+        ].into_iter().collect();\n+        \n+        map.extend(data.clone()).unwrap();\n+        \n+        // Verify all items were inserted\n+        for (k, v) in data {\n+            assert_eq!(map.get(&k).unwrap(), Some(v));\n+        }\n+        assert_eq!(map.len().unwrap(), 3);\n+    }\n+    \n+    #[test]\n+    fn test_complex_keys() {\n+        use serde::{Serialize, Deserialize};\n+        \n+        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]\n+        struct ComplexKey {\n+            id: u64,\n+            name: String,\n+        }\n+        \n+        let (_temp, db) = setup_test_db();\n+        let mut map = DurableMap::::new(&db, \"complex_map\").unwrap();\n+        \n+        let key1 = ComplexKey { id: 1, name: \"first\".to_string() };\n+        let key2 = ComplexKey { id: 2, name: \"second\".to_string() };\n+        \n+        map.insert(key1.clone(), \"value1\".to_string()).unwrap();\n+        map.insert(key2.clone(), \"value2\".to_string()).unwrap();\n+        \n+        assert_eq!(map.get(&key1).unwrap(), Some(\"value1\".to_string()));\n+        assert_eq!(map.get(&key2).unwrap(), Some(\"value2\".to_string()));\n+    }\n+    \n+    #[test]\n+    fn test_multiple_maps_same_db() {\n+        let (_temp, db) = setup_test_db();\n+        \n+        let mut map1 = DurableMap::::new(&db, \"map1\").unwrap();\n+        let mut map2 = DurableMap::::new(&db, \"map2\").unwrap();\n+        \n+        // Insert different data\n+        map1.insert(\"shared_key\".to_string(), 100).unwrap();\n+        map2.insert(\"shared_key\".to_string(), 200).unwrap();\n+        \n+        // Verify isolation\n+        assert_eq!(map1.get(&\"shared_key\".to_string()).unwrap(), Some(100));\n+        assert_eq!(map2.get(&\"shared_key\".to_string()).unwrap(), Some(200));\n+    }\n+    \n+    #[test]\n+    fn test_streaming_iterators() {\n+        let (_temp, db) = setup_test_db();\n+        let mut map = DurableMap::::new(&db, \"stream_map\").unwrap();\n+        \n+        // Insert test data\n+        let data = vec![\n+            (\"alice\".to_string(), 100),\n+            (\"bob\".to_string(), 200),\n+            (\"charlie\".to_string(), 300),\n+        ];\n+        \n+        for (k, v) in &data {\n+            map.insert(k.clone(), *v).unwrap();\n+        }\n+        \n+        // Test streaming iteration\n+        let mut collected = Vec::new();\n+        for item in map.iter() {\n+            let (k, v) = item.unwrap();\n+            collected.push((k, v));\n+        }\n+        collected.sort_by_key(|(k, _)| k.clone());\n+        assert_eq!(collected, data);\n+        \n+        // Test keys iterator\n+        let mut keys = Vec::new();\n+        for key in map.keys() {\n+            keys.push(key.unwrap());\n+        }\n+        keys.sort();\n+        assert_eq!(keys, vec![\"alice\", \"bob\", \"charlie\"]);\n+        \n+        // Test values iterator\n+        let mut values = Vec::new();\n+        for value in map.values() {\n+            values.push(value.unwrap());\n+        }\n+        values.sort();\n+        assert_eq!(values, vec![100, 200, 300]);\n+        \n+        // Test that iterators properly handle prefix boundaries\n+        let mut map2 = DurableMap::::new(&db, \"stream_map2\").unwrap();\n+        map2.insert(\"dave\".to_string(), 400).unwrap();\n+        \n+        // Each iterator should only see its own data\n+        let collected1: Vec<_> = map.iter().map(Result::unwrap).collect();\n+        let collected2: Vec<_> = map2.iter().map(Result::unwrap).collect();\n+        \n+        assert_eq!(collected1.len(), 3);\n+        assert_eq!(collected2.len(), 1);\n+        assert_eq!(collected2[0], (\"dave\".to_string(), 400));\n+    }\n+    \n+    #[test]\n+    fn test_metadata_length_tracking() {\n+        let (_temp, db) = setup_test_db();\n+        let mut map = DurableMap::::new(&db, \"length_map\").unwrap();\n+        \n+        // Empty map\n+        assert_eq!(map.len().unwrap(), 0);\n+        assert!(map.is_empty().unwrap());\n+        \n+        // Insert operations should update length\n+        map.insert(\"key1\".to_string(), \"value1\".to_string()).unwrap();\n+        assert_eq!(map.len().unwrap(), 1);\n+        \n+        map.insert(\"key2\".to_string(), \"value2\".to_string()).unwrap();\n+        assert_eq!(map.len().unwrap(), 2);\n+        \n+        // Updating existing key should not change length\n+        map.insert(\"key1\".to_string(), \"new_value1\".to_string()).unwrap();\n+        assert_eq!(map.len().unwrap(), 2);\n+        \n+        // Remove operations should update length\n+        map.remove(&\"key1\".to_string()).unwrap();\n+        assert_eq!(map.len().unwrap(), 1);\n+        \n+        // Removing non-existent key should not change length\n+        map.remove(&\"non_existent\".to_string()).unwrap();\n+        assert_eq!(map.len().unwrap(), 1);\n+        \n+        // Extend should update length correctly\n+        let data = vec![\n+            (\"key3\".to_string(), \"value3\".to_string()),\n+            (\"key4\".to_string(), \"value4\".to_string()),\n+            (\"key5\".to_string(), \"value5\".to_string()),\n+        ];\n+        map.extend(data).unwrap();\n+        assert_eq!(map.len().unwrap(), 4); // key2 + 3 new keys\n+        \n+        // Extend with existing keys should only count new ones\n+        let mixed_data = vec![\n+            (\"key2\".to_string(), \"updated_value2\".to_string()), // existing\n+            (\"key6\".to_string(), \"value6\".to_string()),         // new\n+        ];\n+        map.extend(mixed_data).unwrap();\n+        assert_eq!(map.len().unwrap(), 5); // only key6 was new\n+        \n+        // Clear should reset length to 0\n+        map.clear().unwrap();\n+        assert_eq!(map.len().unwrap(), 0);\n+        assert!(map.is_empty().unwrap());\n+    }\n+    \n+    #[test]\n+    fn test_put_method() {\n+        let (_temp, db) = setup_test_db();\n+        let mut map = DurableMap::::new(&db, \"put_map\").unwrap();\n+        \n+        // Put new entries\n+        map.put(\"a\".to_string(), 1).unwrap();\n+        map.put(\"b\".to_string(), 2).unwrap();\n+        map.put(\"c\".to_string(), 3).unwrap();\n+        \n+        // Verify entries exist and length is correct\n+        assert_eq!(map.get(&\"a\".to_string()).unwrap(), Some(1));\n+        assert_eq!(map.get(&\"b\".to_string()).unwrap(), Some(2));\n+        assert_eq!(map.get(&\"c\".to_string()).unwrap(), Some(3));\n+        assert_eq!(map.len().unwrap(), 3);\n+        \n+        // Update existing entry with put\n+        map.put(\"b\".to_string(), 20).unwrap();\n+        assert_eq!(map.get(&\"b\".to_string()).unwrap(), Some(20));\n+        assert_eq!(map.len().unwrap(), 3); // Length should not change\n+        \n+        // Compare put vs insert performance characteristics\n+        // put() doesn't return old value but is more efficient\n+        map.put(\"d\".to_string(), 4).unwrap();\n+        assert_eq!(map.len().unwrap(), 4);\n+        \n+        // insert() returns old value\n+        let old = map.insert(\"d\".to_string(), 40).unwrap();\n+        assert_eq!(old, Some(4));\n+        assert_eq!(map.len().unwrap(), 4);\n+    }\n+}\n+\n+#[cfg(all(test, not(miri)))]\n+mod proptests {\n+    use super::*;\n+    use proptest::prelude::*;\n+    use tempfile::TempDir;\n+    use std::collections::HashMap;\n+    \n+    fn setup_test_db() -> (TempDir, Db) {\n+        let temp_dir = TempDir::new().unwrap();\n+        let db = Db::open(temp_dir.path()).unwrap();\n+        (temp_dir, db)\n+    }\n+    \n+    proptest! {\n+        #[test]\n+        fn prop_insert_get_consistency(data: HashMap) {\n+            let (_temp, db) = setup_test_db();\n+            let mut map = DurableMap::::new(&db, \"prop_map\").unwrap();\n+            \n+            // Insert all pairs\n+            for (k, v) in &data {\n+                map.insert(k.clone(), *v).unwrap();\n+            }\n+            \n+            // Verify all can be retrieved\n+            for (k, v) in &data {\n+                prop_assert_eq!(map.get(k).unwrap(), Some(*v));\n+            }\n+            \n+            // Verify length\n+            prop_assert_eq!(map.len().unwrap(), data.len());\n+        }\n+        \n+        #[test]\n+        fn prop_remove_consistency(data: HashMap) {\n+            let (_temp, db) = setup_test_db();\n+            let mut map = DurableMap::::new(&db, \"remove_map\").unwrap();\n+            \n+            // Insert all\n+            map.extend(data.clone()).unwrap();\n+            \n+            // Remove all and verify\n+            for (k, v) in data {\n+                prop_assert_eq!(map.remove(&k).unwrap(), Some(v));\n+                prop_assert_eq!(map.remove(&k).unwrap(), None);\n+                prop_assert!(!map.contains_key(&k).unwrap());\n+            }\n+            \n+            prop_assert!(map.is_empty().unwrap());\n+        }\n+        \n+        #[test]\n+        fn prop_clear_makes_empty(data: HashMap) {\n+            let (_temp, db) = setup_test_db();\n+            let mut map = DurableMap::::new(&db, \"clear_map\").unwrap();\n+            \n+            map.extend(data).unwrap();\n+            map.clear().unwrap();\n+            \n+            prop_assert_eq!(map.len().unwrap(), 0);\n+            prop_assert!(map.is_empty().unwrap());\n+            prop_assert_eq!(map.to_vec().unwrap(), vec![]);\n+        }\n+    }\n+    \n+    #[test]\n+    fn test_nested_collections() {\n+        use crate::DurableVec;\n+        \n+        let (_temp, db) = setup_test_db();\n+        \n+        // Create a map where values are DurableVec\n+        let users_posts: DurableMap> = DurableMap::new_nested(&db, \"user_posts\");\n+        \n+        // Test creating nested collections through the entry API\n+        let mut alice_posts = users_posts.entry(\"alice\".to_string()).unwrap().or_default().unwrap();\n+        alice_posts.push(101).unwrap();\n+        alice_posts.push(102).unwrap();\n+        alice_posts.push(103).unwrap();\n+        \n+        // Test accessing the same collection again\n+        let alice_posts_again = users_posts.entry(\"alice\".to_string()).unwrap().or_default().unwrap();\n+        assert_eq!(alice_posts_again.len().unwrap(), 3);\n+        assert_eq!(alice_posts_again.get(0).unwrap(), Some(101));\n+        assert_eq!(alice_posts_again.get(1).unwrap(), Some(102));\n+        assert_eq!(alice_posts_again.get(2).unwrap(), Some(103));\n+        \n+        // Test creating a different nested collection\n+        let mut bob_posts = users_posts.entry(\"bob\".to_string()).unwrap().or_default().unwrap();\n+        bob_posts.push(201).unwrap();\n+        bob_posts.push(202).unwrap();\n+        \n+        // Verify isolation between nested collections\n+        assert_eq!(alice_posts_again.len().unwrap(), 3);\n+        assert_eq!(bob_posts.len().unwrap(), 2);\n+        \n+        // Test chained calls\n+        users_posts.entry(\"charlie\".to_string()).unwrap().or_default().unwrap().push(301).unwrap();\n+        let charlie_posts = users_posts.entry(\"charlie\".to_string()).unwrap().or_default().unwrap();\n+        assert_eq!(charlie_posts.len().unwrap(), 1);\n+        assert_eq!(charlie_posts.get(0).unwrap(), Some(301));\n+    }\n+    \n+    // Note: Nested DurableMap-in-DurableMap requires implementing Serialize/Deserialize \n+    // for DurableMap, which is not straightforward since it contains database handles.\n+    // For now, let's focus on the more common case of Map-to-Vec nesting.\n+    \n+    // Deep nesting with Map -> Map -> Vec also requires DurableMap serialization\n+    // Let's skip this for now and focus on the fundamental Map -> Vec case\n+    \n+    #[test]\n+    fn test_nested_collection_persistence() {\n+        use crate::DurableVec;\n+        \n+        let (temp_dir, db) = setup_test_db();\n+        \n+        // Create nested structure and populate it\n+        {\n+            let users_data: DurableMap> = DurableMap::new_nested(&db, \"users\");\n+            let mut user1_data = users_data.entry(\"user1\".to_string()).unwrap().or_default().unwrap();\n+            user1_data.push(\"data1\".to_string()).unwrap();\n+            user1_data.push(\"data2\".to_string()).unwrap();\n+            \n+            let mut user2_data = users_data.entry(\"user2\".to_string()).unwrap().or_default().unwrap();\n+            user2_data.push(\"other_data\".to_string()).unwrap();\n+        }\n+        \n+        // Drop the database\n+        drop(db);\n+        \n+        // Reopen and verify persistence\n+        {\n+            let db = Db::open(temp_dir.path()).unwrap();\n+            let users_data: DurableMap> = DurableMap::new_nested(&db, \"users\");\n+            \n+            let user1_data = users_data.entry(\"user1\".to_string()).unwrap().or_default().unwrap();\n+            assert_eq!(user1_data.len().unwrap(), 2);\n+            assert_eq!(user1_data.get(0).unwrap(), Some(\"data1\".to_string()));\n+            assert_eq!(user1_data.get(1).unwrap(), Some(\"data2\".to_string()));\n+            \n+            let user2_data = users_data.entry(\"user2\".to_string()).unwrap().or_default().unwrap();\n+            assert_eq!(user2_data.len().unwrap(), 1);\n+            assert_eq!(user2_data.get(0).unwrap(), Some(\"other_data\".to_string()));\n+        }\n+    }\n+} \n\\ No newline at end of file\ndiff --git a/durable/src/vec.rs b/durable/src/vec.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..28d08fd0786df241aaf9c01b279708e547e4c034\n--- /dev/null\n+++ b/durable/src/vec.rs\n@@ -0,0 +1,658 @@\n+use crate::{Db, Result, DurableError, DurableCollection};\n+use rocksdb::WriteBatch;\n+use serde::{Serialize, Deserialize};\n+use std::marker::PhantomData;\n+\n+/// A persistent vector backed by RocksDB\n+pub struct DurableVec {\n+    db: Db,\n+    prefix: Vec,\n+    _phantom: PhantomData,\n+}\n+\n+impl DurableVec \n+where \n+    T: Serialize + for<'de> Deserialize<'de>\n+{\n+    /// Create a new DurableVec with the given name\n+    pub fn new(db: &Db, name: &str) -> Result {\n+        let prefix = format!(\"vec:{}\", name).into_bytes();\n+        \n+        Ok(DurableVec {\n+            db: db.clone(),\n+            prefix,\n+            _phantom: PhantomData,\n+        })\n+    }\n+    \n+    /// Create a new DurableVec from a prefix (used for nested collections)\n+    pub fn from_prefix(db: Db, prefix: Vec) -> Self {\n+        Self {\n+            db,\n+            prefix,\n+            _phantom: PhantomData,\n+        }\n+    }\n+    \n+    /// Get the length of the vector\n+    pub fn len(&self) -> Result {\n+        let key = self.meta_key(\"len\");\n+        match self.db.rocks().get(&key)? {\n+            Some(bytes) => {\n+                if bytes.len() != 8 {\n+                    return Err(DurableError::Corruption(\"Invalid length bytes size\".into()));\n+                }\n+                let len_bytes: [u8; 8] = bytes[..8].try_into()\n+                    .map_err(|_| DurableError::Corruption(\"Invalid length bytes\".into()))?;\n+                Ok(u64::from_le_bytes(len_bytes) as usize)\n+            }\n+            None => Ok(0),\n+        }\n+    }\n+    \n+    /// Check if the vector is empty\n+    pub fn is_empty(&self) -> Result {\n+        Ok(self.len()? == 0)\n+    }\n+    \n+    /// Push an element to the end of the vector\n+    pub fn push(&mut self, value: T) -> Result<()> {\n+        let len = self.len()?;\n+        let mut batch = WriteBatch::default();\n+        \n+        // Serialize the value\n+        let value_bytes = bincode::serialize(&value)?;\n+        \n+        // Write the element\n+        let elem_key = self.element_key(len);\n+        batch.put(&elem_key, &value_bytes);\n+        \n+        // Update the length\n+        let new_len = (len + 1) as u64;\n+        let len_key = self.meta_key(\"len\");\n+        batch.put(&len_key, &new_len.to_le_bytes());\n+        \n+        // Commit atomically\n+        self.db.rocks().write(batch)?;\n+        self.db.rocks().flush_wal(true)?;\n+        \n+        Ok(())\n+    }\n+    \n+    /// Get an element at the given index\n+    pub fn get(&self, index: usize) -> Result> {\n+        let len = self.len()?;\n+        if index >= len {\n+            return Ok(None);\n+        }\n+        \n+        let key = self.element_key(index);\n+        match self.db.rocks().get(&key)? {\n+            Some(bytes) => {\n+                let value = bincode::deserialize(&bytes)?;\n+                Ok(Some(value))\n+            }\n+            None => Err(DurableError::Corruption(\n+                format!(\"Element at index {} not found but index < len\", index)\n+            )),\n+        }\n+    }\n+    \n+    /// Clear all elements from the vector\n+    pub fn clear(&mut self) -> Result<()> {\n+        let len = self.len()?;\n+        let mut batch = WriteBatch::default();\n+        \n+        // Delete all elements\n+        for i in 0..len {\n+            let key = self.element_key(i);\n+            batch.delete(&key);\n+        }\n+        \n+        // Delete the length meta key\n+        let len_key = self.meta_key(\"len\");\n+        batch.delete(&len_key);\n+        \n+        // Commit atomically\n+        self.db.rocks().write(batch)?;\n+        self.db.rocks().flush_wal(true)?;\n+        \n+        Ok(())\n+    }\n+    \n+    /// Create a streaming iterator over the vector\n+    pub fn iter(&self) -> Result> + '_> {\n+        let prefix = self.element_prefix();\n+        let iter = self.db.rocks().iterator(rocksdb::IteratorMode::From(&prefix, rocksdb::Direction::Forward));\n+        \n+        Ok(VecIterator {\n+            inner: iter,\n+            prefix,\n+            _phantom: PhantomData,\n+        })\n+    }\n+    \n+    /// Convert the entire vector to a Vec in memory\n+    /// \n+    /// Note: This loads the entire collection into memory. For large collections,\n+    /// prefer using `iter()` which streams elements.\n+    pub fn to_vec(&self) -> Result> {\n+        let len = self.len()?;\n+        let mut result = Vec::with_capacity(len);\n+        \n+        for item in self.iter()? {\n+            result.push(item?);\n+        }\n+        \n+        Ok(result)\n+    }\n+    \n+    /// Push multiple elements in a single batch\n+    pub fn extend(&mut self, iter: I) -> Result<()>\n+    where\n+        I: IntoIterator\n+    {\n+        let mut batch = WriteBatch::default();\n+        let mut len = self.len()?;\n+        \n+        for value in iter {\n+            let value_bytes = bincode::serialize(&value)?;\n+            let elem_key = self.element_key(len);\n+            batch.put(&elem_key, &value_bytes);\n+            len += 1;\n+        }\n+        \n+        // Update length\n+        let len_key = self.meta_key(\"len\");\n+        batch.put(&len_key, &(len as u64).to_le_bytes());\n+        \n+        // Commit atomically\n+        self.db.rocks().write(batch)?;\n+        self.db.rocks().flush_wal(true)?;\n+        \n+        Ok(())\n+    }\n+    \n+    /// Remove and return the last element\n+    pub fn pop(&mut self) -> Result> {\n+        let len = self.len()?;\n+        if len == 0 {\n+            return Ok(None);\n+        }\n+        \n+        let last_idx = len - 1;\n+        let value = self.get(last_idx)?;\n+        \n+        let mut batch = WriteBatch::default();\n+        \n+        // Delete the last element\n+        let elem_key = self.element_key(last_idx);\n+        batch.delete(&elem_key);\n+        \n+        // Update length\n+        let len_key = self.meta_key(\"len\");\n+        batch.put(&len_key, &(last_idx as u64).to_le_bytes());\n+        \n+        // Commit atomically\n+        self.db.rocks().write(batch)?;\n+        self.db.rocks().flush_wal(true)?;\n+        \n+        Ok(value)\n+    }\n+    \n+    // Helper methods\n+    \n+    fn element_key(&self, index: usize) -> Vec {\n+        let mut key = self.prefix.clone();\n+        key.push(b':');\n+        key.extend_from_slice(&(index as u64).to_be_bytes());\n+        key\n+    }\n+    \n+    fn meta_key(&self, meta_type: &str) -> Vec {\n+        let mut key = self.prefix.clone();\n+        key.extend_from_slice(b\":__meta:\");\n+        key.extend_from_slice(meta_type.as_bytes());\n+        key\n+    }\n+    \n+    fn element_prefix(&self) -> Vec {\n+        let mut prefix = self.prefix.clone();\n+        prefix.push(b':');\n+        prefix\n+    }\n+}\n+\n+// Implement the DurableCollection trait for DurableVec\n+impl DurableCollection for DurableVec \n+where\n+    T: Serialize + for<'de> Deserialize<'de>\n+{\n+    fn from_prefix(db: Db, prefix: Vec) -> Self {\n+        DurableVec::from_prefix(db, prefix)\n+    }\n+}\n+\n+/// Iterator over a DurableVec\n+pub struct VecIterator<'a, T> {\n+    inner: rocksdb::DBIterator<'a>,\n+    prefix: Vec,\n+    _phantom: PhantomData,\n+}\n+\n+impl<'a, T> Iterator for VecIterator<'a, T>\n+where\n+    T: for<'de> Deserialize<'de>\n+{\n+    type Item = Result;\n+    \n+    fn next(&mut self) -> Option {\n+        loop {\n+            match self.inner.next() {\n+                Some(Ok((key, value))) => {\n+                    // Check if we're still within our prefix\n+                    if !key.starts_with(&self.prefix) {\n+                        return None;\n+                    }\n+                    \n+                    // Check if this is a meta key (skip it)\n+                    // The key pattern is: prefix:element_index or prefix:__meta:type\n+                    // We want to skip any key that contains \"__meta:\"\n+                    if key.windows(7).any(|w| w == b\"__meta:\") {\n+                        continue; // Skip this key and try the next one\n+                    }\n+                    \n+                    // Deserialize the value\n+                    match bincode::deserialize(&value) {\n+                        Ok(item) => return Some(Ok(item)),\n+                        Err(e) => return Some(Err(e.into())),\n+                    }\n+                }\n+                Some(Err(e)) => return Some(Err(e.into())),\n+                None => return None,\n+            }\n+        }\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+    use tempfile::TempDir;\n+    \n+    fn setup_test_db() -> (TempDir, Db) {\n+        let temp_dir = TempDir::new().unwrap();\n+        let db = Db::open(temp_dir.path()).unwrap();\n+        (temp_dir, db)\n+    }\n+    \n+    #[test]\n+    fn test_push_and_get() {\n+        let (_temp, db) = setup_test_db();\n+        let mut vec = DurableVec::::new(&db, \"test_vec\").unwrap();\n+        \n+        // Push some values\n+        vec.push(\"first\".to_string()).unwrap();\n+        vec.push(\"second\".to_string()).unwrap();\n+        vec.push(\"third\".to_string()).unwrap();\n+        \n+        // Check length\n+        assert_eq!(vec.len().unwrap(), 3);\n+        \n+        // Get values\n+        assert_eq!(vec.get(0).unwrap(), Some(\"first\".to_string()));\n+        assert_eq!(vec.get(1).unwrap(), Some(\"second\".to_string()));\n+        assert_eq!(vec.get(2).unwrap(), Some(\"third\".to_string()));\n+        assert_eq!(vec.get(3).unwrap(), None);\n+    }\n+    \n+    #[test]\n+    fn test_persistence() {\n+        let (temp_dir, db) = setup_test_db();\n+        \n+        // Create and populate vector\n+        {\n+            let mut vec = DurableVec::::new(&db, \"persist_vec\").unwrap();\n+            vec.push(42).unwrap();\n+            vec.push(100).unwrap();\n+            vec.push(-7).unwrap();\n+        }\n+        \n+        // Drop the database\n+        drop(db);\n+        \n+        // Reopen and verify data persists\n+        {\n+            let db = Db::open(temp_dir.path()).unwrap();\n+            let vec = DurableVec::::new(&db, \"persist_vec\").unwrap();\n+            \n+            assert_eq!(vec.len().unwrap(), 3);\n+            assert_eq!(vec.get(0).unwrap(), Some(42));\n+            assert_eq!(vec.get(1).unwrap(), Some(100));\n+            assert_eq!(vec.get(2).unwrap(), Some(-7));\n+        }\n+    }\n+    \n+    #[test]\n+    fn test_clear() {\n+        let (_temp, db) = setup_test_db();\n+        let mut vec = DurableVec::::new(&db, \"clear_vec\").unwrap();\n+        \n+        // Add some elements\n+        vec.extend(vec![1, 2, 3, 4, 5]).unwrap();\n+        assert_eq!(vec.len().unwrap(), 5);\n+        \n+        // Clear\n+        vec.clear().unwrap();\n+        assert_eq!(vec.len().unwrap(), 0);\n+        assert!(vec.is_empty().unwrap());\n+        \n+        // Should be able to push again\n+        vec.push(42).unwrap();\n+        assert_eq!(vec.len().unwrap(), 1);\n+        assert_eq!(vec.get(0).unwrap(), Some(42));\n+    }\n+    \n+    #[test]\n+    fn test_pop() {\n+        let (_temp, db) = setup_test_db();\n+        let mut vec = DurableVec::::new(&db, \"pop_vec\").unwrap();\n+        \n+        // Empty pop\n+        assert_eq!(vec.pop().unwrap(), None);\n+        \n+        // Push and pop\n+        vec.push(\"a\".to_string()).unwrap();\n+        vec.push(\"b\".to_string()).unwrap();\n+        vec.push(\"c\".to_string()).unwrap();\n+        \n+        assert_eq!(vec.pop().unwrap(), Some(\"c\".to_string()));\n+        assert_eq!(vec.len().unwrap(), 2);\n+        assert_eq!(vec.pop().unwrap(), Some(\"b\".to_string()));\n+        assert_eq!(vec.len().unwrap(), 1);\n+        assert_eq!(vec.pop().unwrap(), Some(\"a\".to_string()));\n+        assert_eq!(vec.len().unwrap(), 0);\n+        assert_eq!(vec.pop().unwrap(), None);\n+    }\n+    \n+    #[test]\n+    fn test_iteration() {\n+        let (_temp, db) = setup_test_db();\n+        let mut vec = DurableVec::::new(&db, \"iter_vec\").unwrap();\n+        \n+        // Add elements\n+        let values = vec![10, 20, 30, 40, 50];\n+        vec.extend(values.clone()).unwrap();\n+        \n+        // Iterate and collect\n+        let collected = vec.to_vec().unwrap();\n+        \n+        assert_eq!(collected, values);\n+    }\n+    \n+    #[test]\n+    fn test_extend() {\n+        let (_temp, db) = setup_test_db();\n+        let mut vec = DurableVec::::new(&db, \"extend_vec\").unwrap();\n+        \n+        // Extend with iterator\n+        vec.extend(vec![\"a\", \"b\", \"c\"].into_iter().map(String::from)).unwrap();\n+        assert_eq!(vec.len().unwrap(), 3);\n+        \n+        // Extend again\n+        vec.extend(vec![\"d\", \"e\"].into_iter().map(String::from)).unwrap();\n+        assert_eq!(vec.len().unwrap(), 5);\n+        \n+        // Verify all elements\n+        let all = vec.to_vec().unwrap();\n+        assert_eq!(all, vec![\"a\", \"b\", \"c\", \"d\", \"e\"]);\n+    }\n+    \n+    #[test]\n+    fn test_large_dataset() {\n+        let (_temp, db) = setup_test_db();\n+        let mut vec = DurableVec::::new(&db, \"large_vec\").unwrap();\n+        \n+        // Push many elements\n+        let count = 1000;\n+        for i in 0..count {\n+            vec.push(i).unwrap();\n+        }\n+        \n+        assert_eq!(vec.len().unwrap(), count as usize);\n+        \n+        // Verify some random accesses\n+        assert_eq!(vec.get(0).unwrap(), Some(0));\n+        assert_eq!(vec.get(500).unwrap(), Some(500));\n+        assert_eq!(vec.get(999).unwrap(), Some(999));\n+        assert_eq!(vec.get(1000).unwrap(), None);\n+        \n+        // Verify iteration count\n+        let all_values = vec.to_vec().unwrap();\n+        assert_eq!(all_values.len(), count as usize);\n+    }\n+    \n+    #[test] \n+    fn test_complex_types() {\n+        use serde::{Serialize, Deserialize};\n+        \n+        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\n+        struct User {\n+            id: u64,\n+            name: String,\n+            email: String,\n+            active: bool,\n+        }\n+        \n+        let (_temp, db) = setup_test_db();\n+        let mut vec = DurableVec::::new(&db, \"users\").unwrap();\n+        \n+        let user1 = User {\n+            id: 1,\n+            name: \"Alice\".to_string(),\n+            email: \"alice@example.com\".to_string(),\n+            active: true,\n+        };\n+        \n+        let user2 = User {\n+            id: 2,\n+            name: \"Bob\".to_string(),\n+            email: \"bob@example.com\".to_string(),\n+            active: false,\n+        };\n+        \n+        vec.push(user1.clone()).unwrap();\n+        vec.push(user2.clone()).unwrap();\n+        \n+        assert_eq!(vec.get(0).unwrap(), Some(user1));\n+        assert_eq!(vec.get(1).unwrap(), Some(user2));\n+    }\n+    \n+    #[test]\n+    fn test_empty_vec_operations() {\n+        let (_temp, db) = setup_test_db();\n+        let vec = DurableVec::::new(&db, \"empty_vec\").unwrap();\n+        \n+        // Test operations on empty vec\n+        assert_eq!(vec.len().unwrap(), 0);\n+        assert!(vec.is_empty().unwrap());\n+        assert_eq!(vec.get(0).unwrap(), None);\n+        assert_eq!(vec.get(100).unwrap(), None);\n+        assert_eq!(vec.to_vec().unwrap(), Vec::::new());\n+    }\n+    \n+    #[test]\n+    fn test_multiple_vecs_same_db() {\n+        let (_temp, db) = setup_test_db();\n+        \n+        // Create multiple vectors with different names\n+        let mut vec1 = DurableVec::::new(&db, \"vec1\").unwrap();\n+        let mut vec2 = DurableVec::::new(&db, \"vec2\").unwrap();\n+        \n+        // Push different data to each\n+        vec1.push(\"vec1_data\".to_string()).unwrap();\n+        vec2.push(\"vec2_data\".to_string()).unwrap();\n+        \n+        // Verify they don't interfere\n+        assert_eq!(vec1.get(0).unwrap(), Some(\"vec1_data\".to_string()));\n+        assert_eq!(vec2.get(0).unwrap(), Some(\"vec2_data\".to_string()));\n+        assert_eq!(vec1.len().unwrap(), 1);\n+        assert_eq!(vec2.len().unwrap(), 1);\n+    }\n+    \n+    #[test]\n+    fn test_batch_atomicity() {\n+        let (_temp, db) = setup_test_db();\n+        let mut vec = DurableVec::::new(&db, \"batch_vec\").unwrap();\n+        \n+        // Add initial data\n+        vec.push(1).unwrap();\n+        vec.push(2).unwrap();\n+        vec.push(3).unwrap();\n+        \n+        // Verify initial state\n+        assert_eq!(vec.len().unwrap(), 3);\n+        \n+        // Clear should be atomic - either all elements deleted or none\n+        vec.clear().unwrap();\n+        assert_eq!(vec.len().unwrap(), 0);\n+        \n+        // Extend should be atomic - either all elements added or none\n+        vec.extend(vec![10, 20, 30, 40, 50]).unwrap();\n+        assert_eq!(vec.len().unwrap(), 5);\n+        let all = vec.to_vec().unwrap();\n+        assert_eq!(all, vec![10, 20, 30, 40, 50]);\n+    }\n+    \n+    #[test]\n+    fn test_unicode_strings() {\n+        let (_temp, db) = setup_test_db();\n+        let mut vec = DurableVec::::new(&db, \"unicode_vec\").unwrap();\n+        \n+        let test_strings = vec![\n+            \"Hello, 世界!\".to_string(),\n+            \"🦀 Rust 🚀\".to_string(),\n+            \"Ñoño\".to_string(),\n+            \"🏴‍☠️ Pirates\".to_string(),\n+        ];\n+        \n+        vec.extend(test_strings.clone()).unwrap();\n+        \n+        let retrieved = vec.to_vec().unwrap();\n+        assert_eq!(retrieved, test_strings);\n+    }\n+    \n+    #[test]\n+    fn test_streaming_iterator() {\n+        let (_temp, db) = setup_test_db();\n+        let mut vec = DurableVec::::new(&db, \"stream_vec\").unwrap();\n+        \n+        // Add test data\n+        let values = vec![1, 2, 3, 4, 5];\n+        vec.extend(values.clone()).unwrap();\n+        \n+        // Test streaming iteration\n+        let mut collected = Vec::new();\n+        for item in vec.iter().unwrap() {\n+            collected.push(item.unwrap());\n+        }\n+        \n+        assert_eq!(collected, values);\n+        \n+        // Test that iterator properly handles prefix boundaries\n+        let mut vec2 = DurableVec::::new(&db, \"stream_vec2\").unwrap();\n+        vec2.extend(vec![10, 20, 30]).unwrap();\n+        \n+        // Each iterator should only see its own data\n+        let collected1: Vec<_> = vec.iter().unwrap().collect::>>().unwrap();\n+        let collected2: Vec<_> = vec2.iter().unwrap().collect::>>().unwrap();\n+        \n+        assert_eq!(collected1, values);\n+        assert_eq!(collected2, vec![10, 20, 30]);\n+    }\n+}\n+\n+#[cfg(all(test, not(miri)))] // Skip proptest under miri\n+mod proptests {\n+    use super::*;\n+    use proptest::prelude::*;\n+    use tempfile::TempDir;\n+    \n+    fn setup_test_db() -> (TempDir, Db) {\n+        let temp_dir = TempDir::new().unwrap();\n+        let db = Db::open(temp_dir.path()).unwrap();\n+        (temp_dir, db)\n+    }\n+    \n+    proptest! {\n+        #[test]\n+        fn prop_push_get_consistency(values: Vec) {\n+            let (_temp, db) = setup_test_db();\n+            let mut vec = DurableVec::::new(&db, \"prop_vec\").unwrap();\n+            \n+            // Push all values\n+            for value in &values {\n+                vec.push(*value).unwrap();\n+            }\n+            \n+            // Verify length\n+            prop_assert_eq!(vec.len().unwrap(), values.len());\n+            \n+            // Verify all values can be retrieved correctly\n+            for (i, expected) in values.iter().enumerate() {\n+                prop_assert_eq!(vec.get(i).unwrap(), Some(*expected));\n+            }\n+        }\n+        \n+        #[test]\n+        fn prop_extend_iter_roundtrip(values: Vec) {\n+            let (_temp, db) = setup_test_db();\n+            let mut vec = DurableVec::::new(&db, \"extend_vec\").unwrap();\n+            \n+            // Extend with all values\n+            vec.extend(values.clone()).unwrap();\n+            \n+            // Get back via iteration\n+            let retrieved = vec.to_vec().unwrap();\n+            \n+            prop_assert_eq!(retrieved, values);\n+        }\n+        \n+        #[test]\n+        fn prop_pop_removes_last(mut values: Vec) {\n+            let (_temp, db) = setup_test_db();\n+            let mut vec = DurableVec::::new(&db, \"pop_vec\").unwrap();\n+            \n+            // Add all values\n+            vec.extend(values.clone()).unwrap();\n+            \n+            // Pop values and verify\n+            while let Some(expected) = values.pop() {\n+                let popped = vec.pop().unwrap();\n+                prop_assert_eq!(popped, Some(expected));\n+                prop_assert_eq!(vec.len().unwrap(), values.len());\n+            }\n+            \n+            // Vector should be empty\n+            prop_assert!(vec.is_empty().unwrap());\n+            prop_assert_eq!(vec.pop().unwrap(), None);\n+        }\n+        \n+        #[test]\n+        fn prop_clear_makes_empty(values: Vec) {\n+            let (_temp, db) = setup_test_db();\n+            let mut vec = DurableVec::::new(&db, \"clear_vec\").unwrap();\n+            \n+            // Add values\n+            vec.extend(values).unwrap();\n+            \n+            // Clear\n+            vec.clear().unwrap();\n+            \n+            // Should be empty\n+            prop_assert_eq!(vec.len().unwrap(), 0);\n+            prop_assert!(vec.is_empty().unwrap());\n+            prop_assert_eq!(vec.get(0).unwrap(), None);\n+        }\n+    }\n+} \n\\ No newline at end of file\ndiff --git a/durable/todo.tdsl b/durable/todo.tdsl\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..6c9df9cbb25bb72d17aac07296a0c935ccbe89e7\n--- /dev/null\n+++ b/durable/todo.tdsl\n@@ -0,0 +1,4 @@\n+Add streaming iterators to avoid loading entire collections\n+Implement collection nesting (e.g., DurableMap>)\n+Add benchmarks to measure performance\n+Implement schema versioning and migrations\ndiff --git a/server/Cargo.toml b/server/Cargo.toml\nindex 6fb7bf52fa58f46f5e8fb0f7fd395247b57506d7..34672bd7acb9d165eea290e779c548fa4d2b8e3d 100644\n--- a/server/Cargo.toml\n+++ b/server/Cargo.toml\n@@ -22,6 +22,7 @@ async-stream = \"0.3\"\n futures-util = { version = \"0.3\", default-features = false, features = [\"std\"] }\n rand = \"0.8\"\n urlencoding = \"2\"\n+durable = { path = \"../durable\" }\n \n [dev-dependencies]\n reqwest = { version = \"0.12\", features = [\"json\"] }\ndiff --git a/server/src/entity_store.rs b/server/src/entity_store.rs\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..be3300529e97801a4d42f06f3f669b78ddd6856d\n--- /dev/null\n+++ b/server/src/entity_store.rs\n@@ -0,0 +1,90 @@\n+//! Off-heap storage for full entity payloads (Reddit API JSON).\n+//!\n+//! Derived [`crate::reducer::EntityData`] stays in the in-memory tree; raw JSON\n+//! lives in RocksDB via the workspace `durable` crate.\n+\n+use std::path::Path;\n+use std::sync::{Arc, Mutex};\n+\n+use durable::{Db, DurableMap};\n+use serde_json::Value;\n+\n+use crate::path_types::ItemId;\n+\n+#[derive(Debug, thiserror::Error)]\n+pub enum EntityStoreError {\n+    #[error(\"durable error: {0}\")]\n+    Durable(#[from] durable::DurableError),\n+    #[error(\"json error: {0}\")]\n+    Json(#[from] serde_json::Error),\n+    #[error(\"io error: {0}\")]\n+    Io(#[from] std::io::Error),\n+    #[error(\"store lock poisoned\")]\n+    Poisoned,\n+}\n+\n+struct EntityStoreInner {\n+    _db: Db,\n+    payloads: DurableMap,\n+}\n+\n+/// Disk-backed map of entity id → raw JSON payload.\n+#[derive(Clone)]\n+pub struct EntityStore {\n+    inner: Arc>,\n+}\n+\n+impl EntityStore {\n+    /// Open (or create) the entity database under `dir`.\n+    pub fn open(dir: &Path) -> Result {\n+        std::fs::create_dir_all(dir)?;\n+        let db = Db::open(dir)?;\n+        let payloads = DurableMap::new(&db, \"entity_payloads\")?;\n+        Ok(Self {\n+            inner: Arc::new(Mutex::new(EntityStoreInner { _db: db, payloads })),\n+        })\n+    }\n+\n+    /// Persist a payload for `id` (overwrites any existing entry).\n+    pub fn put(&self, id: &ItemId, payload: &Value) -> Result<(), EntityStoreError> {\n+        let json = serde_json::to_string(payload)?;\n+        let mut inner = self\n+            .inner\n+            .lock()\n+            .map_err(|_| EntityStoreError::Poisoned)?;\n+        inner\n+            .payloads\n+            .put(id.as_str().to_string(), json)\n+            .map_err(EntityStoreError::from)\n+    }\n+\n+    /// Load a stored payload, if present.\n+    pub fn get(&self, id: &ItemId) -> Result, EntityStoreError> {\n+        let inner = self\n+            .inner\n+            .lock()\n+            .map_err(|_| EntityStoreError::Poisoned)?;\n+        match inner.payloads.get(&id.as_str().to_string())? {\n+            Some(json) => Ok(Some(serde_json::from_str(&json)?)),\n+            None => Ok(None),\n+        }\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+    use serde_json::json;\n+\n+    #[test]\n+    fn round_trip_payload() {\n+        let tmp = tempfile::tempdir().unwrap();\n+        let store = EntityStore::open(tmp.path()).unwrap();\n+        let id = ItemId::parse(\"reddit.com/r/rust\").unwrap();\n+        let payload = json!({\"kind\": \"t5\", \"data\": {\"display_name\": \"rust\"}});\n+\n+        store.put(&id, &payload).unwrap();\n+        let loaded = store.get(&id).unwrap().unwrap();\n+        assert_eq!(loaded, payload);\n+    }\n+}\ndiff --git a/server/src/event_log.rs b/server/src/event_log.rs\nindex 03c86f7f028cdf167b4943951d724da486b1b82c..838c8830b265f28a03442681575271eef539508c 100644\n--- a/server/src/event_log.rs\n+++ b/server/src/event_log.rs\n@@ -7,6 +7,12 @@ use tokio::{\n \n use crate::events::Event;\n \n+#[derive(Debug, Default)]\n+pub struct ReplayStats {\n+    pub applied: usize,\n+    pub bad_lines: usize,\n+}\n+\n #[derive(Debug, thiserror::Error)]\n pub enum EventLogError {\n     #[error(\"io error: {0}\")]\n@@ -51,30 +57,87 @@ impl EventLog {\n         Ok(())\n     }\n \n-    pub async fn load_all(&self) -> Result<(Vec, Vec<(usize, String)>), EventLogError> {\n+    /// Stream the log one line at a time — parse each [`Event`], apply, drop before the next line.\n+    pub async fn replay(&self, mut apply: F) -> Result\n+    where\n+        F: FnMut(Event) -> Result<(), EventLogError>,\n+    {\n+        let mut stats = ReplayStats::default();\n         if !fs::try_exists(&self.path).await? {\n-            return Ok((vec![], vec![]));\n+            return Ok(stats);\n         }\n \n         let f = fs::File::open(&self.path).await?;\n         let mut reader = BufReader::new(f).lines();\n \n-        let mut events = Vec::new();\n-        let mut bad_lines = Vec::new();\n-\n-        let mut line_no: usize = 0;\n         while let Some(line) = reader.next_line().await? {\n-            line_no += 1;\n             let trimmed = line.trim();\n             if trimmed.is_empty() {\n                 continue;\n             }\n             match serde_json::from_str::(trimmed) {\n-                Ok(ev) => events.push(ev),\n-                Err(_) => bad_lines.push((line_no, line)),\n+                Ok(ev) => match apply(ev) {\n+                    Ok(()) => stats.applied += 1,\n+                    Err(e) => return Err(e),\n+                },\n+                Err(_) => stats.bad_lines += 1,\n             }\n         }\n \n-        Ok((events, bad_lines))\n+        Ok(stats)\n+    }\n+\n+    /// Load every event into memory. Prefer [`Self::replay`] for startup.\n+    pub async fn load_all(&self) -> Result<(Vec, Vec<(usize, String)>), EventLogError> {\n+        let mut events = Vec::new();\n+        let stats = self\n+            .replay(|ev| {\n+                events.push(ev);\n+                Ok(())\n+            })\n+            .await?;\n+        let _ = stats;\n+        Ok((events, vec![]))\n+    }\n+}\n+\n+#[cfg(test)]\n+mod tests {\n+    use super::*;\n+    use crate::events::Event;\n+\n+    #[tokio::test]\n+    async fn replay_applies_one_line_at_a_time() {\n+        let tmp = tempfile::tempdir().unwrap();\n+        let path = tmp.path().join(\"events.jsonl\");\n+        let log = EventLog::new(&path);\n+        log.append(&Event::NodeEnsured {\n+            id: \"reddit.com/r/rust\".into(),\n+        })\n+        .await\n+        .unwrap();\n+        log.append(&Event::VoteRecorded {\n+            ts: 1,\n+            a: \"a\".into(),\n+            b: \"b\".into(),\n+            ratio_left: 2,\n+            ratio_right: 1,\n+            scope: String::new(),\n+        })\n+        .await\n+        .unwrap();\n+\n+        let mut seen = Vec::new();\n+        let stats = log\n+            .replay(|ev| {\n+                seen.push(ev);\n+                Ok(())\n+            })\n+            .await\n+            .unwrap();\n+\n+        assert_eq!(stats.applied, 2);\n+        assert_eq!(stats.bad_lines, 0);\n+        assert_eq!(seen.len(), 2);\n     }\n }\ndiff --git a/server/src/lib.rs b/server/src/lib.rs\nindex 7f7e28c8ac3758de87f1f8e073b24be4132d38da..c9440a1019cb05c410eeb07b0b3ba09e4c6a6c6a 100644\n--- a/server/src/lib.rs\n+++ b/server/src/lib.rs\n@@ -1,4 +1,5 @@\n pub mod api;\n+pub mod entity_store;\n pub mod event_log;\n pub mod events;\n pub mod fetch;\ndiff --git a/server/src/reddit.rs b/server/src/reddit.rs\nindex 626454e5a2f638734193b5190a2beea286af85b6..28fcb83a2dfef01c76b802cec850624197b4b686 100644\n--- a/server/src/reddit.rs\n+++ b/server/src/reddit.rs\n@@ -10,6 +10,7 @@ use serde_json::Value;\n use tokio::sync::{mpsc, oneshot, RwLock};\n \n use crate::{\n+    entity_store::EntityStore,\n     event_log::EventLog,\n     events::Event,\n     fetch::now_ms,\n@@ -80,6 +81,7 @@ impl RedditBroker {\n     pub fn spawn(\n         tree: Arc>,\n         event_log: Arc,\n+        entity_store: EntityStore,\n         config: RedditApiConfig,\n     ) -> Self {\n         let (tx, rx) = mpsc::channel(100);\n@@ -104,7 +106,7 @@ impl RedditBroker {\n             \"reddit worker started\"\n         );\n \n-        tokio::spawn(reddit_worker(rx, tree, event_log, client, config));\n+        tokio::spawn(reddit_worker(rx, tree, event_log, entity_store, client, config));\n \n         Self { tx }\n     }\n@@ -193,9 +195,16 @@ pub fn entity_view_from_payload(id: &ItemId, payload: &Value) -> Option Result<(), String> {\n     let view = entity_view_from_payload(id, &payload);\n-    tree.apply_entity_raw(id, payload, view);\n+    store.put(id, &payload).map_err(|e| e.to_string())?;\n+    tree.apply_entity(id, view);\n+    Ok(())\n }\n \n fn notify(done: Option>, result: FetchJobResult) {\n@@ -208,6 +217,7 @@ async fn reddit_worker(\n     mut rx: mpsc::Receiver,\n     tree: Arc>,\n     event_log: Arc,\n+    entity_store: EntityStore,\n     client: Client,\n     config: RedditApiConfig,\n ) {\n@@ -302,14 +312,16 @@ async fn reddit_worker(\n                         let mut tree = tree.write().await;\n                         if kind == FetchKind::Children {\n                             let view = entity_view_from_payload(&child_id, &child_payload);\n-                            tree.apply_entity_under_parent(\n-                                &fetch_id,\n-                                &child_id,\n-                                child_payload,\n-                                view,\n-                            );\n-                        } else {\n-                            apply_entity_import(&mut tree, &child_id, child_payload);\n+                            if let Err(e) = entity_store.put(&child_id, &child_payload) {\n+                                write_err = Some(e.to_string());\n+                                break;\n+                            }\n+                            tree.apply_entity_under_parent(&fetch_id, &child_id, view);\n+                        } else if let Err(e) =\n+                            apply_entity_import(&mut tree, &entity_store, &child_id, child_payload)\n+                        {\n+                            write_err = Some(e);\n+                            break;\n                         }\n                     }\n                     written += 1;\ndiff --git a/server/src/reducer.rs b/server/src/reducer.rs\nindex fc23f41137d7df33997afe19c533505c250cc305..cf00f235d02e487439f8767d532929fe329a0084 100644\n--- a/server/src/reducer.rs\n+++ b/server/src/reducer.rs\n@@ -1,7 +1,6 @@\n use std::collections::{HashMap, HashSet, VecDeque};\n \n use serde::{Deserialize, Serialize};\n-use serde_json::Value;\n \n use crate::path_types::ItemId;\n \n@@ -134,9 +133,8 @@ pub struct EntityData {\n #[derive(Debug, Clone, Default)]\n pub struct NodeState {\n     pub id: ItemId,\n-    /// Full imported API JSON (persisted in the event log).\n-    pub entity_raw: Option,\n-    /// Domain-specific view derived from `entity_raw` (e.g. Reddit title/author).\n+    /// Domain-specific view derived from imported payload (e.g. Reddit title/author).\n+    /// Raw JSON lives in [`crate::entity_store::EntityStore`].\n     pub data: Option,\n     pub children: HashSet,\n     pub local_ranking: GroupState,\n@@ -206,28 +204,25 @@ impl GlobalTree {\n         }\n     }\n \n-    pub fn apply_entity_raw(&mut self, id: &ItemId, payload: Value, view: Option) {\n+    pub fn apply_entity(&mut self, id: &ItemId, view: Option) {\n         self.ensure_path(id);\n         if let Some(node) = self.nodes.get_mut(id) {\n-            node.entity_raw = Some(payload);\n             node.data = view;\n         }\n     }\n \n-    /// Import entity data for `id` and attach it as a direct child of `parent`\n+    /// Import entity view for `id` and attach it as a direct child of `parent`\n     /// without running [`Self::ensure_path`] on `id` (avoids Reddit `/comments/`\n     /// parent rules pulling intermediate path segments into the subreddit).\n     pub fn apply_entity_under_parent(\n         &mut self,\n         parent: &ItemId,\n         id: &ItemId,\n-        payload: Value,\n         view: Option,\n     ) {\n         self.ensure_path(parent);\n         self.ensure_node(id);\n         if let Some(node) = self.nodes.get_mut(id) {\n-            node.entity_raw = Some(payload);\n             node.data = view;\n         }\n         if let Some(p) = self.nodes.get_mut(parent) {\ndiff --git a/server/src/state.rs b/server/src/state.rs\nindex 4c3008e73cc74d8483a2064a0a280e73d82a01ec..dce04022de7ad9b0ffe0bac05ea79182b616945a 100644\n--- a/server/src/state.rs\n+++ b/server/src/state.rs\n@@ -3,6 +3,7 @@ use std::sync::Arc;\n use tokio::sync::RwLock;\n \n use crate::{\n+    entity_store::EntityStore,\n     event_log::EventLog,\n     events::Event,\n     journal::JournalClient,\n@@ -43,6 +44,42 @@ fn parent_from_event_scope(scope: &str) -> ItemId {\n     }\n }\n \n+fn apply_event(\n+    ev: Event,\n+    tree: &mut GlobalTree,\n+    entity_store: &EntityStore,\n+) -> Result<(), crate::event_log::EventLogError> {\n+    match ev {\n+        Event::VoteRecorded {\n+            ts,\n+            a,\n+            b,\n+            ratio_left,\n+            ratio_right,\n+            scope,\n+        } => {\n+            if let Some(vote) = VoteData::from_recorded(ts, &a, &b, ratio_left, ratio_right) {\n+                let parent = parent_from_event_scope(&scope);\n+                tree.apply_vote(&parent, vote);\n+            }\n+        }\n+        Event::ViewRecorded { .. } => {}\n+        Event::NodeEnsured { id } => {\n+            if let Some(parsed) = ItemId::parse(&id).or_else(|| ItemId::from_url(&id)) {\n+                tree.ensure_path(&parsed);\n+            }\n+        }\n+        Event::EntityImported { id, payload, .. } => {\n+            if let Some(parsed) = ItemId::parse(&id).or_else(|| ItemId::from_url(&id)) {\n+                if let Err(e) = apply_entity_import(tree, entity_store, &parsed, payload) {\n+                    tracing::warn!(item = %id, err = %e, \"entity replay failed\");\n+                }\n+            }\n+        }\n+    }\n+    Ok(())\n+}\n+\n #[derive(Clone)]\n pub struct AppConfig {\n     pub data_dir: String,\n@@ -71,6 +108,7 @@ impl AppConfig {\n pub struct AppState {\n     pub cfg: Arc,\n     pub event_log: Arc,\n+    pub entity_store: EntityStore,\n     pub views: ViewStore,\n     pub tree: Arc>,\n     journal: JournalClient,\n@@ -82,48 +120,31 @@ impl AppState {\n         let event_log = Arc::new(EventLog::new(cfg.event_log_path.clone()));\n         let views_path = format!(\"{}/views.json\", cfg.data_dir);\n         let views = ViewStore::new(&views_path);\n+        let entity_db_path = format!(\"{}/entity_db\", cfg.data_dir);\n+        let entity_store =\n+            EntityStore::open(std::path::Path::new(&entity_db_path)).expect(\"entity store\");\n \n         let mut tree = GlobalTree::new();\n-        if let Ok((events, _)) = event_log.load_all().await {\n-            for ev in events {\n-                match ev {\n-                    Event::VoteRecorded {\n-                        ts,\n-                        a,\n-                        b,\n-                        ratio_left,\n-                        ratio_right,\n-                        scope,\n-                    } => {\n-                        if let Some(vote) =\n-                            VoteData::from_recorded(ts, &a, &b, ratio_left, ratio_right)\n-                        {\n-                            let parent = parent_from_event_scope(&scope);\n-                            tree.apply_vote(&parent, vote);\n-                        }\n-                    }\n-                    Event::ViewRecorded { .. } => {}\n-                    Event::NodeEnsured { id } => {\n-                        if let Some(parsed) = ItemId::parse(&id).or_else(|| ItemId::from_url(&id)) {\n-                            tree.ensure_path(&parsed);\n-                        }\n-                    }\n-                    Event::EntityImported { id, payload, .. } => {\n-                        if let Some(parsed) = ItemId::parse(&id).or_else(|| ItemId::from_url(&id)) {\n-                            apply_entity_import(&mut tree, &parsed, payload);\n-                        }\n-                    }\n-                }\n-            }\n+        if let Err(e) = event_log\n+            .replay(|ev| apply_event(ev, &mut tree, &entity_store))\n+            .await\n+        {\n+            tracing::warn!(err = %e, \"event log replay failed\");\n         }\n \n         let tree = Arc::new(RwLock::new(tree));\n         let journal = JournalClient::spawn(tree.clone(), event_log.clone());\n-        let reddit = RedditBroker::spawn(tree.clone(), event_log.clone(), RedditApiConfig::from_env());\n+        let reddit = RedditBroker::spawn(\n+            tree.clone(),\n+            event_log.clone(),\n+            entity_store.clone(),\n+            RedditApiConfig::from_env(),\n+        );\n \n         Self {\n             cfg: Arc::new(cfg),\n             event_log,\n+            entity_store,\n             views,\n             tree,\n             journal,\n@@ -184,10 +205,10 @@ impl AppState {\n mod tests {\n     use super::{normalize_scope, parse_item_param};\n     use crate::{\n+        entity_store::EntityStore,\n         event_log::EventLog,\n         events::Event,\n         path_types::ItemId,\n-        reddit::apply_entity_import,\n         reducer::GlobalTree,\n     };\n     use serde_json::json;\n@@ -197,6 +218,7 @@ mod tests {\n         let tmp = tempfile::tempdir().unwrap();\n         let log_path = tmp.path().join(\"events.jsonl\");\n         let log = EventLog::new(log_path.to_string_lossy().into_owned());\n+        let entity_store = EntityStore::open(&tmp.path().join(\"entity_db\")).unwrap();\n         let payload = json!({\"kind\":\"t5\",\"data\":{\"title\":\"Rust\",\"display_name\":\"rust\"}});\n         log.append(&Event::EntityImported {\n             id: \"reddit.com/r/rust\".into(),\n@@ -207,19 +229,16 @@ mod tests {\n         .unwrap();\n \n         let mut tree = GlobalTree::new();\n-        let (events, _) = log.load_all().await.unwrap();\n-        for ev in events {\n-            if let Event::EntityImported { id, payload, .. } = ev {\n-                let parsed = ItemId::parse(&id).unwrap();\n-                apply_entity_import(&mut tree, &parsed, payload);\n-            }\n-        }\n+        log.replay(|ev| super::apply_event(ev, &mut tree, &entity_store))\n+            .await\n+            .unwrap();\n         let node = tree.get(&ItemId::parse(\"reddit.com/r/rust\").unwrap()).unwrap();\n         assert_eq!(node.data.as_ref().unwrap().title, \"Rust\");\n-        assert_eq!(\n-            node.entity_raw.as_ref().unwrap()[\"data\"][\"display_name\"],\n-            \"rust\"\n-        );\n+        let stored = entity_store\n+            .get(&ItemId::parse(\"reddit.com/r/rust\").unwrap())\n+            .unwrap()\n+            .unwrap();\n+        assert_eq!(stored[\"data\"][\"display_name\"], \"rust\");\n     }\n \n     #[test]\ndiff --git a/server/tests/integration_ui.rs b/server/tests/integration_ui.rs\nindex 56d2db313b313ffce07eff38a8ccdaa2fbb9498f..32da054b58b97a4751a038c677d213d6c3fa7cdc 100644\n--- a/server/tests/integration_ui.rs\n+++ b/server/tests/integration_ui.rs\n@@ -104,9 +104,17 @@ async fn post_ui_record_vote_morphs_ranking_and_persists() {\n     let log = std::fs::read_to_string(tmp.path().join(\"events.jsonl\")).unwrap();\n     assert!(log.contains(\"vote_recorded\"));\n \n+    // Replay in a fresh data dir (RocksDB locks entity_db while the server runs).\n+    let replay_tmp = TempDir::new().unwrap();\n+    std::fs::copy(\n+        tmp.path().join(\"events.jsonl\"),\n+        replay_tmp.path().join(\"events.jsonl\"),\n+    )\n+    .unwrap();\n+    let replay_data = replay_tmp.path().to_string_lossy().into_owned();\n     let cfg = AppConfig {\n-        data_dir: tmp.path().to_string_lossy().into_owned(),\n-        event_log_path: tmp.path().join(\"events.jsonl\").to_string_lossy().into_owned(),\n+        data_dir: replay_data.clone(),\n+        event_log_path: format!(\"{replay_data}/events.jsonl\"),\n         port: 0,\n     };\n     let state = create_app_state(cfg).await;\n","role":"user"}],"model":"~x-ai/grok-latest"}