ep: Add LCS-based recall (#53509)

This PR adds the `correctly_deleted_chars` field and updates `kept_rate`
to account for it, not just inserted chars.

It also adds `recall_rate` to measure coverage of reference
insertions/deletions.

Finally, it renames "final" to "reference" and "prediction" to
"candidate".

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable


Release Notes:

- N/A
This commit is contained in:
Oleksiy Syvokon authored and GitHub committed 2026-04-10 19:29:52 +03:00
1 parent 302d7fe39a
commit 148317aa5d
4 files changed
+290 -89

No files matched your search

@@ -1690,12 +1690,16 @@ impl EditPredictionStore {
settled_editable_region,
ts_error_count_before_prediction,
ts_error_count_after_prediction,
edit_bytes_predicted_new = kept_rate_result.predicted_new_chars,
edit_bytes_final_new = kept_rate_result.final_new_chars,
edit_bytes_candidate_new = kept_rate_result.candidate_new_chars,
edit_bytes_reference_new = kept_rate_result.reference_new_chars,
edit_bytes_candidate_deleted = kept_rate_result.candidate_deleted_chars,
edit_bytes_reference_deleted = kept_rate_result.reference_deleted_chars,
edit_bytes_kept = kept_rate_result.kept_chars,
edit_bytes_correctly_deleted = kept_rate_result.correctly_deleted_chars,
edit_bytes_discarded = kept_rate_result.discarded_chars,
edit_bytes_context = kept_rate_result.context_chars,
edit_bytes_kept_rate = kept_rate_result.kept_rate,
edit_bytes_recall_rate = kept_rate_result.recall_rate,
example,
e2e_latency = e2e_latency.as_millis(),
);
+160 -79
View File
@@ -13,12 +13,33 @@ pub enum TokenAnnotation {
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct KeptRateResult {
pub predicted_new_chars: usize,
pub final_new_chars: usize,
/// Characters newly introduced by the candidate
pub candidate_new_chars: usize,
/// Characters newly introduced by the reference
pub reference_new_chars: usize,
/// Characters from `base` that are deleted by the candidate.
pub candidate_deleted_chars: usize,
/// Characters from `base` that are deleted by the reference.
pub reference_deleted_chars: usize,
/// Candidate new characters that are also present in the reference.
pub kept_chars: usize,
/// Base characters deleted by both the candidate and the reference.
pub correctly_deleted_chars: usize,
/// Candidate new characters that are not kept in the reference.
pub discarded_chars: usize,
/// Candidate characters treated as unchanged context
pub context_chars: usize,
/// Fraction of candidate edit characters that match the reference edit.
///
/// This includes both kept newly introduced characters and correctly
/// deleted base characters.
pub kept_rate: f64,
/// Fraction of reference edit characters covered by the candidate edit.
///
/// This includes both kept newly introduced characters and correctly
/// deleted base characters.
pub recall_rate: f64,
/// Per-token classification for candidate tokens used by tests.
#[cfg(test)]
pub token_annotations: Vec<TokenAnnotation>,
}
@@ -188,89 +209,127 @@ fn analyze_masked_tokens<'a>(tokens: &[&'a str], mask: &[bool]) -> (Vec<&'a str>
(unmasked_tokens, unmasked_chars, masked_chars)
}
fn should_bail_for_dirty_final(base: &str, predicted: &str, final_text: &str) -> bool {
let predicted_delta_chars = predicted.len().abs_diff(base.len());
let final_delta_chars = final_text.len().abs_diff(base.len());
predicted_delta_chars.abs_diff(final_delta_chars) > MAX_DIRTY_LENGTH_DELTA_CHARS
fn count_unmasked_chars(tokens: &[&str], mask: &[bool]) -> usize {
tokens
.iter()
.zip(mask.iter())
.filter_map(|(&token, &is_masked)| (!is_masked).then_some(token.len()))
.sum()
}
pub fn compute_kept_rate(base: &str, predicted: &str, final_text: &str) -> KeptRateResult {
if base == predicted && predicted == final_text {
let predicted_tokens = tokenize(predicted);
let context_chars = predicted_tokens.iter().map(|token| token.len()).sum();
fn should_bail_for_dirty_final(base: &str, candidate: &str, reference: &str) -> bool {
let candidate_delta_chars = candidate.len().abs_diff(base.len());
let reference_delta_chars = reference.len().abs_diff(base.len());
candidate_delta_chars.abs_diff(reference_delta_chars) > MAX_DIRTY_LENGTH_DELTA_CHARS
}
pub fn compute_kept_rate(base: &str, candidate: &str, reference: &str) -> KeptRateResult {
if base == candidate && candidate == reference {
let candidate_tokens = tokenize(candidate);
let context_chars = candidate_tokens.iter().map(|token| token.len()).sum();
return KeptRateResult {
predicted_new_chars: 0,
final_new_chars: 0,
candidate_new_chars: 0,
reference_new_chars: 0,
candidate_deleted_chars: 0,
reference_deleted_chars: 0,
kept_chars: 0,
correctly_deleted_chars: 0,
discarded_chars: 0,
context_chars,
kept_rate: 1.0,
recall_rate: 1.0,
#[cfg(test)]
token_annotations: vec![TokenAnnotation::Context; predicted_tokens.len()],
token_annotations: vec![TokenAnnotation::Context; candidate_tokens.len()],
};
}
if should_bail_for_dirty_final(base, predicted, final_text) {
let predicted_new_chars = predicted.len().abs_diff(base.len());
let final_new_chars = final_text.len().abs_diff(base.len());
if should_bail_for_dirty_final(base, candidate, reference) {
let candidate_new_chars = candidate.len().abs_diff(base.len());
let reference_new_chars = reference.len().abs_diff(base.len());
return KeptRateResult {
predicted_new_chars,
final_new_chars,
candidate_new_chars,
reference_new_chars,
candidate_deleted_chars: 0,
reference_deleted_chars: 0,
kept_chars: 0,
discarded_chars: predicted_new_chars,
correctly_deleted_chars: 0,
discarded_chars: candidate_new_chars,
context_chars: 0,
kept_rate: 0.0,
recall_rate: 0.0,
#[cfg(test)]
token_annotations: vec![TokenAnnotation::Discarded; tokenize(predicted).len()],
token_annotations: vec![TokenAnnotation::Discarded; tokenize(candidate).len()],
};
}
let base_tokens = tokenize(base);
let predicted_tokens = tokenize(predicted);
let final_tokens = tokenize(final_text);
let candidate_tokens = tokenize(candidate);
let reference_tokens = tokenize(reference);
let pred_base_mask = lcs_keep_mask(&predicted_tokens, &base_tokens);
let (pred_final_mask, final_pred_mask) = lcs_keep_masks(&predicted_tokens, &final_tokens);
let context_mask: Vec<bool> = pred_base_mask
let (candidate_base_mask, base_candidate_mask) =
lcs_keep_masks(&candidate_tokens, &base_tokens);
let (candidate_reference_mask, reference_candidate_mask) =
lcs_keep_masks(&candidate_tokens, &reference_tokens);
let context_mask: Vec<bool> = candidate_base_mask
.iter()
.zip(pred_final_mask.iter())
.map(|(&in_base, &in_final)| in_base && in_final)
.zip(candidate_reference_mask.iter())
.map(|(&in_base, &in_reference)| in_base && in_reference)
.collect();
let (stripped_predicted, predicted_new_chars, context_chars) =
analyze_masked_tokens(&predicted_tokens, &context_mask);
let (stripped_candidate, candidate_new_chars, context_chars) =
analyze_masked_tokens(&candidate_tokens, &context_mask);
let final_base_mask = lcs_keep_mask(&final_tokens, &base_tokens);
let final_context_mask: Vec<bool> = final_base_mask
let (reference_base_mask, base_reference_mask) =
lcs_keep_masks(&reference_tokens, &base_tokens);
let reference_context_mask: Vec<bool> = reference_base_mask
.iter()
.zip(final_pred_mask.iter())
.map(|(&in_base, &in_predicted)| in_base && in_predicted)
.zip(reference_candidate_mask.iter())
.map(|(&in_base, &in_candidate)| in_base && in_candidate)
.collect();
let (stripped_final, final_new_chars, _) =
analyze_masked_tokens(&final_tokens, &final_context_mask);
let (stripped_reference, reference_new_chars, _) =
analyze_masked_tokens(&reference_tokens, &reference_context_mask);
let keep_mask = lcs_keep_mask(&stripped_predicted, &stripped_final);
let keep_mask = lcs_keep_mask(&stripped_candidate, &stripped_reference);
let kept_chars: usize = stripped_predicted
let kept_chars: usize = stripped_candidate
.iter()
.zip(keep_mask.iter())
.filter_map(|(&token, &is_kept)| is_kept.then_some(token.len()))
.sum();
let discarded_chars = predicted_new_chars - kept_chars;
let candidate_deleted_chars = count_unmasked_chars(&base_tokens, &base_candidate_mask);
let reference_deleted_chars = count_unmasked_chars(&base_tokens, &base_reference_mask);
let correctly_deleted_chars: usize = base_tokens
.iter()
.zip(base_candidate_mask.iter().zip(base_reference_mask.iter()))
.filter_map(|(&token, (&in_candidate, &in_reference))| {
(!in_candidate && !in_reference).then_some(token.len())
})
.sum();
let kept_rate = if predicted_new_chars == 0 {
if final_new_chars == 0 { 1.0 } else { 0.0 }
let discarded_chars = candidate_new_chars - kept_chars;
let matched_edit_chars = kept_chars + correctly_deleted_chars;
let candidate_edit_chars = candidate_new_chars + candidate_deleted_chars;
let reference_edit_chars = reference_new_chars + reference_deleted_chars;
let kept_rate = if candidate_edit_chars == 0 {
if reference_edit_chars == 0 { 1.0 } else { 0.0 }
} else {
kept_chars as f64 / predicted_new_chars as f64
matched_edit_chars as f64 / candidate_edit_chars as f64
};
let recall_rate = if reference_edit_chars == 0 {
if candidate_edit_chars == 0 { 1.0 } else { 0.0 }
} else {
matched_edit_chars as f64 / reference_edit_chars as f64
};
#[cfg(test)]
let token_annotations = {
let mut token_annotations = Vec::with_capacity(predicted_tokens.len());
let mut token_annotations = Vec::with_capacity(candidate_tokens.len());
let mut new_index = 0;
for (token_index, _token) in predicted_tokens.iter().enumerate() {
for (token_index, _token) in candidate_tokens.iter().enumerate() {
if context_mask[token_index] {
token_annotations.push(TokenAnnotation::Context);
} else {
@@ -288,12 +347,16 @@ pub fn compute_kept_rate(base: &str, predicted: &str, final_text: &str) -> KeptR
};
KeptRateResult {
predicted_new_chars,
final_new_chars,
candidate_new_chars,
reference_new_chars,
candidate_deleted_chars,
reference_deleted_chars,
kept_chars,
correctly_deleted_chars,
discarded_chars,
context_chars,
kept_rate,
recall_rate,
#[cfg(test)]
token_annotations,
}
@@ -327,7 +390,8 @@ mod test_kept_rate {
fn test_rate_extremes() {
let no_change = compute_kept_rate("foo bar", "foo bar", "foo bar");
assert!((no_change.kept_rate - 1.0).abs() < 1e-6);
assert_eq!(no_change.predicted_new_chars, 0);
assert!((no_change.recall_rate - 1.0).abs() < 1e-6);
assert_eq!(no_change.candidate_new_chars, 0);
assert!(
no_change
.token_annotations
@@ -337,15 +401,17 @@ mod test_kept_rate {
let accepted = compute_kept_rate("old", "new", "new");
assert!((accepted.kept_rate - 1.0).abs() < 1e-6);
assert!((accepted.recall_rate - 1.0).abs() < 1e-6);
let discarded = compute_kept_rate("old", "old", "new");
assert!((discarded.kept_rate - 0.0).abs() < 1e-6);
assert!((discarded.recall_rate - 0.0).abs() < 1e-6);
}
#[test]
fn test_pure_addition() {
let kept = compute_kept_rate("", "brand new line\n", "brand new line\n");
assert_eq!(kept.kept_chars, kept.predicted_new_chars);
assert_eq!(kept.kept_chars, kept.candidate_new_chars);
assert!(
kept.token_annotations
.iter()
@@ -354,26 +420,28 @@ mod test_kept_rate {
let discarded =
compute_kept_rate("", "brand new line\n", "something completely different\n");
assert!(discarded.kept_chars < discarded.predicted_new_chars);
assert!(discarded.kept_chars < discarded.candidate_new_chars);
}
#[test]
fn test_decoy_when_base_excluded() {
let base = " decoy.when(mock_sync_hardware_api.sp()).then_return(SpeedStatus.IDLE)\n";
let predicted = " decoy.when(mock_sync_module_hardware.speed_status).then_return(SpeedStatus.IDLE)\n";
let final_text = " decoy.when(mock_sync_module_hardware.speed_status).then_return(SpeedStatus.IDLE)\n";
let result = compute_kept_rate(base, predicted, final_text);
let candidate = " decoy.when(mock_sync_module_hardware.speed_status).then_return(SpeedStatus.IDLE)\n";
let reference = " decoy.when(mock_sync_module_hardware.speed_status).then_return(SpeedStatus.IDLE)\n";
let result = compute_kept_rate(base, candidate, reference);
let expected_new = "mock_sync_module_hardware".len() + "speed_status".len();
assert_eq!(result.predicted_new_chars, expected_new);
assert_eq!(result.candidate_new_chars, expected_new);
assert!(result.correctly_deleted_chars > 0);
assert!((result.kept_rate - 1.0).abs() < 1e-6);
assert!((result.recall_rate - 1.0).abs() < 1e-6);
}
#[test]
fn test_missing_deletion() {
let base = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n epr\n";
let predicted = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n epr\neprintln!(\"\");\n";
let final_text = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n eprintln!(\"\");\n";
let result = compute_kept_rate(base, predicted, final_text);
let candidate = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n epr\neprintln!(\"\");\n";
let reference = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n eprintln!(\"\");\n";
let result = compute_kept_rate(base, candidate, reference);
assert!(
result.kept_rate < 0.85,
"expected kept_rate < 0.85, got {}",
@@ -385,7 +453,12 @@ mod test_kept_rate {
#[test]
fn test_empty_prediction() {
let result = compute_kept_rate("old line\n", "", "new line\n");
assert!((result.kept_rate - 0.0).abs() < 1e-6);
assert_eq!(result.candidate_new_chars, 0);
assert!(result.candidate_deleted_chars > 0);
assert!(result.correctly_deleted_chars > 0);
assert!(result.correctly_deleted_chars < result.candidate_deleted_chars);
assert!(result.kept_rate > 0.0 && result.kept_rate < 1.0);
assert!(result.recall_rate > 0.0 && result.recall_rate < 1.0);
}
#[test]
@@ -399,24 +472,25 @@ mod test_kept_rate {
#[test]
fn test_bails_for_dirty_final() {
let base = "fn example() {\n work();\n}\n";
let predicted = "fn example() {\n work();\n predicted();\n}\n";
let final_text = format!(
let candidate = "fn example() {\n work();\n predicted();\n}\n";
let reference = format!(
"fn example() {{\n work();\n {}\n}}\n",
"settled();\n ".repeat(MAX_DIRTY_LENGTH_DELTA_CHARS / 8 + 64)
);
let result = compute_kept_rate(base, predicted, &final_text);
let result = compute_kept_rate(base, candidate, &reference);
assert_eq!(result.kept_rate, 0.0);
assert_eq!(result.recall_rate, 0.0);
assert_eq!(result.kept_chars, 0);
assert_eq!(result.discarded_chars, result.predicted_new_chars);
assert_eq!(result.discarded_chars, result.candidate_new_chars);
}
#[test]
fn test_eprintln_token_alignment() {
let base = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n epr\n";
let predicted = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n eprintln!(\"hello world!\");\n";
let final_text = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n eprintln!(\"\");\n";
let result = compute_kept_rate(base, predicted, final_text);
let candidate = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n eprintln!(\"hello world!\");\n";
let reference = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n eprintln!(\"\");\n";
let result = compute_kept_rate(base, candidate, reference);
assert!(result.discarded_chars > 0);
assert!(result.kept_chars > 0);
assert!(result.kept_rate > 0.0 && result.kept_rate < 1.0);
@@ -427,14 +501,18 @@ mod test_kept_rate {
#[test]
fn test_annotations_rename() {
let base = " foo(old_name)\n";
let predicted = " foo(new_name)\n";
let final_text = " foo(new_name)\n";
let result = compute_kept_rate(base, predicted, final_text);
let candidate = " foo(new_name)\n";
let reference = " foo(new_name)\n";
let result = compute_kept_rate(base, candidate, reference);
assert_eq!(result.predicted_new_chars, "new_name".len());
assert_eq!(result.token_annotations.len(), tokenize(predicted).len());
assert_eq!(result.candidate_new_chars, "new_name".len());
assert_eq!(result.candidate_deleted_chars, "old_name".len());
assert_eq!(result.reference_deleted_chars, "old_name".len());
assert_eq!(result.correctly_deleted_chars, "old_name".len());
assert!((result.recall_rate - 1.0).abs() < 1e-6);
assert_eq!(result.token_annotations.len(), tokenize(candidate).len());
for (&token, &annotation) in tokenize(predicted).iter().zip(&result.token_annotations) {
for (&token, &annotation) in tokenize(candidate).iter().zip(&result.token_annotations) {
if token == "new_name" {
assert_eq!(annotation, TokenAnnotation::Kept);
} else {
@@ -446,12 +524,12 @@ mod test_kept_rate {
#[test]
fn test_annotations_eprintln_coloring() {
let base = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n epr\n";
let predicted = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n eprintln!(\"hello world!\");\n";
let final_text = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n eprintln!(\"\");\n";
let result = compute_kept_rate(base, predicted, final_text);
let predicted_tokens = tokenize(predicted);
let candidate = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n eprintln!(\"hello world!\");\n";
let reference = " fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {\n eprintln!(\"\");\n";
let result = compute_kept_rate(base, candidate, reference);
let candidate_tokens = tokenize(candidate);
let eprintln_index = predicted_tokens
let eprintln_index = candidate_tokens
.iter()
.position(|&token| token == "eprintln")
.expect("eprintln token not found");
@@ -485,12 +563,15 @@ mod test_kept_rate {
#[test]
fn test_repetitive_tokens_remain_discarded() {
let base = "foo + foo + foo + foo + foo\n".repeat(16);
let predicted = "foo + foo + prediction_token + foo + foo\n".repeat(16);
let final_text = "foo + foo + kept_token + foo + foo\n".repeat(16);
let result = compute_kept_rate(&base, &predicted, &final_text);
let candidate = "foo + foo + prediction_token + foo + foo\n".repeat(16);
let reference = "foo + foo + kept_token + foo + foo\n".repeat(16);
let result = compute_kept_rate(&base, &candidate, &reference);
assert_eq!(result.kept_chars, 0);
assert_eq!(result.discarded_chars, result.predicted_new_chars);
assert_eq!(result.predicted_new_chars, "prediction_token".len() * 16);
assert_eq!(result.correctly_deleted_chars, "foo".len() * 16);
assert_eq!(result.discarded_chars, result.candidate_new_chars);
assert_eq!(result.candidate_new_chars, "prediction_token".len() * 16);
assert!(result.kept_rate > 0.0);
assert!(result.recall_rate > 0.0);
}
}
@@ -187,6 +187,14 @@ pub struct ExampleScore {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kept_rate: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recall_rate: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kept_chars: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub correctly_deleted_chars: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub discarded_chars: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cumulative_logprob: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub avg_logprob: Option<f64>,
+116 -8
View File
@@ -85,6 +85,10 @@ pub async fn run_scoring(
inserted_tokens: 0,
deleted_tokens: 0,
kept_rate: None,
recall_rate: None,
kept_chars: None,
correctly_deleted_chars: None,
discarded_chars: None,
cumulative_logprob: None,
avg_logprob: None,
};
@@ -187,9 +191,20 @@ pub async fn run_scoring(
prediction.actual_cursor.as_ref(),
);
let kept_rate = best_expected_text.map(|final_text| {
metrics::compute_kept_rate(original_text, &actual_text, final_text).kept_rate
});
let (kept_rate, recall_rate, kept_chars, correctly_deleted_chars, discarded_chars) =
best_expected_text
.map(|reference_text| {
let result =
metrics::compute_kept_rate(original_text, &actual_text, reference_text);
(
Some(result.kept_rate),
Some(result.recall_rate),
Some(result.kept_chars),
Some(result.correctly_deleted_chars),
Some(result.discarded_chars),
)
})
.unwrap_or((None, None, None, None, None));
scores.push(ExampleScore {
delta_chr_f: best_delta_chr_f_metrics.score as f32,
@@ -211,6 +226,10 @@ pub async fn run_scoring(
inserted_tokens: token_changes.inserted_tokens,
deleted_tokens: token_changes.deleted_tokens,
kept_rate,
recall_rate,
kept_chars,
correctly_deleted_chars,
discarded_chars,
cumulative_logprob: prediction.cumulative_logprob,
avg_logprob: prediction.avg_logprob,
});
@@ -277,6 +296,11 @@ pub fn print_report(examples: &[Example], verbose: bool) {
let mut isolated_whitespace_count: usize = 0;
let mut kept_rate_sum: f64 = 0.0;
let mut kept_rate_count: usize = 0;
let mut kept_chars_total: usize = 0;
let mut correctly_deleted_chars_total: usize = 0;
let mut discarded_chars_total: usize = 0;
let mut recall_rate_sum: f64 = 0.0;
let mut recall_rate_count: usize = 0;
let mut patch_inserted_tokens: Vec<usize> = Vec::new();
let mut patch_deleted_tokens: Vec<usize> = Vec::new();
let mut predictions_with_patch: usize = 0;
@@ -369,11 +393,24 @@ pub fn print_report(examples: &[Example], verbose: bool) {
isolated_whitespace_count += 1;
}
// Accumulate kept rate metrics
// Accumulate kept and recall rate metrics
if let Some(kr) = score.kept_rate {
kept_rate_sum += kr;
kept_rate_count += 1;
}
if let Some(kept_chars) = score.kept_chars {
kept_chars_total += kept_chars;
}
if let Some(correctly_deleted_chars) = score.correctly_deleted_chars {
correctly_deleted_chars_total += correctly_deleted_chars;
}
if let Some(discarded_chars) = score.discarded_chars {
discarded_chars_total += discarded_chars;
}
if let Some(rr) = score.recall_rate {
recall_rate_sum += rr;
recall_rate_count += 1;
}
// Accumulate token change metrics (only for predictions that produced a patch)
let has_patch = example
@@ -504,13 +541,24 @@ pub fn print_report(examples: &[Example], verbose: bool) {
println!("Isolated whitespace changes: {}", isolated_ws_str);
}
// Print kept rate metrics
// Print kept and recall rate metrics
if kept_rate_count > 0 {
let avg_kept_rate = kept_rate_sum / kept_rate_count as f64;
println!(
"Kept rate: {:.1}% avg ({} evaluated)",
"Kept rate: {:.1}% avg ({} evaluated, kept chars: {}, correctly deleted chars: {}, discarded chars: {})",
avg_kept_rate * 100.0,
kept_rate_count
kept_rate_count,
kept_chars_total,
correctly_deleted_chars_total,
discarded_chars_total
);
}
if recall_rate_count > 0 {
let avg_recall_rate = recall_rate_sum / recall_rate_count as f64;
println!(
"Recall rate: {:.1}% avg ({} evaluated)",
avg_recall_rate * 100.0,
recall_rate_count
);
}
@@ -618,6 +666,14 @@ pub struct SummaryJson {
pub isolated_whitespace_rate: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub avg_kept_rate: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub avg_recall_rate: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_kept_chars: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_correctly_deleted_chars: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_discarded_chars: Option<usize>,
}
pub fn compute_summary(examples: &[Example]) -> SummaryJson {
@@ -645,6 +701,14 @@ pub fn compute_summary(examples: &[Example]) -> SummaryJson {
let mut isolated_whitespace_count: usize = 0;
let mut kept_rate_sum: f64 = 0.0;
let mut kept_rate_count: usize = 0;
let mut kept_chars_total: usize = 0;
let mut kept_chars_count: usize = 0;
let mut correctly_deleted_chars_total: usize = 0;
let mut correctly_deleted_chars_count: usize = 0;
let mut discarded_chars_total: usize = 0;
let mut discarded_chars_count: usize = 0;
let mut recall_rate_sum: f64 = 0.0;
let mut recall_rate_count: usize = 0;
for example in examples {
for (score_idx, score) in example.score.iter().enumerate() {
@@ -685,11 +749,27 @@ pub fn compute_summary(examples: &[Example]) -> SummaryJson {
isolated_whitespace_count += 1;
}
// Accumulate kept rate metrics
// Accumulate kept and recall rate metrics
if let Some(kr) = score.kept_rate {
kept_rate_sum += kr;
kept_rate_count += 1;
}
if let Some(kept_chars) = score.kept_chars {
kept_chars_total += kept_chars;
kept_chars_count += 1;
}
if let Some(correctly_deleted_chars) = score.correctly_deleted_chars {
correctly_deleted_chars_total += correctly_deleted_chars;
correctly_deleted_chars_count += 1;
}
if let Some(discarded_chars) = score.discarded_chars {
discarded_chars_total += discarded_chars;
discarded_chars_count += 1;
}
if let Some(rr) = score.recall_rate {
recall_rate_sum += rr;
recall_rate_count += 1;
}
// Accumulate cursor metrics
if let Some(exact_match) = score.cursor_exact_match {
@@ -771,6 +851,30 @@ pub fn compute_summary(examples: &[Example]) -> SummaryJson {
None
};
let avg_recall_rate = if recall_rate_count > 0 {
Some(recall_rate_sum / recall_rate_count as f64)
} else {
None
};
let total_kept_chars = if kept_chars_count > 0 {
Some(kept_chars_total)
} else {
None
};
let total_correctly_deleted_chars = if correctly_deleted_chars_count > 0 {
Some(correctly_deleted_chars_total)
} else {
None
};
let total_discarded_chars = if discarded_chars_count > 0 {
Some(discarded_chars_total)
} else {
None
};
SummaryJson {
total_examples: total_scores,
avg_delta_chr_f,
@@ -804,6 +908,10 @@ pub fn compute_summary(examples: &[Example]) -> SummaryJson {
wrong_editable_region_rate,
isolated_whitespace_rate,
avg_kept_rate,
avg_recall_rate,
total_kept_chars,
total_correctly_deleted_chars,
total_discarded_chars,
}
}