remove edges, petgraph, and DAG ordering
Strip the entire edges system (before + blocks edge types), petgraph
dependency, Ordering enum, and all DAG code paths. Back to pure
lexicographic position ordering.

- Remove ops/edge.rs, commands/edge.rs, edge CLI subcommand
- Remove EdgeType, TaskEdge, InvalidEdgeTypeError from models
- Remove CycleDetected error variant
- Remove Ordering enum and RANGER_DAG_ORDER env var
- Remove DAG paths from list, move_task, edit, reorder
- Drop task_edges table via migration 011
- Remove petgraph from Cargo.toml

Blockers will be re-added as a simpler mechanism.
change tmrstnuomyutntwuquytxxoktlsuppqp
commit 3f0c680e58a903aee9ccecd3cc6e06f9558d2579
author Alpha Chen <alpha@kejadlen.dev>
date
parent nkpmryzv
diff --git a/Cargo.lock b/Cargo.lock
index 6156cd0..3207865 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -524,12 +524,6 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
 
-[[package]]
-name = "fixedbitset"
-version = "0.5.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
-
 [[package]]
 name = "float-cmp"
 version = "0.10.0"
@@ -1300,18 +1294,6 @@ version = "2.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
 
-[[package]]
-name = "petgraph"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
-dependencies = [
- "fixedbitset",
- "hashbrown 0.15.5",
- "indexmap",
- "serde",
-]
-
 [[package]]
 name = "pin-project-lite"
 version = "0.2.17"
