constitution · epochs · watch · epoch 3

comparison

c_0307a4ccb911 (tommy-mor) vs c_ed5070da05a1 (tommy-mor)

download prompt · raw event · cmp_9c180869eb633d

council reasoning

~anthropic/claude-sonnet-latest · winner B · 20:80 · permalink

Side B is a small, surgical commit: it adds a real bugfix (room garden pages no longer silently fall back to the public scope when a private room has no content, preventing content leakage), factors out a well-documented `thread_post_index_chronological` helper to eliminate duplicated fragile lookup logic across rpc.rs/garden.rs/ingest.rs, and backs the fix with new integration and browser regression tests. Side A is a sprawling 'reshuffle' commit that mostly moves large chunks of working code into a `legacy/` directory, checks in a huge generated Cargo.lock, and stands up a mostly-empty demo scaffold (health check, counter bump) — it's a project reset with high line count but little durable engineering value per line, and it discards more functionality than it adds.

~x-ai/grok-latest · winner B · 2:5 · permalink

Side B fixes real behavior bugs (empty private-room gardens no longer fall through to public content; thread_post_index is centralized as stable 0-based chrono indices with redact-safe rebuild semantics) and adds focused integration/browser regression tests. Side A is mostly reshuffle noise—renames, legacy moves, Cargo.lock, and scaffolding—with far less net correctness per line despite its size.

openai/gpt-chat-latest · winner B · 4:1 · permalink

Side B introduces lasting abstractions by centralizing thread post index lookup into `ReducerState::{try_thread_post_index_chronological, thread_post_index_chronological}`, replacing duplicated indexing logic across RPC and HTML code, and adds regression tests ensuring redacted posts preserve stable chronological indices. Side A is dominated by a large project reshuffle (moving files into `legacy/`, renames, workspace/bootstrap, generated `Cargo.lock`, CI, and scaffolding) with relatively little new enduring behavior compared to the amount of churn.

sides

A — c_0307a4ccb911 (tommy-mor)

message

[3908604c] reshuffle

diff preview

diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..2793eb9c61883eaeb6cc2335eb06b196ae96a0d4
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,7 @@
+target/
+.git/
+.cpcache/
+.clj-kondo/
+legacy/
+*.tdsl
+test/
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..083345813f8760b936291c0d171fc2b4ce743424
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,34 @@
+name: CI
+
+on:
+  pull_request:
+  push:
+    branches: [main]
+  workflow_dispatch: {}
+
+concurrency:
+  group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: dtolnay/rust-toolchain@stable
+
+      - name: Rust tests
+        run: cargo test --all
+
+      - uses: DeLaGuardo/setup-clojure@12.5
+        with:
+          cli: "1.12.0.1530"
+
+      - name: Clojure tests (Kaocha)
+        run: |
+          mkdir -p target
+          clojure -M:kaocha
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000000000000000000000000000000000000..24c07a3875d6a92e90eff5f67def0a718f5f2f0f
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,30 @@
+name: Deploy (Fly.io)
+
+on:
+  push:
+    branches: [main]
+    paths:
+      - "server/**"
+      - "Cargo.toml"
+      - "Cargo.lock"
+      - "Dockerfile"
+      - "fly.toml"
+      - ".github/workflows/deploy.yml"
+  workflow_dispatch: {}
+
+concurrency:
+  group: fly-deploy-sorter2-main
+  cancel-in-progress: true
+
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: superfly/flyctl-actions/setup-flyctl@master
+
+      - name: Deploy sorter2
+        env:
+          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
+        run: flyctl deploy --remote-only
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..16de5edb7185b04ef5bc64512814d7dfe2c1f50c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/target/
+/.cpcache/
+/.clj-kondo/
+/.lsp/
+*.swp
+.DS_Store
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000000000000000000000000000000000000..266e876bb7ccbe788beb1d5bd53ad5b45ee5825b
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,2088 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "axum"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "axum-macros",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-extra"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04"
+dependencies = [
+ "axum",
+ "axum-core",
+ "bytes",
+ "cookie",
+ "fastrand",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "multer",
+ "pin-project-lite",
+ "serde",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-macros"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "cc"
+version = "1.2.62"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cookie"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "171fefbc92fe4a4de27e0698d6a

… preview truncated; 526,840 characters omitted

download full diff A

B — c_ed5070da05a1 (tommy-mor)

message

[39d32611] helpers and cleanups

diff preview

diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index 5049a26b096bd8435d6eb9e75ccb751b6f489061..0ff2d701dd1e8661f58abd55672cd91280e9491e 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -1333,11 +1333,8 @@ pub async fn handle_rpc_batch(
                             }).collect()
                         })
                         .unwrap_or_default();
