Compare commits

...
1 Commits
Author SHA1 Message Date
CodeWhale BotandClaude Sonnet 5.5 8748a54682 fix(receipts): count edit lines with a line diff, not both whole texts
edit_line_counts reported every line of the replacement as added and every
line of the searched text as removed, so keeping a 5-line block and adding two
lines printed (+7 -5) where git and the edit card show (+2 -0). Each
search/replace pair is now diffed with make_unified_diff, the diff the edit
card is built from, and counted through the existing diff_counts_by_path;
multi-edit calls sum per pair. Snippets lacking a trailing newline are given
one so counts equal git's whole-file numbers. Adds a regression for insert,
replace, delete, no-trailing-newline, whole new text and multi-edit shapes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 18:28:51 -08:00
2 changed files with 46 additions and 10 deletions

No files matched your search

+21 -10
View File
@@ -45,6 +45,7 @@ use crate::runtime_threads::{
RuntimeEventRecord, RuntimeTurnStatus, ThreadRecord, TurnItemKind, TurnItemLifecycleStatus,
TurnItemRecord, TurnRecord,
};
use crate::tools::diff_format::make_unified_diff;
const MAX_COMMAND_CHARS: usize = 200;
const MAX_ERROR_CHARS: usize = 160;
@@ -1348,8 +1349,9 @@ fn files_from_input(semantic: &str, input: &Value, outcome: StepOutcome) -> Vec<
}
}
/// `(+, -)` from an edit call's replacement pairs: the lines of every
/// replacement text against the lines of every searched text.
/// `(+, -)` from an edit call's replacement pairs: the line diff of each
/// searched text against its replacement, summed, the same diff the edit
/// card shows.
fn edit_line_counts(input: &Value) -> (Option<u64>, Option<u64>) {
const SEARCH: &[&str] = &["search", "old_string", "old_str", "oldText", "old_text"];
const REPLACE: &[&str] = &[
@@ -1374,8 +1376,10 @@ fn edit_line_counts(input: &Value) -> (Option<u64>, Option<u64>) {
continue;
}
any = true;
removed += line_count(old.as_deref().unwrap_or(""));
added += line_count(new.as_deref().unwrap_or(""));
let (pair_added, pair_removed) =
pair_line_counts(old.as_deref().unwrap_or(""), new.as_deref().unwrap_or(""));
added += pair_added;
removed += pair_removed;
}
if any {
(Some(added), Some(removed))
@@ -1384,12 +1388,19 @@ fn edit_line_counts(input: &Value) -> (Option<u64>, Option<u64>) {
}
}
fn line_count(text: &str) -> u64 {
if text.is_empty() {
0
} else {
text.lines().count() as u64
}
fn pair_line_counts(old: &str, new: &str) -> (u64, u64) {
let whole_lines = |text: &str| {
if text.is_empty() || text.ends_with('\n') {
text.to_string()
} else {
format!("{text}\n")
}
};
let diff = make_unified_diff("edit", &whole_lines(old), &whole_lines(new));
diff_counts_by_path(&diff)
.get("edit")
.copied()
.unwrap_or_default()
}
/// Files and counts from a patch the call supplied: `*** Add/Update/Delete
+25
View File
@@ -1292,3 +1292,28 @@ fn thread_receipt_sums_turn_tokens_and_points_to_the_usage_endpoint_for_cost() {
.any(|note| note.starts_with("Cost:"))
);
}
#[test]
fn edit_line_counts_match_git_numstat_for_the_same_edit() {
let block = "a\nb\nc\nd\ne\n";
let counts = |old: &str, new: &str| {
edit_line_counts(&json!({"path": "f", "search": old, "replace": new}))
};
let some = |added: u64, removed: u64| (Some(added), Some(removed));
assert_eq!(counts(block, "a\nb\nc\nd\ne\nx\ny\n"), some(2, 0), "insert");
assert_eq!(
counts(block, "a\nb\nX\nd\ne\n"),
some(1, 1),
"replace one line"
);
assert_eq!(counts(block, "a\nb\nd\ne\n"), some(0, 1), "delete");
assert_eq!(counts("a\nb", "a\nb\nc"), some(1, 0), "no trailing newline");
assert_eq!(counts("", "x\ny\nz\n"), some(3, 0), "whole new text");
let multi = edit_line_counts(&json!({"path": "f", "edits": [
{"search": block, "replace": "a\nb\nX\nd\ne\n"},
{"search": "q\nr\n", "replace": "q\nr\ns\n"},
]}));
assert_eq!(multi, some(2, 1), "multi-edit sums per pair");
}