From 8b5015d5e93084911f32f5633542df16462a1bee Mon Sep 17 00:00:00 2001 From: Florian BRUNIAUX Date: Sat, 28 Mar 2026 01:06:44 +0100 Subject: [PATCH 1/4] feat(discover): track passthrough git/cargo/pnpm subcommands Add checkout/merge/rebase to git pattern, run/publish to cargo pattern, and build/exec to pnpm pattern in PATTERNS regex. Mark each as RtkStatus::Passthrough in subcmd_status so discover correctly classifies them as "already handled via passthrough" instead of "TOP UNHANDLED". Also update test_registry_covers_all_git/cargo_subcommands to include the new subcommands, and add 11 focused tests for classify + rewrite. Impact: 1277 commands (git checkout 493, git merge 154, git rebase 137, pnpm build 171, pnpm exec 173, cargo run 78, cargo publish 71) will appear in "MISSED SAVINGS" instead of "TOP UNHANDLED COMMANDS". Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Florian BRUNIAUX --- src/discover/registry.rs | 133 +++++++++++++++++++++++++++++++++++++-- src/discover/rules.rs | 23 +++++-- 2 files changed, 146 insertions(+), 10 deletions(-) diff --git a/src/discover/registry.rs b/src/discover/registry.rs index cc975b3b..2ea90ecd 100644 --- a/src/discover/registry.rs +++ b/src/discover/registry.rs @@ -890,9 +890,8 @@ mod tests { #[test] fn test_registry_covers_all_cargo_subcommands() { - // Verify that every CargoCommand variant (Build, Test, Clippy, Check, Fmt) - // except Other has a matching pattern in the registry - for subcmd in ["build", "test", "clippy", "check", "fmt"] { + // Verify that every CargoCommand variant except Other has a matching pattern + for subcmd in ["build", "test", "clippy", "check", "fmt", "run", "publish"] { let cmd = format!("cargo {subcmd}"); match classify_command(&cmd) { Classification::Supported { .. } => {} @@ -906,7 +905,7 @@ mod tests { // Verify that every GitCommand subcommand has a matching pattern for subcmd in [ "status", "log", "diff", "show", "add", "commit", "push", "pull", "branch", "fetch", - "stash", "worktree", + "stash", "worktree", "checkout", "merge", "rebase", ] { let cmd = format!("git {subcmd}"); match classify_command(&cmd) { @@ -2348,4 +2347,130 @@ mod tests { assert_eq!(strip_git_global_opts("git status"), "git status"); assert_eq!(strip_git_global_opts("cargo test"), "cargo test"); } + + // Passthrough subcommand coverage: discover should classify these as Supported + // (routing to passthrough handlers) rather than "TOP UNHANDLED COMMANDS". + + #[test] + fn test_classify_git_checkout_passthrough() { + assert_eq!( + classify_command("git checkout main"), + Classification::Supported { + rtk_equivalent: "rtk git", + category: "Git", + estimated_savings_pct: 70.0, + status: RtkStatus::Passthrough, + } + ); + } + + #[test] + fn test_classify_git_merge_passthrough() { + assert_eq!( + classify_command("git merge feature/my-branch"), + Classification::Supported { + rtk_equivalent: "rtk git", + category: "Git", + estimated_savings_pct: 70.0, + status: RtkStatus::Passthrough, + } + ); + } + + #[test] + fn test_classify_git_rebase_passthrough() { + assert_eq!( + classify_command("git rebase origin/main"), + Classification::Supported { + rtk_equivalent: "rtk git", + category: "Git", + estimated_savings_pct: 70.0, + status: RtkStatus::Passthrough, + } + ); + } + + #[test] + fn test_classify_cargo_run_passthrough() { + assert_eq!( + classify_command("cargo run -- verify"), + Classification::Supported { + rtk_equivalent: "rtk cargo", + category: "Cargo", + estimated_savings_pct: 80.0, + status: RtkStatus::Passthrough, + } + ); + } + + #[test] + fn test_classify_cargo_publish_passthrough() { + assert_eq!( + classify_command("cargo publish -p my-crate"), + Classification::Supported { + rtk_equivalent: "rtk cargo", + category: "Cargo", + estimated_savings_pct: 80.0, + status: RtkStatus::Passthrough, + } + ); + } + + #[test] + fn test_classify_pnpm_build_passthrough() { + assert_eq!( + classify_command("pnpm build"), + Classification::Supported { + rtk_equivalent: "rtk pnpm", + category: "PackageManager", + estimated_savings_pct: 80.0, + status: RtkStatus::Passthrough, + } + ); + } + + #[test] + fn test_classify_pnpm_exec_passthrough() { + assert_eq!( + classify_command("pnpm exec eslint --max-warnings=0"), + Classification::Supported { + rtk_equivalent: "rtk pnpm", + category: "PackageManager", + estimated_savings_pct: 80.0, + status: RtkStatus::Passthrough, + } + ); + } + + #[test] + fn test_rewrite_git_checkout() { + assert_eq!( + rewrite_command("git checkout main", &[]), + Some("rtk git checkout main".to_string()) + ); + } + + #[test] + fn test_rewrite_git_merge() { + assert_eq!( + rewrite_command("git merge --no-edit feature", &[]), + Some("rtk git merge --no-edit feature".to_string()) + ); + } + + #[test] + fn test_rewrite_cargo_run() { + assert_eq!( + rewrite_command("cargo run -- verify", &[]), + Some("rtk cargo run -- verify".to_string()) + ); + } + + #[test] + fn test_rewrite_pnpm_build() { + assert_eq!( + rewrite_command("pnpm build", &[]), + Some("rtk pnpm build".to_string()) + ); + } } diff --git a/src/discover/rules.rs b/src/discover/rules.rs index e5a36e7c..80066a79 100644 --- a/src/discover/rules.rs +++ b/src/discover/rules.rs @@ -15,10 +15,10 @@ pub struct RtkRule { // Patterns ordered to match RULES indices exactly. pub const PATTERNS: &[&str] = &[ - r"^git\s+(?:-[Cc]\s+\S+\s+)*(status|log|diff|show|add|commit|push|pull|branch|fetch|stash|worktree)", + r"^git\s+(?:-[Cc]\s+\S+\s+)*(status|log|diff|show|add|commit|push|pull|branch|fetch|stash|worktree|checkout|merge|rebase)", r"^gh\s+(pr|issue|run|repo|api|release)", - r"^cargo\s+(build|test|clippy|check|fmt|install)", - r"^pnpm\s+(list|ls|outdated|install)", + r"^cargo\s+(build|test|clippy|check|fmt|install|run|publish)", + r"^pnpm\s+(list|ls|outdated|install|build|exec)", r"^npm\s+(run|exec)", r"^npx\s+", r"^(cat|head|tail)\s+", @@ -102,7 +102,11 @@ pub const RULES: &[RtkRule] = &[ ("add", 59.0), ("commit", 59.0), ], - subcmd_status: &[], + subcmd_status: &[ + ("checkout", RtkStatus::Passthrough), + ("merge", RtkStatus::Passthrough), + ("rebase", RtkStatus::Passthrough), + ], }, RtkRule { rtk_cmd: "rtk gh", @@ -118,7 +122,11 @@ pub const RULES: &[RtkRule] = &[ category: "Cargo", savings_pct: 80.0, subcmd_savings: &[("test", 90.0), ("check", 80.0)], - subcmd_status: &[("fmt", RtkStatus::Passthrough)], + subcmd_status: &[ + ("fmt", RtkStatus::Passthrough), + ("run", RtkStatus::Passthrough), + ("publish", RtkStatus::Passthrough), + ], }, RtkRule { rtk_cmd: "rtk pnpm", @@ -126,7 +134,10 @@ pub const RULES: &[RtkRule] = &[ category: "PackageManager", savings_pct: 80.0, subcmd_savings: &[], - subcmd_status: &[], + subcmd_status: &[ + ("build", RtkStatus::Passthrough), + ("exec", RtkStatus::Passthrough), + ], }, RtkRule { rtk_cmd: "rtk npm", From 981f561a014035366e7d8749ef7c7d5e2c4d871b Mon Sep 17 00:00:00 2001 From: Florian BRUNIAUX Date: Sat, 28 Mar 2026 00:33:36 +0100 Subject: [PATCH 2/4] docs: add supported ecosystems reference to CLAUDE.md Adds a one-liner listing all supported ecosystems (ruff, pytest, pip, golangci-lint, etc.) in the Architecture section to satisfy the pre-push documentation validation script. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Florian BRUNIAUX --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 0dddf14e..e6183a51 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,6 +75,8 @@ For the full architecture, component details, and module development patterns, s Module responsibilities are documented in each folder's `README.md` and each file's `//!` doc header. Browse `src/cmds/*/` to discover available filters. +Supported ecosystems: git/gh/gt, cargo, go/golangci-lint, npm/pnpm/npx, ruff/pytest/pip/mypy, rspec/rubocop/rake, dotnet, playwright/vitest/jest, docker/kubectl/aws. + ### Proxy Mode **Purpose**: Execute commands without filtering but track usage for metrics. From b3ab873f4b17749e4503f956e1a27dfcbea99654 Mon Sep 17 00:00:00 2001 From: Florian BRUNIAUX Date: Sat, 28 Mar 2026 01:33:46 +0100 Subject: [PATCH 3/4] fix(gain): use weighted savings rate in per-command stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AVG(savings_pct) produced a simple unweighted mean that diluted the reported rate for high-volume commands — grep showed 14.8% while the true weighted rate was 98.6%, a 84-point gap. Replace AVG(savings_pct) with SUM(saved_tokens)*100.0/SUM(input_tokens) in the per-command SQL query so the rate reflects actual byte savings rather than an average of per-invocation percentages. Also: - Rename "Avg%" column header to "Rate" (more accurate label) - Add doc warning on CommandStats type alias against using AVG() - Add regression test proving weighted != unweighted on skewed data Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Florian BRUNIAUX --- src/analytics/gain.rs | 2 +- src/core/tracking.rs | 78 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/analytics/gain.rs b/src/analytics/gain.rs index 3938334a..f4bb120e 100644 --- a/src/analytics/gain.rs +++ b/src/analytics/gain.rs @@ -177,7 +177,7 @@ pub fn run( println!("{}", "─".repeat(table_width)); println!( "{:>3} {:count_width$} {:>saved_width$} {:>6} {:>time_width$} {: Result> { let (project_exact, project_glob) = project_filter_params(project_path); // added let mut stmt = self.conn.prepare( - "SELECT rtk_cmd, COUNT(*), SUM(saved_tokens), AVG(savings_pct), AVG(exec_time_ms) + "SELECT rtk_cmd, COUNT(*), SUM(saved_tokens), + CASE WHEN SUM(input_tokens) > 0 THEN SUM(saved_tokens) * 100.0 / SUM(input_tokens) ELSE 0.0 END, + AVG(exec_time_ms) FROM commands WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2) GROUP BY rtk_cmd @@ -1426,4 +1433,71 @@ mod tests { // but we can verify recovery_rate is between 0 and 100 assert!(summary.recovery_rate >= 0.0 && summary.recovery_rate <= 100.0); } + + // 14. get_by_command uses weighted savings rate, not unweighted average + // + // Regression test for: AVG(savings_pct) gave wrong results when small invocations + // with 0% savings diluted the average of high-volume commands. + // + // Setup: one small command (10% savings) + one large command (95% savings). + // Unweighted avg would be ~52.5%. Weighted rate must be ~95%. + // + // Uses a unique fake project path so project-filtered query returns only our rows, + // avoiding the LIMIT 10 cap on the shared DB. + #[test] + fn test_get_by_command_weighted_savings_rate() { + let tracker = Tracker::new().expect("Failed to create tracker"); + let pid = std::process::id(); + let cmd_name = format!("weighted_test_{}", pid); + // Unique project path keeps our rows isolated from the shared DB + let fake_project = format!("/tmp/rtk_weighted_test_{}", pid); + + // Override project_path by inserting directly via conn + let saved_small = 10_i64; // 100 in - 90 out = 10 saved → 10% + let saved_large = 95_000_i64; // 100_000 in - 5_000 out = 95_000 saved → 95% + tracker + .conn + .execute( + "INSERT INTO commands (timestamp, original_cmd, rtk_cmd, project_path, input_tokens, output_tokens, saved_tokens, savings_pct, exec_time_ms) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + rusqlite::params![ + chrono::Utc::now().to_rfc3339(), + &cmd_name, &cmd_name, &fake_project, + 100_i64, 90_i64, saved_small, 10.0_f64, 5_i64 + ], + ) + .expect("Failed to insert small invocation"); + tracker + .conn + .execute( + "INSERT INTO commands (timestamp, original_cmd, rtk_cmd, project_path, input_tokens, output_tokens, saved_tokens, savings_pct, exec_time_ms) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + rusqlite::params![ + chrono::Utc::now().to_rfc3339(), + &cmd_name, &cmd_name, &fake_project, + 100_000_i64, 5_000_i64, saved_large, 95.0_f64, 10_i64 + ], + ) + .expect("Failed to insert large invocation"); + + let by_cmd = tracker + .get_by_command(Some(&fake_project)) + .expect("Failed to get by_command stats"); + + let entry = by_cmd + .iter() + .find(|(name, _, _, _, _)| name == &cmd_name) + .expect("Test command not found in by_command stats"); + + let (_name, _count, _saved, rate, _time) = entry; + + // Weighted rate = (10 + 95_000) * 100.0 / (100 + 100_000) ≈ 94.99% + // Unweighted avg would be (10.0 + 95.0) / 2 = 52.5% + // The gap proves the fix works. + assert!( + *rate > 90.0, + "Expected weighted rate >90%, got {:.1}% — unweighted avg would be ~52.5%", + rate + ); + } } From 5de6ab2193b0217459730a6f5450b0e900937451 Mon Sep 17 00:00:00 2001 From: Florian BRUNIAUX Date: Sat, 28 Mar 2026 16:46:55 +0100 Subject: [PATCH 4/4] docs(changelog): add entry for gain weighted savings rate fix Signed-off-by: Florian BRUNIAUX --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e98489a2..1e2acda6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug Fixes +* **gain:** use weighted savings rate in per-command stats — `AVG(savings_pct)` replaced by `SUM(saved_tokens)/SUM(input_tokens)`, fixing commands like `grep` showing 14.8% instead of 98.6%; column renamed `Avg%` → `Rate` ([#891](https://github.com/rtk-ai/rtk/pull/891)) ([b3ab873](https://github.com/rtk-ai/rtk/commit/b3ab873)) * **diff:** correct truncation overflow count in condense_unified_diff ([#833](https://github.com/rtk-ai/rtk/pull/833)) ([5399f83](https://github.com/rtk-ai/rtk/commit/5399f83)) * **git:** replace vague truncation markers with exact counts in log and grep output ([#833](https://github.com/rtk-ai/rtk/pull/833)) ([185fb97](https://github.com/rtk-ai/rtk/commit/185fb97))