From 789db30b4fa16b97a1aa9756aab5f0f984f80148 Mon Sep 17 00:00:00 2001 From: Amolith Date: Mon, 2 Mar 2026 12:46:49 -0700 Subject: [PATCH] Restore verbose output for next command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Loro migration stripped the verbose breakdown down to just an ID and final score. Restore the three pieces that were lost: - Active scoring mode printed once before the per-task list - Expanded formula with actual values substituted, branching on mode: impact: (downstream + 1.00) × priority / effort^0.25 = score effort: (downstream × 0.25 + 1.00) × priority / effort² = score - Unblocks count: N tasks (M directly), with correct singular/plural All data was already present in ScoredTask; only the rendering in src/cmd/next.rs needed updating. Update the two cli_next verbose tests whose assertions reflected the old 'score:' label rather than the restored output. Add two new tests covering the singular 'task' grammar and transitive unblocks counts, both of which were untested before. --- src/cmd/next.rs | 31 ++++++++++++++++++++++++-- tests/cli_next.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/cmd/next.rs b/src/cmd/next.rs index 42ca063662c62797980b5f74a2138676dd1c87c5..4d03c388aa8ff084a8e14ecee4d231d412c98c0f 100644 --- a/src/cmd/next.rs +++ b/src/cmd/next.rs @@ -107,10 +107,37 @@ pub fn run(root: &Path, mode_str: &str, verbose: bool, limit: usize, json: bool) println!("{table}"); if verbose { - println!(); + let mode_label = match mode { + Mode::Impact => "impact", + Mode::Effort => "effort", + }; + println!("\nmode: {mode_label}"); for (i, s) in scored.iter().enumerate() { let short = db::TaskId::display_id(&s.id); - println!("{}. {} — score: {:.2}", i + 1, short, s.score); + let formula = match mode { + Mode::Impact => format!( + "({:.2} + 1.00) × {:.2} / {:.2}^0.25 = {:.2}", + s.downstream_score, s.priority_weight, s.effort_weight, s.score + ), + Mode::Effort => format!( + "({:.2} × 0.25 + 1.00) × {:.2} / {:.2}² = {:.2}", + s.downstream_score, s.priority_weight, s.effort_weight, s.score + ), + }; + let task_word = if s.total_unblocked == 1 { + "task" + } else { + "tasks" + }; + println!( + "\n{}. {}\n {}\n Unblocks: {} {} ({} directly)", + i + 1, + short, + formula, + s.total_unblocked, + task_word, + s.direct_unblocked + ); } } } diff --git a/tests/cli_next.rs b/tests/cli_next.rs index 18de6d379a3a57d5466dd1261b64998a412327ba..958726f7ded0051f201f19a12e26d97a5ece5fcb 100644 --- a/tests/cli_next.rs +++ b/tests/cli_next.rs @@ -115,7 +115,9 @@ fn next_verbose_shows_equation() { .assert() .success() .stdout(predicate::str::contains("SCORE")) - .stdout(predicate::str::contains("score:")); + .stdout(predicate::str::contains("mode: impact")) + .stdout(predicate::str::contains("^0.25")) + .stdout(predicate::str::contains("Unblocks:")); } #[test] @@ -129,7 +131,58 @@ fn next_verbose_effort_mode_shows_squared() { .assert() .success() .stdout(predicate::str::contains("SCORE")) - .stdout(predicate::str::contains("score:")); + .stdout(predicate::str::contains("mode: effort")) + .stdout(predicate::str::contains("²")) + .stdout(predicate::str::contains("Unblocks:")); +} + +#[test] +fn next_verbose_unblocks_singular() { + // A blocks B. A is ready and directly unblocks exactly 1 task, so the + // output must say "1 task", not "1 tasks". + let tmp = init_tmp(); + let a = create_task(&tmp, "Blocker", "medium", "low"); + let b = create_task(&tmp, "Blocked", "medium", "low"); + + td(&tmp) + .args(["dep", "add", &b, &a]) + .current_dir(&tmp) + .assert() + .success(); + + td(&tmp) + .args(["next", "--verbose"]) + .current_dir(&tmp) + .assert() + .success() + .stdout(predicate::str::contains("Unblocks: 1 task (1 directly)")); +} + +#[test] +fn next_verbose_unblocks_count_with_transitive() { + // A blocks B, B blocks C. A is ready with 2 total unblocked (1 directly). + let tmp = init_tmp(); + let a = create_task(&tmp, "Root", "medium", "low"); + let b = create_task(&tmp, "Mid", "medium", "low"); + let c = create_task(&tmp, "Leaf", "medium", "low"); + + td(&tmp) + .args(["dep", "add", &b, &a]) + .current_dir(&tmp) + .assert() + .success(); + td(&tmp) + .args(["dep", "add", &c, &b]) + .current_dir(&tmp) + .assert() + .success(); + + td(&tmp) + .args(["next", "--verbose"]) + .current_dir(&tmp) + .assert() + .success() + .stdout(predicate::str::contains("Unblocks: 2 tasks (1 directly)")); } #[test]