constitution · epochs · watch · epoch 3

comparison

c_9bced108c8aa (tommy-mor) vs c_73d485b8085b (tommy-mor)

download prompt · raw event · cmp_6f0a2f4f6b0655

council reasoning

~x-ai/grok-latest · winner A · 3:2 · permalink

A lands a complete, carefully designed URL semantic graph (DFA traversal, absorb edges, builder validation, parse/normalize, generic fallback) with dense regression tests for Reddit/YouTube equivalence and encoding safety—core lasting domain logic. B’s OAuth/session/pseudonym gating and identity projection are valuable product work, but a large share of the patch is moving durable out-of-tree (mass deletion + lockfile/git dep churn) rather than net new in-repo design.

sides

A — c_9bced108c8aa (tommy-mor)

message

[15e1037a] url stuff

diff preview

diff --git a/server/src/url_rules/graph.rs b/server/src/url_rules/graph.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f7ac0f9a551a1727cb2f9294778c283b9885b147
--- /dev/null
+++ b/server/src/url_rules/graph.rs
@@ -0,0 +1,831 @@
+//! Semantic URL graph: DFA traversal on host + path, query in context, generic fallback.
+
+use std::collections::HashMap;
+use std::sync::OnceLock;
+
+use url::Url;
+
+use super::graph_builder::GraphBuilder;
+use super::parse::{normalize_match_host, strip_tracking_query, UrlParts};
+
+#[derive(Debug, Clone, Default)]
+pub struct Context {
+    pub vars: HashMap<String, String>,
+    pub query: HashMap<String, String>,
+}
+
+pub type CanonicalFn = fn(&Context) -> Option<String>;
+
+#[derive(Clone, Copy)]
+pub enum EdgePattern {
+    Literal(&'static str),
+    Variable(&'static str),
+    /// Absorb any trailing segment without leaving this node (e.g. post title slug).
+    AbsorbAny,
+    /// Absorb segment when `cond(seg)` (e.g. subreddit listing suffix).
+    AbsorbIf(fn(&str) -> bool),
+}
+
+pub struct Edge {
+    pub pattern: EdgePattern,
+    pub target: &'static str,
+}
+
+pub struct Node {
+    pub edges: Vec<Edge>,
+    pub canonical: CanonicalFn,
+    pub parent: Option<&'static str>,
+}
+
+impl Node {
+    pub(crate) fn empty() -> Self {
+        Self {
+            edges: Vec::new(),
+            canonical: |_| None,
+            parent: None,
+        }
+    }
+}
+
+pub struct Graph {
+    pub nodes: HashMap<&'static str, Node>,
+}
+
+static GRAPH: OnceLock<Graph> = OnceLock::new();
+
+pub fn graph() -> &'static Graph {
+    GRAPH.get_or_init(build_graph)
+}
+
+impl Graph {
+    pub fn resolve_canonical(&self, parts: &UrlParts) -> Option<String> {
+        let mut query = parts.query.clone();
+        strip_tracking_query(&mut query);
+        let mut ctx = Context {
+            vars: HashMap::new(),
+            query,
+        };
+
+        if let Some(node_id) = self.traverse(parts, &mut ctx) {
+            if let Some(canon) = (self.nodes.get(node_id)?.canonical)(&ctx) {
+                return Some(canon);
+            }
+        }
+        Some(generic_canonical(parts))
+    }
+
+    pub fn breadcrumbs(&self, parts: &UrlParts) -> Vec<String> {
+        let mut query = parts.query.clone();
+        strip_tracking_query(&mut query);
+        let mut ctx = Context {
+            vars: HashMap::new(),
+            query,
+        };
+
+        if let Some(mut node_id) = self.traverse(parts, &mut ctx) {
+            let mut paths = Vec::new();
+            loop {
+                let node = match self.nodes.get(node_id) {
+                    Some(n) => n,
+                    None => break,
+                };
+                if let Some(url) = (node.canonical)(&ctx) {
+                    if paths.last() != Some(&url) {
+                        paths.push(url);
+                    }
+                }
+                match node.parent {
+                    Some(p) => node_id = p,
+                    None => break,
+                }
+            }
+            paths.reverse();
+            if !paths.is_empty() {
+                return paths;
+            }
+        }
+        generic_breadcrumbs(parts)
+    }
+
+    fn traverse(&self, parts: &UrlParts, ctx: &mut Context) -> Option<&'static str> {
+        let host = parts.match_host();
+        let mut node_id = match host.as_str() {
+            "reddit.com" => "reddit_root",
+            "youtube.com" => "youtube_root",
+            "youtu.be" => "youtu_be_entry",
+            _ => return None,
+        };
+
+        let segs: Vec<&str> = parts.path_segments.iter().map(String::as_str).collect();
+        let mut i = 0;
+        while i < segs.len() {
+            let seg = segs[i];
+            match self.follow_edge(node_id, seg, ctx) {
+                Ok(next) => {
+                    node_id = next;
+                    i += 1;
+                }
+                Err(()) => {
+                    if self.try_absorb(node_id, seg) {
+                        i += 1;
+                        continue;
+                    }
+                    return None;
+                }
+            }
+        }
+        Some(node_id)
+    }
+
+    fn follow_edge(
+        &self,
+        node_id: &'static str,
+        seg: &str,
+        ctx: &mut Context,
+    ) -> Result<&'static str, ()> {
+        let node = self.nodes.get(node_id).ok_or(())?;
+        for edge in &node.edges {
+            match edge.pattern {
+                EdgePattern::Literal(lit) if lit == seg => return Ok(edge.target),
+                EdgePattern::Variable(name) => {
+                    ctx.vars.insert(name.to_string(), seg.to_string());
+                    return Ok(edge.target);
+                }
+                EdgePattern::AbsorbAny
+                | EdgePattern::AbsorbIf(_)
+                | EdgePattern::Literal(_)
+                | EdgePattern::Variable(_) => {}
+            }
+        }
+        Err(())
+    }
+
+    fn try_absorb(&self, node_id: &'static str, seg: &str) -> bool {
+        let node = match self.nodes.get(node_id) {
+            Some(n) => n,
+            None => return false,
+        };
+        for edge in &node.edges {
+            match edge.pattern {
+                EdgePattern::AbsorbAny => return true,
+                EdgePattern::AbsorbIf(cond) if cond(seg) => return true,
+                EdgePattern::AbsorbIf(_) | EdgePattern::Literal(_) | EdgePattern::Variable(_) => {}
+            }
+        }
+        false
+    }
+
+    /// Test hook: terminal graph node and captured context after traversal.
+    #[cfg(test)]
+    pub fn traverse_terminal(&self, parts: &UrlParts) -> Option<(&'static str, Context)> {
+        let mut query = parts.query.clone();
+        strip_tracking_query(&mut query);
+        let mut ctx = Context {
+            vars: HashMap::new(),
+            query,
+        };
+        let node = self.traverse(parts, &mut ctx)?;
+        Some((node, ctx))
+    }
+}
+
+fn is_reddit_listing_suffix(seg: &str) -> bool {
+    matches!(seg, "hot" | "top" | "new" | "rising" | "controversial")
+}
+
+/// Percent-encode a path or query fragment so `&`, `?`, etc. cannot break URL structure.
+fn enc(s: &str) -> String {
+    urlencoding::encode(s).into_owned()
+}
+
+// --- Canonical formatters ---
+
+fn canon_reddit_root(_: &Context) -> Option<String> {
+    Some("https://reddit.com".to_string())
+}
+
+fn canon_reddit_r_hub(_: &Context) -> Option<String> {
+    Some("https://reddit.com/r".to_string())
+}
+
+fn canon_reddit_subreddit(ctx: &Context) -> Option<String> {
+    let sub = ctx.vars.get("subreddit")?;
+    Some(format!(
+        "https://reddit.com/r/{}",
+        enc(&sub.to_ascii_lowercase())
+    ))
+}
+
+fn canon_reddit_post(ctx: &Context) -> Option<String> {
+    let sub = ctx.vars.get("subreddit")?.to_ascii_lowercase();
+    let id = ctx.vars.get("post_id")?;
+    Some(format!(
+        "https://reddit.com/r/{}/comments/{}",
+        enc(&sub),
+        enc(id)
+    ))
+}
+
+fn canon_youtube_root(_: &Context) -> Option<String> {
+    Some("https://youtube.com".to_string())
+}
+
+fn canon_youtube_watch(ctx: &Context) -> Option<String> {
+    let v = ctx
+        .query
+        .get("v")
+        .or_else(|| ctx.vars.get("video_id"))?;
+    Some(format!("https://youtube.com/watch?v={}", enc(v)))
+}
+
+fn canon_youtu_be(ctx: &Context) -> Option<String> {
+    let v = ctx.vars.get("vid_id")?;
+    Some(format!("https://youtube.com/watch?v={}", enc(v)))
+}
+
+pub fn build_graph() -> Graph {
+    GraphBuilder::new()
+        .node("reddit_root")
+        .canonical(canon_reddit_root)
+        .edge(EdgePattern::Literal("r"), "reddit_r_hub")
+        .node("reddit_r_hub")
+        .parent("reddit_root")
+        .canonical(canon_reddit_r_hub)
+        .edge(EdgePattern::Variable("subreddit"), "reddit_subreddit")
+        .node("reddit_subreddit")
+        .parent("reddit_r_hub")
+        .canonical(canon_reddit_subreddit)
+        .edge(
+            EdgePattern::AbsorbIf(is_reddit_listing_suffix),
+            "reddit_subreddit",
+        )
+        .edge(EdgePattern::Literal("comments"), "reddit_comments_gate")
+        .node("reddit_comments_gate")
+        .parent("reddit_subreddit")
+        .canonical(canon_reddit_subreddit)
+        .edge(EdgePattern::Variable("post_id"), "reddit_post")
+        .node("reddit_post")
+        .parent("reddit_subreddit")
+        .canonical(canon_reddit_post)
+        .edge(EdgePattern::AbsorbAny, "reddit_post")
+        .node("youtube_root")
+        .canonical(canon_youtube_root)
+        .edge(EdgePattern::Literal("watch"), "youtube_watch")
+        .edge(EdgePattern::Literal("shorts"), "youtube_shorts_gate")
+        .node("youtube_watch")
+        .parent("youtube_root")
+        .canonical(canon_youtube_watch)
+        .node("youtube_shorts_gate")
+        .parent("youtube_root")
+        .canonical(canon_youtube_root)
+        .edge(EdgePattern::Variable("video_id"), "youtube_watch")
+        .node("youtu_be_entry")
+        .canonical(canon_youtube_root)
+        .edge(EdgePattern::Variable("vid_id"), "youtu_be_video")
+        .node("youtu_be_video")
+        .parent("youtube_root")
+        .canonical(canon_youtu_be)
+        .build()
+}
+
+// --- Generic internet fallback ---
+
+pub fn generic_canonical(parts: &UrlParts) -> String {
+    let host = normalize_match_host(&parts.host);
+    let path_segments: Vec<String> = parts.path_segments.clone();
+    let mut query = parts.query.clone();
+    strip_tracking_query(&mut query);
+
+    let mut url = if path_segments.is_empty() {
+        Url::parse(&format!("https://{host}"))
+            .unwrap_or_else(|_| Url::parse("https://invalid").unwrap())
+    } else {
+        let path = format!("/{}", path_segments.join("/"));
+        Url::parse(&format!("https://{host}{path}"))
+            .unwrap_or_else(|_| Url::parse("https://invalid").unwrap())
+    };
+
+    if !query.is_empty() {
+        let mut pairs: Vec<_> = query.iter().collect();
+        pairs.sort_by(|a, b| a.0.cmp(b.0));
+        url.query_pairs_mut().clear();
+        for (k, v) in pairs {
+            url.query_pairs_mut().append_pair(k, v);
+        }
+    }
+
+    let mut s = url.to_string();
+    if path_segments.is_empty() {
+        s = s.trim_end_matches('/').to_string();
+    }
+    s
+}
+
+pub fn generic_breadcrumbs(parts: &UrlParts) -> Vec<String> {
+    let host = normalize_match_host(&parts.host);
+    let n = parts.path_segments.len();
+    let mut out = Vec::new();
+
+    let base = generic_canonical(&UrlParts {
+        scheme: "https".to_string(),
+        host: host.clone(),
+        path_segments: vec![],
+        query: HashMap::new(),
+    });
+    out.push(base);
+
+    for i in 0..n {
+        let segs: Vec<String> = parts.path_segments[..=i].to_vec();
+        let url = generic_canonical(&UrlParts {
+            scheme: "https".to_string(),
+            host: host.clone(),
+            path_segments: segs,
+            query: HashMap::new(),
+        });
+        if out.last() != Some(&url) {
+            out.push(url);
+        }
+    }
+    out
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::url_rules::parse::test_parts;
+
+    fn g() -> &'static Graph {
+        graph()
+    }
+
+    fn canon(parts: &UrlParts) -> String {
+        g().resolve_canonical(parts).unwrap()
+    }
+
+    fn crumbs(parts: &UrlParts) -> Vec<String> {
+        g().breadcrumbs(parts)
+    }
+
+    fn terminal(parts: &UrlParts) -> Option<&'static str> {
+        g().traverse_terminal(parts).map(|(n, _)| n)
+    }
+
+    fn vars(parts: &UrlParts) -> HashMap<String, String> {
+        g().traverse_terminal(parts)
+            .map(|(_, c)| c.vars)
+            .unwrap_or_default()
+    }
+
+    #[test]
+    fn youtu_be_malicious_segment_encoded_not_injected() {
+        let p = test_parts("youtu.be", &["abc&t=1"], &[]);
+        assert_eq!(canon(&p

… preview truncated; 29,502 characters omitted

download full diff A

B — c_73d485b8085b (tommy-mor)

message

[9be54f7d] Add GitHub OAuth auth and move durable out of tree.

Gate votes behind session + pseudonym claim, project identity events into durable maps, and depend on tommy-mor/durable from git instead of the in-repo crates.

Co-authored-by: Cursor <cursoragent@cursor.com>

diff preview

diff --git a/Cargo.lock b/Cargo.lock
index 49a908ef935c430dbe63c6a28d8a24e38b489486..aa02997ad85777195f135bfd9456bcee0fc9a590 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -69,12 +69,6 @@ version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
 
-[[package]]
-name = "autocfg"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
-
 [[package]]
 name = "axum"
 version = "0.7.9"
@@ -211,21 +205,6 @@ dependencies = [
  "syn",
 ]
 
-[[package]]
-name = "bit-set"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
-dependencies = [
- "bit-vec",
-]
-
-[[package]]
-name = "bit-vec"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
-
 [[package]]
 name = "bitflags"
 version = "1.3.2"
@@ -435,19 +414,19 @@ dependencies = [
 [[package]]
 name = "durable"
 version = "0.2.0"
+source = "git+https://github.com/tommy-mor/durable.git?branch=main#a6c14eaa809693140eea0c22b07ef24d8e74adaf"
 dependencies = [
  "ciborium",
  "durable-derive",
- "proptest",
  "rocksdb",
  "serde",
- "tempfile",
  "thiserror",
 ]
 
 [[package]]
 name = "durable-derive"
 version = "0.2.0"
+source = "git+https://github.com/tommy-mor/durable.git?branch=main#a6c14eaa809693140eea0c22b07ef24d8e74adaf"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1238,15 +1217,6 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
 
-[[package]]
-name = "num-traits"
-version = "0.2.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
-dependencies = [
- "autocfg",
-]
-
 [[package]]
 name = "once_cell"
 version = "1.21.4"
@@ -1358,7 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
 dependencies = [
  "phf_shared",
- "rand 0.8.6",
+ "rand",
 ]
 
 [[package]]
@@ -1467,31 +1437,6 @@ dependencies = [
  "unicode-ident",
 ]
 
-[[package]]
-name = "proptest"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
-dependencies = [
- "bit-set",
- "bit-vec",
- "bitflags 2.11.1",
- "num-traits",
- "rand 0.9.4",
- "rand_chacha 0.9.0",
- "rand_xorshift",
- "regex-syntax",
- "rusty-fork",
- "tempfile",
- "unarray",
-]
-
-[[package]]
-name = "quick-error"
-version = "1.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
-
 [[package]]
 name = "quote"
 version = "1.0.45"
@@ -1520,18 +1465,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
 dependencies = [
  "libc",
- "rand_chacha 0.3.1",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
-dependencies = [
- "rand_chacha 0.9.0",
- "rand_core 0.9.5",
+ "rand_chacha",
+ "rand_core",
 ]
 
 [[package]]
@@ -1541,17 +1476,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.9.5",
+ "rand_core",
 ]
 
 [[package]]
@@ -1563,24 +1488,6 @@ dependencies = [
  "getrandom 0.2.17",
 ]
 
-[[package]]
-name = "rand_core"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
-dependencies = [
- "getrandom 0.3.4",
-]
-
-[[package]]
-name = "rand_xorshift"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
-dependencies = [
- "rand_core 0.9.5",
-]
-
 [[package]]
 name = "redox_syscall"
 version = "0.5.18"
@@ -1747,18 +1654,6 @@ version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
 
-[[package]]
-name = "rusty-fork"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
-dependencies = [
- "fnv",
- "quick-error",
- "tempfile",
- "wait-timeout",
-]
-
 [[package]]
 name = "ryu"
 version = "1.0.23"
@@ -1940,7 +1835,7 @@ dependencies = [
  "durable",
  "futures-util",
  "maud",
- "rand 0.8.6",
+ "rand",
  "reqwest",
  "serde",
  "serde_json",
@@ -2335,12 +2230,6 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
 
-[[package]]
-name = "unarray"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
-
 [[package]]
 name = "unicode-ident"
 version = "1.0.24"
@@ -2407,15 +2296,6 @@ version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
 
-[[package]]
-name = "wait-timeout"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
-dependencies = [
- "libc",
-]
-
 [[package]]
 name = "want"
 version = "0.3.1"
diff --git a/Cargo.toml b/Cargo.toml
index 9c387a8e106861dae210eaf07857eee3b8dad92a..156820ec5151b709a254dafc177da10488fee566 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,3 @@
 [workspace]
-members = ["server", "durable", "durable-derive"]
+members = ["server"]
 resolver = "2"
diff --git a/durable-derive/Cargo.toml b/durable-derive/Cargo.toml
deleted file mode 100644
index 1e9979ef32073a9fdaff80269342637ba641f798..0000000000000000000000000000000000000000
--- a/durable-derive/Cargo.toml
+++ /dev/null
@@ -1,15 +0,0 @@
-[package]
-name = "durable-derive"
-version = "0.2.0"
-edition = "2021"
-authors = ["Durable Contributors"]
-description = "#[derive(Durable)] macro for the durable crate"
-license = "MIT OR Apache-2.0"
-
-[lib]
-proc-macro = true
-
-[dependencies]
-syn = { version = "2.0", features = ["full"] }
-quote = "1.0"
-proc-macro2 = "1.0"
diff --git a/durable-derive/src/lib.rs b/durable-derive/src/lib.rs
deleted file mode 100644
index e646ca36ae9813bbe0b3d20af4ba81e4b0e306c2..0000000000000000000000000000000000000000
--- a/durable-derive/src/lib.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-//! `#[derive(Durable)]` for the `durable` crate.
-//!
-//! Turns a struct whose fields are durable schema types into a navigable schema:
-//!
-//! - implements `durable::Schema` for the struct,
-//! - generates a `{Name}Fields` extension trait (implemented for
-//!   `durable::Path<Name>`) with one navigator method per field, and
-//! - adds `Name::root()` / `Name::namespaced(name)` constructors.
-//!
-//! Each field is assigned a stable numeric id from its declaration order, which
-//! is encoded into the on-disk key. Reordering fields changes the layout; add new
-//! fields at the end.
-
-use proc_macro::TokenStream;
-use quote::quote;
-use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident};
-
-#[proc_macro_derive(Durable)]
-pub fn derive_durable(input: TokenStream) -> TokenStream {
-    let input = parse_macro_input!(input as DeriveInput);
-    let name = &input.ident;
-    let vis = &input.vis;
-    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
-
-    let fields = match &input.data {
-        Data::Struct(data) => match &data.fields {
-            Fields::Named(named) => &named.named,
-            _ => {
-                return syn::Error::new_spanned(
-                    name,
-                    "#[derive(Durable)] requires a struct with named fields",
-                )
-                .to_compile_error()
-                .into();
-            }
-        },
-        _ => {
-            return syn::Error::new_spanned(name, "#[derive(Durable)] is only supported on structs")
-                .to_compile_error()
-                .into();
-        }
-    };
-
-    let mut trait_methods = Vec::new();
-    let mut impl_methods = Vec::new();
-
-    for (index, field) in fields.iter().enumerate() {
-        let field_ident = field.ident.as_ref().expect("named field");
-        let field_ty = &field.ty;
-        let field_id = index as u32;
-        trait_methods.push(quote! {
-            fn #field_ident(&self) -> ::durable::Path<#field_ty>;
-        });
-        impl_methods.push(quote! {
-            fn #field_ident(&self) -> ::durable::Path<#field_ty> {
-                self.child_field(#field_id)
-            }
-        });
-    }
-
-    let trait_name = Ident::new(&format!("{name}Fields"), name.span());
-    let trait_doc = format!("Field navigators for [`{name}`], implemented for `durable::Path<{name}>`.");
-
-    let expanded = quote! {
-        impl #impl_generics ::durable::Schema for #name #ty_generics #where_clause {}
-
-        #[doc = #trait_doc]
-        #vis trait #trait_name {
-            #(#trait_methods)*
-        }
-
-        impl #impl_generics #trait_name for ::durable::Path<#name #ty_generics> #where_clause {
-            #(#impl_methods)*
-        }
-
-        impl #impl_generics #name #ty_generics #where_clause {
-            /// The root path of this schema (empty prefix; one root per database).
-            #vis fn root() -> ::durable::Path<#name #ty_generics> {
-                ::durable::Path::root()
-            }
-
-            /// A root path namespaced under `name`, to share a database between schemas.
-            #vis fn namespaced(name: &str) -> ::durable::Path<#name #ty_generics> {
-                ::durable::Path::namespaced(name)
-            }
-        }
-    };
-
-    expanded.into()
-}
diff --git a/durable/.gitignore b/durable/.gitignore
deleted file mode 100644
index 90c273ef12d6313594dde541c3465f2a47739729..0000000000000000000000000000000000000000
--- a/durable/.gitignore
+++ /dev/null
@@ -1,35 +0,0 @@
-all.txt
-
-# Generated by Cargo
-# will have compiled files and executables
-debug/
-target/
-
-# These are backup files generated by rustfmt
-**/*.rs.bk
-
-# MSVC Windows builds of rustc generate these, which store debugging information
-*.pdb
-
-# Generated by cargo mutants
-# Contains mutation testing data
-**/mutants.out*/
-
-# RustRover
-#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-#  and can be added to the global gitignore or merged into this file.  For a more nuclear
-#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
-
-
-# Added by cargo
-
-/target
-
-
-# Added by cargo
-#
-# already existing elements were commented out
-
-#/target
diff --git a/durable/Cargo.toml b/durable/Cargo.toml
deleted file mode 100644
index 95be0c71d64862c7cdb78e64fca768dcf83be961..000000000000000000000000000000000

… preview truncated; 175,341 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.