From 9833d66464e3fd98181aa5b0f25a967ee39fb23e Mon Sep 17 00:00:00 2001 From: TechWizard9999 <119874392+TechWizard9999@users.noreply.github.com> Date: Sat, 26 Sep 2026 09:08:58 +0530 Subject: [PATCH 1/7] fix: allow tee mode to record recall stats - Modify record_tee_elision() to create the recall store if needed - Modify record_tee_recall_with() to create the recall store if needed - Update misleading message in gain --recalls to mention tee mode This allows tee-only users to see their recall stats in 'rtk gain --recalls'. The stats table is lightweight and separate from the full recall store. Fixes #4219. --- src/analytics/gain.rs | 2 +- src/core/retriever.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/analytics/gain.rs b/src/analytics/gain.rs index 9502fd3539..fe984bed55 100644 --- a/src/analytics/gain.rs +++ b/src/analytics/gain.rs @@ -496,7 +496,7 @@ fn show_recall_stats() -> Result<()> { if stats.is_empty() { println!("No recall activity recorded yet."); - println!("Stats appear once filters elide output (failures, trimmed lists)."); + println!("Stats appear once filters elide output (failures, trimmed lists) in sqlite or tee mode."); return Ok(()); } diff --git a/src/core/retriever.rs b/src/core/retriever.rs index 046ecde9f9..58c67eb1de 100644 --- a/src/core/retriever.rs +++ b/src/core/retriever.rs @@ -384,9 +384,9 @@ pub fn record_tee_elision(cfg: &RetrieverConfig, slug: &str) { if cfg.mode == RecoveryMode::Disabled { return; } - // Never create the store from the tee path: choosing tee must leave no - // sqlite artifact behind. Stats are recorded only into an existing store. - if let Ok(Some(conn)) = open_existing(cfg) { + // Create the store if needed to record tee elision stats. The stats table + // is lightweight and separate from the full recall store. + if let Ok(conn) = open(cfg) { bump_stat(&conn, slug, "tee", "elisions"); } } @@ -430,7 +430,7 @@ fn record_tee_recall_with(cfg: &RetrieverConfig, slug: &str, path: &str) { if recovery_disabled_by_env() || cfg.mode == RecoveryMode::Disabled { return; } - if let Ok(Some(conn)) = open_existing(cfg) { + if let Ok(conn) = open(cfg) { record_tee_recall_on(&conn, slug, path); } } From 637a2d182704f97d0d1d244d6f6ded3bc9a36e85 Mon Sep 17 00:00:00 2001 From: TechWizard9999 <119874392+TechWizard9999@users.noreply.github.com> Date: Sat, 26 Sep 2026 09:10:35 +0530 Subject: [PATCH 2/7] fmt: fix line length in gain.rs message --- src/analytics/gain.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/analytics/gain.rs b/src/analytics/gain.rs index fe984bed55..77b84512ed 100644 --- a/src/analytics/gain.rs +++ b/src/analytics/gain.rs @@ -496,7 +496,9 @@ fn show_recall_stats() -> Result<()> { if stats.is_empty() { println!("No recall activity recorded yet."); - println!("Stats appear once filters elide output (failures, trimmed lists) in sqlite or tee mode."); + println!( + "Stats appear once filters elide output (failures, trimmed lists) in sqlite or tee mode." + ); return Ok(()); } From 58f42f2ae0078e8488acc90b4f005d7611c7369d Mon Sep 17 00:00:00 2001 From: TechWizard9999 <119874392+TechWizard9999@users.noreply.github.com> Date: Sat, 26 Sep 2026 09:42:30 +0530 Subject: [PATCH 3/7] fix: allow clean-run summaries to bypass never_worse guard for injected JSON Add empty_json_patterns to RunOptions so stdout-only filters that inject JSON format can register their clean-run empty output (e.g., ruff's [], pint's {"files":[]}). When raw stdout matches, the filter's human summary is preferred over the raw JSON. Fixes #4218. --- src/cmds/php/pint_cmd.rs | 2 +- src/cmds/python/ruff_cmd.rs | 2 +- src/core/runner.rs | 26 +++++++++++++++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/cmds/php/pint_cmd.rs b/src/cmds/php/pint_cmd.rs index 76923f474b..dcc74592d8 100644 --- a/src/cmds/php/pint_cmd.rs +++ b/src/cmds/php/pint_cmd.rs @@ -69,7 +69,7 @@ pub fn run(args: &[String], verbose: u8) -> Result { "pint", &args.join(" "), filter, - runner::RunOptions::stdout_only().tee("pint"), + runner::RunOptions::stdout_only().tee("pint").empty_json_patterns(&[r#"{"files":[]}"#]), ) } diff --git a/src/cmds/python/ruff_cmd.rs b/src/cmds/python/ruff_cmd.rs index 2bf6761107..07f8976535 100644 --- a/src/cmds/python/ruff_cmd.rs +++ b/src/cmds/python/ruff_cmd.rs @@ -111,7 +111,7 @@ pub fn run(args: &[String], verbose: u8) -> Result { truncate(stdout.trim(), config::limits().passthrough_max_chars) } }, - runner::RunOptions::stdout_only(), + runner::RunOptions::stdout_only().empty_json_patterns(&["[]"]), ) } diff --git a/src/core/runner.rs b/src/core/runner.rs index 1ee82d7dc0..3844db7754 100644 --- a/src/core/runner.rs +++ b/src/core/runner.rs @@ -44,6 +44,10 @@ pub struct RunOptions<'a> { /// can read from a pipe (e.g. `cat file | rtk wc`); without it the child /// gets an empty stdin and reports zero. pub inherit_stdin: bool, + /// Patterns that the tool emits on a clean run when JSON output is injected. + /// When raw stdout matches one of these, the filter's summary is preferred + /// over the raw JSON by the never_worse guard. + pub empty_json_patterns: &'a [&'a str], } impl<'a> RunOptions<'a> { @@ -80,6 +84,14 @@ impl<'a> RunOptions<'a> { self.inherit_stdin = true; self } + + /// Register empty JSON patterns that the tool emits on a clean run. + /// When raw stdout matches one of these, the filter's summary is preferred + /// over the raw JSON by the never_worse guard. + pub fn empty_json_patterns(mut self, patterns: &'a [&'a str]) -> Self { + self.empty_json_patterns = patterns; + self + } } pub type CaptureFilter<'a> = Box String + 'a>; @@ -144,7 +156,19 @@ where let shown = if let Some(label) = opts.tee_label { print_with_hint(&filtered, raw, raw_for_tracking, label, exit_code) } else { - let guarded = crate::core::guard::never_worse(raw_for_tracking, &filtered).to_string(); + let guarded = if opts.filter_stdout_only && !opts.empty_json_patterns.is_empty() { + // For stdout-only filters that inject JSON format, prefer the filter's summary + // when raw stdout matches a known empty-run pattern (e.g., "[]", "{\"files\":[]}"). + let raw_trimmed = raw_for_tracking.trim(); + let is_empty_json = opts.empty_json_patterns.iter().any(|p| raw_trimmed == *p); + if is_empty_json { + filtered.to_string() + } else { + crate::core::guard::never_worse(raw_for_tracking, &filtered).to_string() + } + } else { + crate::core::guard::never_worse(raw_for_tracking, &filtered).to_string() + }; if opts.no_trailing_newline { print!("{}", guarded); } else { From 8c033aa566b4ba58bf9ca17bdb8d2011004bd78c Mon Sep 17 00:00:00 2001 From: TechWizard9999 <119874392+TechWizard9999@users.noreply.github.com> Date: Sat, 26 Sep 2026 09:43:18 +0530 Subject: [PATCH 4/7] fmt: fix line length in pint_cmd.rs --- src/cmds/php/pint_cmd.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cmds/php/pint_cmd.rs b/src/cmds/php/pint_cmd.rs index dcc74592d8..24ba02ef07 100644 --- a/src/cmds/php/pint_cmd.rs +++ b/src/cmds/php/pint_cmd.rs @@ -69,7 +69,9 @@ pub fn run(args: &[String], verbose: u8) -> Result { "pint", &args.join(" "), filter, - runner::RunOptions::stdout_only().tee("pint").empty_json_patterns(&[r#"{"files":[]}"#]), + runner::RunOptions::stdout_only() + .tee("pint") + .empty_json_patterns(&[r#"{"files":[]}"#]), ) } From 7a446f1467743213f9c631de500341b2db787c47 Mon Sep 17 00:00:00 2001 From: TechWizard9999 <119874392+TechWizard9999@users.noreply.github.com> Date: Sat, 26 Sep 2026 09:45:38 +0530 Subject: [PATCH 5/7] fix: use contains() instead of iter().any() for clippy --- src/core/runner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/runner.rs b/src/core/runner.rs index 3844db7754..3a42b89c4f 100644 --- a/src/core/runner.rs +++ b/src/core/runner.rs @@ -160,7 +160,7 @@ where // For stdout-only filters that inject JSON format, prefer the filter's summary // when raw stdout matches a known empty-run pattern (e.g., "[]", "{\"files\":[]}"). let raw_trimmed = raw_for_tracking.trim(); - let is_empty_json = opts.empty_json_patterns.iter().any(|p| raw_trimmed == *p); + let is_empty_json = opts.empty_json_patterns.contains(&raw_trimmed); if is_empty_json { filtered.to_string() } else { From f9f40924aa1665e2e5a833dc25700745d6eccd62 Mon Sep 17 00:00:00 2001 From: Nicolas Le Cam Date: Sat, 26 Sep 2026 14:08:05 +0200 Subject: [PATCH 6/7] revert: leave the tee-mode recall store change to #4263 The two commits that let the tee path create the recall store fix #4219, which #4263 carries on its own. #4219 is an open design question (a tee install currently leaves no sqlite artifact behind), so it is decided there, and this branch stays on #4218. Co-Authored-By: Claude Opus 5.5 --- src/analytics/gain.rs | 4 +--- src/core/retriever.rs | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/analytics/gain.rs b/src/analytics/gain.rs index 77b84512ed..9502fd3539 100644 --- a/src/analytics/gain.rs +++ b/src/analytics/gain.rs @@ -496,9 +496,7 @@ fn show_recall_stats() -> Result<()> { if stats.is_empty() { println!("No recall activity recorded yet."); - println!( - "Stats appear once filters elide output (failures, trimmed lists) in sqlite or tee mode." - ); + println!("Stats appear once filters elide output (failures, trimmed lists)."); return Ok(()); } diff --git a/src/core/retriever.rs b/src/core/retriever.rs index 58c67eb1de..046ecde9f9 100644 --- a/src/core/retriever.rs +++ b/src/core/retriever.rs @@ -384,9 +384,9 @@ pub fn record_tee_elision(cfg: &RetrieverConfig, slug: &str) { if cfg.mode == RecoveryMode::Disabled { return; } - // Create the store if needed to record tee elision stats. The stats table - // is lightweight and separate from the full recall store. - if let Ok(conn) = open(cfg) { + // Never create the store from the tee path: choosing tee must leave no + // sqlite artifact behind. Stats are recorded only into an existing store. + if let Ok(Some(conn)) = open_existing(cfg) { bump_stat(&conn, slug, "tee", "elisions"); } } @@ -430,7 +430,7 @@ fn record_tee_recall_with(cfg: &RetrieverConfig, slug: &str, path: &str) { if recovery_disabled_by_env() || cfg.mode == RecoveryMode::Disabled { return; } - if let Ok(conn) = open(cfg) { + if let Ok(Some(conn)) = open_existing(cfg) { record_tee_recall_on(&conn, slug, path); } } From bff334ca84ae7d563d52abb6a2e9ced3936e37f9 Mon Sep 17 00:00:00 2001 From: Nicolas Le Cam Date: Sat, 26 Sep 2026 14:08:45 +0200 Subject: [PATCH 7/7] fix(ruff): bypass never_worse only for the format rtk injected `[]` is a clean run only when rtk chose `--output-format=json`. A user who asked for JSON must get `[]` back, so `rtk ruff check --output-format=json | jq` keeps working on a clean tree; register the pattern only when rtk injected the format. Move the choice between the raw capture and the filter summary into guard_stdout so it can be tested, list the exemption in guard.rs next to the one for `rtk diff`, and drop the pint registration: pint runs under a tee label, which never reads it, and a clean pint run prints a ~140-byte JSON that already loses to its summary. Co-Authored-By: Claude Opus 5.5 --- src/cmds/php/pint_cmd.rs | 4 +-- src/cmds/python/ruff_cmd.rs | 8 ++++- src/core/guard.rs | 6 +++- src/core/runner.rs | 60 +++++++++++++++++++++++-------------- 4 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/cmds/php/pint_cmd.rs b/src/cmds/php/pint_cmd.rs index 24ba02ef07..76923f474b 100644 --- a/src/cmds/php/pint_cmd.rs +++ b/src/cmds/php/pint_cmd.rs @@ -69,9 +69,7 @@ pub fn run(args: &[String], verbose: u8) -> Result { "pint", &args.join(" "), filter, - runner::RunOptions::stdout_only() - .tee("pint") - .empty_json_patterns(&[r#"{"files":[]}"#]), + runner::RunOptions::stdout_only().tee("pint"), ) } diff --git a/src/cmds/python/ruff_cmd.rs b/src/cmds/python/ruff_cmd.rs index 07f8976535..484ad9110b 100644 --- a/src/cmds/python/ruff_cmd.rs +++ b/src/cmds/python/ruff_cmd.rs @@ -64,6 +64,12 @@ pub fn run(args: &[String], verbose: u8) -> Result { || (a == "--output-format" && args.get(i + 1).is_some_and(|n| n == "json")) }); let use_json_filter = !user_set_output_format || user_json_format; + // `[]` is a clean run only when rtk chose the format; a user who asked for JSON gets `[]` back. + let clean_outputs: &[&str] = if is_check && !user_set_output_format { + &["[]"] + } else { + &[] + }; if is_check { if user_set_output_format { @@ -111,7 +117,7 @@ pub fn run(args: &[String], verbose: u8) -> Result { truncate(stdout.trim(), config::limits().passthrough_max_chars) } }, - runner::RunOptions::stdout_only().empty_json_patterns(&["[]"]), + runner::RunOptions::stdout_only().clean_outputs(clean_outputs), ) } diff --git a/src/core/guard.rs b/src/core/guard.rs index 54a77d5c72..3824d448ba 100644 --- a/src/core/guard.rs +++ b/src/core/guard.rs @@ -1,6 +1,6 @@ //! Never-worse output guard: RTK never emits more tokens than the raw command. //! -//! One caller is allowed past it. `rtk diff` prints a one-line message for a +//! Two callers are allowed past it. `rtk diff` prints a one-line message for a //! difference `str::lines()` cannot render — CRLF against LF, or a missing //! final newline — where the raw fallback is two blobs that look identical and //! answer the question worse at any size. The exception is bounded by the case @@ -10,6 +10,10 @@ //! the shortest form of the message is ~20 tokens and a one-line pair is ~2, so //! the ceiling sat under the message's own floor and dropped it on 90% of //! one-line pairs. +//! +//! The other is a clean run of a command whose output format rtk injected +//! (`runner::RunOptions::clean_outputs`): ruff's `[]` answers a question the +//! user never asked, so the filter's summary is shown although it is longer. use crate::core::tracking::estimate_tokens; diff --git a/src/core/runner.rs b/src/core/runner.rs index 3a42b89c4f..3478c90f39 100644 --- a/src/core/runner.rs +++ b/src/core/runner.rs @@ -44,10 +44,9 @@ pub struct RunOptions<'a> { /// can read from a pipe (e.g. `cat file | rtk wc`); without it the child /// gets an empty stdin and reports zero. pub inherit_stdin: bool, - /// Patterns that the tool emits on a clean run when JSON output is injected. - /// When raw stdout matches one of these, the filter's summary is preferred - /// over the raw JSON by the never_worse guard. - pub empty_json_patterns: &'a [&'a str], + /// What the tool prints on a clean run in the output format rtk injected, e.g. ruff's + /// `[]`. See [`guard_stdout`]. Not consulted when `tee_label` is set. + pub clean_outputs: &'a [&'a str], } impl<'a> RunOptions<'a> { @@ -85,15 +84,24 @@ impl<'a> RunOptions<'a> { self } - /// Register empty JSON patterns that the tool emits on a clean run. - /// When raw stdout matches one of these, the filter's summary is preferred - /// over the raw JSON by the never_worse guard. - pub fn empty_json_patterns(mut self, patterns: &'a [&'a str]) -> Self { - self.empty_json_patterns = patterns; + pub fn clean_outputs(mut self, outputs: &'a [&'a str]) -> Self { + self.clean_outputs = outputs; self } } +/// The stdout to show: `filtered`, unless it costs more tokens than `raw`. +/// +/// A `raw` that is exactly one of `clean_outputs` is the empty form of a format rtk injected, +/// not output the user asked for, so the filter's summary is shown even though it is longer. +fn guard_stdout<'a>(raw: &'a str, filtered: &'a str, clean_outputs: &[&str]) -> &'a str { + if clean_outputs.contains(&raw.trim()) { + filtered + } else { + crate::core::guard::never_worse(raw, filtered) + } +} + pub type CaptureFilter<'a> = Box String + 'a>; pub type ExitAwareCaptureFilter<'a> = Box String + 'a>; @@ -156,19 +164,7 @@ where let shown = if let Some(label) = opts.tee_label { print_with_hint(&filtered, raw, raw_for_tracking, label, exit_code) } else { - let guarded = if opts.filter_stdout_only && !opts.empty_json_patterns.is_empty() { - // For stdout-only filters that inject JSON format, prefer the filter's summary - // when raw stdout matches a known empty-run pattern (e.g., "[]", "{\"files\":[]}"). - let raw_trimmed = raw_for_tracking.trim(); - let is_empty_json = opts.empty_json_patterns.contains(&raw_trimmed); - if is_empty_json { - filtered.to_string() - } else { - crate::core::guard::never_worse(raw_for_tracking, &filtered).to_string() - } - } else { - crate::core::guard::never_worse(raw_for_tracking, &filtered).to_string() - }; + let guarded = guard_stdout(raw_for_tracking, &filtered, opts.clean_outputs).to_string(); if opts.no_trailing_newline { print!("{}", guarded); } else { @@ -1014,6 +1010,26 @@ fn is_bun_count_line(trimmed: &str) -> bool { ) } +#[cfg(test)] +mod guard_stdout_tests { + use super::*; + + const SUMMARY: &str = "Ruff: No issues found"; + + #[test] + fn an_injected_clean_output_shows_the_summary() { + assert_eq!(guard_stdout("[]\n", SUMMARY, &["[]"]), SUMMARY); + assert_eq!(guard_stdout("[]\r\n", SUMMARY, &["[]"]), SUMMARY); + } + + #[test] + fn any_other_short_output_still_goes_through_never_worse() { + assert_eq!(guard_stdout("{}\n", SUMMARY, &["[]"]), "{}\n"); + assert_eq!(guard_stdout("[ ]\n", SUMMARY, &["[]"]), "[ ]\n"); + assert_eq!(guard_stdout("[]\n", SUMMARY, &[]), "[]\n"); + } +} + #[cfg(test)] mod forwarded_stderr_tests { use super::*;