@@ -1507,7 +1489,6 @@ dependencies = [
  "color-eyre",
  "jiff",
  "maud",
- "petgraph",
  "predicates",
  "rand",
  "serde",
diff --git a/Cargo.toml b/Cargo.toml
index eee5950..0936b4d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,7 +19,6 @@ serde = { version = "*", features = ["derive"] }
 serde_json = "*"
 sqlx = { version = "*", features = ["runtime-tokio", "sqlite", "migrate"] }
 maud = { version = "*", features = ["axum"] }
-petgraph = "*"
 thiserror = "*"
 tokio = { version = "*", features = ["full"] }
 xdg = "*"
diff --git a/migrations/011_drop_task_edges.sql b/migrations/011_drop_task_edges.sql
new file mode 100644
index 0000000..f5706e5
--- /dev/null
+++ b/migrations/011_drop_task_edges.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS task_edges;
diff --git a/src/bin/ranger/commands/backlog.rs b/src/bin/ranger/commands/backlog.rs
index 31c279f..a611656 100644
--- a/src/bin/ranger/commands/backlog.rs
+++ b/src/bin/ranger/commands/backlog.rs
@@ -7,7 +7,6 @@ use ranger::models::{Backlog, State};
 use ranger::ops;
 use ranger::ops::task::ListFilter;
 
-use super::task::resolve_ordering;
 use crate::completions;
 use crate::output;
 
@@ -50,7 +49,6 @@ pub enum BacklogCommands {
 
 pub async fn run(pool: &SqlitePool, command: BacklogCommands, json: bool) -> Result<()> {
     let mut conn = pool.acquire().await?;
-    let ordering = resolve_ordering();
 
     match command {
         BacklogCommands::Create { name } => {
@@ -86,7 +84,7 @@ pub async fn run(pool: &SqlitePool, command: BacklogCommands, json: bool) -> Res
                         state: Some(state.clone()),
                         ..Default::default()
                     };
-                    let tasks = ops::task::list(&mut conn, backlog.id, &filter, ordering).await?;
+                    let tasks = ops::task::list(&mut conn, backlog.id, &filter).await?;
                     if !tasks.is_empty() {
                         state_groups
                             .insert(state.to_string(), serde_json::to_value(&tasks).unwrap());
@@ -108,7 +106,7 @@ pub async fn run(pool: &SqlitePool, command: BacklogCommands, json: bool) -> Res
                         state: Some(state.clone()),
                         ..Default::default()
                     };
-                    let tasks = ops::task::list(&mut conn, backlog.id, &filter, ordering).await?;
+                    let tasks = ops::task::list(&mut conn, backlog.id, &filter).await?;
                     if !tasks.is_empty() {
                         println!("\n[{}]", state);
                         for t in &tasks {
diff --git a/src/bin/ranger/commands/edge.rs b/src/bin/ranger/commands/edge.rs
deleted file mode 100644
index f4fbb8e..0000000
--- a/src/bin/ranger/commands/edge.rs
+++ /dev/null
@@ -1,106 +0,0 @@
-use clap::Subcommand;
-use clap_complete::engine::ArgValueCompleter;
-use color_eyre::eyre::Result;
-use ranger::db::SqlitePool;
-use ranger::models::EdgeType;
-use ranger::ops;
-
-use super::task::default_backlog_id;
-use crate::completions;
-use crate::output;
-
-#[derive(Subcommand)]
-pub enum EdgeCommands {
-    /// Add an edge between tasks
-    Add {
-        /// Source task key or prefix
-        #[arg(add = ArgValueCompleter::new(completions::complete_task_keys))]
-        from: String,
-        /// Edge type: blocks or before
-        edge_type: EdgeType,
-        /// Target task key or prefix
-        #[arg(add = ArgValueCompleter::new(completions::complete_task_keys))]
-        to: String,
-    },
-    /// Remove an edge between tasks
-    #[command(visible_alias = "rm")]
-    Remove {
-        /// Source task key or prefix
-        #[arg(add = ArgValueCompleter::new(completions::complete_task_keys))]
-        from: String,
-        /// Edge type: blocks or before
-        edge_type: EdgeType,
-        /// Target task key or prefix
-        #[arg(add = ArgValueCompleter::new(completions::complete_task_keys))]
-        to: String,
-    },
-    /// List edges for a task
-    #[command(visible_alias = "ls")]
-    List {
-        /// Task key or prefix (omit to list all edges)
-        #[arg(add = ArgValueCompleter::new(completions::complete_task_keys))]
-        task: Option<String>,
-    },
-}
-
-pub async fn run(pool: &SqlitePool, command: EdgeCommands, json: bool) -> Result<()> {
-    let backlog_scope = default_backlog_id(pool).await;
-    let mut conn = pool.acquire().await?;
-
-    match command {
-        EdgeCommands::Add {
-            from,
-            edge_type,
-            to,
-        } => {
-            let from_task = ops::task::get_by_key_prefix(&mut conn, &from, backlog_scope).await?;
-            let to_task = ops::task::get_by_key_prefix(&mut conn, &to, backlog_scope).await?;
-
-            let edge = ops::edge::add(&mut conn, from_task.id, to_task.id, edge_type).await?;
-            output::print(&edge, json, |e| {
-                println!("{} {} {}", from, e.edge_type, to);
-            });
-        }
-        EdgeCommands::Remove {
-            from,
-            edge_type,
-            to,
-        } => {
-            let from_task = ops::task::get_by_key_prefix(&mut conn, &from, backlog_scope).await?;
-            let to_task = ops::task::get_by_key_prefix(&mut conn, &to, backlog_scope).await?;
-
-            let removed = ops::edge::remove(&mut conn, from_task.id, to_task.id, edge_type).await?;
-            if !json {
-                if removed {
-                    println!("Removed edge from {} to {}", from, to);
-                } else {
-                    println!("No matching edge found");
-                }
-            }
-        }
-        EdgeCommands::List { task } => {
-            let edges = if let Some(ref key) = task {
-                let t = ops::task::get_by_key_prefix(&mut conn, key, backlog_scope).await?;
-                ops::edge::list_for_task(&mut conn, t.id).await?
-            } else {
-                ops::edge::list_all(&mut conn).await?
-            };
-
-            if json {
-                output::print_list(&edges, json, |_| {});
-            } else {
-                for e in &edges {
-                    let from = ops::task::get_by_id(&mut conn, e.from_task_id).await?;
-                    let to = ops::task::get_by_id(&mut conn, e.to_task_id).await?;
-                    println!(
-                        "{} {} {}",
-                        &from.key[..8.min(from.key.len())],
-                        e.edge_type,
-                        &to.key[..8.min(to.key.len())],
-                    );
-                }
-            }
-        }
-    }
-    Ok(())
-}
diff --git a/src/bin/ranger/commands/mod.rs b/src/bin/ranger/commands/mod.rs
index c0fd35b..18d6800 100644
--- a/src/bin/ranger/commands/mod.rs
+++ b/src/bin/ranger/commands/mod.rs
@@ -1,6 +1,5 @@
 pub mod backlog;
 pub mod comment;
-pub mod edge;
 pub mod serve;
 pub mod tag;
 pub mod task;
diff --git a/src/bin/ranger/commands/serve.rs b/src/bin/ranger/commands/serve.rs
index 2a4cbfa..703a481 100644
--- a/src/bin/ranger/commands/serve.rs
+++ b/src/bin/ranger/commands/serve.rs
@@ -4,15 +4,13 @@ use axum::response::{IntoResponse, Redirect};
 use axum::{Router, routing::get};
 use maud::{DOCTYPE, Markup, PreEscaped, html};
 use ranger::key;
-use ranger::models::{Ordering, Task};
+use ranger::models::Task;
 use ranger::ops;
 use ranger::ops::task::ListFilter;
 use sqlx::SqlitePool;
 use std::net::SocketAddr;
 use tokio::net::TcpListener;
 
-use super::task::resolve_ordering;
-
 /// Static CSS embedded at compile time from `static/style.css`.
 const STYLE_CSS: &str = include_str!("../../../../static/style.css");
 
@@ -20,7 +18,6 @@ const STYLE_CSS: &str = include_str!("../../../../static/style.css");
 struct AppState {
     pool: SqlitePool,
     default_backlog: Option<String>,
-    ordering: Ordering,
 }
 
 pub async fn run(
@@ -31,7 +28,6 @@ pub async fn run(
     let state = AppState {
         pool: pool.clone(),
         default_backlog,
-        ordering: resolve_ordering(),
     };
 
     let app = Router::new()
@@ -122,7 +118,7 @@ async fn render_board(state: &AppState, backlog_name: &str) -> color_eyre::Resul
             state: Some(s.clone()),
             ..Default::default()
         };
-        let tasks = ops::task::list(&mut conn, backlog.id, &filter, state.ordering).await?;
+        let tasks = ops::task::list(&mut conn, backlog.id, &filter).await?;
         let views = to_task_views(&tasks, &prefixes, &mut conn).await?;
         match s {
             ranger::models::State::InProgress => in_progress = views,
diff --git a/src/bin/ranger/commands/task.rs b/src/bin/ranger/commands/task.rs
index 78444d1..4fc5db4 100644
--- a/src/bin/ranger/commands/task.rs
+++ b/src/bin/ranger/commands/task.rs
@@ -5,22 +5,13 @@ use clap_complete::engine::ArgValueCompleter;
 use color_eyre::eyre::{Result, bail};
 use ranger::db::{SqliteConnection, SqlitePool};
 use ranger::key;
-use ranger::models::{Ordering, State, Task};
+use ranger::models::{State, Task};
 use ranger::ops;
 use ranger::ops::task::{ListFilter, Placement};
 
 use crate::completions;
 use crate::output;
 
-/// Read the ordering strategy from the `RANGER_DAG_ORDER` env var.
-/// Set `RANGER_DAG_ORDER=1` to use DAG topological ordering.
-pub fn resolve_ordering() -> Ordering {
-    match std::env::var("RANGER_DAG_ORDER").as_deref() {
-        Ok("1") | Ok("true") => Ordering::Dag,
-        _ => Ordering::Position,
-    }
-}
-
 /// Positioning flags shared by create, edit, and move.
 #[derive(Args)]
 pub struct PositionArgs {
@@ -179,7 +170,6 @@ pub async fn default_backlog_id(pool: &SqlitePool) -> Option<i64> {
 
 pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result<()> {
     let backlog_scope = default_backlog_id(pool).await;
-    let ordering = resolve_ordering();
 
     match command {
         TaskCommands::Create {
@@ -207,7 +197,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
             .await?;
 
             if let Some(ref anchors) = anchors {
-                ops::task::move_task(&mut tx, &task, anchors.as_placement(), ordering).await?;
+                ops::task::move_task(&mut tx, &task, anchors.as_placement()).await?;
             }
 
             tx.commit().await?;
@@ -234,7 +224,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
                 let bl = ops::backlog::get_by_name(&mut conn, backlog_name).await?;
                 let backlog_keys = ops::task::keys_for_backlog(&mut conn, bl.id).await?;
                 let prefixes = key::unique_prefix_lengths(&backlog_keys);
-                let tasks = ops::task::list(&mut conn, bl.id, &filter, ordering).await?;
+                let tasks = ops::task::list(&mut conn, bl.id, &filter).await?;
                 output::print_list(&tasks, json, |t| print_task(t, &prefixes));
             } else {
                 // List all tasks (no backlog filter)
@@ -243,7 +233,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
                 let backlogs = ops::backlog::list(&mut conn).await?;
                 let mut all_tasks = Vec::new();
                 for bl in &backlogs {
-                    let tasks = ops::task::list(&mut conn, bl.id, &filter, ordering).await?;
+                    let tasks = ops::task::list(&mut conn, bl.id, &filter).await?;
                     for t in tasks {
                         if !all_tasks.iter().any(|at: &Task| at.id == t.id) {
                             all_tasks.push(t);
@@ -258,14 +248,12 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
             let task = ops::task::get_by_key_prefix(&mut conn, &key, backlog_scope).await?;
             let comments = ops::comment::list(&mut conn, task.id).await?;
             let tags = ops::tag::list_for_task(&mut conn, task.id).await?;
-            let edges = ops::edge::list_for_task(&mut conn, task.id).await?;
 
             if json {
                 let detail = serde_json::json!({
                     "task": task,
                     "comments": comments,
                     "tags": tags,
-                    "edges": edges,
                 });
                 println!("{}", serde_json::to_string_pretty(&detail).unwrap());
             } else {
@@ -277,22 +265,6 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
                     let tag_names: Vec<&str> = tags.iter().map(|t| t.name.as_str()).collect();
                     println!("Tags:    {}", tag_names.join(", "));
                 }
-                if !edges.is_empty() {
-                    for e in &edges {
-                        let other_id = if e.from_task_id == task.id {
-                            e.to_task_id
-                        } else {
-                            e.from_task_id
-                        };
-                        let other = ops::task::get_by_id(&mut conn, other_id).await?;
-                        let other_key = output::format_key_from_map(&other.key, &prefixes);
-                        if e.from_task_id == task.id {
-                            println!("Edge:    {} → {}", e.edge_type, other_key);
-                        } else {
-                            println!("Edge:    {} ← {}", e.edge_type, other_key);
-                        }
-                    }
-                }
                 if !comments.is_empty() {
                     println!();
                     for c in &comments {
@@ -320,12 +292,11 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
                 title.as_deref(),
                 description.as_deref(),
                 state,
-                ordering,
             )
             .await?;
 
             if let Some(ref anchors) = anchors {
-                ops::task::move_task(&mut conn, &updated, anchors.as_placement(), ordering).await?;
+                ops::task::move_task(&mut conn, &updated, anchors.as_placement()).await?;
             }
 
             let all_keys = ops::task::all_keys(&mut conn).await?;
@@ -339,8 +310,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
 
             match anchors {
                 Some(anchors) => {
-                    ops::task::move_task(&mut conn, &task, anchors.as_placement(), ordering)
-                        .await?;
+                    ops::task::move_task(&mut conn, &task, anchors.as_placement()).await?;
                     let all_keys = ops::task::all_keys(&mut conn).await?;
                     let prefixes = key::unique_prefix_lengths(&all_keys);
                     println!(
diff --git a/src/bin/ranger/main.rs b/src/bin/ranger/main.rs
index eb11752..71c9664 100644
--- a/src/bin/ranger/main.rs
+++ b/src/bin/ranger/main.rs
@@ -45,12 +45,6 @@ enum Commands {
         #[command(subcommand)]
         command: commands::comment::CommentCommands,
     },
-    /// Manage task edges (dependencies and ordering)
-    #[command(visible_alias = "e")]
-    Edge {
-        #[command(subcommand)]
-        command: commands::edge::EdgeCommands,
-    },
     /// Manage tags
     #[command(visible_alias = "g")]
     Tag {
@@ -110,9 +104,6 @@ async fn async_main() -> color_eyre::Result<()> {
         Some(Commands::Comment { command }) => {
             commands::comment::run(&pool, command, cli.json).await?;
         }
-        Some(Commands::Edge { command }) => {
-            commands::edge::run(&pool, command, cli.json).await?;
-        }
         Some(Commands::Tag { command }) => {
             commands::tag::run(&pool, command, cli.json).await?;
         }
diff --git a/src/error.rs b/src/error.rs
index 92e71de..e361f0e 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -11,8 +11,6 @@ pub enum RangerError {
     },
     #[error("backlog not found: '{0}'")]
     BacklogNotFound(String),
-    #[error("adding this edge would create a cycle")]
-    CycleDetected,
     #[error("database error: {0}")]
     Db(#[from] sqlx::Error),
     #[error("migration error: {0}")]
diff --git a/src/models.rs b/src/models.rs
index 1ea0a12..9702c9c 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -3,16 +3,6 @@ use sqlx::FromRow;
 
 use crate::timestamp::Timestamp;
 
-/// Controls how tasks are ordered within a backlog/state group.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub enum Ordering {
-    /// Lexicographic position strings (legacy).
-    #[default]
-    Position,
-    /// DAG topological sort using `before` edges, with task ID as tiebreaker.
-    Dag,
-}
-
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(rename_all = "snake_case")]
 pub enum State {
@@ -125,74 +115,6 @@ pub struct Comment {
     pub created_at: Timestamp,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum EdgeType {
-    Blocks,
-    Before,
-}
-
-impl EdgeType {
-    pub fn as_str(&self) -> &'static str {
-        match self {
-            EdgeType::Blocks => "blocks",
-            EdgeType::Before => "before",
-        }
-    }
-}
-
-#[derive(Debug, thiserror::Error)]
-#[error("invalid edge type: '{0}'")]
-pub struct InvalidEdgeTypeError(String);
-
-impl std::str::FromStr for EdgeType {
-    type Err = InvalidEdgeTypeError;
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s {
-            "blocks" => Ok(EdgeType::Blocks),
-            "before" => Ok(EdgeType::Before),
-            _ => Err(InvalidEdgeTypeError(s.to_string())),
-        }
-    }
-}
-
-impl std::fmt::Display for EdgeType {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(self.as_str())
-    }
-}
-
-impl sqlx::Type<sqlx::Sqlite> for EdgeType {
-    fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
-        <str as sqlx::Type<sqlx::Sqlite>>::type_info()
-    }
-}
-
-impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for EdgeType {
-    fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
-        let s = <&str as sqlx::Decode<sqlx::Sqlite>>::decode(value)?;
-        Ok(s.parse::<EdgeType>()?)
-    }
-}
-
-impl sqlx::Encode<'_, sqlx::Sqlite> for EdgeType {
-    fn encode_by_ref(
-        &self,
-        buf: &mut <sqlx::Sqlite as sqlx::Database>::ArgumentBuffer<'_>,
-    ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
-        <&str as sqlx::Encode<sqlx::Sqlite>>::encode_by_ref(&self.as_str(), buf)
-    }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
-pub struct TaskEdge {
-    pub id: i64,
-    pub from_task_id: i64,
-    pub to_task_id: i64,
-    pub edge_type: EdgeType,
-    pub created_at: Timestamp,
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -212,39 +134,6 @@ mod tests {
         }
     }
 
-    #[test]
-    fn edge_type_roundtrips_through_display_and_parse() {
-        for (edge_type, expected) in [(EdgeType::Blocks, "blocks"), (EdgeType::Before, "before")] {
-            assert_eq!(edge_type.as_str(), expected);
-            assert_eq!(edge_type.to_string(), expected);
-            let parsed: EdgeType = expected.parse().unwrap();
-            assert_eq!(parsed.as_str(), expected);
-        }
-    }
-
-    #[test]
-    fn edge_type_parse_invalid_returns_error() {
-        let err = "bogus".parse::<EdgeType>().unwrap_err();
-        assert_eq!(err.to_string(), "invalid edge type: 'bogus'");
-    }
-
-    #[tokio::test]
-    async fn edge_type_sqlx_encode_roundtrips() {
-        let dir = tempfile::tempdir().unwrap();
-        let pool = crate::db::connect(&dir.path().join("test.db"))
-            .await
-            .unwrap();
-        let mut conn = pool.acquire().await.unwrap();
-
-        let edge_type = EdgeType::Blocks;
-        let row: (String,) = sqlx::query_as("SELECT ?")
-            .bind(&edge_type)
-            .fetch_one(&mut *conn)
-            .await
-            .unwrap();
-        assert_eq!(row.0, "blocks");
-    }
-
     #[test]
     fn state_parse_invalid_returns_error() {
         let err = "bogus".parse::<State>().unwrap_err();
diff --git a/src/ops/edge.rs b/src/ops/edge.rs
deleted file mode 100644
index 4085081..0000000
--- a/src/ops/edge.rs
+++ /dev/null
@@ -1,861 +0,0 @@
-use crate::error::RangerError;
-use crate::models::{EdgeType, TaskEdge};
-use petgraph::graph::DiGraph;
-use petgraph::graph::NodeIndex;
-use sqlx::sqlite::SqliteConnection;
-use std::cmp::Reverse;
-use std::collections::{BinaryHeap, HashMap, HashSet};
-
-pub async fn add(
-    conn: &mut SqliteConnection,
-    from_task_id: i64,
-    to_task_id: i64,
-    edge_type: EdgeType,
-) -> Result<TaskEdge, RangerError> {
-    // Load the current DAG and check if adding this edge would create a cycle
-    let edges = list_all(&mut *conn).await?;
-    check_cycle(&edges, from_task_id, to_task_id)?;
-
-    let edge = sqlx::query_as::<_, TaskEdge>(
-        "INSERT INTO task_edges (from_task_id, to_task_id, edge_type) \
-         VALUES (?, ?, ?) \
-         RETURNING id, from_task_id, to_task_id, edge_type, created_at",
-    )
-    .bind(from_task_id)
-    .bind(to_task_id)
-    .bind(&edge_type)
-    .fetch_one(&mut *conn)
-    .await?;
-
-    Ok(edge)
-}
-
-pub async fn remove(
-    conn: &mut SqliteConnection,
-    from_task_id: i64,
-    to_task_id: i64,
-    edge_type: EdgeType,
-) -> Result<bool, RangerError> {
-    let result = sqlx::query(
-        "DELETE FROM task_edges WHERE from_task_id = ? AND to_task_id = ? AND edge_type = ?",
-    )
-    .bind(from_task_id)
-    .bind(to_task_id)
-    .bind(&edge_type)
-    .execute(&mut *conn)
-    .await?;
-
-    Ok(result.rows_affected() > 0)
-}
-
-pub async fn list_for_task(
-    conn: &mut SqliteConnection,
-    task_id: i64,
-) -> Result<Vec<TaskEdge>, RangerError> {
-    let edges = sqlx::query_as::<_, TaskEdge>(
-        "SELECT id, from_task_id, to_task_id, edge_type, created_at \
-         FROM task_edges \
-         WHERE from_task_id = ? OR to_task_id = ? \
-         ORDER BY created_at",
-    )
-    .bind(task_id)
-    .bind(task_id)
-    .fetch_all(&mut *conn)
-    .await?;
-
-    Ok(edges)
-}
-
-pub async fn list_all(conn: &mut SqliteConnection) -> Result<Vec<TaskEdge>, RangerError> {
-    let edges = sqlx::query_as::<_, TaskEdge>(
-        "SELECT id, from_task_id, to_task_id, edge_type, created_at \
-         FROM task_edges \
-         ORDER BY created_at",
-    )
-    .fetch_all(&mut *conn)
-    .await?;
-
-    Ok(edges)
-}
-
-/// Build a petgraph DiGraph from task edges. Returns the graph and a mapping
-/// from task_id to NodeIndex.
-pub fn build_dag(edges: &[TaskEdge]) -> (DiGraph<i64, EdgeType>, HashMap<i64, NodeIndex>) {
-    let mut graph = DiGraph::new();
-    let mut node_map: HashMap<i64, NodeIndex> = HashMap::new();
-
-    for edge in edges {
-        let from_idx = *node_map
-            .entry(edge.from_task_id)
-            .or_insert_with(|| graph.add_node(edge.from_task_id));
-        let to_idx = *node_map
-            .entry(edge.to_task_id)
-            .or_insert_with(|| graph.add_node(edge.to_task_id));
-        graph.add_edge(from_idx, to_idx, edge.edge_type.clone());
-    }
-
-    (graph, node_map)
-}
-
-/// Check if adding an edge from → to would create a cycle.
-fn check_cycle(
-    existing_edges: &[TaskEdge],
-    from_task_id: i64,
-    to_task_id: i64,
-) -> Result<(), RangerError> {
-    let (mut graph, mut node_map) = build_dag(existing_edges);
-
-    let from_idx = *node_map
-        .entry(from_task_id)
-        .or_insert_with(|| graph.add_node(from_task_id));
-    let to_idx = *node_map
-        .entry(to_task_id)
-        .or_insert_with(|| graph.add_node(to_task_id));
-
-    graph.add_edge(from_idx, to_idx, EdgeType::Blocks);
-
-    if petgraph::algo::is_cyclic_directed(&graph) {
-        return Err(RangerError::CycleDetected);
-    }
-
-    Ok(())
-}
-
-/// Produce a topological sort of task IDs from edges. Tasks with no edges
-/// are not included — callers should merge with the full task list.
-/// Uses task_id as a stable tiebreaker for deterministic ordering.
-pub fn topological_sort(edges: &[TaskEdge]) -> Vec<i64> {
-    if edges.is_empty() {
-        return Vec::new();
-    }
-
-    let (graph, node_map) = build_dag(edges);
-
-    // petgraph's toposort returns nodes in topological order
-    match petgraph::algo::toposort(&graph, None) {
-        Ok(sorted) => sorted.iter().map(|idx| graph[*idx]).collect(),
-        Err(_) => {
-            // cov-excl-start — cycles are prevented on insert; this is defensive
-            let mut ids: Vec<i64> = node_map.keys().copied().collect();
-            ids.sort();
-            ids
-            // cov-excl-stop
-        }
-    }
-}
-
-/// Deterministic topological sort using Kahn's algorithm with min-ID tiebreaker.
-///
-/// Only considers `before` edges between task IDs in `task_ids`.
-/// Every ID in `task_ids` appears exactly once in the output.
-pub fn ordered_ids(task_ids: &[i64], edges: &[TaskEdge]) -> Vec<i64> {
-    if task_ids.is_empty() {
-        return Vec::new();
-    }
-
-    let id_set: HashSet<i64> = task_ids.iter().copied().collect();
-
-    // Build adjacency list and in-degree map — only 'before' edges within the set
-    let mut adj: HashMap<i64, Vec<i64>> = HashMap::new();
-    let mut in_degree: HashMap<i64, usize> = HashMap::new();
-
-    for &id in task_ids {
-        adj.entry(id).or_default();
-        in_degree.entry(id).or_insert(0);
-    }
-
-    for edge in edges {
-        if edge.edge_type == EdgeType::Before
-            && id_set.contains(&edge.from_task_id)
-            && id_set.contains(&edge.to_task_id)
-        {
-            adj.entry(edge.from_task_id)
-                .or_default()
-                .push(edge.to_task_id);
-            *in_degree.entry(edge.to_task_id).or_insert(0) += 1;
-        }
-    }
-
-    // Kahn's algorithm with a min-heap for deterministic ordering
-    let mut heap: BinaryHeap<Reverse<i64>> = BinaryHeap::new();
-    for (&id, &deg) in &in_degree {
-        if deg == 0 {
-            heap.push(Reverse(id));
-        }
-    }
-
-    let mut result = Vec::with_capacity(task_ids.len());
-    while let Some(Reverse(id)) = heap.pop() {
-        result.push(id);
-        for &next in &adj[&id] {
-            let deg = in_degree.get_mut(&next).unwrap();
-            *deg -= 1;
-            if *deg == 0 {
-                heap.push(Reverse(next));
-            }
-        }
-    }
-
-    result
-}
-
-/// Remove a task from any `before` chains it participates in.
-///
-/// If the task has predecessors (P → task) and a successor (task → S),
-/// reconnects each predecessor directly to the successor (P → S).
-pub async fn splice_out_before(
-    conn: &mut SqliteConnection,
-    task_id: i64,
-) -> Result<(), RangerError> {
-    // Get the outgoing 'before' edge: task → successor
-    let outgoing: Option<TaskEdge> = sqlx::query_as(
-        "SELECT id, from_task_id, to_task_id, edge_type, created_at \
-         FROM task_edges WHERE from_task_id = ? AND edge_type = 'before'",
-    )
-    .bind(task_id)
-    .fetch_optional(&mut *conn)
-    .await?;
-
-    // Get all incoming 'before' edges: predecessor → task
-    let incoming: Vec<TaskEdge> = sqlx::query_as(
-        "SELECT id, from_task_id, to_task_id, edge_type, created_at \
-         FROM task_edges WHERE to_task_id = ? AND edge_type = 'before'",
-    )
-    .bind(task_id)
-    .fetch_all(&mut *conn)
-    .await?;
-
-    // Delete all 'before' edges involving this task
-    sqlx::query("DELETE FROM task_edges WHERE (from_task_id = ? OR to_task_id = ?) AND edge_type = 'before'")
-        .bind(task_id)
-        .bind(task_id)
-        .execute(&mut *conn)
-        .await?;
-
-    // Reconnect: each predecessor → successor
-    if let Some(succ) = &outgoing {
-        for pred in &incoming {
-            sqlx::query(
-                "INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, edge_type) \
-                 VALUES (?, ?, 'before')",
-            )
-            .bind(pred.from_task_id)
-            .bind(succ.to_task_id)
-            .execute(&mut *conn)
-            .await?;
-        }
-    }
-
-    Ok(())
-}
-
-/// Insert a task immediately before a target in the `before` chain.
-///
-/// Any predecessors of the target are rewired to point to the inserted task.
-pub async fn insert_before_task(
-    conn: &mut SqliteConnection,
-    task_id: i64,
-    target_id: i64,
-) -> Result<(), RangerError> {
-    // Rewire incoming 'before' edges to target → point to task instead
-    let incoming: Vec<TaskEdge> = sqlx::query_as(
-        "SELECT id, from_task_id, to_task_id, edge_type, created_at \
-         FROM task_edges WHERE to_task_id = ? AND edge_type = 'before'",
-    )
-    .bind(target_id)
-    .fetch_all(&mut *conn)
-    .await?;
-
-    for pred in &incoming {
-        sqlx::query("DELETE FROM task_edges WHERE id = ?")
-            .bind(pred.id)
-            .execute(&mut *conn)
-            .await?;
-        sqlx::query(
-            "INSERT OR IGNORE INTO task_edges (from_task_id, to_task_id, edge_type) \
-             VALUES (?, ?, 'before')",
-        )
-        .bind(pred.from_task_id)
-        .bind(task_id)
-        .execute(&mut *conn)
-        .await?;
-    }
-
-    // Add task → target
-    add(&mut *conn, task_id, target_id, EdgeType::Before).await?;
-    Ok(())
-}
-
-/// Insert a task immediately after an anchor in the `before` chain.
-///
-/// All outgoing `before` edges from the anchor are rewired through the task.
-pub async fn insert_after_task(
-    conn: &mut SqliteConnection,
-    task_id: i64,
-    anchor_id: i64,
-) -> Result<(), RangerError> {
-    // Collect all outgoing 'before' edges from anchor
-    let outgoing: Vec<TaskEdge> = sqlx::query_as(
-        "SELECT id, from_task_id, to_task_id, edge_type, created_at \
-         FROM task_edges WHERE from_task_id = ? AND edge_type = 'before'",
-    )
-    .bind(anchor_id)
-    .fetch_all(&mut *conn)
-    .await?;
-
-    for succ in &outgoing {
-        let successor_id = succ.to_task_id;
-        // Remove anchor → successor
-        remove(&mut *conn, anchor_id, successor_id, EdgeType::Before).await?;
-        // Add task → successor
-        add(&mut *conn, task_id, successor_id, EdgeType::Before).await?;
-    }
-
-    // Add anchor → task
-    add(&mut *conn, anchor_id, task_id, EdgeType::Before).await?;
-    Ok(())
-}
-
-/// Fetch all edges involving any of the given task IDs.
-pub async fn list_for_task_ids(
-    conn: &mut SqliteConnection,
-    task_ids: &[i64],
-) -> Result<Vec<TaskEdge>, RangerError> {
-    if task_ids.is_empty() {
-        return Ok(Vec::new());
-    }
-
-    // Build a comma-separated placeholder list
-    let placeholders: String = task_ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
-    let query = format!(
-        "SELECT id, from_task_id, to_task_id, edge_type, created_at \
-         FROM task_edges \
-         WHERE from_task_id IN ({placeholders}) OR to_task_id IN ({placeholders}) \
-         ORDER BY created_at"
-    );
-
-    let mut q = sqlx::query_as::<_, TaskEdge>(&query);
-    // Bind task_ids twice (once for each IN clause)
-    for &id in task_ids {
-        q = q.bind(id);
-    }
-    for &id in task_ids {
-        q = q.bind(id);
-    }
-
-    let edges = q.fetch_all(&mut *conn).await?;
-    Ok(edges)
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::models::State;
-    use crate::ops::{self, backlog, task};
-
-    async fn test_pool() -> sqlx::SqlitePool {
-        let dir = tempfile::tempdir().unwrap();
-        // Leak the tempdir so it lives for the duration of the test
-        let path = dir.path().join("test.db");
-        let pool = crate::db::connect(&path).await.unwrap();
-        std::mem::forget(dir);
-        pool
-    }
-
-    async fn create_task(conn: &mut SqliteConnection, backlog_id: i64, title: &str) -> i64 {
-        task::create(
-            conn,
-            task::CreateTask {
-                title,
-                backlog_id,
-                state: Some(State::Queued),
-                description: None,
-            },
-        )
-        .await
-        .unwrap()
-        .id
-    }
-
-    #[tokio::test]
-    async fn add_and_list_edges() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-
-        let edge = add(&mut conn, t1, t2, EdgeType::Blocks).await.unwrap();
-        assert_eq!(edge.from_task_id, t1);
-        assert_eq!(edge.to_task_id, t2);
-        assert_eq!(edge.edge_type, EdgeType::Blocks);
-
-        let edges = list_for_task(&mut conn, t1).await.unwrap();
-        assert_eq!(edges.len(), 1);
-
-        let edges = list_for_task(&mut conn, t2).await.unwrap();
-        assert_eq!(edges.len(), 1);
-    }
-
-    #[tokio::test]
-    async fn remove_edge() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-
-        add(&mut conn, t1, t2, EdgeType::Blocks).await.unwrap();
-        let removed = remove(&mut conn, t1, t2, EdgeType::Blocks).await.unwrap();
-        assert!(removed);
-
-        let edges = list_for_task(&mut conn, t1).await.unwrap();
-        assert!(edges.is_empty());
-    }
-
-    #[tokio::test]
-    async fn remove_nonexistent_edge() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-
-        let removed = remove(&mut conn, t1, t2, EdgeType::Blocks).await.unwrap();
-        assert!(!removed);
-    }
-
-    #[tokio::test]
-    async fn cycle_detection_direct() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-
-        add(&mut conn, t1, t2, EdgeType::Blocks).await.unwrap();
-        let err = add(&mut conn, t2, t1, EdgeType::Blocks).await.unwrap_err();
-        assert!(err.to_string().contains("cycle"));
-    }
-
-    #[tokio::test]
-    async fn cycle_detection_indirect() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-        let t3 = create_task(&mut conn, bl.id, "Task 3").await;
-
-        add(&mut conn, t1, t2, EdgeType::Blocks).await.unwrap();
-        add(&mut conn, t2, t3, EdgeType::Blocks).await.unwrap();
-        let err = add(&mut conn, t3, t1, EdgeType::Before).await.unwrap_err();
-        assert!(err.to_string().contains("cycle"));
-    }
-
-    #[tokio::test]
-    async fn cycle_detection_self_loop() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-
-        let err = add(&mut conn, t1, t1, EdgeType::Blocks).await.unwrap_err();
-        assert!(err.to_string().contains("cycle"));
-    }
-
-    #[tokio::test]
-    async fn multiple_before_edges_allowed() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-        let t3 = create_task(&mut conn, bl.id, "Task 3").await;
-
-        add(&mut conn, t1, t2, EdgeType::Before).await.unwrap();
-        add(&mut conn, t1, t3, EdgeType::Before).await.unwrap();
-
-        let edges = list_for_task(&mut conn, t1).await.unwrap();
-        let before_edges: Vec<_> = edges
-            .iter()
-            .filter(|e| e.edge_type == EdgeType::Before)
-            .collect();
-        assert_eq!(before_edges.len(), 2);
-    }
-
-    #[tokio::test]
-    async fn multiple_blocks_edges_allowed() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-        let t3 = create_task(&mut conn, bl.id, "Task 3").await;
-
-        add(&mut conn, t1, t2, EdgeType::Blocks).await.unwrap();
-        add(&mut conn, t1, t3, EdgeType::Blocks).await.unwrap();
-
-        let edges = list_for_task(&mut conn, t1).await.unwrap();
-        assert_eq!(edges.len(), 2);
-    }
-
-    #[test]
-    fn build_dag_empty() {
-        let (graph, node_map) = build_dag(&[]);
-        assert_eq!(graph.node_count(), 0);
-        assert!(node_map.is_empty());
-    }
-
-    #[test]
-    fn topological_sort_empty() {
-        let result = topological_sort(&[]);
-        assert!(result.is_empty());
-    }
-
-    #[tokio::test]
-    async fn topological_sort_linear_chain() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-        let t3 = create_task(&mut conn, bl.id, "Task 3").await;
-
-        add(&mut conn, t1, t2, EdgeType::Before).await.unwrap();
-        add(&mut conn, t2, t3, EdgeType::Before).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        let sorted = topological_sort(&edges);
-        assert_eq!(sorted, vec![t1, t2, t3]);
-    }
-
-    #[tokio::test]
-    async fn topological_sort_diamond() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-        let t3 = create_task(&mut conn, bl.id, "Task 3").await;
-        let t4 = create_task(&mut conn, bl.id, "Task 4").await;
-
-        // t1 → t2, t1 → t3, t2 → t4, t3 → t4
-        add(&mut conn, t1, t2, EdgeType::Blocks).await.unwrap();
-        add(&mut conn, t1, t3, EdgeType::Blocks).await.unwrap();
-        add(&mut conn, t2, t4, EdgeType::Blocks).await.unwrap();
-        add(&mut conn, t3, t4, EdgeType::Blocks).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        let sorted = topological_sort(&edges);
-
-        // t1 must come first, t4 must come last
-        assert_eq!(sorted[0], t1);
-        assert_eq!(*sorted.last().unwrap(), t4);
-        // t2 and t3 must come before t4
-        let pos2 = sorted.iter().position(|&id| id == t2).unwrap();
-        let pos3 = sorted.iter().position(|&id| id == t3).unwrap();
-        let pos4 = sorted.iter().position(|&id| id == t4).unwrap();
-        assert!(pos2 < pos4);
-        assert!(pos3 < pos4);
-    }
-
-    #[tokio::test]
-    async fn edges_deleted_on_task_delete() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-
-        add(&mut conn, t1, t2, EdgeType::Blocks).await.unwrap();
-        ops::task::delete(&mut conn, t1).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        assert!(edges.is_empty());
-    }
-
-    #[tokio::test]
-    async fn mixed_edge_types_in_dag() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-        let t3 = create_task(&mut conn, bl.id, "Task 3").await;
-
-        // t1 blocks t2, t2 before t3 — both in same DAG
-        add(&mut conn, t1, t2, EdgeType::Blocks).await.unwrap();
-        add(&mut conn, t2, t3, EdgeType::Before).await.unwrap();
-
-        // t3 → t1 would create a cycle across edge types
-        let err = add(&mut conn, t3, t1, EdgeType::Blocks).await.unwrap_err();
-        assert!(err.to_string().contains("cycle"));
-
-        let edges = list_all(&mut conn).await.unwrap();
-        let sorted = topological_sort(&edges);
-        assert_eq!(sorted, vec![t1, t2, t3]);
-    }
-
-    // -- ordered_ids tests --
-
-    #[test]
-    fn ordered_ids_empty() {
-        assert!(ordered_ids(&[], &[]).is_empty());
-    }
-
-    #[test]
-    fn ordered_ids_no_edges_sorts_by_id() {
-        let result = ordered_ids(&[30, 10, 20], &[]);
-        assert_eq!(result, vec![10, 20, 30]);
-    }
-
-    #[tokio::test]
-    async fn ordered_ids_respects_before_edges() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-        let t3 = create_task(&mut conn, bl.id, "Task 3").await;
-
-        // t3 before t1 (override natural id order)
-        add(&mut conn, t3, t1, EdgeType::Before).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        let result = ordered_ids(&[t1, t2, t3], &edges);
-        // t2 has lowest id among unconstrained, t3 must come before t1
-        assert_eq!(result, vec![t2, t3, t1]);
-    }
-
-    #[tokio::test]
-    async fn ordered_ids_ignores_blocks_edges() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-
-        // t2 blocks t1 — should NOT affect ordering
-        add(&mut conn, t2, t1, EdgeType::Blocks).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        let result = ordered_ids(&[t1, t2], &edges);
-        assert_eq!(result, vec![t1, t2], "blocks edges should not affect order");
-    }
-
-    #[tokio::test]
-    async fn ordered_ids_filters_to_given_ids() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-        let t3 = create_task(&mut conn, bl.id, "Task 3").await;
-
-        add(&mut conn, t1, t2, EdgeType::Before).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        // Only ask about t2 and t3 — edge t1→t2 should be ignored since t1 not in set
-        let result = ordered_ids(&[t2, t3], &edges);
-        assert_eq!(result, vec![t2, t3]);
-    }
-
-    // -- splice_out_before tests --
-
-    #[tokio::test]
-    async fn splice_out_middle_of_chain() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-        let t3 = create_task(&mut conn, bl.id, "Task 3").await;
-
-        add(&mut conn, t1, t2, EdgeType::Before).await.unwrap();
-        add(&mut conn, t2, t3, EdgeType::Before).await.unwrap();
-
-        splice_out_before(&mut conn, t2).await.unwrap();
-
-        // t1 → t3 should now exist; no edges involving t2
-        let edges = list_all(&mut conn).await.unwrap();
-        assert_eq!(edges.len(), 1);
-        assert_eq!(edges[0].from_task_id, t1);
-        assert_eq!(edges[0].to_task_id, t3);
-        assert_eq!(edges[0].edge_type, EdgeType::Before);
-    }
-
-    #[tokio::test]
-    async fn splice_out_head_of_chain() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-
-        add(&mut conn, t1, t2, EdgeType::Before).await.unwrap();
-
-        splice_out_before(&mut conn, t1).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        assert!(edges.is_empty());
-    }
-
-    #[tokio::test]
-    async fn splice_out_tail_of_chain() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-
-        add(&mut conn, t1, t2, EdgeType::Before).await.unwrap();
-
-        splice_out_before(&mut conn, t2).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        assert!(edges.is_empty());
-    }
-
-    #[tokio::test]
-    async fn splice_out_preserves_blocks_edges() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-
-        add(&mut conn, t1, t2, EdgeType::Before).await.unwrap();
-        add(&mut conn, t1, t2, EdgeType::Blocks).await.unwrap();
-
-        splice_out_before(&mut conn, t2).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        assert_eq!(edges.len(), 1);
-        assert_eq!(edges[0].edge_type, EdgeType::Blocks);
-    }
-
-    #[tokio::test]
-    async fn splice_out_no_edges_is_noop() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-
-        // Should not error
-        splice_out_before(&mut conn, t1).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        assert!(edges.is_empty());
-    }
-
-    // -- insert_before_task tests --
-
-    #[tokio::test]
-    async fn insert_before_into_chain() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-        let t3 = create_task(&mut conn, bl.id, "Task 3").await;
-
-        // Chain: t1 → t2
-        add(&mut conn, t1, t2, EdgeType::Before).await.unwrap();
-
-        // Insert t3 before t2 → chain becomes t1 → t3 → t2
-        insert_before_task(&mut conn, t3, t2).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        let before_edges: Vec<_> = edges
-            .iter()
-            .filter(|e| e.edge_type == EdgeType::Before)
-            .collect();
-        assert_eq!(before_edges.len(), 2);
-
-        let sorted = ordered_ids(&[t1, t2, t3], &edges);
-        assert_eq!(sorted, vec![t1, t3, t2]);
-    }
-
-    #[tokio::test]
-    async fn insert_before_at_head() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-
-        // Insert t2 before t1 (t1 has no predecessor)
-        insert_before_task(&mut conn, t2, t1).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        let sorted = ordered_ids(&[t1, t2], &edges);
-        assert_eq!(sorted, vec![t2, t1]);
-    }
-
-    // -- insert_after_task tests --
-
-    #[tokio::test]
-    async fn insert_after_into_chain() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-        let t3 = create_task(&mut conn, bl.id, "Task 3").await;
-
-        // Chain: t1 → t2
-        add(&mut conn, t1, t2, EdgeType::Before).await.unwrap();
-
-        // Insert t3 after t1 → chain becomes t1 → t3 → t2
-        insert_after_task(&mut conn, t3, t1).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        let sorted = ordered_ids(&[t1, t2, t3], &edges);
-        assert_eq!(sorted, vec![t1, t3, t2]);
-    }
-
-    #[tokio::test]
-    async fn insert_after_at_tail() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-
-        // Insert t2 after t1 (t1 has no successor)
-        insert_after_task(&mut conn, t2, t1).await.unwrap();
-
-        let edges = list_all(&mut conn).await.unwrap();
-        let sorted = ordered_ids(&[t1, t2], &edges);
-        assert_eq!(sorted, vec![t1, t2]);
-    }
-
-    // -- list_for_task_ids tests --
-
-    #[tokio::test]
-    async fn list_for_task_ids_empty() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let edges = list_for_task_ids(&mut conn, &[]).await.unwrap();
-        assert!(edges.is_empty());
-    }
-
-    #[tokio::test]
-    async fn list_for_task_ids_returns_relevant_edges() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-        let t1 = create_task(&mut conn, bl.id, "Task 1").await;
-        let t2 = create_task(&mut conn, bl.id, "Task 2").await;
-        let t3 = create_task(&mut conn, bl.id, "Task 3").await;
-
-        add(&mut conn, t1, t2, EdgeType::Before).await.unwrap();
-        add(&mut conn, t2, t3, EdgeType::Before).await.unwrap();
-
-        // Only ask about t1 and t2
-        let edges = list_for_task_ids(&mut conn, &[t1, t2]).await.unwrap();
-        // Should get t1→t2 and t2→t3 (t2 is involved in both)
-        assert_eq!(edges.len(), 2);
-    }
-}
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
index 2ade7f5..c746aa9 100644
--- a/src/ops/mod.rs
+++ b/src/ops/mod.rs
@@ -1,5 +1,4 @@
 pub mod backlog;
 pub mod comment;
