Side A fixes a real semantic issue (a silent unwrap_or(0) masking a should-never-happen case) and reconciles a 1-vs-0 indexing inconsistency across rpc.rs, garden.rs, and types docs, backed by new integration test assertions verifying the corrected indices. Side B is almost entirely rustfmt/tooling churn (reformatting examples, toolchain pin, .vscode settings) plus a trivial CSS dedupe, which is useful hygiene but contributes no functional or correctness value.
constitution · epochs · watch · epoch 3
c_6a02ffb06a41 (tommy-mor) vs c_9e1ff4fc0186 (tommy-mor)
download prompt · raw event · cmp_e0eb135ac7887a
council reasoning
Commit A makes a real semantic fix: switches rank-history thread_post_index to consistent 0-based indexing matching /t/tag/N routes, replaces silent unwrap_or(0) with expect, drops the conditional UI hide, documents the contract, and adds integration assertions. Commit B is almost entirely rustfmt churn across examples and crates plus minor tooling (toolchain components, VS Code settings, one duplicated CSS rule merge), which adds little lasting design or correctness value.
Side A makes a semantic correctness change by treating `thread_post_index` as a guaranteed 0-based index, replacing silent `unwrap_or(0)` fallbacks with `expect(...)`, always rendering the post link, updating API/docs to distinguish 0-based URL indices from 1-based feed ordinals, and adding integration tests to lock in the behavior. Side B is overwhelmingly workspace-wide rustfmt reformatting plus tooling/editor configuration (pinning rustfmt/clippy, VS Code settings, minor CSS cleanup), with little lasting change to project behavior.
sides
A — c_6a02ffb06a41 (tommy-mor)
message
[3bc88847] removed optional
diff preview
diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index ba93ca87182d49f57dffc8220f604ffa150f9a6c..5049a26b096bd8435d6eb9e75ccb751b6f489061 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -1337,8 +1337,7 @@ pub async fn handle_rpc_batch(
.ingests_by_scope_thread
.get(&(scope.clone(), e.thread.clone()))
.and_then(|q| q.iter().rev().position(|id| id == &e.post_id))
- .map(|i| i + 1)
- .unwrap_or(0);
+ .expect("rank history post_id must be in ingests_by_scope_thread for (scope, thread)");
RankHistoryRow {
ts: e.ts,
scope_rank: e.scope_rank,
diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 319feb15a6b68d2b5df98b4289fedbc9bdd048d3..23245d6fdfc9019b199ab8417150faf5f3297067 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -450,6 +450,7 @@ struct RankHistoryEntryView {
scope_total: usize,
scope_rank_delta: i32,
thread: String,
+ /// 0-based index as [`crate::html::forum::ingest::thread_post_index_in_scope`] / `/t/tag/N`.
thread_post_index: usize,
caused_by: Vec<crate::reducer::VoteData>,
}
@@ -573,11 +574,11 @@ fn build_rank_history(
})
.unwrap_or_default();
- let thread_post_index = reduced.ingests_by_scope_thread
+ 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))
- .map(|i| i + 1)
- .unwrap_or(0);
+ .expect("rank history post_id must be in ingests_by_scope_thread for (scope, thread)");
RankHistoryEntryView {
ts: e.ts,
@@ -704,11 +705,9 @@ async fn render_scope_view(
span class="muted" { (ago) (label) }
" · "
a href=(thread_href(&e.thread)) { "#" (e.thread) }
- @if e.thread_post_index > 0 {
- " "
- a href=(format!("{}/{}", thread_href(&e.thread), e.thread_post_index)) {
- span class="muted" { "post #" (e.thread_post_index) }
- }
+ " "
+ a href=(format!("{}/{}", thread_href(&e.thread), e.thread_post_index)) {
+ span class="muted" { "post #" (e.thread_post_index) }
}
}
@if e.caused_by.is_empty() {
diff --git a/server/tests/integration.rs b/server/tests/integration.rs
index d4c5bfe9c6f1c71dc61878bd8c5e729b1b7c69ef..9766adb43ad315a64f5df17b79f66718ce149509 100644
--- a/server/tests/integration.rs
+++ b/server/tests/integration.rs
@@ -1322,6 +1322,11 @@ async fn test_rank_history() {
assert_eq!(entry["scope_rank_delta"], 0, "delta is 0 on first appearance");
let caused_by = entry["caused_by"].as_array().unwrap();
assert_eq!(caused_by.len(), 2, "both votes in the ingest touched rust");
+ assert_eq!(
+ entry["thread_post_index"],
+ 0,
+ "rank history links use same 0-based index as /t/hist-test/0"
+ );
ingest(
"00000000-0000-0000-0000-000000000002:rig:test/model",
@@ -1349,6 +1354,16 @@ async fn test_rank_history() {
assert_eq!(caused_by2.len(), 1);
assert!(caused_by2[0]["a"].as_str().unwrap().ends_with("python") ||
caused_by2[0]["b"].as_str().unwrap().ends_with("python"));
+ assert_eq!(
+ hist2[0]["thread_post_index"],
+ 0,
+ "first hist-test post is chronological index 0"
+ );
+ assert_eq!(
+ hist2[1]["thread_post_index"],
+ 1,
+ "second ingest is chronological index 1"
+ );
let hist_rust2 = rpc_batch(
&client,
diff --git a/types/src/lib.rs b/types/src/lib.rs
index edbb923e4d7d41ad82dfc254c3bd697562383527..49abba8ea9f786ef68e3157d2a5d309e15e09ba0 100644
--- a/types/src/lib.rs
+++ b/types/src/lib.rs
@@ -253,7 +253,7 @@ pub struct FeedPost {
/// Primary thread tag (without #), if the ingest declared one.
#[serde(skip_serializing_if = "Option::is_none")]
pub thread: Option<String>,
- /// 1-indexed chronological position of this post within the thread.
+ /// 1-based display ordinal for this post within the thread (feed only; URLs use 0-based paths).
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_post_index: Option<usize>,
/// Full raw body of the ingest document.
@@ -628,7 +628,7 @@ pub struct RankHistoryRow {
pub score: f64,
/// Thread tag of the ingest that triggered this rank change.
pub thread: String,
- /// 1-indexed chronological position of this post within the thread.
+ /// 0-indexed chronological position of this post within the thread (same as `/t/tag/N` routes).
pub thread_post_index: usize,
/// Votes from this ingest that directly touched this item. Empty when change was transitive.
pub caused_by: Vec<VoteRow>,
B — c_9e1ff4fc0186 (tommy-mor)
message
[2a94401b] Run rustfmt workspace-wide and fix lint tooling. Pin rustfmt and clippy in rust-toolchain.toml after a broken component install, merge a duplicate vote-slider CSS rule, and add VS Code settings so rust-analyzer uses the project toolchain. Co-authored-by: Cursor <cursoragent@cursor.com>
diff preview
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..98ebd754a6d5a972550506c69c5a10c10e3210b6
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,8 @@
+{
+ "rust-analyzer.rustc.source": "discover",
+ "rust-analyzer.check.command": "check",
+ "rust-analyzer.procMacro.enable": true,
+ "rust-analyzer.cargo.extraEnv": {
+ "RUSTUP_TOOLCHAIN": "1.88.0"
+ }
+}
diff --git a/durable/examples/combined_example.rs b/durable/examples/combined_example.rs
index 626a6e1cf7e3c9c26f9f2edc58950d9bc31ec67e..6e9cb3210714d74c9a2cc0ed4f87e1b8d84788da 100644
--- a/durable/examples/combined_example.rs
+++ b/durable/examples/combined_example.rs
@@ -1,5 +1,5 @@
use durable::{Db, DurableMap, DurableVec};
-use serde::{Serialize, Deserialize};
+use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -28,36 +28,48 @@ fn get_timestamp() -> u64 {
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Open or create a database
let db = Db::open("chat_db")?;
-
+
// Create our collections
let mut users = DurableMap::<String, User>::new(&db, "users")?;
let mut messages = DurableVec::<Message>::new(&db, "messages")?;
let mut user_message_indices = DurableMap::<String, Vec<usize>>::new(&db, "user_messages")?;
-
+
// Create some users
- users.insert("alice".to_string(), User {
- username: "alice".to_string(),
- display_name: "Alice Smith".to_string(),
- message_count: 0,
- })?;
-
- users.insert("bob".to_string(), User {
- username: "bob".to_string(),
- display_name: "Bob Johnson".to_string(),
- message_count: 0,
- })?;
-
- users.insert("charlie".to_string(), User {
- username: "charlie".to_string(),
- display_name: "Charlie Brown".to_string(),
- message_count: 0,
- })?;
-
+ users.insert(
+ "alice".to_string(),
+ User {
+ username: "alice".to_string(),
+ display_name: "Alice Smith".to_string(),
+ message_count: 0,
+ },
+ )?;
+
+ users.insert(
+ "bob".to_string(),
+ User {
+ username: "bob".to_string(),
+ display_name: "Bob Johnson".to_string(),
+ message_count: 0,
+ },
+ )?;
+
+ users.insert(
+ "charlie".to_string(),
+ User {
+ username: "charlie".to_string(),
+ display_name: "Charlie Brown".to_string(),
+ message_count: 0,
+ },
+ )?;
+
// Helper to send a message
- let send_message = |from: &str, to: &str, content: &str,
+ let send_message = |from: &str,
+ to: &str,
+ content: &str,
messages: &mut DurableVec<Message>,
users: &mut DurableMap<String, User>,
- indices: &mut DurableMap<String, Vec<usize>>| -> Result<(), Box<dyn std::error::Error>> {
+ indices: &mut DurableMap<String, Vec<usize>>|
+ -> Result<(), Box<dyn std::error::Error>> {
// Create message
let msg_id = messages.len()? as u64;
let message = Message {
@@ -67,61 +79,93 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
content: content.to_string(),
timestamp: get_timestamp(),
};
-
+
// Store message
messages.push(message)?;
let msg_index = messages.len()? - 1;
-
+
// Update sender's message count
if let Some(mut sender) = users.get(&from.to_string())? {
sender.message_count += 1;
users.insert(from.to_string(), sender)?;
}
-
+
// Track message indices for recipient
let mut recipient_indices = indices.get(&to.to_string())?.unwrap_or_default();
recipient_indices.push(msg_index);
indices.insert(to.to_string(), recipient_indices)?;
-
+
Ok(())
};
-
+
// Send some messages
println!("💬 Chat Application Demo\n");
println!("Sending messages...");
-
- send_message("alice", "bob", "Hey Bob, how's the Durable library coming along?",
- &mut messages, &mut users, &mut user_message_indices)?;
-
- send_message("bob", "alice", "It's going great! We have DurableVec and DurableMap working!",
- &mut messages, &mut users, &mut user_message_indices)?;
-
- send_message("charlie", "alice", "That sounds awesome! Can I help with testing?",
- &mut messages, &mut users, &mut user_message_indices)?;
-
- send_message("alice", "charlie", "Absolutely! The more testing the better!",
- &mut messages, &mut users, &mut user_message_indices)?;
-
- send_message("bob", "charlie", "Check out the examples directory for usage patterns",
- &mut messages, &mut users, &mut user_message_indices)?;
-
+
+ send_message(
+ "alice",
+ "bob",
+ "Hey Bob, how's the Durable library coming along?",
+ &mut messages,
+ &mut users,
+ &mut user_message_indices,
+ )?;
+
+ send_message(
+ "bob",
+ "alice",
+ "It's going great! We have DurableVec and DurableMap working!",
+ &mut messages,
+ &mut users,
+ &mut user_message_indices,
+ )?;
+
+ send_message(
+ "charlie",
+ "alice",
+ "That sounds awesome! Can I help with testing?",
+ &mut messages,
+ &mut users,
+ &mut user_message_indices,
+ )?;
+
+ send_message(
+ "alice",
+ "charlie",
+ "Absolutely! The more testing the better!",
+ &mut messages,
+ &mut users,
+ &mut user_message_indices,
+ )?;
+
+ send_message(
+ "bob",
+ "charlie",
+ "Check out the examples directory for usage patterns",
+ &mut messages,
+ &mut users,
+ &mut user_message_indices,
+ )?;
+
// Display all users and their message counts
println!("\n👥 Users:");
let mut all_users = users.to_vec()?;
all_users.sort_by_key(|(username, _)| username.clone());
-
+
for (username, user) in all_users {
- println!(" {} ({}) - {} messages sent",
- user.display_name, username, user.message_count);
+ println!(
+ " {} ({}) - {} messages sent",
+ user.display_name, username, user.message_count
+ );
}
-
+
// Display all messages
println!("\n📨 All messages:");
for (i, msg) in messages.iter()?.enumerate() {
let msg = msg?;
println!(" [{}] {} → {}: {}", i, msg.from, msg.to, msg.content);
}
-
+
// Show inbox for each user
println!("\n📥 User inboxes:");
for item in users.iter() {
@@ -135,26 +179,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
}
-
+
// Statistics
println!("\n📊 Statistics:");
println!(" Total users: {}", users.len()?);
println!(" Total messages: {}", messages.len()?);
-
+
// Demonstrate persistence
println!("\n💾 Data has been persisted to disk!");
println!(" Database location: ./chat_db");
-
+
// Clean up
drop(messages);
drop(users);
drop(user_message_indices);
drop(db);
-
+
// Remove the database for this example
std::fs::remove_dir_all("chat_db").ok();
-
+
println!("\n✅ Example completed!");
-
+
Ok(())
-}
\ No newline at end of file
+}
diff --git a/durable/examples/map_example.rs b/durable/examples/map_example.rs
index 08b8f2c8826caf53c4c20b422a540c92f6624029..1d9c4a14b0f4cfe39ade84ebb1f1a14033e8ff1b 100644
--- a/durable/examples/map_example.rs
+++ b/durable/examples/map_example.rs
@@ -1,5 +1,5 @@
use durable::{Db, DurableMap};
-use serde::{Serialize, Deserialize};
+use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct UserProfile {
@@ -11,10 +11,10 @@ struct UserProfile {
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Open or create a database
let db = Db::open("example_db")?;
-
+
// Create a persistent map of user profiles
let mut users = DurableMap::<String, UserProfile>::new(&db, "users")?;
-
+
// Insert some users
// Using put() when we don't need the old value - more efficient!
users.put(
@@ -25,7 +25,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
score: 1500,
},
)?;
-
+
users.put(
"bob".to_string(),
UserProfile {
@@ -34,7 +34,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
score: 1200,
},
)?;
-
+
// Using insert() when we might need the old value
let old_charlie = users.insert(
"charlie".to_string(),
@@ -44,21 +44,24 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
score: 1800,
},
)?;
-
+
if old_charlie.is_some() {
println!("Replaced existing charlie entry");
}
-
+
println!("Total users: {}", users.len()?);
-
+
// Look up a specific user
if let Some(alice) = users.get(&"alice".to_string())? {
println!("\nAlice's profile: {:?}", alice);
}
-
+
// Check if a user exists
- println!("\nDoes 'david' exist? {}", users.contains_key(&"david".to_string())?);
-
+ println!(
+ "\nDoes 'david' exist? {}",
+ users.contains_key(&"david".to_string())?
+ );
+
// Update a user's score
if let Some(mut bob) = users.get(&"bob".to_string())? {
bob.score += 100;
@@ -66,34 +69,40 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
users.put("bob".to_string(), bob)?;
println!("Updated Bob's score!");
}
-
+
// Iterate over all users
println!("\nAll users (sorted by username):");
let mut all_users = users.to_vec()?;
all_users.sort_by_key(|(username, _)| username.clone());
-
+
for (username, profile) in all_users {
- println!(" {} ({}) - Score: {}", username, profile.email, profile.score);
+ println!(
+ " {} ({}) - Score: {}",
+ username, profile.email, profile.score
+ );
}
-
+
// Get just the usernames
let mut usernames = users.keys_vec()?;
usernames.sort();
println!("\nAll usernames: {:?}", usernames);
-
+
// Find the highest scoring user
let profiles = users.values_vec()?;
if let Some(top_user) = profiles.iter().max_by_key(|p| p.score) {
- println!("\nTop scorer: {} with {} points", top_user.name, top_user.score);
+ println!(
+ "\nTop scorer: {} with {} points",
+ top_user.name, top_user.score
+ );
}
-
+
// Remove a user
if let Some(removed) = users.remove(&"charlie".to_string())? {
println!("\nRemoved user: {}", removed.name);
println!("Users remaining: {}", users.len()?);
}
-
+
println!("\nData has been persisted to disk.");
-
+
Ok(())
-}
\ No newline at end of file
+}
diff --git a/durable/examples/nested_example.rs b/durable/examples/nested_example.rs
index 3b880f2c8b8ad2633d4b5fcf2016652216c9de8e..f16090cf6fb854ac7a1b00acfd31fbd12c25dbce 100644
--- a/durable/examples/nested_example.rs
+++ b/durable/examples/nested_example.rs
@@ -3,27 +3,31 @@ use durable::{Db, DurableMap, DurableVec};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Open a database
let db = Db::open("nested_example_db")?;
-
+
// Create a map where each user has a list of posts
- let user_posts: DurableMap<String, DurableVec<String>> = DurableMap::new_nested(&db, "user_posts");
-
+ let user_posts: DurableMap<String, DurableVec<String>> =
+ DurableMap::new
… preview truncated; 96,900 characters omittedHardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.