#!/usr/bin/env bash # Scores one catch-rate run produced by run-catch-rate.sh and writes result.json beside it. # Usage: score-catch-rate.sh # Env: OUT_DIR same scratch directory run-catch-rate.sh used # GRADER_MODEL optional, defaults to opus set -euo pipefail EVALS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" EVAL_SET="$EVALS_DIR/catch-rate-eval.json" id="$1" config="$2" run="$3" : "${OUT_DIR:?set OUT_DIR}" [[ "$OUT_DIR" == /* ]] || { echo "OUT_DIR must be an absolute path" >&2; exit 1; } grader_model="${GRADER_MODEL:-opus}" [[ "$id" =~ ^[0-9]+$ ]] || { echo "eval id must be numeric" >&2; exit 1; } [[ "$run" =~ ^[0-9]+$ ]] || { echo "run number must be numeric" >&2; exit 1; } case "$config" in with_skill|without_skill) ;; *) echo "unknown configuration: $config" >&2; exit 1 ;; esac case_json="$(jq -ce --argjson id "$id" '.evals[] | select(.id == $id)' "$EVAL_SET")" pr_url="$(jq -r .pr_url <<<"$case_json")" expected="$(jq -r .expected_location <<<"$case_json")" bug_summary="$(jq -r .bug_summary <<<"$case_json")" exp_file="${expected%:*}" exp_range="${expected##*:}" exp_start="${exp_range%-*}" exp_end="${exp_range#*-}" run_dir="$OUT_DIR/$id/$config/run-$run" report_dir="$run_dir/report" result="$run_dir/result.json" run_error="$(jq -rs 'map(select(.type == "result")) | if length == 0 then "no result event" elif .[-1].is_error then (.[-1].result // "error") else empty end' "$run_dir/run.jsonl" 2>/dev/null || echo "unreadable transcript")" if [[ -n "$run_error" ]]; then jq -n --argjson id "$id" --arg c "$config" --argjson r "$run" --arg e "$run_error" \ '{eval_id: $id, configuration: $c, run_number: $r, error: $e, hit: false, hit_d: false, hit_g: false, validated_findings: 0, false_findings: 0}' >"$result" exit 0 fi shopt -s nullglob reports=("$report_dir"/*.md) if (( ${#reports[@]} != 1 )); then jq -n --argjson id "$id" --arg c "$config" --argjson r "$run" --argjson n "${#reports[@]}" \ '{eval_id: $id, configuration: $c, run_number: $r, note: "expected exactly one report, found \($n)", hit: false, hit_d: false, hit_g: false, validated_findings: 0, false_findings: 0}' >"$result" exit 0 fi report="${reports[0]}" # Validated findings are everything from "## Findings" up to "## Reviewed and Dismissed" or the end. # "**Caught by:**" lines name the pipeline stage; dropping them keeps the grader blind to the configuration. findings="$(awk '/^## Findings/{on=1} /^## Reviewed and Dismissed/{on=0} on' "$report" | grep -v '^\*\*Caught by:\*\*' || true)" # D: any cited location in the bug's file whose line range overlaps the recorded range. hit_d=false while IFS= read -r cite; do file="${cite%:*}" range="${cite##*:}" start="${range%-*}" end="${range#*-}" if [[ "$file" == "$exp_file" ]] && (( start <= exp_end && end >= exp_start )); then hit_d=true fi done < <(grep -oE '`[^` ]+:[0-9]+(-[0-9]+)?`' <<<"$findings" | tr -d '`' || true) checkout="$run_dir/repo" # --setting-sources local reads .claude/settings.local.json from the checkout, which a corpus PR can commit. [[ ! -e "$checkout/.claude/settings.local.json" && ! -L "$checkout/.claude/settings.local.json" ]] \ || { echo "corpus checkout carries .claude/settings.local.json; refusing to load it" >&2; exit 1; } schema='{"type":"object","additionalProperties":false,"required":["findings","describes_known_bug"],"properties":{"describes_known_bug":{"type":"boolean"},"findings":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["location","verdict","reason"],"properties":{"location":{"type":"string"},"verdict":{"enum":["real","false"]},"reason":{"type":"string"}}}}}}' grader_prompt="$(cat < $findings EOF )" grade_json="$(cd "$checkout" && env -u CLAUDECODE claude -p "$grader_prompt" \ --model "$grader_model" \ --setting-sources local \ --permission-mode dontAsk \ --allowedTools Read Grep Glob "Bash(gh pr diff:*)" "Bash(git show:*)" "Bash(git log:*)" \ --disallowedTools "Bash(git * --output*)" "Bash(git *--output=*)" \ --no-session-persistence \ --output-format json \ --json-schema "$schema" 2>>"$run_dir/stderr.log")" || true grade="$(jq -c '.structured_output // empty' <<<"$grade_json" 2>/dev/null || true)" [[ -n "$grade" ]] || grade='{"error":"grader returned no structured output","findings":[],"describes_known_bug":false}' printf '%s\n' "$grade" >"$run_dir/grade.json" jq -n --argjson id "$id" --arg c "$config" --argjson r "$run" --argjson d "$hit_d" --argjson g "$grade" \ --arg report "${report#"$OUT_DIR"/}" ' {eval_id: $id, configuration: $c, run_number: $r, report: $report, hit_d: $d, hit_g: $g.describes_known_bug, hit: ($d and $g.describes_known_bug), validated_findings: ($g.findings | length), false_findings: ([$g.findings[] | select(.verdict == "false")] | length), grades: $g.findings} + (if $g.error then {error: $g.error} else {} end)' >"$result"