-pub mod edge;
 pub mod tag;
 pub mod task;
diff --git a/src/ops/task.rs b/src/ops/task.rs
index b0d1f4c..31c774b 100644
--- a/src/ops/task.rs
+++ b/src/ops/task.rs
@@ -1,10 +1,8 @@
 use crate::error::RangerError;
 use crate::key;
-use crate::models::{Ordering, State, Task};
-use crate::ops::edge;
+use crate::models::{State, Task};
 use crate::position;
 use sqlx::sqlite::SqliteConnection;
-use std::collections::HashMap;
 
 const TASK_COLUMNS: &str = "tasks.id, tasks.key, tasks.backlog_id, tasks.title, tasks.description, tasks.state, tasks.position, tasks.archived, tasks.created_at, tasks.updated_at, tasks.done_at";
 
@@ -70,7 +68,6 @@ pub async fn list(
     conn: &mut SqliteConnection,
     backlog_id: i64,
     filter: &ListFilter,
-    ordering: Ordering,
 ) -> Result<Vec<Task>, RangerError> {
     let archived_clause = if filter.include_archived {
         ""
@@ -86,16 +83,12 @@ pub async fn list(
 
     let is_done_only = filter.state.as_ref() == Some(&State::Done);
     let order_clause = if is_done_only {
-        // Done tasks order by completion time (most recent last)
         " ORDER BY done_at"
     } else {
-        match ordering {
-            Ordering::Position => " ORDER BY position",
-            Ordering::Dag => " ORDER BY id",
-        }
+        " ORDER BY position"
     };
 
-    let mut tasks = if let Some(state) = &filter.state {
+    let tasks = if let Some(state) = &filter.state {
         let query = format!(
             "SELECT {TASK_COLUMNS} FROM tasks{tag_join} \
              WHERE backlog_id = ? AND state = ?{archived_clause}{order_clause}"
@@ -120,19 +113,6 @@ pub async fn list(
         q.bind(backlog_id).fetch_all(&mut *conn).await?
     };
 
-    if ordering == Ordering::Dag {
-        let task_ids: Vec<i64> = tasks.iter().map(|t| t.id).collect();
-        let edges = edge::list_for_task_ids(&mut *conn, &task_ids).await?;
-        let sorted_ids = edge::ordered_ids(&task_ids, &edges);
-
-        let task_map: HashMap<i64, usize> = sorted_ids
-            .iter()
-            .enumerate()
-            .map(|(i, &id)| (id, i))
-            .collect();
-        tasks.sort_by_key(|t| task_map.get(&t.id).copied().unwrap_or(usize::MAX));
-    }
-
     Ok(tasks)
 }
 
@@ -198,7 +178,6 @@ pub async fn edit(
     title: Option<&str>,
     description: Option<&str>,
     state: Option<State>,
-    ordering: Ordering,
 ) -> Result<Task, RangerError> {
     if let Some(title) = title {
         sqlx::query("UPDATE tasks SET title = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?")
@@ -237,7 +216,7 @@ pub async fn edit(
             .await?;
 
         if old_state != *new_state {
-            reorder(&mut *conn, task_id, &old_state, new_state, ordering).await?;
+            reorder(&mut *conn, task_id, &old_state, new_state).await?;
         }
     }
 
@@ -286,7 +265,6 @@ pub async fn move_task(
     conn: &mut SqliteConnection,
     task: &Task,
     placement: Placement<'_>,
-    ordering: Ordering,
 ) -> Result<(), RangerError> {
     for anchor in placement.anchors() {
         if anchor.state != task.state {
@@ -297,17 +275,6 @@ pub async fn move_task(
         }
     }
 
-    match ordering {
-        Ordering::Position => move_task_position(&mut *conn, task, &placement).await,
-        Ordering::Dag => move_task_dag(&mut *conn, task, &placement).await,
-    }
-}
-
-async fn move_task_position(
-    conn: &mut SqliteConnection,
-    task: &Task,
-    placement: &Placement<'_>,
-) -> Result<(), RangerError> {
     let new_pos = match placement {
         Placement::After(anchor) => {
             let next =
@@ -327,50 +294,6 @@ async fn move_task_position(
     set_position(&mut *conn, task.id, &new_pos).await
 }
 
-async fn move_task_dag(
-    conn: &mut SqliteConnection,
-    task: &Task,
-    placement: &Placement<'_>,
-) -> Result<(), RangerError> {
-    edge::splice_out_before(&mut *conn, task.id).await?;
-
-    match placement {
-        Placement::Before(anchor) => {
-            edge::insert_before_task(&mut *conn, task.id, anchor.id).await?;
-        }
-        Placement::After(anchor) => {
-            edge::insert_after_task(&mut *conn, task.id, anchor.id).await?;
-        }
-        Placement::Between { after, before } => {
-            // Remove direct edge between anchors if it exists
-            edge::remove(
-                &mut *conn,
-                after.id,
-                before.id,
-                crate::models::EdgeType::Before,
-            )
-            .await?;
-            // Insert: after → task → before
-            edge::add(
-                &mut *conn,
-                after.id,
-                task.id,
-                crate::models::EdgeType::Before,
-            )
-            .await?;
-            edge::add(
-                &mut *conn,
-                task.id,
-                before.id,
-                crate::models::EdgeType::Before,
-            )
-            .await?;
-        }
-    }
-
-    Ok(())
-}
-
 /// Reorder a task when its state changes.
 ///
 /// Moving up (toward done): place at end of target state group.
@@ -380,19 +303,6 @@ async fn reorder(
     task_id: i64,
     old_state: &State,
     new_state: &State,
-    ordering: Ordering,
-) -> Result<(), RangerError> {
-    match ordering {
-        Ordering::Position => reorder_position(&mut *conn, task_id, old_state, new_state).await,
-        Ordering::Dag => reorder_dag(&mut *conn, task_id, old_state, new_state).await,
-    }
-}
-
-async fn reorder_position(
-    conn: &mut SqliteConnection,
-    task_id: i64,
-    old_state: &State,
-    new_state: &State,
 ) -> Result<(), RangerError> {
     let backlog_id: i64 = sqlx::query_scalar("SELECT backlog_id FROM tasks WHERE id = ?")
         .bind(task_id)
@@ -434,76 +344,6 @@ async fn reorder_position(
     set_position(&mut *conn, task_id, &new_pos).await
 }
 
-async fn reorder_dag(
-    conn: &mut SqliteConnection,
-    task_id: i64,
-    _old_state: &State,
-    new_state: &State,
-) -> Result<(), RangerError> {
-    let backlog_id: i64 = sqlx::query_scalar("SELECT backlog_id FROM tasks WHERE id = ?")
-        .bind(task_id)
-        .fetch_one(&mut *conn)
-        .await?;
-
-    // Remove the task from its old before-edge chains
-    edge::splice_out_before(&mut *conn, task_id).await?;
-
-    // Find tasks in the target state to determine placement
-    let target_tasks: Vec<Task> = {
-        let query = format!(
-            "SELECT {TASK_COLUMNS} FROM tasks \
-             WHERE backlog_id = ? AND state = ? AND id != ? \
-             ORDER BY id"
-        );
-        sqlx::query_as::<_, Task>(&query)
-            .bind(backlog_id)
-            .bind(new_state.as_str())
-            .bind(task_id)
-            .fetch_all(&mut *conn)
-            .await?
-    };
-
-    if target_tasks.is_empty() {
-        return Ok(());
-    }
-
-    let task_ids: Vec<i64> = target_tasks.iter().map(|t| t.id).collect();
-    let edges = edge::list_for_task_ids(&mut *conn, &task_ids).await?;
-    let before_edge_type = crate::models::EdgeType::Before;
-
-    // Find roots (no incoming before) and leaves (no outgoing before) within the state
-    let has_incoming: std::collections::HashSet<i64> = edges
-        .iter()
-        .filter(|e| e.edge_type == before_edge_type && task_ids.contains(&e.from_task_id))
-        .map(|e| e.to_task_id)
-        .collect();
-    let has_outgoing: std::collections::HashSet<i64> = edges
-        .iter()
-        .filter(|e| e.edge_type == before_edge_type && task_ids.contains(&e.to_task_id))
-        .map(|e| e.from_task_id)
-        .collect();
-
-    let moving_up = new_state.rank() > _old_state.rank();
-
-    if moving_up {
-        // Place at end: add leaf → task for every leaf (no outgoing before)
-        for &id in &task_ids {
-            if !has_outgoing.contains(&id) {
-                edge::add(&mut *conn, id, task_id, before_edge_type.clone()).await?;
-            }
-        }
-    } else {
-        // Place at beginning: add task → root for every root (no incoming before)
-        for &id in &task_ids {
-            if !has_incoming.contains(&id) {
-                edge::add(&mut *conn, task_id, id, before_edge_type.clone()).await?;
-            }
-        }
-    }
-
-    Ok(())
-}
-
 // -- Position query helpers --
 
 async fn last_position(
@@ -631,7 +471,6 @@ pub async fn rebalance(conn: &mut SqliteConnection, backlog_id: i64) -> Result<u
             include_archived: true,
             ..Default::default()
         },
-        Ordering::Position,
     )
     .await?;
     let positions = position::spread(tasks.len());
@@ -728,7 +567,7 @@ mod tests {
         .await
         .unwrap();
 
-        let tasks = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Position)
+        let tasks = list(&mut conn, bl.id, &ListFilter::default())
             .await
             .unwrap();
         assert_eq!(tasks.len(), 3);
@@ -772,7 +611,6 @@ mod tests {
                 state: Some(State::Icebox),
                 ..Default::default()
             },
-            Ordering::Position,
         )
         .await
         .unwrap();
@@ -786,7 +624,6 @@ mod tests {
                 state: Some(State::Queued),
                 ..Default::default()
             },
-            Ordering::Position,
         )
         .await
         .unwrap();
@@ -840,7 +677,6 @@ mod tests {
             Some("Updated"),
             Some("A description"),
             Some(State::Queued),
-            Ordering::Position,
         )
         .await
         .unwrap();
@@ -872,7 +708,7 @@ mod tests {
         let result = get_by_key_prefix(&mut conn, &task.key, None).await;
         assert!(result.is_err());
 
-        let tasks = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Position)
+        let tasks = list(&mut conn, bl.id, &ListFilter::default())
             .await
             .unwrap();
         assert_eq!(tasks.len(), 0);
@@ -974,16 +810,9 @@ mod tests {
         .await
         .unwrap();
 
-        let updated = edit(
-            &mut conn,
-            task.id,
-            Some("New title"),
-            None,
-            None,
-            Ordering::Position,
-        )
-        .await
-        .unwrap();
+        let updated = edit(&mut conn, task.id, Some("New title"), None, None)
+            .await
+            .unwrap();
         assert_eq!(updated.title, "New title");
         assert_eq!(updated.state, State::Icebox);
     }
@@ -1017,11 +846,11 @@ mod tests {
         .unwrap();
 
         // Move first task after second
-        move_task(&mut conn, &t1, Placement::After(&t2), Ordering::Position)
+        move_task(&mut conn, &t1, Placement::After(&t2))
             .await
             .unwrap();
 
-        let tasks = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Position)
+        let tasks = list(&mut conn, bl.id, &ListFilter::default())
             .await
             .unwrap();
         assert_eq!(tasks[0].id, t2.id);
@@ -1067,11 +896,11 @@ mod tests {
         .await
         .unwrap();
 
-        move_task(&mut conn, &t3, Placement::Before(&t1), Ordering::Position)
+        move_task(&mut conn, &t3, Placement::Before(&t1))
             .await
             .unwrap();
 
-        let tasks = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Position)
+        let tasks = list(&mut conn, bl.id, &ListFilter::default())
             .await
             .unwrap();
         assert_eq!(tasks[0].id, t3.id);
@@ -1118,11 +947,11 @@ mod tests {
         .await
         .unwrap();
 
-        move_task(&mut conn, &t1, Placement::After(&t3), Ordering::Position)
+        move_task(&mut conn, &t1, Placement::After(&t3))
             .await
             .unwrap();
 
-        let tasks = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Position)
+        let tasks = list(&mut conn, bl.id, &ListFilter::default())
             .await
             .unwrap();
         assert_eq!(tasks[0].id, t2.id);
@@ -1176,12 +1005,11 @@ mod tests {
                 after: &t1,
                 before: &t2,
             },
-            Ordering::Position,
         )
         .await
         .unwrap();
 
-        let tasks = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Position)
+        let tasks = list(&mut conn, bl.id, &ListFilter::default())
             .await
             .unwrap();
         assert_eq!(tasks[0].id, t1.id);
@@ -1217,14 +1045,9 @@ mod tests {
         .await
         .unwrap();
 
-        let err = move_task(
-            &mut conn,
-            &queued,
-            Placement::Before(&done),
-            Ordering::Position,
-        )
-        .await
-        .unwrap_err();
+        let err = move_task(&mut conn, &queued, Placement::Before(&done))
+            .await
+            .unwrap_err();
         assert!(err.to_string().contains("queued"));
         assert!(err.to_string().contains("done"));
     }
@@ -1271,16 +1094,9 @@ mod tests {
         .unwrap();
 
         // Move queued task to done — should land after Done 2
-        let updated = edit(
-            &mut conn,
-            q1.id,
-            None,
-            None,
-            Some(State::Done),
-            Ordering::Position,
-        )
-        .await
-        .unwrap();
+        let updated = edit(&mut conn, q1.id, None, None, Some(State::Done))
+            .await
+            .unwrap();
         assert_eq!(updated.state, State::Done);
 
         let done = list(
@@ -1290,7 +1106,6 @@ mod tests {
                 state: Some(State::Done),
                 ..Default::default()
             },
-            Ordering::Position,
         )
         .await
         .unwrap();
@@ -1345,16 +1160,9 @@ mod tests {
         .unwrap();
 
         // Move in_progress task to queued — should land before Queued 1
-        let updated = edit(
-            &mut conn,
-            ip.id,
-            None,
-            None,
-            Some(State::Queued),
-            Ordering::Position,
-        )
-        .await
-        .unwrap();
+        let updated = edit(&mut conn, ip.id, None, None, Some(State::Queued))
+            .await
+            .unwrap();
         assert_eq!(updated.state, State::Queued);
 
         let queued = list(
@@ -1364,7 +1172,6 @@ mod tests {
                 state: Some(State::Queued),
                 ..Default::default()
             },
-            Ordering::Position,
         )
         .await
         .unwrap();
@@ -1409,16 +1216,9 @@ mod tests {
         let original_pos = t1.position.clone();
 
         // Edit to same state — position should not change
-        let updated = edit(
-            &mut conn,
-            t1.id,
-            None,
-            None,
-            Some(State::Queued),
-            Ordering::Position,
-        )
-        .await
-        .unwrap();
+        let updated = edit(&mut conn, t1.id, None, None, Some(State::Queued))
+            .await
+            .unwrap();
         assert_eq!(updated.position, original_pos);
 
         let queued = list(
@@ -1428,7 +1228,6 @@ mod tests {
                 state: Some(State::Queued),
                 ..Default::default()
             },
-            Ordering::Position,
         )
         .await
         .unwrap();
@@ -1455,16 +1254,9 @@ mod tests {
         .unwrap();
 
         // Move to done (empty group) — should succeed
-        let updated = edit(
-            &mut conn,
-            t1.id,
-            None,
-            None,
-            Some(State::Done),
-            Ordering::Position,
-        )
-        .await
-        .unwrap();
+        let updated = edit(&mut conn, t1.id, None, None, Some(State::Done))
+            .await
+            .unwrap();
         assert_eq!(updated.state, State::Done);
 
         let done = list(
@@ -1474,7 +1266,6 @@ mod tests {
                 state: Some(State::Done),
                 ..Default::default()
             },
-            Ordering::Position,
         )
         .await
         .unwrap();
@@ -1501,16 +1292,9 @@ mod tests {
         .unwrap();
 
         // Move to icebox (empty group) — should succeed
-        let updated = edit(
-            &mut conn,
-            t1.id,
-            None,
-            None,
-            Some(State::Icebox),
-            Ordering::Position,
-        )
-        .await
-        .unwrap();
+        let updated = edit(&mut conn, t1.id, None, None, Some(State::Icebox))
+            .await
+            .unwrap();
         assert_eq!(updated.state, State::Icebox);
 
         let icebox = list(
@@ -1520,7 +1304,6 @@ mod tests {
                 state: Some(State::Icebox),
                 ..Default::default()
             },
-            Ordering::Position,
         )
         .await
         .unwrap();
@@ -1549,18 +1332,17 @@ mod tests {
             .unwrap();
         }
 
-        let before: Vec<String> =
-            list(&mut conn, bl.id, &ListFilter::default(), Ordering::Position)
-                .await
-                .unwrap()
-                .iter()
-                .map(|t| t.position.clone())
-                .collect();
+        let before: Vec<String> = list(&mut conn, bl.id, &ListFilter::default())
+            .await
+            .unwrap()
+            .iter()
+            .map(|t| t.position.clone())
+            .collect();
 
         let count = rebalance(&mut conn, bl.id).await.unwrap();
         assert_eq!(count, 3);
 
-        let after = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Position)
+        let after = list(&mut conn, bl.id, &ListFilter::default())
             .await
             .unwrap();
         let after_positions: Vec<String> = after.iter().map(|t| t.position.clone()).collect();
@@ -1663,7 +1445,7 @@ mod tests {
         assert!(archived.archived);
 
         // Default list excludes archived
-        let visible = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Position)
+        let visible = list(&mut conn, bl.id, &ListFilter::default())
             .await
             .unwrap();
         assert_eq!(visible.len(), 1);
@@ -1677,7 +1459,6 @@ mod tests {
                 include_archived: true,
                 ..Default::default()
             },
-            Ordering::Position,
         )
         .await
         .unwrap();
@@ -1687,7 +1468,7 @@ mod tests {
         let restored = set_archived(&mut conn, t2.id, false).await.unwrap();
         assert!(!restored.archived);
 
-        let visible = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Position)
+        let visible = list(&mut conn, bl.id, &ListFilter::default())
             .await
             .unwrap();
         assert_eq!(visible.len(), 2);
@@ -1731,7 +1512,6 @@ mod tests {
                 tag: Some("bug".to_string()),
                 ..Default::default()
             },
-            Ordering::Position,
         )
         .await
         .unwrap();
@@ -1739,846 +1519,103 @@ mod tests {
         assert_eq!(results[0].title, "Tagged queued");
     }
 
-    // ---- DAG ordering tests ----
+    // ---- done_at tests ----
 
     #[tokio::test]
-    async fn dag_list_orders_by_id_with_no_edges() {
+    async fn create_with_done_state_sets_done_at() {
         let pool = test_pool().await;
         let mut conn = pool.acquire().await.unwrap();
         let bl = backlog::create(&mut conn, "Test").await.unwrap();
 
-        let t1 = create(
-            &mut conn,
-            CreateTask {
-                title: "A",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t2 = create(
-            &mut conn,
-            CreateTask {
-                title: "B",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t3 = create(
+        let task = create(
             &mut conn,
             CreateTask {
-                title: "C",
+                title: "Done task",
                 backlog_id: bl.id,
-                state: None,
+                state: Some(State::Done),
                 description: None,
             },
         )
         .await
         .unwrap();
 
-        let tasks = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Dag)
-            .await
-            .unwrap();
-        assert_eq!(tasks.len(), 3);
-        assert_eq!(tasks[0].id, t1.id);
-        assert_eq!(tasks[1].id, t2.id);
-        assert_eq!(tasks[2].id, t3.id);
+        assert!(task.done_at.is_some(), "done task should have done_at set");
     }
 
     #[tokio::test]
-    async fn dag_list_respects_before_edges() {
+    async fn create_with_non_done_state_has_no_done_at() {
         let pool = test_pool().await;
         let mut conn = pool.acquire().await.unwrap();
         let bl = backlog::create(&mut conn, "Test").await.unwrap();
 
-        let t1 = create(
-            &mut conn,
-            CreateTask {
-                title: "A",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t2 = create(
-            &mut conn,
-            CreateTask {
-                title: "B",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t3 = create(
+        let task = create(
             &mut conn,
             CreateTask {
-                title: "C",
+                title: "Queued task",
                 backlog_id: bl.id,
-                state: None,
+                state: Some(State::Queued),
                 description: None,
             },
         )
         .await
         .unwrap();
 
-        // t3 should come before t1 (override natural id order)
-        crate::ops::edge::add(&mut conn, t3.id, t1.id, crate::models::EdgeType::Before)
-            .await
-            .unwrap();
-
-        let tasks = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Dag)
-            .await
-            .unwrap();
-        assert_eq!(tasks[0].id, t2.id, "t2 has lowest id, no constraints");
-        assert_eq!(tasks[1].id, t3.id, "t3 before t1 by edge");
-        assert_eq!(tasks[2].id, t1.id);
+        assert!(
+            task.done_at.is_none(),
+            "non-done task should not have done_at"
+        );
     }
 
     #[tokio::test]
-    async fn dag_move_before() {
+    async fn edit_to_done_sets_done_at() {
         let pool = test_pool().await;
         let mut conn = pool.acquire().await.unwrap();
         let bl = backlog::create(&mut conn, "Test").await.unwrap();
 
-        let t1 = create(
-            &mut conn,
-            CreateTask {
-                title: "A",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t2 = create(
+        let task = create(
             &mut conn,
             CreateTask {
-                title: "B",
+                title: "Task",
                 backlog_id: bl.id,
-                state: None,
+                state: Some(State::Queued),
                 description: None,
             },
         )
         .await
         .unwrap();
+        assert!(task.done_at.is_none());
 
-        // Move t2 before t1
-        move_task(&mut conn, &t2, Placement::Before(&t1), Ordering::Dag)
-            .await
-            .unwrap();
-
-        let tasks = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Dag)
+        let updated = edit(&mut conn, task.id, None, None, Some(State::Done))
             .await
             .unwrap();
-        assert_eq!(tasks[0].id, t2.id);
-        assert_eq!(tasks[1].id, t1.id);
+        assert!(
+            updated.done_at.is_some(),
+            "should set done_at on transition to done"
+        );
     }
 
     #[tokio::test]
-    async fn dag_move_after() {
+    async fn edit_from_done_clears_done_at() {
         let pool = test_pool().await;
         let mut conn = pool.acquire().await.unwrap();
         let bl = backlog::create(&mut conn, "Test").await.unwrap();
 
-        let t1 = create(
-            &mut conn,
-            CreateTask {
-                title: "A",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t2 = create(
+        let task = create(
             &mut conn,
             CreateTask {
-                title: "B",
+                title: "Task",
                 backlog_id: bl.id,
-                state: None,
+                state: Some(State::Done),
                 description: None,
             },
         )
         .await
         .unwrap();
-        let t3 = create(
-            &mut conn,
-            CreateTask {
-                title: "C",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        // Move t1 after t3 (t1 is normally first by id)
-        move_task(&mut conn, &t1, Placement::After(&t3), Ordering::Dag)
-            .await
-            .unwrap();
-
-        let tasks = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Dag)
-            .await
-            .unwrap();
-        assert_eq!(tasks[0].id, t2.id);
-        assert_eq!(tasks[1].id, t3.id);
-        assert_eq!(tasks[2].id, t1.id);
-    }
-
-    #[tokio::test]
-    async fn dag_move_between() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        let t1 = create(
-            &mut conn,
-            CreateTask {
-                title: "A",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t2 = create(
-            &mut conn,
-            CreateTask {
-                title: "B",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t3 = create(
-            &mut conn,
-            CreateTask {
-                title: "C",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        // Set up chain: t1 → t2
-        crate::ops::edge::add(&mut conn, t1.id, t2.id, crate::models::EdgeType::Before)
-            .await
-            .unwrap();
-
-        // Move t3 between t1 and t2
-        move_task(
-            &mut conn,
-            &t3,
-            Placement::Between {
-                after: &t1,
-                before: &t2,
-            },
-            Ordering::Dag,
-        )
-        .await
-        .unwrap();
-
-        let tasks = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Dag)
-            .await
-            .unwrap();
-        assert_eq!(tasks[0].id, t1.id);
-        assert_eq!(tasks[1].id, t3.id);
-        assert_eq!(tasks[2].id, t2.id);
-    }
-
-    #[tokio::test]
-    async fn dag_move_rejects_cross_state() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        let queued = create(
-            &mut conn,
-            CreateTask {
-                title: "Q",
-                backlog_id: bl.id,
-                state: Some(State::Queued),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let done = create(
-            &mut conn,
-            CreateTask {
-                title: "D",
-                backlog_id: bl.id,
-                state: Some(State::Done),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        let err = move_task(&mut conn, &queued, Placement::Before(&done), Ordering::Dag)
-            .await
-            .unwrap_err();
-        assert!(err.to_string().contains("queued"));
-        assert!(err.to_string().contains("done"));
-    }
-
-    #[tokio::test]
-    async fn dag_state_change_up_places_at_end() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        let d1 = create(
-            &mut conn,
-            CreateTask {
-                title: "Done 1",
-                backlog_id: bl.id,
-                state: Some(State::Done),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let d2 = create(
-            &mut conn,
-            CreateTask {
-                title: "Done 2",
-                backlog_id: bl.id,
-                state: Some(State::Done),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let q1 = create(
-            &mut conn,
-            CreateTask {
-                title: "Queued 1",
-                backlog_id: bl.id,
-                state: Some(State::Queued),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        let updated = edit(
-            &mut conn,
-            q1.id,
-            None,
-            None,
-            Some(State::Done),
-            Ordering::Dag,
-        )
-        .await
-        .unwrap();
-        assert_eq!(updated.state, State::Done);
-
-        let done = list(
-            &mut conn,
-            bl.id,
-            &ListFilter {
-                state: Some(State::Done),
-                ..Default::default()
-            },
-            Ordering::Dag,
-        )
-        .await
-        .unwrap();
-        assert_eq!(done.len(), 3);
-        assert_eq!(done[0].id, d1.id);
-        assert_eq!(done[1].id, d2.id);
-        assert_eq!(done[2].id, q1.id, "newly done task should be at end");
-    }
-
-    #[tokio::test]
-    async fn dag_state_change_down_places_at_beginning() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        let q1 = create(
-            &mut conn,
-            CreateTask {
-                title: "Queued 1",
-                backlog_id: bl.id,
-                state: Some(State::Queued),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let q2 = create(
-            &mut conn,
-            CreateTask {
-                title: "Queued 2",
-                backlog_id: bl.id,
-                state: Some(State::Queued),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let ip = create(
-            &mut conn,
-            CreateTask {
-                title: "In Progress",
-                backlog_id: bl.id,
-                state: Some(State::InProgress),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        let updated = edit(
-            &mut conn,
-            ip.id,
-            None,
-            None,
-            Some(State::Queued),
-            Ordering::Dag,
-        )
-        .await
-        .unwrap();
-        assert_eq!(updated.state, State::Queued);
-
-        let queued = list(
-            &mut conn,
-            bl.id,
-            &ListFilter {
-                state: Some(State::Queued),
-                ..Default::default()
-            },
-            Ordering::Dag,
-        )
-        .await
-        .unwrap();
-        assert_eq!(queued.len(), 3);
-        assert_eq!(queued[0].id, ip.id, "demoted task should be at beginning");
-        assert_eq!(queued[1].id, q1.id);
-        assert_eq!(queued[2].id, q2.id);
-    }
-
-    #[tokio::test]
-    async fn dag_state_change_to_empty_group() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        let t1 = create(
-            &mut conn,
-            CreateTask {
-                title: "Queued",
-                backlog_id: bl.id,
-                state: Some(State::Queued),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        let updated = edit(
-            &mut conn,
-            t1.id,
-            None,
-            None,
-            Some(State::Done),
-            Ordering::Dag,
-        )
-        .await
-        .unwrap();
-        assert_eq!(updated.state, State::Done);
-
-        let done = list(
-            &mut conn,
-            bl.id,
-            &ListFilter {
-                state: Some(State::Done),
-                ..Default::default()
-            },
-            Ordering::Dag,
-        )
-        .await
-        .unwrap();
-        assert_eq!(done.len(), 1);
-        assert_eq!(done[0].id, t1.id);
-    }
-
-    #[tokio::test]
-    async fn dag_state_change_same_state_is_noop() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        let t1 = create(
-            &mut conn,
-            CreateTask {
-                title: "Queued",
-                backlog_id: bl.id,
-                state: Some(State::Queued),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        let updated = edit(
-            &mut conn,
-            t1.id,
-            None,
-            None,
-            Some(State::Queued),
-            Ordering::Dag,
-        )
-        .await
-        .unwrap();
-        assert_eq!(updated.state, State::Queued);
-
-        // No edges should have been created
-        let edges = crate::ops::edge::list_all(&mut conn).await.unwrap();
-        assert!(edges.is_empty());
-    }
-
-    #[tokio::test]
-    async fn dag_state_change_up_into_group_with_existing_edges() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        // Done tasks with an existing chain: d1 → d2
-        let d1 = create(
-            &mut conn,
-            CreateTask {
-                title: "Done 1",
-                backlog_id: bl.id,
-                state: Some(State::Done),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let d2 = create(
-            &mut conn,
-            CreateTask {
-                title: "Done 2",
-                backlog_id: bl.id,
-                state: Some(State::Done),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        crate::ops::edge::add(&mut conn, d1.id, d2.id, crate::models::EdgeType::Before)
-            .await
-            .unwrap();
-
-        // Queued task to promote
-        let q1 = create(
-            &mut conn,
-            CreateTask {
-                title: "Queued 1",
-                backlog_id: bl.id,
-                state: Some(State::Queued),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        edit(
-            &mut conn,
-            q1.id,
-            None,
-            None,
-            Some(State::Done),
-            Ordering::Dag,
-        )
-        .await
-        .unwrap();
-
-        let done = list(
-            &mut conn,
-            bl.id,
-            &ListFilter {
-                state: Some(State::Done),
-                ..Default::default()
-            },
-            Ordering::Dag,
-        )
-        .await
-        .unwrap();
-        // d1 → d2 chain, q1 added after d2 (d2 is the only leaf)
-        assert_eq!(done.len(), 3);
-        assert_eq!(done[0].id, d1.id);
-        assert_eq!(done[1].id, d2.id);
-        assert_eq!(done[2].id, q1.id);
-    }
-
-    #[tokio::test]
-    async fn dag_state_change_down_into_group_with_existing_edges() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        // Queued tasks with an existing chain: q1 → q2
-        let q1 = create(
-            &mut conn,
-            CreateTask {
-                title: "Queued 1",
-                backlog_id: bl.id,
-                state: Some(State::Queued),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let q2 = create(
-            &mut conn,
-            CreateTask {
-                title: "Queued 2",
-                backlog_id: bl.id,
-                state: Some(State::Queued),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        crate::ops::edge::add(&mut conn, q1.id, q2.id, crate::models::EdgeType::Before)
-            .await
-            .unwrap();
-
-        // In-progress task to demote
-        let ip = create(
-            &mut conn,
-            CreateTask {
-                title: "IP",
-                backlog_id: bl.id,
-                state: Some(State::InProgress),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        edit(
-            &mut conn,
-            ip.id,
-            None,
-            None,
-            Some(State::Queued),
-            Ordering::Dag,
-        )
-        .await
-        .unwrap();
-
-        let queued = list(
-            &mut conn,
-            bl.id,
-            &ListFilter {
-                state: Some(State::Queued),
-                ..Default::default()
-            },
-            Ordering::Dag,
-        )
-        .await
-        .unwrap();
-        // ip added before q1 (q1 is the only root), chain: ip → q1 → q2
-        assert_eq!(queued.len(), 3);
-        assert_eq!(queued[0].id, ip.id);
-        assert_eq!(queued[1].id, q1.id);
-        assert_eq!(queued[2].id, q2.id);
-    }
-
-    #[tokio::test]
-    async fn dag_move_splices_out_of_old_chain() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        let t1 = create(
-            &mut conn,
-            CreateTask {
-                title: "A",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t2 = create(
-            &mut conn,
-            CreateTask {
-                title: "B",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        let t3 = create(
-            &mut conn,
-            CreateTask {
-                title: "C",
-                backlog_id: bl.id,
-                state: None,
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        // Chain: t1 → t2 → t3
-        crate::ops::edge::add(&mut conn, t1.id, t2.id, crate::models::EdgeType::Before)
-            .await
-            .unwrap();
-        crate::ops::edge::add(&mut conn, t2.id, t3.id, crate::models::EdgeType::Before)
-            .await
-            .unwrap();
-
-        // Move t2 after t3 — should splice out and re-chain t1 → t3
-        move_task(&mut conn, &t2, Placement::After(&t3), Ordering::Dag)
-            .await
-            .unwrap();
+        assert!(task.done_at.is_some());
 
-        let tasks = list(&mut conn, bl.id, &ListFilter::default(), Ordering::Dag)
+        let updated = edit(&mut conn, task.id, None, None, Some(State::Queued))
             .await
             .unwrap();
-        assert_eq!(tasks[0].id, t1.id);
-        assert_eq!(tasks[1].id, t3.id);
-        assert_eq!(tasks[2].id, t2.id);
-    }
-
-    // ---- done_at tests ----
-
-    #[tokio::test]
-    async fn create_with_done_state_sets_done_at() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        let task = create(
-            &mut conn,
-            CreateTask {
-                title: "Done task",
-                backlog_id: bl.id,
-                state: Some(State::Done),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        assert!(task.done_at.is_some(), "done task should have done_at set");
-    }
-
-    #[tokio::test]
-    async fn create_with_non_done_state_has_no_done_at() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        let task = create(
-            &mut conn,
-            CreateTask {
-                title: "Queued task",
-                backlog_id: bl.id,
-                state: Some(State::Queued),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-
-        assert!(
-            task.done_at.is_none(),
-            "non-done task should not have done_at"
-        );
-    }
-
-    #[tokio::test]
-    async fn edit_to_done_sets_done_at() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        let task = create(
-            &mut conn,
-            CreateTask {
-                title: "Task",
-                backlog_id: bl.id,
-                state: Some(State::Queued),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        assert!(task.done_at.is_none());
-
-        let updated = edit(
-            &mut conn,
-            task.id,
-            None,
-            None,
-            Some(State::Done),
-            Ordering::Position,
-        )
-        .await
-        .unwrap();
-        assert!(
-            updated.done_at.is_some(),
-            "should set done_at on transition to done"
-        );
-    }
-
-    #[tokio::test]
-    async fn edit_from_done_clears_done_at() {
-        let pool = test_pool().await;
-        let mut conn = pool.acquire().await.unwrap();
-        let bl = backlog::create(&mut conn, "Test").await.unwrap();
-
-        let task = create(
-            &mut conn,
-            CreateTask {
-                title: "Task",
-                backlog_id: bl.id,
-                state: Some(State::Done),
-                description: None,
-            },
-        )
-        .await
-        .unwrap();
-        assert!(task.done_at.is_some());
-
-        let updated = edit(
-            &mut conn,
-            task.id,
-            None,
-            None,
-            Some(State::Queued),
-            Ordering::Position,
-        )
-        .await
-        .unwrap();
         assert!(
             updated.done_at.is_none(),
             "should clear done_at on transition away from done"
@@ -2657,7 +1694,6 @@ mod tests {
                 state: Some(State::Done),
                 ..Default::default()
             },
-            Ordering::Position,
         )
         .await
         .unwrap();