Skip to content

Commit b4bd85b

Browse files
authored
feat: add feedback repeat-rate trend
Add current-vs-previous 7-day repeat-rate metrics to the weekly review feedback guardrail report.
1 parent ce75ed5 commit b4bd85b

2 files changed

Lines changed: 133 additions & 2 deletions

File tree

.github/scripts/sweep-recent-review-feedback.rb

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,41 @@ def guardrail_lifecycle_json(backlog, issue_results:, generated_at: Time.now.utc
739739
}
740740
end
741741

742-
def weekly_guardrail_report_markdown(backlog, lifecycle: nil, generated_at: Time.now.utc, top_limit: 5)
742+
def repeat_rate_metrics(ledger, generated_at: Time.now.utc, bucket_days: 7)
743+
current_start = generated_at.utc - (bucket_days * 24 * 60 * 60)
744+
previous_start = generated_at.utc - (bucket_days * 2 * 24 * 60 * 60)
745+
buckets = Hash.new { |hash, key| hash[key] = { "current" => 0, "previous" => 0 } }
746+
Array(ledger.fetch("findings")).each do |finding|
747+
merged_at = Time.parse(finding.fetch("merged_at")).utc
748+
next if merged_at < previous_start || merged_at > generated_at.utc
749+
750+
class_key = guardrail_class(finding).fetch("key")
751+
bucket = merged_at >= current_start ? "current" : "previous"
752+
buckets[class_key][bucket] += 1
753+
rescue ArgumentError, KeyError
754+
next
755+
end
756+
757+
buckets.map do |class_key, counts|
758+
current = counts.fetch("current")
759+
previous = counts.fetch("previous")
760+
delta = current - previous
761+
change_percent = if previous.zero?
762+
current.zero? ? 0 : nil
763+
else
764+
((delta.to_f / previous) * 100).round
765+
end
766+
{
767+
"class_key" => class_key,
768+
"current_count" => current,
769+
"previous_count" => previous,
770+
"delta" => delta,
771+
"change_percent" => change_percent
772+
}
773+
end.sort_by { |entry| [-entry.fetch("current_count"), -entry.fetch("previous_count"), entry.fetch("class_key")] }
774+
end
775+
776+
def weekly_guardrail_report_markdown(backlog, lifecycle: nil, ledger: nil, generated_at: Time.now.utc, top_limit: 5)
743777
classes = backlog.fetch("classes")
744778
repo_counts = classes.each_with_object(Hash.new(0)) do |entry, counts|
745779
entry.fetch("repos").each do |repo|
@@ -749,6 +783,7 @@ def weekly_guardrail_report_markdown(backlog, lifecycle: nil, generated_at: Time
749783
end
750784
end.sort_by { |repo, count| [-count, repo] }
751785
prevented = Array(lifecycle&.fetch("issues", nil)).select { |issue| issue["action"] == "already_closed" }
786+
repeat_rates = ledger ? repeat_rate_metrics(ledger, generated_at: generated_at) : []
752787
active = classes.first(top_limit)
753788

754789
lines = [
@@ -795,6 +830,24 @@ def weekly_guardrail_report_markdown(backlog, lifecycle: nil, generated_at: Time
795830
lines << "| #{repo} | #{count} |"
796831
end
797832

833+
lines.concat(
834+
[
835+
"",
836+
"## Repeat-rate trend",
837+
"",
838+
"| Class | Current 7d | Previous 7d | Delta | Change |",
839+
"| --- | ---: | ---: | ---: | ---: |"
840+
]
841+
)
842+
if repeat_rates.empty?
843+
lines << "| _No dated findings in the last two 7-day buckets_ | 0 | 0 | 0 | 0% |"
844+
else
845+
repeat_rates.first(top_limit).each do |entry|
846+
change = entry.fetch("change_percent").nil? ? "new" : "#{entry.fetch("change_percent")}%"
847+
lines << "| `#{entry.fetch("class_key")}` | #{entry.fetch("current_count")} | #{entry.fetch("previous_count")} | #{entry.fetch("delta")} | #{change} |"
848+
end
849+
end
850+
798851
lines.concat(
799852
[
800853
"",
@@ -948,7 +1001,7 @@ def upsert_issue(repo:, title:, body:)
9481001
end
9491002

9501003
if options[:weekly_report_issue_repo] && !options.fetch(:dry_run)
951-
report_body = EvalOpsReviewFeedbackSweep.weekly_guardrail_report_markdown(backlog || EvalOpsReviewFeedbackSweep.guardrail_backlog_json(ledger), lifecycle: lifecycle)
1004+
report_body = EvalOpsReviewFeedbackSweep.weekly_guardrail_report_markdown(backlog || EvalOpsReviewFeedbackSweep.guardrail_backlog_json(ledger), lifecycle: lifecycle, ledger: ledger)
9521005
EvalOpsReviewFeedbackSweep.upsert_issue(
9531006
repo: options.fetch(:weekly_report_issue_repo),
9541007
title: options.fetch(:weekly_report_issue_title),

test/sweep_recent_review_feedback_test.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,21 +417,99 @@ def test_weekly_guardrail_report_markdown_summarizes_candidates_and_prevented_cl
417417
}
418418
]
419419
}
420+
ledger = {
421+
"findings" => [
422+
{
423+
"repo" => "evalops/platform",
424+
"pr_title" => "proto: regenerate SDKs",
425+
"path" => "proto/codex/v1/codex.proto",
426+
"body_first_line" => "generated TypeScript SDK is stale",
427+
"feedback_class" => "review_thread",
428+
"kind" => "review_thread",
429+
"merged_at" => "2026-05-09T06:00:00Z"
430+
},
431+
{
432+
"repo" => "evalops/proto",
433+
"pr_title" => "buf: add meter event",
434+
"path" => "gen/go/meter/v1/event.pb.go",
435+
"body_first_line" => "generated Go output was not committed",
436+
"feedback_class" => "review_thread",
437+
"kind" => "review_thread",
438+
"merged_at" => "2026-05-01T06:00:00Z"
439+
},
440+
{
441+
"repo" => "evalops/deploy",
442+
"pr_title" => "fix: harden parser",
443+
"path" => nil,
444+
"body_first_line" => "Parse real CLI flags instead of substring matching",
445+
"feedback_class" => "top_level_pr_comment",
446+
"kind" => "pr_comment",
447+
"merged_at" => "2026-05-08T06:00:00Z"
448+
}
449+
]
450+
}
420451

421452
report = EvalOpsReviewFeedbackSweep.weekly_guardrail_report_markdown(
422453
backlog,
423454
lifecycle: lifecycle,
455+
ledger: ledger,
424456
generated_at: Time.utc(2026, 5, 10, 6, 15, 0)
425457
)
426458

427459
assert_includes report, "# Weekly review feedback guardrail report"
428460
assert_includes report, "<!-- evalops-review-feedback-weekly-report -->"
429461
assert_includes report, "| 1 | `generated-contract-drift` Generated contract drift | 140 | 2 | evalops/platform, evalops/proto |"
430462
assert_includes report, "| evalops/platform | 1 |"
463+
assert_includes report, "## Repeat-rate trend"
464+
assert_includes report, "| `parser-cli-contract` | 1 | 0 | 1 | new |"
465+
assert_includes report, "| `generated-contract-drift` | 1 | 1 | 0 | 0% |"
431466
assert_includes report, "`parser-cli-contract` https://github.com/evalops/.github/issues/50"
432467
assert_includes report, "`generated-contract-drift`: Add generated-output drift checks."
433468
end
434469

470+
def test_repeat_rate_metrics_buckets_findings_by_merged_at
471+
ledger = {
472+
"findings" => [
473+
{
474+
"repo" => "evalops/deploy",
475+
"pr_title" => "fix parser",
476+
"body_first_line" => "Parse CLI flags",
477+
"feedback_class" => "top_level_pr_comment",
478+
"kind" => "pr_comment",
479+
"merged_at" => "2026-05-09T00:00:00Z"
480+
},
481+
{
482+
"repo" => "evalops/deploy",
483+
"pr_title" => "fix parser",
484+
"body_first_line" => "Parse CLI args",
485+
"feedback_class" => "top_level_pr_comment",
486+
"kind" => "pr_comment",
487+
"merged_at" => "2026-05-02T00:00:00Z"
488+
},
489+
{
490+
"repo" => "evalops/deploy",
491+
"pr_title" => "old parser",
492+
"body_first_line" => "Parse CLI command",
493+
"feedback_class" => "top_level_pr_comment",
494+
"kind" => "pr_comment",
495+
"merged_at" => "2026-04-20T00:00:00Z"
496+
}
497+
]
498+
}
499+
500+
metrics = EvalOpsReviewFeedbackSweep.repeat_rate_metrics(
501+
ledger,
502+
generated_at: Time.utc(2026, 5, 10, 0, 0, 0)
503+
)
504+
505+
assert_equal 1, metrics.length
506+
assert_equal "parser-cli-contract", metrics.first.fetch("class_key")
507+
assert_equal 1, metrics.first.fetch("current_count")
508+
assert_equal 1, metrics.first.fetch("previous_count")
509+
assert_equal 0, metrics.first.fetch("delta")
510+
assert_equal 0, metrics.first.fetch("change_percent")
511+
end
512+
435513
def test_weekly_guardrail_report_markdown_handles_empty_backlog
436514
backlog = {
437515
"schema_version" => "evalops.review_feedback_guardrail_backlog.v1",

0 commit comments

Comments
 (0)