Repository navigation
feat(git): filter large git show blob dumps with recovery and Latin-1 decoding - #3265
Conversation
|
Gentle nudge — open ~10 days, CI green and CLA signed. @aeppling @KuSh, a review whenever you get a chance would be much appreciated. This is the highest-value of my open PRs technically: it filters large |
|
Follow-up from review — none of these block merge, flagging for awareness / potential fast-follow. 1. The ambiguous-encoding guard only checks for bytes in if bytes.iter().any(|&b| (0x80..=0x9F).contains(&b)) {
Decoded::Binary
} else {
Decoded::Latin1(bytes.iter().map(|&b| b as char).collect())
}That correctly catches Shift-JIS/GBK/Big5 (lead bytes overlap Not a regression for the case this PR targets (ISO-8859-1 European source), but worth a follow-up — e.g. sniffing a UTF-16/UTF-32 BOM explicitly, and/or treating a high density of bytes clustered in 2. Ambiguous CP1252 detection is conservative in the other direction
3. Multiple blob args always pass through whole
4. fn blob_truncation(raw: &str, budget: usize, max_recoverable: usize) -> Option<(&str, usize, usize)>Three |
|
One more follow-up, on
Considered making Narrower alternative: keep Not a blocker — just a low-risk follow-up to consider if |
|
Thanks for the thorough read, @KuSh — these are all fair, and I agree with the "fidelity over guessing" framing. Quick pass on each:
I'll bundle (1), (4) and (5) — since none block merge, I'm glad to do them as a follow-up right after this lands, or push them here now if you'd prefer to review them together. Just say which. Unrelated, only if it helps your batching: my other single-focus PRs off |
Take a look at commit 57872f8 that just landed, you may find something useful there (also, I'll try to have a look at your other work as soon as I get time! |
4c555db to
b19d9b0
Compare
|
Rebased onto current
New tests cover EUC-JP/KR → Binary, UTF-16/UTF-8 BOM decode, UTF-32 BOM → Binary, BOM-prefixed binary → Binary, and the two documented low-density limits. On your other non-blocking nits (4 |
|
On the two open "your call" nits from before — please just fold both into this PR now, they're small:
Both are self-contained and low-risk enough that a fast-follow PR is more overhead than doing it here. |
KuSh
left a comment
There was a problem hiding this comment.
Re-reviewed against the latest commit (b19d9b06b), after the CJK/BOM hardening. That part looks solid — nice use of for_bom and the adjacency-based CJK density check is a good call (sparse accents vs. whole-file CJK is exactly the right signal). Two correctness bypasses below block merge; the rest are non-blocking but worth a look.
Addresses KuSh's review on rtk-ai#3265: - --stat/--numstat/--pretty/--format with a <rev>:<blob> target now take the byte-safe blob path, not the lossy-UTF-8 summary passthrough (git ignores those flags for a blob and dumps the file; verified byte-identical). Routing is now a single show_route() classifier that decides Blob first. - A trailing `-- <path>` beside a blob arg no longer disables blob handling. run_show now calls restore_double_dash() (like run_diff/run_checkout) so the separator reaches blob_show_targets(), also fixing a latent mis-route of `git show <rev> -- <path:with:colon>` to a blob dump. - Fold nits: Budget/MaxRecoverable newtypes on blob_truncation; shared capture_raw() (restores the signal diagnostic on the raw path); looks_binary() samples head+tail; blob-windowing savings test; note the "tree " false positive.
|
Pushed a follow-up ( Both bypasses had the same root you pointed at — blob detection sat beside the other gates instead of ahead of them — so I collapsed routing into a single For the Nits:
|
KuSh
left a comment
There was a problem hiding this comment.
Thanks for the follow-up work — I went through f4b883a line by line and re-verified each of my earlier points against real git 2.53. Five of the eight threads are genuinely closed and I've resolved them: the two blocking routing bugs (I confirmed live that --stat/--numstat/--shortstat/--pretty/--format on a blob target are sha256-identical to a plain dump, and that a trailing -- <path> still dumps the blob), the head-only binary scan, the tree false-positive note, and the signal diagnostic on the raw capture path. The Budget/MaxRecoverable newtypes and the shared capture_raw() helper both landed as discussed. fmt, clippy --all-targets -D warnings and cargo test --all are clean on my side too.
Three earlier threads I've left open, and a fresh pass on the new code turned up six issues — one of which I think is a genuine regression against develop. Requesting changes for that one; the rest are calibration, not blockers.
Still open from before
git.rs:487/git.rs:524— the cross-file pointers (see core::tee::write_tee_file for the same fix,Mirrors the binary path in cloud/curl_cmd.rs) are unchanged. Thewhyformax_recoverablemoved from theblob_truncationdoc to theMaxRecoverablenewtype doc, which is an improvement, but the comments I flagged still read as "go look over there" rather than explaining themselves.git.rs:279—stderris still dropped on success in both theStatOrFormatandBlobbranches. Pre-existing and minor, just not addressed.git.rs:2615—test_blob_windowing_token_savingsgenerates 600 formatted lines rather than using a real fixture, and that isn't cosmetic here: I measuredtests/fixtures/git/blob_large.txt(the real fixture this PR ships) at 24.9% savings — 10 765 B → 8 165 B head. The synthetic input passes only because it's ~40 KB. A fixture large enough to clear the 60% floor would make the assertion mean something; as written it mostly asserts that 40 KB is bigger than 8 KiB.
New this pass — six inline comments below. #1 is the one I'd like fixed before merge: the whole-buffer Latin-1 fallback corrupts predominantly-UTF-8 files worse than the develop behaviour it replaces, on exactly the mixed-encoding legacy files this PR is aimed at. #2 and #4 are also worth a look since both change output for inputs that previously passed through untouched.
None of this takes away from the core of the PR — the routing rework is clean, the line math is right (including the no-trailing-newline case), the is_char_boundary walk-back is panic-safe, and I confirmed end-to-end recovery is byte-identical: head -122 out && tail -n +123 tee.log reproduces git show HEAD:big.txt exactly.
|
Thanks for the thorough pass. Follow-up in Blocking
|
KuSh
left a comment
There was a problem hiding this comment.
Re-reviewed at 3e07c40 in a clean worktree. Gate is green: cargo fmt --check, cargo clippy --all-targets (zero warnings) and cargo test --all all pass.
Verified fixed — resolved
- The blocking whole-buffer Latin-1 re-decode. Confirmed with a before/after build of both heads rather than from the diff: at
f4b883aa predominantly-UTF-8 blob came out asC3 83 C2 A9(é); at3e07c40it is byte-identicalC3 A9(é). I also pushed the newvalid_mb > invalidheuristic past the single repeated-kanji test that ships with it — real prose in EUC-JP, Shift_JIS, EUC-KR, GBK, GB18030 and Big5 all still route toBinarywith a 3–8x margin, and French/German/Spanish ISO-8859-1 and CP1252 scorevalid_mb = 0, so genuine Latin-1 cannot be stolen by the new arm. The fix is sound. - The synthetic savings fixture. Reproduced the number independently:
src/main.rsgives exactly 91.7%, against 24.9% for the shippedblob_large.txt. Good call on the swap.
Still requesting changes
The is_blob_show_arg work is the problem. The partial fix landed :(, :!, and an inline comment waving off the option-value case as "a known (pre-existing) false positive" — but that reasoning does not survive the routing change this PR makes.
On develop the misclassification was genuinely harmless, because wants_blob_show ended in a plain print!("{}", result.stdout) — full passthrough. This PR sends the same misclassified args to ShowRoute::Blob, which windows at 8 KiB. The false positive is pre-existing; silently truncating a commit diff because of it is not. Two live instances below, one of them the :^ synonym the partial fix missed.
Also outstanding from the previous round and untouched, on their existing threads: the stripped UTF-8 BOM, the 1 MB max_recoverable cap that no-ops the feature on the largest blobs, metrics tracked against the transcoded string, and the tee file written before never_worse may discard it.
Everything below is reproduced against 3e07c40, not read off the diff.
3e07c40 to
275f245
Compare
Addresses KuSh's review on rtk-ai#3265: - --stat/--numstat/--pretty/--format with a <rev>:<blob> target now take the byte-safe blob path, not the lossy-UTF-8 summary passthrough (git ignores those flags for a blob and dumps the file; verified byte-identical). Routing is now a single show_route() classifier that decides Blob first. - A trailing `-- <path>` beside a blob arg no longer disables blob handling. run_show now calls restore_double_dash() (like run_diff/run_checkout) so the separator reaches blob_show_targets(), also fixing a latent mis-route of `git show <rev> -- <path:with:colon>` to a blob dump. - Fold nits: Budget/MaxRecoverable newtypes on blob_truncation; shared capture_raw() (restores the signal diagnostic on the raw path); looks_binary() samples head+tail; blob-windowing savings test; note the "tree " false positive.
|
Rebased onto Both blockers
Non-blockers: BOM preserved (decode only when windowing), tee-independent recovery hint One thing I want to flag for your call — the I'd added it to honor "truncation must not change a piped consumer's bytes", but on measuring it, it defeats the feature's purpose. rtk is invoked from the agent hook with stdout piped, so
Only the blob path self-disabled in a pipe; So I removed the gate and aligned with If you still want pipe byte-exactness as a hard rule I'm happy to restore the gate and instead scope the PR's savings claims to interactive TTY — but flagging that it makes the feature inert for piped agents. Your call. fmt/clippy ( |
KuSh
left a comment
There was a problem hiding this comment.
Round 3, verified at 275f2450 in a dedicated worktree against 3e07c40 as the reference point. Gate is green — cargo fmt --check, cargo clippy --all-targets (0 warnings), cargo test --all (3141) — and still green merged with the latest develop, which has moved 12 commits past your merge-base (3154 tests, clippy clean).
Everything from round 2 is genuinely fixed
I ran each repro rather than reading the diff:
-S 'url:1'and:^subnow take the commit-diff route — confirmed by the route marker ([full diff: rtk git diff --no-compact]), not by byte count, since a smaller output could equally have meant a blob truncation.- BOM preserved byte-identically (
ef bb bf). - The 1 MB cap is gone: a 1.38 MB blob now windows, where it previously passed through whole.
- The reworked recovery hint reconstructs byte-exactly, and
shell_single_quotehandles',$and spaces correctly. decode_process_outputon both stderr paths;track_bytesmeasures the bytes git actually wrote.
Dropping the tee for a self-describing git show <rev>:<path> | tail -n +N hint is a genuine improvement — it removed two of my previous findings structurally rather than patching them. Nice.
Two corrections I owe you
1. My 60% floor claim was wrong. I told you blob_large.txt's 24.9% was "far under the repo's 60% release-blocker floor". It is not. CONTRIBUTING.md:76 and :268 say 20%+, and docs/contributing/TECHNICAL.md:369/401/408 says the same — the sample there is literally assert!(savings >= 20.0). Only .claude/rules/cli-testing.md still says 60%, and it is stale on that point. 24.9% always cleared the real floor.
That one wrong finding cost you two rounds of fixture churn and pushed you off a real committed fixture onto 110 KB of synthetic filler — which the still-valid part of that same rules file lists as an anti-pattern ("use real command output, not synthetic data"). Sorry for the runaround. See the inline note on the test.
2. My is_terminal suggestion was wrong, and your revert is right. I verified it: a command run the way the agent hook runs it reports stdout isatty: False, so gating on a TTY would have made blob windowing inert in its primary scenario. Worth noting your commit message cites hooks/init.rs for this, but that comment guards stdin().is_terminal() for an interactive prompt — it is not evidence about stdout. The argument stands on its own without it. For what it is worth, the reason nobody hit this before is that every existing stdout().is_terminal() in the tree gates presentation only (--line-buffered, colour, curl formatting); this would have been the first gating content preservation. That distinction is worth a line in the comment, because the next reader will ask.
Still requesting changes
The classification bug class is now in its third round: round 1 :(exclude), round 2 :^ and -S 'url:1', and now two more instances. Rather than filing them one at a time I fuzzed it — 732 generated git show invocations, differential against real git:
- 3 misroutes — commit diff truncated into the 8 KiB blob window (all clustered shorts:
-pwG,-wpG,-wG … -I) - 15 silent savings losses — a ~23 KB commit diff passed through completely uncompacted: no error, no marker, zero savings
- 0 broken recovery hints among correctly-routed blobs
That is 18/732 ≈ 2.5% of realistic invocations. The second bucket is the one that worries me, because it is invisible — the filter simply does nothing and nothing says so. The useful negative result: the fuzzer found no new classes; every hit traces to the two root causes below.
Details and the suggested shape are inline. Full findings list, ranked, with the two blocking ones first.
… decoding `git show <rev>:<path>` dumps raw file content that RTK previously passed through unfiltered (0% savings). Large text blobs are now capped to an 8 KiB byte budget with a `tail -n +N` recovery pointer (the full blob is tee'd), mirroring the commit-diff path. Small blobs, tree listings, binary, and blobs too large for the recovery file to store intact are passed through unchanged. Blobs are captured as raw bytes and decoded encoding-aware: ISO-8859 / Latin-1 files (e.g. Oracle PL/SQL packages) are transcoded losslessly instead of being corrupted into U+FFFD by lossy UTF-8 decoding, which also unblocks their compression. Ambiguous single-byte encodings (CP1252 range) and binary content are passed through raw rather than guessed.
Follow-up to KuSh's review of the git-show blob filter. The Latin-1 fallback in decode_output silently mis-decoded legacy multi-byte CJK encodings and had no BOM handling. Now, before the Latin-1 transcode: - BOM sniff via encoding_rs::Encoding::for_bom (now a first-class, all-platform dep after rtk-ai#2717): honor UTF-8/UTF-16 BOMs and decode them correctly instead of passing them through as Binary. - Guard the UTF-32 BOM (FF FE 00 00 / 00 00 FE FF) explicitly first: encoding_rs has no UTF-32, and the UTF-32LE BOM shares its first two bytes with UTF-16LE, so for_bom would decode a UTF-32 body as garbage. - Sanity-check the BOM decode: a binary that merely starts with a UTF-16 BOM decodes to control-char garbage, so text_looks_binary sends the common (NUL/ control-dense) case back to raw passthrough. A clean BMP-text decode is kept (honoring a BOM is standard); catching the rare clean-decode-of-binary case would need a full charset detector — documented as a known, negligible limit. - Density guard looks_multibyte_cjk: a dense run of adjacent 0xA1-0xFE pairs is EUC-JP/KR/GBK/Big5, not Latin-1, so return Binary (raw passthrough) rather than emit silent mojibake. Targets substantially-CJK blobs (the whole-file case); sparse CJK in mostly-ASCII text stays below the floor — a documented limit, as density alone can't tell it from a Latin-1 accent without a charset detector. Adversarially reviewed (challenger + Opus auditor with a reproduction harness): UTF-32 collision and a BOM-prefixed-binary corruption path were found and fixed; residual is bounded, read-only, and reversible. 13 decode tests, suite 2635/0.
Addresses KuSh's review on rtk-ai#3265: - --stat/--numstat/--pretty/--format with a <rev>:<blob> target now take the byte-safe blob path, not the lossy-UTF-8 summary passthrough (git ignores those flags for a blob and dumps the file; verified byte-identical). Routing is now a single show_route() classifier that decides Blob first. - A trailing `-- <path>` beside a blob arg no longer disables blob handling. run_show now calls restore_double_dash() (like run_diff/run_checkout) so the separator reaches blob_show_targets(), also fixing a latent mis-route of `git show <rev> -- <path:with:colon>` to a blob dump. - Fold nits: Budget/MaxRecoverable newtypes on blob_truncation; shared capture_raw() (restores the signal diagnostic on the raw path); looks_binary() samples head+tail; blob-windowing savings test; note the "tree " false positive.
…ffer Latin-1 Address review (blocking + two non-blocking): - decode_output: the Err arm Latin-1-transcoded the whole buffer on a single invalid byte, mangling every multibyte char in a near-UTF-8 file (é C3 A9 -> é) — worse than develop's from_utf8_lossy. Add utf8_multibyte_scan and, when well-formed multibyte sequences outnumber invalid bytes, decode lossy (keep the text, replace only bad bytes). Runs before the C1 guard so a UTF-8 continuation in 0x80–0x9F (ć C4 87) isn't dumped as Binary. Genuine Latin-1 has ~no valid multibyte runs (valid_mb≈0) so it stays on the Latin-1 path; verified natural CJK (EUC-JP/KR, GBK, Big5) still routes to Binary. - is_blob_show_arg: exclude magic pathspecs (:(exclude), :!) — pathspecs, not blobs. The -S 'a:b' option-value colon stays a documented pre-existing FP. - test_blob_windowing_token_savings: use a real large blob (src/main.rs, the git show HEAD:<src> case) instead of synthetic lines; 91.7% savings. - Add large-dense-EUC-JP-blob and mostly-UTF-8-with-bad-byte regression tests.
…ted flag operands B1: `:^` is an exact synonym of `:!` (exclude magic) and is a pathspec, not a blob, so exclude it from `is_blob_show_arg` — `git show <rev> :^dir` now routes to the commit-diff path instead of a raw blob dump. B2: only git show's first positional object can be a `<rev>:<path>` blob. `show_positionals` walks the args with git's own flag/value grammar (`consumes_next_token_as_value`), so a value-taking flag's separate operand (`-S 'url:1'`, `-L 1,2:file`) is skipped rather than misread as a blob and truncated to 8 KiB. This also resolves the blob/commit-mix case.
…iew non-blockers)
N1: emit small blobs (<= 8 KiB) as git's raw bytes with no decode, so a
passthrough blob keeps its BOM.
N2: derive the recovery hint from the blob arg itself
(`git show <rev>:<path> | tail -n +N`) instead of a tee file, so the
windowing works for blobs larger than the tee's max_file_size cap; drop
the MaxRecoverable cap and tee::max_recoverable_bytes entirely.
N3: track savings against the raw bytes git wrote, not the transcoded text,
via TimedExecution::track_bytes (avoids overstating the reduction).
N5: decode captured stderr with decode_process_output, not from_utf8_lossy.
N6: skip windowing when stdout is not a TTY — truncation changes the bytes a
piped consumer receives, not just the on-screen presentation.
N8: window the token-savings test against a committed deterministic fixture
instead of include_str!(main.rs).
N9: replace bare see-also doc pointers with the real rationale.
N10: surface git's stderr on success too, not only on failure.
N4/N7 do not apply: with N2 this path no longer writes a tee file, so there
is no tee-write ordering to fix nor a tee.mode to honour here.
The is_terminal gate disabled blob windowing whenever stdout was not a TTY.
Since rtk is invoked by the agent hook with stdout piped, that left the
feature inert in its primary scenario (0 token savings on a piped
git show <rev>:<path>), while diff/log/status all compact in a pipe. It was
also unsound as a safety guard: CI agents hand rtk a pseudo-TTY (see
hooks/init.rs), so is_terminal() does not reliably tell a human from a
downstream consumer.
Window large text blobs regardless of TTY and rely on the tee-independent
'git show <rev>:<path> | tail -n +N' recovery hint for exact reconstruction --
the same tradeoff 'git log | grep' already accepts. Small (<=budget) and
binary blobs still pass through byte-identically. Also skip colons-only
tokens (':' , '::') in is_blob_show_arg so they route to git rather than the
blob window.
…variant
The blob-vs-non-blob classifier tried to mirror git's flag grammar and was
wrong three rounds running: it missed --ignore-matching-lines and the combined
short-flag clusters (-pS, -wG, -pI, -pwG, -wpG) whose tail git re-parses, so a
colon-carrying flag VALUE was windowed as if it were a blob.
Ask git instead. show_route stays a cheap pure pre-filter (does the first
positional look like rev:path?); run_show then confirms authoritatively with
'git cat-file -t <arg>' via git_cmd(global_args) and only windows an exact
'blob'. tree/commit/tag or a non-zero exit route to the normal commit-diff /
passthrough path. This subsumes and removes the raw.starts_with("tree ")
content-sniff (a false positive for Newick/NEXUS blobs). Marked with a
TODO(after rtk-ai#3681) so a flag pre-filter can later avoid the probe on the common
path.
Fidelity invariant ('byte-identical unless we successfully windowed'): window
ONLY content that is valid UTF-8 byte-for-byte, so the 'git show rev:path |
tail -n +N' hint always reconstructs exactly. Latin-1, UTF-16/BOM, binary and
lossy-UTF-8 now pass through byte-identically instead of being transcoded and
windowed (which broke recovery and could inflate output past git). This orphans
decode_output and its Latin-1/CJK/BOM helpers in core::stream, which are
removed. The recovery hint now carries the command's global args (-C, -c,
--git-dir, --work-tree) so it is runnable from outside the repo.
When rtk emitted MORE than the wrapped command, record() saturated saved to 0 and reported a fake '0% — did nothing' instead of reflecting the regression. Compute saved as signed (input - output) and store the honest negative in both saved_tokens and savings_pct. Aggregate readers clamp a negative to 0 for their unsigned token-count API so a regression can never wrap to a huge usize, while per-command savings_pct keeps the honest negative. Covered by a new test.
…uncate A large blob is windowed to a preview with a recovery hint, so redirecting a manual 'rtk git show HEAD:x > file' can silently truncate (exit stays 0). Note it in the git-show user docs and point at plain 'git show' / the recovery hint.
Hermetic temp repo with objects of every relevant type (large/small UTF-8, Latin-1 and UTF-16 blobs, a tree, commits). A fixed-seed PRNG generates 600 'git show <random flags> <arg>' invocations and checks rtk's classification (windowed / passthrough / commit-diff) against 'git cat-file -t' ground truth: 0 misroutes (never window a non-blob), 0 silent losses (a large recoverable UTF-8 blob always windows), and byte-identical passthrough for declined blobs. A companion test proves the recovery hint reconstructs a windowed blob byte-for-byte.
…r-candidate probe The blob-show classifier still had a selection gap for short-flag CLUSTERS. git re-parses a cluster's tail (`-wG x:y` == `-w -G x:y`), so `git show -wG x:y HEAD:big` feeds `x:y` to -G and dumps only `HEAD:big`. But `show_positionals` knew only single-letter value flags, so it kept `x:y` as the first positional; the cat-file probe rejected it and the real blob fell through to the commit-diff path — either dropping the windowing savings (large UTF-8 blob) or lossily decoding a Latin-1 blob into U+FFFD mojibake. Two changes, neither of which mirrors git's flag grammar: * Cluster-aware walker. `flag_token_consumes_next` extends the existing flag/value table to a short cluster's tail: the first value-taking short flag takes the rest of the cluster inline (`-Sfoo`) or, if it is the last char, the next token (`-wG x`). `is_short_value_flag` derives its char set FROM `consumes_next_token_as_value` so the table stays the single source of truth. (TODO points at rtk-ai#3681's ValueSpec.) * Probe every candidate, keep any blob on the byte-safe path. `run_show` now cat-file-probes EVERY `rev:path` positional, not just the first, and takes the byte-safe path whenever ANY resolves to a blob — so even a walker miss can't misroute a blob's raw bytes through the lossy commit-diff decode. Windowing itself stays gated on a single sole positional, which also keeps `git show <commit> <rev:path>` (two real objects git concatenates) as byte-identical passthrough. Also force byte-identical passthrough (never window) when a content-transforming flag (--textconv/--filters/--ext-diff) or a trailing `-- <pathspec>` is present: the `git show rev:path | tail` recovery hint omits them, so windowing could not reconstruct git's bytes exactly. Narrows the fidelity-invariant comment accordingly. `blob_show_target` (first-positional-only) is replaced by `blob_candidates` (all candidates); unit tests migrated and extended to cover the cluster shapes.
…ial fuzzer The blocker hid for three rounds because the fuzzer only ever paired a colon-carrying flag cluster (`-wG x:y`, `-pS url:1`, …) with a blob-LESS `HEAD` (the CommitMisroute trap). It never placed such a cluster BEFORE a real blob, which is exactly where the walker gap bit. Adds four crossed kinds: * ClusterLargeUtf8 — cluster + large UTF-8 blob → MUST window (0 silent loss); * ClusterLatin1 — cluster + Latin-1 blob → byte-identical to `git show <same args>`, no U+FFFD mojibake; * TextconvLargeUtf8 / PathspecLargeUtf8 — `--textconv` or a trailing `-- <path>` → never windowed, byte-identical passthrough. Plus a dedicated end-to-end test that `-wG x:y HEAD:large.txt` windows and that its recovery hint (which names only the blob arg) reconstructs the blob byte-for-byte. The extended fuzzer FAILS on the pre-fix code (verified: cluster windowing regression + Latin-1 fidelity break) and passes on the fix. Seed and iteration count unchanged.
275f245 to
06bae5a
Compare
|
Thanks for the depth of the round-3 review — and for the two corrections, both taken:
The classification class — closed at the root, with the fuzzer as the proofYou were right that an allowlist mirroring git's grammar keeps drifting. Two changes:
Windowing gate: Differential fuzzer, committed ( Fidelity invariant (round-3 blocker)Windowing now happens only when Other round-3 items
Honest residualA long value-flag in space form that isn't in the table ( Rebased onto the current |
KuSh
left a comment
There was a problem hiding this comment.
Round 6 (landing) — APPROVED, follow-ups filed
Claim: git show <rev>:<path> on a large blob is windowed to an 8 KiB head with an exact recovery hint, instead of dumping the whole file.
Scope: accept. (Derived and frozen now — the earlier rounds predate the current template.)
Ran: 851 unique git show invocations diffed against real git (UTF-8 / Latin-1 / UTF-16+BOM / binary blobs crossed with colon-bearing flag values and short-flag clusters); recovery round-trip; -C from outside the repo; gate on the head. Savings 24.9% vs the 20% floor (CONTRIBUTING.md).
Blocks merge (0)
Both round-3 blockers are closed, verified by execution rather than by reading the diff.
Classification class. My own fuzzer, written independently of the one you committed, on 851 invocations: misroutes 0, fidelity breaks 0, bad/inexact hints 0, silent savings losses 0. Round 3 was 3 and 15. The two instances that round found are gone:
--ignore-matching-lines a:b raw 22838 -> rtk 5736 (commit-diff route, compacted)
-pS / -pwG / -wpG / -pI no blob window, no truncation
The cat-file probe is the right call — it decides by asking git rather than mirroring its grammar, which is what kept drifting.
Fidelity invariant. "Window only when from_utf8 is Ok" holds exactly:
big_lossy.txt 25990 B byte-identical (was ef bf bd corruption at round 3)
u16.txt 56580 B byte-identical (was transcoded, BOM stripped)
latin1.txt 25089 B byte-identical
bin.dat 20480 B byte-identical
utf8_big.txt 32289 -> 8230 B windowed, head is a byte-exact prefix, `| tail -n +103` reconstructs exactly
Deleting the now-orphaned decode_output and its helpers rather than leaving ~420 lines of dead code was the right instinct.
Also verified: the hint carries global args (git '-C' '<path>' show '<arg>' | tail -n +N runs from outside the repo); tracking records signed savings (input - output as i64, no clamp); the savings test is back on the real blob_large.txt at 24.9% against a >= 20.0 assert with the synthetic filler deleted; the is_terminal comment now states the real argument and the content-vs-presentation distinction. Gate green on the head: fmt, clippy -D warnings 0, 3459 tests. PR sits exactly on the develop tip, 0 commits behind.
Filed as follow-ups
- Value-flag table gaps — #3954 —
--notes,--conflict-marker-size,--submoduletake a separated value but are not in the table, so the operand becomes a phantom positional and the real blob is never windowed. I confirmed your disclosed asymmetry, and that it is benign in both directions:Savings gap, never corruption — the conservative side, exactly as you described. Worth knowing this is the same class already filed three times against--ignore-matching-lines a:b HEAD:big.txt 32289 -> 32289 byte-identical (no savings) --ws-error-highlight all HEAD:big.txt 32289 -> 8225 windows correctlyrtk grep'sVALUE_FLAGS_SHORT(#3882, #3881, #2253, #3663), where it produces wrong answers rather than merely lost savings; thecat-fileprobe is what keeps the git instance benign. Adjacent but distinct from #3910, which is about output shape routing rather than operand consumption. Both close via #3681'sValueSpec, which yourTODO(after #3681)already points at. - Trailing newline on empty output — recorded as a comment on #3924 rather than a new issue, since that one already frames this exact byte-exactness question (and #3303 / #3659 are the same class).
println!at the stat/format branch emits a lone\nwhere git emits nothing. Not a behaviour change you introduced: the idiom is unchanged fromdevelop, and it only became reachable because the probe correctly stopped treatingHEAD:xas a blob.
Checked and correct — no need to re-verify
cat-file probe built via git_cmd(global_args); the positionals.len()==1 && blob_objects.len()==1 windowing gate; recovery offset arithmetic and shell_single_quote (verified against ', $, spaces in round 3, unchanged); content-transforming flags (--textconv/--filters/--ext-diff) forced to passthrough; your committed differential fuzzer (3 tests, passes, and I confirmed the extended version is what earns the "no other classes" claim).
Hypotheses built and dropped
- 96 "fidelity breaks" from my first fuzzer run — every one was the 1-byte trailing newline above, not a fidelity break. My detector was too strict; re-run with it excluded gives 0.
- Unaccented French in the new
FEATURES.mdwarning — the file has zero accented characters across 1447 lines, so you matched its existing convention. Not a defect.
Next
Nothing — approving. Two follow-up issues filed and linked above; both are #3681's territory or cosmetic. Thanks for the depth on this one: the cat-file probe and the committed differential fuzzer between them turn a class that produced findings in three consecutive rounds into one with a standing regression test.
Rounds: 6 (landing). Threads: 0 open, 6 resolved this round.
Dismissing my own approval: CI fails on all three runners at 06bae5a with a regression my round-3 finding introduced. Details in the comment below. Paused pending a maintainer decision on the telemetry endpoint.
Approval dismissed — on hold, and this one is my faultCI fails on all three runners at ubuntu, macos and windows, identical. I approved on a green local Root cause: my round-3 findingYou implemented exactly what I asked for at - let saved = input_tokens.saturating_sub(output_tokens);
+ let saved = input_tokens as i64 - output_tokens as i64;You then clamped the aggregate token-count readers with SELECT COALESCE(AVG(avg_sav), 0.0) FROM (
SELECT rtk_cmd, AVG(savings_pct) as avg_sav
FROM commands WHERE input_tokens > 0 GROUP BY rtk_cmd)and Reproduced deterministically: It stayed invisible in my local gate because that test reads the real user DB through Please hold — the fix is a design call, not a patchTwo ways to close it, and they disagree about what the metric means:
I lean towards 2, since the honest-negative behaviour was the point. What stops me committing to it: So this is paused on the maintainer confirming what the telemetry endpoint accepts for that field. Once that is settled the fix is a couple of lines either way, and nothing else in the PR is affected — the blob-show work all still verifies (851-invocation differential run clean, byte-fidelity invariant holding, gate otherwise green). Sorry for the churn: the finding was right, the approval was not. I should have grepped every reader of |
The signed record() change (9da3270) lets a net-regressing filter store a negative savings_pct, so both overall_savings_pct and avg_savings_per_command can now be negative. Two telemetry tests still asserted `(0.0..=100.0)` against the real user DB, so they panicked deterministically on any machine whose DB held a net-negative row -- which is why CI failed on all three runners after the approval, on test_enriched_stats_returns_valid_data. Widen both assertions (test_enriched_stats and its twin test_get_stats_returns_tuple) to the real invariant -- a saving never exceeds 100% -- dropping the stale 0 floor, and add a hermetic in-memory test pinning that a worsening command yields a negative aggregate instead of a fake 0.
|
Pushed a fix for the CI failure that dismissed the approval. Root cause. This PR's signed Fix. Instead of clamping the readers, I widened both assertions to the real invariant — a saving never exceeds 100% — and dropped the stale I went widen-not-clamp because the payload already sends negative Re-requesting review. (You also flagged that these telemetry tests read the developer's real DB — I kept that out of scope here, but happy to make them fully hermetic in a follow-up if you'd like.) |
`tokens_saved_24h` and `total_tokens_saved` both return `i64` from an unclamped `SUM(saved_tokens)` -- deliberately, unlike the `as usize` readers that take `.max(0)` for their unsigned API. A window whose filters emitted more than they saved sums honestly negative, so `assert!(saved_24h >= 0)` panics on any DB holding a net-negative row. No ordering invariant replaces the floor: with a positive row inside the 24h window and a larger negative one outside it, saved_24h (90) exceeds saved_total (-110). Nothing about either value is assertable, so the test stops binding them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KuSh
left a comment
There was a problem hiding this comment.
Round 7 (landing) — APPROVED
Claim: git show <rev>:<path> on a large blob is windowed to an 8 KiB head with an exact recovery hint, instead of dumping the whole file.
Scope: accept (frozen).
CI @96a8ecf1: all 11 green — test on ubuntu, macos and windows, the three that failed at 06bae5ab.
Merged with the latest develop (75 commits ahead of the merge-base): merges clean, clippy -D warnings 0, 3565 tests pass.
The blocker is closed
1ba6f1a6 took the option the maintainer has now confirmed: the telemetry endpoint already accepts negative values, so widening the assertions rather than clamping the metric was right.
It also went further than the round-6 comment asked. I only named test_enriched_stats_returns_valid_data; you found its twin test_get_stats_returns_tuple, bounded both at the real invariant (<= 100.0) rather than restoring an arbitrary floor, documented the signed contract on avg_savings_per_command, and added a hermetic Tracker::new_in_memory() test. I checked that last one pins what it claims instead of trusting it — re-clamping avg_savings_per_command to 0 makes test_avg_savings_per_command_reflects_negative fail. Being hermetic, it also sidesteps the "reads the developer's own DB" problem that let the original regression through.
Fixup pushed: 96a8ecf1
One arm of the same class was still open, and it is mine, not yours — my round-3 finding is what made saved_tokens signed, and I should have swept every reader when I asked for it.
Seeding a DB with a net-negative row, test_get_stats_returns_tuple still panicked at assert!(saved_24h >= 0) — three lines above the assertion you widened:
thread '...test_get_stats_returns_tuple' panicked at src/core/telemetry.rs:572:9:
assertion failed: saved_24h >= 0
tokens_saved_24h and total_tokens_saved both return i64 from an unclamped SUM(saved_tokens), deliberately — unlike the as usize readers that take .max(0) for their unsigned API. So both sums go honestly negative.
Worth recording why the fix drops the assertions rather than replacing them: my first attempt asserted saved_24h <= saved_total, which is false. With a positive row inside the 24h window and a larger negative one outside it, saved_24h (90) exceeds saved_total (-110). They are unbounded signed sums with no ordering between them, so the test stops binding them via let (cmds, top, pct, ..) and a comment records the reason.
Verified before pushing: both telemetry tests pass against a net-negative DB, fmt ok, clippy 0, 3460 tests, and the blob-show differential suite still green.
Checked and correct — no need to re-verify
The blob-show work is untouched by the last two commits (telemetry/tracking only) and still verifies: cat-file probe classification, the byte-fidelity invariant (window only when from_utf8 is Ok), exact | tail -n +N recovery including global args, and your committed differential fuzzer (3 tests green).
Hypotheses built and dropped
saved_24h <= saved_totalas a replacement invariant — disproved by construction before it shipped; see above.
Your questions
- "Pushed a fix for the CI failure that dismissed the approval." → Confirmed fixed, plus the one remaining arm folded into
96a8ecf1.
Next
Nothing — approving. Follow-ups already filed: #3954 (value-flag table gaps, closes via #3681) and a note on #3924 (trailing newline on empty output). Both are out of this PR's scope.
Seven rounds is far more than this should have taken, and at least two of them were my doing — the 60% floor that sent you through two fixture churns, and the signed-savings finding whose readers I failed to sweep. Thank you for the patience, and for consistently fixing the class rather than the instance I happened to name.
Rounds: 7 (landing). Threads: 0 open, 0 resolved this round.
develop rewrote `run_show`'s routing underneath this branch (rtk-ai#3265, eight commits) into `ShowRoute` with a `cat-file` probe for blob detection. That structure is newer and better than what this branch had, so it is kept whole and this branch's contribution is folded into it rather than the other way round. `commit_or_stat_route` now classifies with the tokenizer instead of scanning strings. develop matched `a == "--stat"` and `a.starts_with("--pretty")`, which reads a pathspec named `--stat` past the boundary as the flag and `--prettyish` as `--pretty`, and covers three stat spellings where the set of shapes the compaction cannot render is larger. `git show -- --stat` takes the compact path again. `consumes_next_token_as_value` is reimplemented on `log_takes_value` rather than restored as its own table. develop's copy is a subset -- it omits `--max-count`, `--ignore-matching-lines`, `--min-age`, `--max-age` and `--stat-graph-width` -- and keeping both would be the two-lists-drift this branch exists to remove. Its callers are unchanged, so blob-show keeps its own grammar rules; the attachment distinction (`-M50` attaches, `-M 50` does not) is what the tokenizer adds. This branch's `is_blob_show_arg` and free-positional blob pre-filter are dropped in favour of develop's, which also handles index and merge-stage blobs (`:file`, `:2:file`) and probes with `cat-file` rather than trusting the shape of the argument. Verified: seven blob-show invocations byte-identical to develop, including the `-wG a:b HEAD:blob` cluster their walker exists for; 3536 unit tests and every integration suite green; clippy clean. develop's `show_positionals` carries a `TODO(after rtk-ai#3681)` to replace its hand-rolled short-cluster walk with the ValueSpec factorization. Left alone deliberately -- that is follow-up on develop, as its author intended, not something to change inside this merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Summary
feat(git):git show <rev>:<path>blob dumps (previously passed through at 0% savings) are now filtered — large text blobs are capped to an 8 KiB preview with atail -n +Nrecovery pointer (the full blob is tee'd); small blobs, tree listings, binary, and blobs too large to recover intact pass through unchanged..pck) are transcoded losslessly instead of being corrupted into U+FFFD by lossy UTF-8, which also unblocks their compression. Ambiguous single-byte encodings (CP1252) and binary pass through raw. Measured on a real 77 KB Latin-1 blob: 0% → 89% savings with theÉ→�corruption removed. (Same class of fix asrtk curl http://....tar.gz | tar -xzffails due rtk truncation, eg "(153 more lines, 94292 bytes total)" #1087.)Test plan
cargo fmt --all --check && cargo clippy --all-targets && cargo test— clean; blob/decode tests green on currentdevelop.git show HEAD:src/main.rs→ ~120 KB → 8 KB,tail -n +Nrecovery reconstructs byte-for-bytegit show HEAD:<latin1.pck>→ 89% savings, no�, recovery byte-identical toiconv -f ISO-8859-1git show HEAD:<dir>(tree) andHEAD:<binary>→ passthrough unchanged