-                    let thread_post_index = reduced
-                        .ingests_by_scope_thread
-                        .get(&(scope.clone(), e.thread.clone()))
-                        .and_then(|q| q.iter().rev().position(|id| id == &e.post_id))
-                        .expect("rank history post_id must be in ingests_by_scope_thread for (scope, thread)");
+                    let thread_post_index =
+                        reduced.thread_post_index_chronological(&scope, &e.thread, &e.post_id);
                     RankHistoryRow {
                         ts: e.ts,
                         scope_rank: e.scope_rank,
@@ -1504,11 +1501,11 @@ pub async fn handle_rpc_batch(
                                         .map(|ing| {
                                             let scope = scope_from_room_wire(&ing.room_id);
                                             let thread_post_index = reduced
-                                                .ingests_by_scope_thread
-                                                .get(&(scope, ing.thread_tag.clone()))
-                                                .and_then(|q| {
-                                                    q.iter().rev().position(|pid| pid == &ing.id).map(|i| i + 1)
-                                                });
+                                                .try_thread_post_index_chronological(
+                                                    &scope,
+                                                    &ing.thread_tag,
+                                                    &ing.id,
+                                                );
                                             FeedPost {
                                                 ts: ing.ts,
                                                 id: ing.id.clone(),
@@ -1565,11 +1562,11 @@ pub async fn handle_rpc_batch(
                                     .map(|ing| {
                                         let scope = scope_from_room_wire(&ing.room_id);
                                         let thread_post_index = reduced
-                                            .ingests_by_scope_thread
-                                            .get(&(scope, ing.thread_tag.clone()))
-                                            .and_then(|q| {
-                                                q.iter().rev().position(|pid| pid == &ing.id).map(|i| i + 1)
-                                            });
+                                            .try_thread_post_index_chronological(
+                                                &scope,
+                                                &ing.thread_tag,
+                                                &ing.id,
+                                            );
                                         FeedPost {
                                             ts: ing.ts,
                                             id: ing.id.clone(),
diff --git a/server/src/html/forum/ingest.rs b/server/src/html/forum/ingest.rs
index 308358abc6cd6811079d9ec41b114093c0294d02..27d1852cf3b2a1718605e4633d4238383adfe33d 100644
--- a/server/src/html/forum/ingest.rs
+++ b/server/src/html/forum/ingest.rs
@@ -1,4 +1,3 @@
-use crate::canonical_path::canonicalize_tag;
 use crate::form_template::template_json_compact;
 use crate::reducer::{scope_from_room_wire, ReducerState};
 use maud::{html, Markup};
@@ -31,11 +30,7 @@ pub(super) fn thread_nav_for_ingest(ing: &crate::events::Ingest) -> Option<Threa
 
 pub(super) fn thread_post_index_in_scope(reduced: &ReducerState, ing: &crate::events::Ingest) -> Option<usize> {
     let scope = scope_from_room_wire(&ing.room_id);
-    let tag = canonicalize_tag(&ing.thread_tag);
-    reduced
-        .ingests_by_scope_thread
-        .get(&(scope, tag))
-        .and_then(|q| q.iter().rev().position(|id| id == &ing.id))
+    reduced.try_thread_post_index_chronological(&scope, &ing.thread_tag, &ing.id)
 }
 
 fn post_header_meta(
diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 23245d6fdfc9019b199ab8417150faf5f3297067..423f23fd8c9ad7b7f454d6ea7a9a7607a4c9c5b9 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -11,7 +11,7 @@ use crate::{
     canonical_path::canonicalize_item,
     events::ThreadCapability,
     path_types::CanonicalItemUrl,
-    reducer::{ReducerState, ScopeId},
+    reducer::{ContentState, ReducerState, ScopeId},
     ranking::{connected_components_from_voted_pairs, ranked_items_subset},
     scope_rank::{build_children_rankings, ChildrenRankings},
     state::AppState,
@@ -133,6 +133,23 @@ fn user_can_view_room(reduced: &ReducerState, room_id: &str, username: Option<&s
     reduced.user_has_cap(room_id, u, ThreadCapability::View)
 }
 
+/// Private `~/` / `-/` garden pages require at least one ingest in that scope (a `content` entry).
+fn room_scope_has_garden_content(reduced: &ReducerState, nav: &ThreadNav) -> bool {
+    match nav.scope() {
+        ScopeId::Public => true,
+        ScopeId::Room(_) => reduced.content_for_scope(&nav.scope()).is_some(),
+    }
+}
+
+fn content_for_garden_view<'a>(reduced: &'a ReducerState, scope: &ScopeId) -> &'a ContentState {
+    match scope {
+        ScopeId::Public => reduced.public(),
+        ScopeId::Room(_) => reduced.content_for_scope(scope).expect(
+            "room garden only renders after room_scope_has_garden_content returned true",
+        ),
+    }
+}
+
 /// Ontology index — root-level paths. Private (UUID) roots are excluded.
 pub async fn garden_index(
     State(state): State<AppState>,
@@ -298,16 +315,20 @@ pub async fn room_garden_index(
     uri: Uri,
 ) -> impl IntoResponse {
     let room_id = format!("{room_short}/{room_slug}");
+    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let reduced = state.reduced.read().await;
     let user = optional_principal(&headers, &jar, &reduced);
     if !user_can_view_room(&reduced, &room_id, user.as_deref()) {
         drop(reduced);
         return room_not_found_page(&jar, &uri).into_response();
     }
+    if !room_scope_has_garden_content(&reduced, &nav) {
+        drop(reduced);
+        return room_not_found_page(&jar, &uri).into_response();
+    }
     drop(reduced);
-    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
-        return (StatusCode::NOT_FOUND, "bad room path").into_response();
-    };
     render_scope_view(
         state,
         GardenBrowsePath::Tilde(OntologyPath::root()),
@@ -326,25 +347,26 @@ pub async fn room_external_garden_index(
     uri: Uri,
 ) -> impl IntoResponse {
     let room_id = format!("{room_short}/{room_slug}");
+    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let reduced = state.reduced.read().await;
     let user = optional_principal(&headers, &jar, &reduced);
     if !user_can_view_room(&reduced, &room_id, user.as_deref()) {
         drop(reduced);
         return room_not_found_page(&jar, &uri).into_response();
     }
-    drop(reduced);
-    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
-        return (StatusCode::NOT_FOUND, "bad room path").into_response();
-    };
+    if !room_scope_has_garden_content(&reduced, &nav) {
+        drop(reduced);
+        return room_not_found_page(&jar, &uri).into_response();
+    }
     let ext_path = ExternalOntologyPath::from_input("");
     let parent = CanonicalItemUrl::parse("https://.").unwrap();
-    let child_rankings = {
-        let reduced = state.reduced.read().await;
-        let content = reduced
-            .content_for_scope(&nav.scope())
-            .unwrap_or_else(|| reduced.public());
-        build_children_rankings(content, &parent)
-    };
+    let child_rankings = build_children_rankings(
+        content_for_garden_view(&reduced, &nav.scope()),
+        &parent,
+    );
+    drop(reduced);
 
     let page = layout(
         "-/",
@@ -400,16 +422,20 @@ pub async fn room_external_ontology_path(
     uri: Uri,
 ) -> impl IntoResponse {
     let room_id = format!("{room_short}/{room_slug}");
+    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let reduced = state.reduced.read().await;
     let user = optional_principal(&headers, &jar, &reduced);
     if !user_can_view_room(&reduced, &room_id, user.as_deref()) {
         drop(reduced);
         return room_not_found_page(&jar, &uri).into_response();
     }
+    if !room_scope_has_garden_content(&reduced, &nav) {
+        drop(reduced);
+        return room_not_found_page(&jar, &uri).into_response();
+    }
     drop(reduced);
-    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
-        return (StatusCode::NOT_FOUND, "bad room path").into_response();
-    };
     let path = ExternalOntologyPath::from_input(&path);
     render_scope_view(state, GardenBrowsePath::External(path), nav, jar, uri).await
 }
@@ -422,16 +448,20 @@ pub async fn room_ontology_path(
     uri: Uri,
 ) -> impl IntoResponse {
     let room_id = format!("{room_short}/{room_slug}");
+    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
+        return (StatusCode::NOT_FOUND, "bad room path").into_response();
+    };
     let reduced = state.reduced.read().await;
     let user = optional_principal(&headers, &jar, &reduced);
     if !user_can_view_room(&reduced, &room_id, user.as_deref()) {
         drop(reduced);
         return room_not_found_page(&jar, &uri).into_response();
     }
+    if !room_scope_has_garden_content(&reduced, &nav) {
+        drop(reduced);
+        return room_not_found_page(&jar, &uri).into_response();
+    }
     drop(reduced);
-    let Some(nav) = ThreadNav::from_room_id(&room_id) else {
-        return (StatusCode::NOT_FOUND, "bad room path").into_response();
-    };
     let path = OntologyPath::from_input(&path);
     render_scope_view(state, GardenBrowsePath::Tilde(path), nav, jar, uri).await
 }
@@ -474,9 +504,7 @@ fn build_sibling_rank(
     item: &CanonicalItemUrl,
 ) -> Option<SiblingRank> {
     let item = item.clone().normalized_storage();
-    let content = reduced
-        .content_for_scope(scope)
-        .unwrap_or_else(|| reduced.public());
+    let content = content_for_garden_view(reduced, scope);
     let group = &content.ranking_group;
     let parent = item.parent()?.normalized_storage();
     let siblings: Vec<CanonicalItemUrl> = content
@@ -537,9 +565,7 @@ fn build_rank_history(
     scope: &ScopeId,
     item: &str,
 ) -> Vec<RankHistoryEntryView> {
-    let content = reduced
-        .content_for_scope(scope)
-        .unwrap_or_else(|| reduced.public());
+    let content = content_for_garden_view(reduced, scope);
     let item_key = CanonicalItemUrl(item.to_string());
     let entries = match content.rank_history.get(&item_key) {
         None => return vec![],
@@ -574,11 +600,8 @@ fn build_rank_history(
             })
             .unwrap_or_default();
 
-        let thread_post_index = reduced
-            .ingests_by_scope_thread
-            .get(&(scope.clone(), e.thread.clone()))
-            .and_then(|q| q.iter().rev().position(|id| id == &e.post_id))
-            .expect("rank history post_id must be in ingests_by_scope_thread for (scope, thread)");
+        let thread_post_index =
+            reduced.thread_post_index_chronological(scope, &e.thread, &e.post_id);
 
         RankHistoryEntryView {
             ts: e.ts,
@@ -597,9 +620,7 @@ fn build_item_

… preview truncated; 15,058 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.