@@ -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