Skip to content

DEV-1515: tolerant grader + cascading-phase1 report + annotation CLI - #16

Merged
ZmeiGorynych merged 27 commits into
mainfrom
egor/dev-1515-multi-variant-gold-annotations-tolerant-grader-post-dev-1478
Jun 3, 2026
Merged

DEV-1515: tolerant grader + cascading-phase1 report + annotation CLI#16
ZmeiGorynych merged 27 commits into
mainfrom
egor/dev-1515-multi-variant-gold-annotations-tolerant-grader-post-dev-1478

Conversation

@ZmeiGorynych

@ZmeiGorynych ZmeiGorynych commented May 31, 2026

Copy link
Copy Markdown
Member

Summary

  • Lands the 8-row cascading-phase1 verdict (N1 original → N8 column-order tolerance) produced by an inline grader on the cloud worker and the local runner, with monotone enforcement, content-hashed LLM-judge caching, Tier 2 informational diff, and a no-overwrite fetch-side merge into <main_checkout>/annotations/.
  • Drops the legacy phase1_passed_audited / phase1_passed_original raw bool columns end-to-end (run.py, results_db.py, cloud/collation.py, agents/_submit.py, 9 agent files) — per-task verdicts now live in SubmissionAnnotation.
  • Adds the annotation-skeleton CLI (eval/annotate.py + scripts/generate_annotation_skeletons.py) with init/refresh/force-all modes and a sentinel-preserving idempotency contract, plus the offline eval/regrade.py CLI.

Closes DEV-1515.

Design notes

  • N1/N2/N3 use SET-equality (matches today's ex_base default; preserves phase1_count → counts["n1"] back-compat). N4 (bucket-by-ORDER-BY-then-set-equal) is therefore a no-op vs N3 in practice but kept for spec compliance.
  • N4 bucket spec sourced from the ORIGINAL gold's ORDER BY (deliberate simplification).
  • N8 bundles Linear's separate "column-name-case" tolerance — case-insensitive header align is a prerequisite for name-aligned column comparison.
  • LLM-judge cache key = sha256(model | annotation-content-hash | gold-variants-content-hash | normalized SQL). Run-id NOT in key so offline regrade reuses worker-side decisions.
  • annotations/ is gitignored (separate-repo planned; same posture as audited_gold/); the cloud image bakes it in via --build-context annotations=<paths.annotations_root()> so worktree builds match main-checkout content.

Codex review

Two rounds folded in:

  • Round 1 (plan) — 9 findings: schema couldn't store N6-N8 → extended; implicit-annotation factory needed real builder; GCS plumbing missing; Docker copy target wrong; legacy field removal scope way bigger than initially named; N8 needed cursor.description; local-run aggregator had no source data → built shared grade_in_place; LLM cache key incomplete.
  • Round 2 (tests) — 11 findings: --force-llm-judge was a no-op assertion; GCS roundtrip not tested; schema-extension fields not pinned; VariantInformational weakly covered; N5 gating + N4 ORDER-BY-source tests didn't actually distinguish; image-hash path-keying tested by source grep instead of behaviorally; cache key dimensions incomplete; aggregator monotone test passed without enforcement; phase1_count rewrite test passed without rewrite; submission-mode overwrite + dry-run untested.

All findings have specific commits / tests.

Test plan

  • Full non-integration suite: 1800 passing, 95 skipped, 0 failed (baseline 1704; +96 net new tests, 12 obsolete dual-eval tests deleted alongside the legacy columns).
  • tests/test_legacy_field_removal.py grep-sweep — no production source emits the legacy fields.
  • tests/test_tolerant_grader_orchestration.py::test_cascade_is_monotone_for_every_possible_pass_pattern — property test over all 256 raw bool patterns.
  • tests/cloud/test_image_annotations.py::test_data_hash_invariant_to_host_path_of_annotations_root — worktree-safety regression mirrors the existing audited_gold posture.
  • tests/cloud/test_fetch_annotation_merge.py — end-to-end GCS round-trip via the existing fake-bucket fixture + no-overwrite + schema-validate + audit-report.
  • Operational follow-ups (separate PRs / sessions): fetch + analyze the 38-task run (20260531t1343-claudes-slayer-b39bfc, terminal); 53-instance annotation pass; validate prompt + grader fix on a small re-run from this branch. Tracked in the DEV-1515 Linear comment.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Documentation

    • Updated cloud smoke/benchmark guidance with multi-instance run examples, sizing/cost notes, and OTF behavior clarifications.
  • New Features

    • Added structured task & submission annotation workflow and an inline grader producing per-task annotations.
    • Added a tolerant SQL grader with extended cascade tiers (including case‑fold tolerance).
    • Added cascading phase‑1 reporting with per‑tier pass rates/deltas and single-file multi‑variant audited‑gold support.
  • Improvements

    • Cloud image/build and fetch now incorporate submission annotations and post‑run annotation merge.
  • Analysis

    • Multiple detailed failure‑analysis reports added.

ZmeiGorynych and others added 6 commits May 31, 2026 17:56
…museum runs

analyses/: per-run summaries for `20260531t1008-claudes-slayer-890419`
(households 11/15) and `20260531t1013-claudes-slayer-48eb0f` (museum 4/10).

analyses/raw/: per-task deep dives for the 10 failures (museum_2..10 +
households_2/10/12/15) and one cross-cutting observations file. Each
references the trajectory items, KB ids, column-meaning lines, and
slayer model paths used as evidence; documents whether the answer was
derivable in principle from supplied metadata and which signal the agent
failed to use (when applicable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves mini-interact from the per_db sidecar layout
(audited_gold/<db>/<db>_audited.jsonl) to a single consolidated JSONL at
audited_gold/mini_interact_audited.jsonl, matching the livesqlbench
layout introduced in DEV-1510.

Each row gains three fields:
  - benchmark: "mini_interact" (required by harness defence-in-depth)
  - variant_id: "primary"      (DEV-1515 multi-variant gold support)
  - primary: true              (interaction-time feedback discriminator)

livesqlbench's existing single_file JSONL retro-tagged with the same
variant_id / primary fields for layout consistency across benchmarks.

The consolidation itself is in scripts/consolidate_mini_interact_audited.py
(idempotent: skips fields already present with expected values; rejects
benchmark-tag mismatches). audited_gold/ is fully gitignored, so this
commit ships the script + descriptor change; the on-disk JSONLs are
local-only artefacts other developers regenerate by running the script.

apply_audited_gold_overlay's single_file branch already handles this
layout (DEV-1510), so the only src change is flipping mini-interact's
Benchmark.audited_gold_layout from per_db to single_file.

Tests:
- test_benchmark.py: assertion flipped per_db → single_file
- test_paths.py: per_db_layout_raises → single_file path resolves
- test_dual_eval.py: per_db_explicit_matches_default rewritten as
  single_file dispatch test
- Full non-integration suite: 1697 passed, 94 skipped, 50 deselected

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude_sdk_otf + claude_sdk_otf_ainteract: add a symmetric companion to
the existing "drop predicate when KB literals are absent from sampled
values" rule. The new rule tells the agent: when the column's
`Sample values` show variants of KB-named literals (case, whitespace,
abbreviations, alternate phrasings of the same concept), normalise and
extend the IN-set to include them.

Targets the households_10 + museum_2 / museum_10 class of failure:
KB enumerations are non-exhaustive ("etc.", "like", "include"); the
sampled values are the authoritative inventory of what's actually
present. A canonical-only IN-set silently misses matching rows.

Also adds a paragraph promoting `search(entities=["<db>.<model>.<col>"],
max_memories=0, max_example_queries=0)` to the front of the slayer-tools
section. EntityHit.text carries Description + Sample values inline;
this is the canonical "read by known ref" primitive for verifying any
filter / projection / join-key column before committing.

Out of scope: the rendered `Sample values` is the truncated comma-string
(motley-slayer's `render_column_text`), not the full structured
`sampled_values: List[str]`. Wider variant coverage requires a separate
slayer-upstream change to expose the structured field via search /
inspect_model.

scripts/verify_audited_gold.py: reads the consolidated
mini_interact_audited.jsonl when audit-set=inhouse and filters by
selected_database (per DEV-1515 consolidation). SAR audit set is
unchanged.

Tests:
- Existing non-integration suite: 1696 passed, 95 skipped, 50 deselected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…chemas

src/bird_interact_agents/eval/: new module hosting the DEV-1515
annotation infrastructure.

annotation_schema.py:
  - TaskAnnotation: per-instance, run-independent. Captures
    metadata_sufficiency.verdict ∈ {sufficient, ambiguous, insufficient},
    a list of GoldVariantRef (each pointing to a row in the consolidated
    audited-gold JSONL by (instance_id, variant_id), with one tagged
    primary=True for interaction-time feedback), and an evaluator_prompt
    (only invoked when verdict==insufficient).
  - SubmissionAnnotation: per-(instance, run). Carries the cascading
    SubmissionEvaluation block (original gold → primary variant → any
    variant → tie tolerance → LLM judge), FailureClassification
    (top-level enum primary + free-form details), decision-point
    reference, and UserSimInteraction.
  - Every model is ConfigDict(extra="forbid") so silent schema drift is
    impossible. Container fields use typed BaseModels rather than
    Dict[...].

annotation_io.py:
  - read_/write_ helpers for both kinds with Pydantic JSON round-trip.
  - task_annotation_path / submission_annotation_path: canonical
    `annotations/<benchmark>/<db>/<instance_id>.{task,submission.<run_id>}.json`
    anchored at the main checkout (matches audited_gold/results
    contract).
  - iter_task_annotations / iter_submission_annotations for batch
    consumers.

tests/test_eval_annotation_schema.py: 8 cases covering minimal
construction, JSON round-trip, forbid-extra, path helpers, and the
on-disk 2-space-indent + trailing-newline invariant.

Out of scope (follow-up commits):
  - generate_task_annotation / generate_submission_annotation that
    materialise skeletons from existing artefacts (task data + audited
    gold + attempt JSON).
  - CLI entry point (`bird-interact-annotate ...`).
  - tolerant_grader.py implementing Tier 1 + Tier 2 evaluation that
    populates SubmissionEvaluation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eract}

The cloud-submit snippets shipped with `--workers 1 --actors-per-worker 1`
which is correct for the 1-task smokes shown but silently serialises
larger runs through a single actor. The DEV-1515 38-task remainder run
inherited that and took ~5 hours instead of ~80 minutes (~25 min of
cluster boot + 38 × ~7 min per task).

Add a sizing note + a 10-instance example using `--actors-per-worker 4`
on each framework. e2-standard-4 (4 vCPU / 16 GB) comfortably runs 4
concurrent Opus + Sonnet pairs since the workload is network-bound on
the LLM APIs, so 4 is a safe default for 10-50 task batches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands the 8-row cascading verdict (N1=original / N2=audited primary /
N3=any audited variant / N4=tie-order / N5=LLM-judge / N6=numeric-eps /
N7=trailing-whitespace / N8=column-order via case-insensitive name align),
the inline-graded per-task SubmissionAnnotation produced by ray_app and
the local run path, the no-overwrite fetch-side merge into
<main_checkout>/annotations/, and the offline regrade CLI.

* `eval/tolerant_grader.py` — 8-row monotone cascade, ORDER BY parser,
  cell-level relaxations, content-hashed LLM-judge cache (model + annotation
  hash + gold-variants hash + normalized SQL; runId NOT in key so offline
  re-grade reuses cloud-side decisions), Tier 2 informational diff.
* `eval/implicit_annotation.py` — schema-valid in-memory default for
  unannotated instances; cascade collapses to N1.
* `eval/grade_in_place.py` — shared inline grader called by both
  ray_app (cloud) and run.py (local) so eval.json's `cascading_phase1`
  has source data on both paths.
* `eval/cascading_report.py` — aggregator over per-row
  submission_annotation.json; enforces cascade monotonicity at aggregate
  time; rewrites legacy `phase1_count`/`phase1_rate` from cascade N1.
* `eval/annotate.py` + `scripts/generate_annotation_skeletons.py` —
  skeleton CLI with init|refresh|force-all task modes, overwrite|init
  submission modes, dry-run, idempotent-preserve human edits.
* `eval/regrade.py` — explicit OVERWRITE re-grade path; writes
  eval_regraded.json alongside historical eval.json.
* `paths.annotations_root()` + Dockerfile.cloud `--build-context
  annotations=` + `image.data_hash` keyed under `annotations/<rel>` so
  worktree builds match main-checkout content.
* `cloud/gcs.{write,read}_submission_annotation` +
  `cloud/post_run_merge.merge_submission_annotations` no-overwrite
  + schema-validated + audit-report.

Legacy `phase1_passed_audited` / `phase1_passed_original` raw bool
columns + their plumbing have been removed end-to-end: `run.py`,
`results_db.py`, `cloud/collation.py`, `agents/_submit.py`, all 9 agent
files. The dual-eval block in eval.json is replaced by
`cascading_phase1`; `phase1_count`/`phase1_rate` stay as back-compat
aliases for N1. Two-round Codex review (plan + tests) folded in; 20
findings addressed.

Tests: 1800 passing (+96 net over the 1704 baseline; 12 obsolete
dual-eval tests deleted alongside the legacy columns). New coverage:
test_implicit_task_annotation, test_paths_annotations,
test_schema_extension, test_tolerant_grader_{comparators,orchestration},
test_cascading_report, test_legacy_field_removal,
test_local_run_cascading, test_eval_annotate_cli, test_regrade_cli,
cloud/test_{image_annotations,inline_grader,fetch_annotation_merge}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@linear

linear Bot commented May 31, 2026

Copy link
Copy Markdown

DEV-1515

DEV-1478

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds per-task and per-submission JSON annotation schema/IO and CLI; implements a 9-tier tolerant SQL grader with miss diagnostics and LLM-judge caching; wires inline cloud grading, GCS upload, fetch-time merge, and cascading-phase1 aggregation; bakes annotations into cloud image; removes legacy dual-eval fields; adds conversion scripts and many tests/docs.

Changes

DEV-1515 Annotations & Grading

Layer / File(s) Summary
Annotation schema, IO and CLI
src/bird_interact_agents/eval/annotation_schema.py, src/bird_interact_agents/eval/annotation_io.py, src/bird_interact_agents/eval/annotate.py, src/bird_interact_agents/eval/implicit_annotation.py
Add Pydantic task/submission schemas, path helpers, read/write helpers, annotate CLI to generate skeletons, and implicit in-memory task annotations when files are missing.
Tolerant grader and comparators
src/bird_interact_agents/eval/tolerant_grader.py, tests/test_tolerant_grader_comparators.py, tests/test_tolerant_grader_orchestration.py
Implement monotone 9-tier cascade (N1–N9), comparator relaxations (numeric epsilon, whitespace, column-order, case-fold), ORDER BY tie bucketing, LLM-judge with on-disk cache, miss diagnostics, and many unit/integration tests.
Inline grader, submissions, regrade & cascading report
src/bird_interact_agents/eval/grade_in_place.py, src/bird_interact_agents/eval/regrade.py, src/bird_interact_agents/eval/cascading_report.py, src/bird_interact_agents/cloud/post_run_merge.py
Add grade_and_write to write per-task submission_annotation.json, regrade CLI, aggregation producing cascading_phase1 block, and post-fetch merge report for annotations.
Cloud image & worker wiring
Dockerfile.cloud, src/bird_interact_agents/cloud/image.py, src/bird_interact_agents/cloud/gcs.py, src/bird_interact_agents/cloud/ray_app.py, src/bird_interact_agents/cloud/cli.py
Bake annotations/ into Docker build context (--build-context), include annotations in data hash/image tag, add GCS helpers for submission annotations, run inline grading on workers and publish annotations.
Remove legacy dual-eval fields; DB and agent payloads
src/bird_interact_agents/results_db.py, src/bird_interact_agents/run.py, src/bird_interact_agents/cloud/collation.py, src/bird_interact_agents/agents/*
Drop phase1_passed_audited/phase1_passed_original and related dual-eval persistence; update TaskResultRow schema and inserts; adjust agent finalize payloads to stop emitting legacy fields; rely on annotations for dual-eval breakdown.
Scripts, converters, and utilities
scripts/consolidate_mini_interact_audited.py, scripts/dev1515_*, scripts/dev1515_convert_livesqlbench.py
Add consolidation, conversion, remap, cascade-summary, strict-miss diagnostics, and other DEV-1515 utilities to migrate datasets and reclassify annotations.
Tests & docs
tests/**, .claude/skills/**, analyses/**, README.md, .gitignore
Extensive tests for schema, grader comparators/orchestration, cascading report, fetch merge, image annotations, regrade, annotate CLI, miss diagnostics; add SKILL docs and analysis reports; ignore annotations/ locally and update README guidance.

Sequence Diagram(s)

sequenceDiagram
  participant Worker
  participant GradeInPlace
  participant GCS
  participant Fetch
  Worker->>GradeInPlace: grade_and_write(rows_dir, instance_id)
  GradeInPlace-->>Worker: writes submission_annotation.json
  Worker->>GCS: write_submission_annotation(run_id, instance_id, annotation)
  Fetch->>GCS: download per-run rows/*
  Fetch->>Fetch: merge_submission_annotations(downloaded_run_dir, main_checkout)
  Fetch->>Fetch: emit_cascading_phase1_on_fetch(dest)
Loading

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

"A rabbit hopped with careful pen,
I stitched annotations into den,
Grader sings N1 through N9,
Cloud bakes notes, tests align —
Tiny carrots of JSON, safe and then."

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch egor/dev-1515-multi-variant-gold-annotations-tolerant-grader-post-dev-1478

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (10)
tests/cloud/test_collation.py (1)

31-43: 💤 Low value

Optional: Remove unused helper function.

The _read_dual_cols function is no longer called after the dual-eval test removal. Consider removing it to keep the test suite clean.

🧹 Proposed cleanup
-def _read_dual_cols(db_path: Path) -> dict[str, dict]:
-    conn = sqlite3.connect(str(db_path))
-    conn.row_factory = sqlite3.Row
-    out = {
-        r["instance_id"]: dict(r)
-        for r in conn.execute(
-            "SELECT instance_id, phase1_passed_audited, phase1_passed_original, "
-            "phase1_observation_audited, phase1_observation_original "
-            "FROM task_results"
-        )
-    }
-    conn.close()
-    return out
-
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cloud/test_collation.py` around lines 31 - 43, Remove the now-unused
helper function _read_dual_cols from tests/cloud/test_collation.py: locate the
def _read_dual_cols(...) block and delete it (including its sqlite3 usage and
return value) so the test file no longer contains dead code; ensure no other
references to _read_dual_cols remain in the file after removal.
tests/test_eval_annotation_schema.py (1)

218-219: 💤 Low value

Consider using a more descriptive variable name.

The single-letter variable l (lowercase L) can be confused with 1 (one) or I (uppercase i) in some fonts. Consider renaming to line for clarity.

♻️ Suggested refactor
-    indent_lines = [l for l in body_lines[1:-1] if l.strip()]
-    assert all(l.startswith("  ") for l in indent_lines)
+    indent_lines = [line for line in body_lines[1:-1] if line.strip()]
+    assert all(line.startswith("  ") for line in indent_lines)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_eval_annotation_schema.py` around lines 218 - 219, Rename the
ambiguous loop variable `l` used in the list comprehension and generator
expression to a clearer name like `line` for readability: update the list
comprehension that assigns `indent_lines = [l for l in body_lines[1:-1] if
l.strip()]` to use `line` (e.g., `indent_lines = [line for line in
body_lines[1:-1] if line.strip()]`) and likewise change the subsequent assertion
`assert all(l.startswith("  ") for l in indent_lines)` to use `line` (`assert
all(line.startswith("  ") for line in indent_lines)`), ensuring both references
to the variable in this test are renamed consistently.
tests/test_tolerant_grader_orchestration.py (1)

735-753: 💤 Low value

Minor: Consider consistent attribute initialization.

The CountingInner stub uses a class attribute calls = 0 (line 736), while other test stubs in this file use instance attributes in __init__ (e.g., line 676). This works due to attribute shadowing on write, but is unconventional. For consistency, consider:

♻️ Optional refactor for consistency
 class CountingInner:
-    calls = 0
+    def __init__(self):
+        self.calls = 0
+        
     def judge(self, **kwargs):
         self.calls += 1
         return True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_tolerant_grader_orchestration.py` around lines 735 - 753, The
CountingInner test stub uses a class-level attribute calls = 0 which is
unconventional; change it to an instance attribute by adding an __init__ that
sets self.calls = 0 and keep the judge(self, **kwargs) method to increment
self.calls, so the behavior with CachedLLMJudge (inner, judge) remains the same
but follows the same instance-initialization pattern used elsewhere in the
tests.
src/bird_interact_agents/eval/tolerant_grader.py (1)

507-507: 💤 Low value

rowset_relations is declared but never populated.

grade_submission only fills variant_matches; rowset_relations always stays the empty default and grade_in_place never reads it. Either wire it up or drop the field to avoid a misleading empty list on every persisted verdict.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bird_interact_agents/eval/tolerant_grader.py` at line 507, The model has
an unused field rowset_relations: List[VariantMatch] defaulting to empty; either
remove it or actually populate and consume it. If you want to keep it, update
grade_submission (where variant_matches is populated) to also build and assign
rowset_relations from the same matching logic or source (use the same
VariantMatch construction logic), and modify grade_in_place to read/serialize
rowset_relations alongside variant_matches; otherwise drop the rowset_relations
Field declaration to avoid persisting a misleading empty list. Ensure you
reference and update the methods grade_submission and grade_in_place and the
VariantMatch construction to keep behavior consistent.
src/bird_interact_agents/eval/regrade.py (2)

200-227: ⚡ Quick win

Move imports outside the inline function.

Importing inside the _grader function (lines 203-208) is inefficient and runs on every regrade invocation. Move these to module-level imports.

♻️ Proposed fix
+from bird_interact_agents.cloud.ray_app import (
+    _load_audited_gold_rows_for,
+    _load_task_annotation_or_implicit,
+)
+from bird_interact_agents.eval.implicit_annotation import (
+    implicit_task_annotation,
+)
+from bird_interact_agents.eval.tolerant_grader import grade_submission

Then remove the imports from inside _grader.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bird_interact_agents/eval/regrade.py` around lines 200 - 227, The inline
imports inside the _grader function (implicit_task_annotation,
_load_audited_gold_rows_for, _load_task_annotation_or_implicit) should be moved
to module-level imports to avoid re-importing on every regrade call; add those
import statements at the top of the module and remove the corresponding from ...
import ... lines from inside the _grader function so _grader simply calls
implicit_task_annotation/_load_task_annotation_or_implicit/_load_audited_gold_rows_for
directly.

113-120: ⚡ Quick win

Move imports outside the loop.

Importing _eval_from_cascade, _user_sim_interaction_from_trajectory, FailureClassification, and SubmissionMetadata inside the per-instance loop is inefficient. Move these to module-level imports.

♻️ Proposed fix
 from bird_interact_agents.eval.annotation_io import (
     submission_annotation_path,
     write_submission_annotation,
 )
 from bird_interact_agents.eval.annotation_schema import SubmissionAnnotation
+from bird_interact_agents.eval.annotation_schema import (
+    FailureClassification,
+    SubmissionMetadata,
+)
+from bird_interact_agents.eval.annotate import (
+    _eval_from_cascade,
+    _user_sim_interaction_from_trajectory,
+)
 from bird_interact_agents.eval.cascading_report import emit_cascading_eval_json

Then remove lines 113-120 from inside the loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bird_interact_agents/eval/regrade.py` around lines 113 - 120, The imports
for _eval_from_cascade, _user_sim_interaction_from_trajectory,
FailureClassification, and SubmissionMetadata are currently inside the
per-instance loop; move those import statements to the top-level of the module
(module-level imports) so they are executed once, and then remove the in-loop
import lines (the ones importing _eval_from_cascade,
_user_sim_interaction_from_trajectory from bird_interact_agents.eval.annotate
and FailureClassification, SubmissionMetadata from
bird_interact_agents.eval.annotation_schema) so the loop uses the
already-imported symbols.
src/bird_interact_agents/cloud/ray_app.py (2)

690-699: ⚡ Quick win

Simplify repeated row.get("usage", {}).get(...) pattern.

Extracting usage = row.get("usage", {}) once and checking isinstance(usage, dict) would reduce repetition and improve readability.

♻️ Proposed fix
+        usage = row.get("usage", {})
+        usage_safe = usage if isinstance(usage, dict) else {}
         ann_path = _grade_one_submission(
             task_data=task_data,
             submitted_sql=str(row.get("submitted_sql") or ""),
             rows_dir=annotation_dir,
             run_id=run_id,
             benchmark=_cloud_benchmark(cfg),
             db_path=Path(data_dir)
                 / str(task_data.get("selected_database", ""))
                 / f"{task_data.get('selected_database', '')}.sqlite",
-            cost_usd_agent=row.get("usage", {}).get("cost_usd_agent")
-                if isinstance(row.get("usage"), dict) else None,
-            cost_usd_user_sim=row.get("usage", {}).get("cost_usd_user_sim")
-                if isinstance(row.get("usage"), dict) else None,
+            cost_usd_agent=usage_safe.get("cost_usd_agent"),
+            cost_usd_user_sim=usage_safe.get("cost_usd_user_sim"),
             duration_s=row.get("duration_s"),
-            n_agent_turns=row.get("usage", {}).get("n_agent_turns")
-                if isinstance(row.get("usage"), dict) else None,
-            n_ask_user_calls=row.get("usage", {}).get("n_ask_user_calls")
-                if isinstance(row.get("usage"), dict) else None,
+            n_agent_turns=usage_safe.get("n_agent_turns"),
+            n_ask_user_calls=usage_safe.get("n_ask_user_calls"),
             predicted_row_count=None,
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bird_interact_agents/cloud/ray_app.py` around lines 690 - 699, Extract
usage = row.get("usage") once, check if isinstance(usage, dict), and then
replace repeated row.get("usage", {}).get(...) calls with usage.get(...) so
cost_usd_agent, cost_usd_user_sim, n_agent_turns, and n_ask_user_calls read from
the local usage variable; keep duration_s = row.get("duration_s") and
predicted_row_count=None unchanged and ensure the fallback when usage is not a
dict remains None for those fields (use a single boolean or conditional
expression based on isinstance(usage, dict)).

676-679: ⚡ Quick win

Avoid relying on locals().get("data_dir").

Using locals().get("data_dir") is fragile—if an exception occurs before data_dir is assigned (line 644), this will be None. While the code raises RuntimeError and swallows it, it's clearer to reference data_dir directly or use a try/except block.

♻️ Proposed fix
-    _grader_data_dir = locals().get("data_dir")
     try:
-        if _grader_data_dir is None:
-            raise RuntimeError("data_dir unbound; grader skipped")
+        # data_dir is bound at line 644; if not, NameError is caught below
         annotation_dir = Path(tempfile.mkdtemp(prefix="bird_submission_annot_"))
         ann_path = _grade_one_submission(
             task_data=task_data,
             submitted_sql=str(row.get("submitted_sql") or ""),
             rows_dir=annotation_dir,
             run_id=run_id,
             benchmark=_cloud_benchmark(cfg),
-            db_path=Path(_grader_data_dir)
+            db_path=Path(data_dir)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bird_interact_agents/cloud/ray_app.py` around lines 676 - 679, The code
currently uses _grader_data_dir = locals().get("data_dir"), which is fragile;
instead access data_dir directly and handle the case where it isn't defined by
wrapping the reference in a try/except NameError (or test for None after a
direct lookup) and then raise the existing RuntimeError("data_dir unbound;
grader skipped") if it's missing; update the block around _grader_data_dir and
the subsequent try/except so the code references data_dir directly (or assigns
_grader_data_dir = data_dir inside a try) and does not rely on locals().get,
keeping the same error message and behavior in the grader setup code.
src/bird_interact_agents/eval/implicit_annotation.py (1)

37-37: ⚡ Quick win

Avoid creating a set on every call.

Converting benchmark_names() to a set on each invocation is inefficient. Either cache the set at module level or compare directly against the list if it's small.

♻️ Proposed fix
+_KNOWN_BENCHMARKS = set(benchmark_names())
+
 def _benchmark_task_jsonl_name(benchmark: str) -> str:
     """Return the basename of the benchmark's task JSONL for provenance.

     Falls back to a generic ``"<benchmark>.jsonl"`` placeholder when the
     benchmark token doesn't match a registered ``Benchmark`` descriptor
     (which is fine for tests / forks that haven't registered theirs)."""
-    if benchmark in set(benchmark_names()):
+    if benchmark in _KNOWN_BENCHMARKS:
         return get_benchmark(benchmark).data_file
     return f"{benchmark}.jsonl"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bird_interact_agents/eval/implicit_annotation.py` at line 37, The
condition currently builds a set each call with "if benchmark in
set(benchmark_names()):" which is wasteful; create a module-level cached set
(e.g., BENCHMARK_NAMES_SET = set(benchmark_names())) and replace the call-site
check with "if benchmark in BENCHMARK_NAMES_SET:" (or, if the list is tiny, use
direct list membership "if benchmark in benchmark_names():"). Update
implicit_annotation.py to define the cached set once at import and reference
BENCHMARK_NAMES_SET in the function that checks benchmark membership.
src/bird_interact_agents/eval/annotate.py (1)

57-60: ⚡ Quick win

Deduplicate _benchmark_task_jsonl_name.

This helper is identical to implicit_annotation._benchmark_task_jsonl_name. Import it from there instead of duplicating.

♻️ Proposed fix
+from bird_interact_agents.eval.implicit_annotation import (
+    _benchmark_task_jsonl_name,
+)
+
-def _benchmark_task_jsonl_name(benchmark: str) -> str:
-    if benchmark in set(benchmark_names()):
-        return get_benchmark(benchmark).data_file
-    return f"{benchmark}.jsonl"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bird_interact_agents/eval/annotate.py` around lines 57 - 60, The function
_benchmark_task_jsonl_name is duplicated here; remove this local definition and
import the canonical helper from implicit_annotation instead. Replace the
function block in this file with an import statement that brings in
_benchmark_task_jsonl_name from the module implicit_annotation (the same module
that currently defines the original helper), ensuring all references in this
file use the imported symbol. Keep the import at the top with other imports and
run tests/linters to ensure no name collisions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@analyses/raw/households_10.md`:
- Around line 41-45: The markdown fenced code blocks listing KB values (e.g.,
the blocks containing "KB 6 Dwelling Type", "KB 7 Cable TV Status", and "KB 10
Vehicle Year Range") are missing a language tag and trigger MD040; update each
opening triple-backtick to include a language identifier (use "text") for those
specific fenced blocks and likewise add "text" to the other missing fenced
blocks referenced (the blocks around lines noted in the review, e.g., the long
value lists at the later locations) so every ``` becomes ```text.

In `@analyses/raw/museum_2.md`:
- Around line 38-42: The fenced code block containing the YAML mapping starting
with "erf:" should declare its language for MD040 compliance; update the
backticks that open the block to include "yaml" (i.e., change the code fence for
the block that contains erf, formula, and description to "```yaml") so the block
is explicitly marked as YAML.

In `@analyses/raw/museum_4.md`:
- Around line 16-18: The fenced code blocks in the markdown contain math
expressions but lack a language tag, triggering MD040; update each
triple-backtick fence (the blocks containing "CBE = \frac{\sum_{i \in artifacts}
(CPI_i \times BudgetRatio_i)}{|artifacts|}, where BudgetRatio..." and the block
with "CBE = sum(CPI*1/N)/N = sum(CPI)/N^2 assuming uniform BudgetRatio") to
include a language identifier such as "text" (e.g., ```text) so both fenced
blocks validate against the linter and remain unchanged otherwise.

In `@src/bird_interact_agents/eval/regrade.py`:
- Around line 218-227: The code currently calls grade_submission(...) with
db_path=Path("/dev/null") and conn=None which lets grade_submission fall back to
default_executor and actually execute SQL; fix by passing an explicit safe
executor (e.g., a no-op or validation-only function) via the executor parameter
to grade_submission so it will not open or execute against a DB, and also wrap
the call site in regrade_run (the function that invokes grader(...)) in a
try/except to catch sqlite/SQL or executor errors and skip/fail the item
gracefully; reference grade_submission, default_executor, regrade_run, db_path,
and conn when making these changes.

In `@src/bird_interact_agents/eval/tolerant_grader.py`:
- Around line 551-569: grade_submission currently lets default_executor
open/close a new SQLite connection per call so multi-statement submissions lose
TEMP/session state; fix by creating a single shared SQLite connection when conn
is None at the start of grade_submission (and ensuring executor is the
default_executor if unset), pass that shared conn into every call to executor
and _multi_sql_execute (including calls for submitted_sql, original_sol_sql, and
audited_sol_sql variants), and close the connection in a finally block; update
references in this function (grade_submission, default_executor, and calls to
_multi_sql_execute) so all statements for a submission use the same connection.

---

Nitpick comments:
In `@src/bird_interact_agents/cloud/ray_app.py`:
- Around line 690-699: Extract usage = row.get("usage") once, check if
isinstance(usage, dict), and then replace repeated row.get("usage", {}).get(...)
calls with usage.get(...) so cost_usd_agent, cost_usd_user_sim, n_agent_turns,
and n_ask_user_calls read from the local usage variable; keep duration_s =
row.get("duration_s") and predicted_row_count=None unchanged and ensure the
fallback when usage is not a dict remains None for those fields (use a single
boolean or conditional expression based on isinstance(usage, dict)).
- Around line 676-679: The code currently uses _grader_data_dir =
locals().get("data_dir"), which is fragile; instead access data_dir directly and
handle the case where it isn't defined by wrapping the reference in a try/except
NameError (or test for None after a direct lookup) and then raise the existing
RuntimeError("data_dir unbound; grader skipped") if it's missing; update the
block around _grader_data_dir and the subsequent try/except so the code
references data_dir directly (or assigns _grader_data_dir = data_dir inside a
try) and does not rely on locals().get, keeping the same error message and
behavior in the grader setup code.

In `@src/bird_interact_agents/eval/annotate.py`:
- Around line 57-60: The function _benchmark_task_jsonl_name is duplicated here;
remove this local definition and import the canonical helper from
implicit_annotation instead. Replace the function block in this file with an
import statement that brings in _benchmark_task_jsonl_name from the module
implicit_annotation (the same module that currently defines the original
helper), ensuring all references in this file use the imported symbol. Keep the
import at the top with other imports and run tests/linters to ensure no name
collisions.

In `@src/bird_interact_agents/eval/implicit_annotation.py`:
- Line 37: The condition currently builds a set each call with "if benchmark in
set(benchmark_names()):" which is wasteful; create a module-level cached set
(e.g., BENCHMARK_NAMES_SET = set(benchmark_names())) and replace the call-site
check with "if benchmark in BENCHMARK_NAMES_SET:" (or, if the list is tiny, use
direct list membership "if benchmark in benchmark_names():"). Update
implicit_annotation.py to define the cached set once at import and reference
BENCHMARK_NAMES_SET in the function that checks benchmark membership.

In `@src/bird_interact_agents/eval/regrade.py`:
- Around line 200-227: The inline imports inside the _grader function
(implicit_task_annotation, _load_audited_gold_rows_for,
_load_task_annotation_or_implicit) should be moved to module-level imports to
avoid re-importing on every regrade call; add those import statements at the top
of the module and remove the corresponding from ... import ... lines from inside
the _grader function so _grader simply calls
implicit_task_annotation/_load_task_annotation_or_implicit/_load_audited_gold_rows_for
directly.
- Around line 113-120: The imports for _eval_from_cascade,
_user_sim_interaction_from_trajectory, FailureClassification, and
SubmissionMetadata are currently inside the per-instance loop; move those import
statements to the top-level of the module (module-level imports) so they are
executed once, and then remove the in-loop import lines (the ones importing
_eval_from_cascade, _user_sim_interaction_from_trajectory from
bird_interact_agents.eval.annotate and FailureClassification, SubmissionMetadata
from bird_interact_agents.eval.annotation_schema) so the loop uses the
already-imported symbols.

In `@src/bird_interact_agents/eval/tolerant_grader.py`:
- Line 507: The model has an unused field rowset_relations: List[VariantMatch]
defaulting to empty; either remove it or actually populate and consume it. If
you want to keep it, update grade_submission (where variant_matches is
populated) to also build and assign rowset_relations from the same matching
logic or source (use the same VariantMatch construction logic), and modify
grade_in_place to read/serialize rowset_relations alongside variant_matches;
otherwise drop the rowset_relations Field declaration to avoid persisting a
misleading empty list. Ensure you reference and update the methods
grade_submission and grade_in_place and the VariantMatch construction to keep
behavior consistent.

In `@tests/cloud/test_collation.py`:
- Around line 31-43: Remove the now-unused helper function _read_dual_cols from
tests/cloud/test_collation.py: locate the def _read_dual_cols(...) block and
delete it (including its sqlite3 usage and return value) so the test file no
longer contains dead code; ensure no other references to _read_dual_cols remain
in the file after removal.

In `@tests/test_eval_annotation_schema.py`:
- Around line 218-219: Rename the ambiguous loop variable `l` used in the list
comprehension and generator expression to a clearer name like `line` for
readability: update the list comprehension that assigns `indent_lines = [l for l
in body_lines[1:-1] if l.strip()]` to use `line` (e.g., `indent_lines = [line
for line in body_lines[1:-1] if line.strip()]`) and likewise change the
subsequent assertion `assert all(l.startswith("  ") for l in indent_lines)` to
use `line` (`assert all(line.startswith("  ") for line in indent_lines)`),
ensuring both references to the variable in this test are renamed consistently.

In `@tests/test_tolerant_grader_orchestration.py`:
- Around line 735-753: The CountingInner test stub uses a class-level attribute
calls = 0 which is unconventional; change it to an instance attribute by adding
an __init__ that sets self.calls = 0 and keep the judge(self, **kwargs) method
to increment self.calls, so the behavior with CachedLLMJudge (inner, judge)
remains the same but follows the same instance-initialization pattern used
elsewhere in the tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a57df611-9330-4d19-ac38-6ca160fe4249

📥 Commits

Reviewing files that changed from the base of the PR and between af58457 and 83751ce.

📒 Files selected for processing (72)
  • .gitignore
  • Dockerfile.cloud
  • README.md
  • analyses/households_failure_analysis_20260531t1008-claudes-slayer-890419.md
  • analyses/museum_failure_analysis_20260531t1013-claudes-slayer-48eb0f.md
  • analyses/raw/cross_cutting_observations.md
  • analyses/raw/households_10.md
  • analyses/raw/households_12.md
  • analyses/raw/households_15.md
  • analyses/raw/households_2.md
  • analyses/raw/museum_10.md
  • analyses/raw/museum_2.md
  • analyses/raw/museum_3.md
  • analyses/raw/museum_4.md
  • analyses/raw/museum_5.md
  • analyses/raw/museum_9.md
  • scripts/consolidate_mini_interact_audited.py
  • scripts/generate_annotation_skeletons.py
  • scripts/verify_audited_gold.py
  • src/bird_interact_agents/agents/_submit.py
  • src/bird_interact_agents/agents/agno/agent.py
  • src/bird_interact_agents/agents/claude_sdk/agent.py
  • src/bird_interact_agents/agents/claude_sdk_otf/agent.py
  • src/bird_interact_agents/agents/claude_sdk_otf/prompts.py
  • src/bird_interact_agents/agents/claude_sdk_otf_ainteract/agent.py
  • src/bird_interact_agents/agents/claude_sdk_otf_ainteract/prompts.py
  • src/bird_interact_agents/agents/mcp_agent/agent.py
  • src/bird_interact_agents/agents/pydantic_ai/agent.py
  • src/bird_interact_agents/agents/pydantic_ai_otf_encode/agent.py
  • src/bird_interact_agents/agents/pydantic_ai_recursive/agent.py
  • src/bird_interact_agents/agents/smolagents/agent.py
  • src/bird_interact_agents/benchmark.py
  • src/bird_interact_agents/cloud/cli.py
  • src/bird_interact_agents/cloud/collation.py
  • src/bird_interact_agents/cloud/driver.py
  • src/bird_interact_agents/cloud/gcs.py
  • src/bird_interact_agents/cloud/image.py
  • src/bird_interact_agents/cloud/post_run_merge.py
  • src/bird_interact_agents/cloud/ray_app.py
  • src/bird_interact_agents/eval/__init__.py
  • src/bird_interact_agents/eval/annotate.py
  • src/bird_interact_agents/eval/annotation_io.py
  • src/bird_interact_agents/eval/annotation_schema.py
  • src/bird_interact_agents/eval/cascading_report.py
  • src/bird_interact_agents/eval/grade_in_place.py
  • src/bird_interact_agents/eval/implicit_annotation.py
  • src/bird_interact_agents/eval/regrade.py
  • src/bird_interact_agents/eval/tolerant_grader.py
  • src/bird_interact_agents/paths.py
  • src/bird_interact_agents/results_db.py
  • src/bird_interact_agents/run.py
  • tests/cloud/test_cli.py
  • tests/cloud/test_collation.py
  • tests/cloud/test_fetch_annotation_merge.py
  • tests/cloud/test_image_annotations.py
  • tests/cloud/test_inline_grader.py
  • tests/test_benchmark.py
  • tests/test_cascading_report.py
  • tests/test_claude_sdk_otf_agent.py
  • tests/test_claude_sdk_otf_ainteract_agent.py
  • tests/test_claude_sdk_usage.py
  • tests/test_dual_eval.py
  • tests/test_eval_annotate_cli.py
  • tests/test_eval_annotation_schema.py
  • tests/test_implicit_task_annotation.py
  • tests/test_legacy_field_removal.py
  • tests/test_local_run_cascading.py
  • tests/test_paths.py
  • tests/test_paths_annotations.py
  • tests/test_schema_extension.py
  • tests/test_tolerant_grader_comparators.py
  • tests/test_tolerant_grader_orchestration.py
💤 Files with no reviewable changes (12)
  • src/bird_interact_agents/agents/smolagents/agent.py
  • tests/test_claude_sdk_otf_agent.py
  • src/bird_interact_agents/agents/pydantic_ai_otf_encode/agent.py
  • src/bird_interact_agents/agents/pydantic_ai_recursive/agent.py
  • tests/test_claude_sdk_usage.py
  • src/bird_interact_agents/agents/pydantic_ai/agent.py
  • src/bird_interact_agents/agents/mcp_agent/agent.py
  • src/bird_interact_agents/agents/claude_sdk/agent.py
  • tests/test_claude_sdk_otf_ainteract_agent.py
  • src/bird_interact_agents/agents/claude_sdk_otf_ainteract/agent.py
  • src/bird_interact_agents/agents/agno/agent.py
  • src/bird_interact_agents/agents/claude_sdk_otf/agent.py

Comment thread analyses/raw/households_10.md Outdated
Comment thread analyses/raw/museum_2.md Outdated
Comment thread analyses/raw/museum_4.md Outdated
Comment thread src/bird_interact_agents/eval/regrade.py
Comment thread src/bird_interact_agents/eval/tolerant_grader.py
…iant audits, audit-correctness pass

session-3:
- FailurePrimary enum: drop grader_stability; add no_fail, novel_reading_accepted,
  numerical_precision, row_order, trailing_whitespace, column_order, case_sensitivity.
- _auto_failure_class shared classifier mapping cascade tier -> FailurePrimary
  (no_fail / row_order / novel_reading_accepted / numerical_precision / trailing_whitespace
  / column_order / other).
- InternalInconsistency record on TaskAnnotation for multi-source conflicts
  (KB vs sql_snippet etc.); MetadataSufficiency docstring sharpened to encode
  the user_sim disclosure rule.
- Multi-variant gold pattern: variant_id + primary on every audited_gold row;
  audit-gold-sql skill gains source_conflict clause_kind + sidecar example.
- annotate-task-submission skill (NEW): canonical decision flow for
  mini-interact (a-interact) vs livesqlbench (one-shot), audit-correctness gate,
  failure-class table, cascade-tier -> failure-class mapping.
- dev1515 conversion / re-classification / summary scripts under scripts/.

session-4:
- Multi-variant test refactor in tests/test_livesqlbench_audited_gold.py:
  _load_audit_rows returns PRIMARY rows; test_no_duplicate_instance_ids ->
  test_no_duplicate_instance_id_variant_pairs (dedup on the pair, enforce
  exactly one primary per instance).

Households audit-correctness pass (audited_gold/mini_interact_audited.jsonl):
- households_5: backfilled justified_by on the R$ 2,640 cutoff (anchored in
  snippet); task verdict insufficient -> ambiguous with rewritten rationale.
- households_10: pruned 10 drifted IN-list literals to snippet-exact; agent
  flips agent_miss -> no_fail (equal_rowset, 153 housenums).
- households_12: audit_status edited -> unrecoverable; one changes entry
  documents the gap (0..11 ordinal mapping is policy under any monotonic
  encoding).

Museum audit work (audited_gold/livesqlbench_audited.jsonl):
- museum_10: re-authored from buggy ERF copy-paste to KB-anchored 7-column
  DSD+ERPS+CASE per KBs [0,1,4,8,38,52]; agent submission flips to no_fail
  with gold_audit_quality secondary.
- museum_2/4/9: added non-primary variant audit rows (four_dimension_erf,
  uniform_budget_ratio, conditionassessments_join_path) capturing the
  KB-defensible alternate readings; submissions flip other -> no_fail with
  metadata_ambiguity secondary.
- museum_5: classified agent_miss (KB 7 latest-reading was clear).
- museum_7: filled PENDING_HUMAN_REVIEW rationale + variant interpretation.

Full non-integration suite: 1800 passed, 95 skipped, 50 deselected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/skills/annotate-task-submission/SKILL.md:
- Around line 163-179: Replace the unlabeled fenced code blocks that contain the
"Audit-correctness gate — is the audited gold itself correct?" steps and the
later block referencing user_query_ambiguity.critical_ambiguity[].sql_snippet /
knowledge_ambiguity[].sql_snippet by adding a language tag (e.g., change ``` to
```text) so the two fenced blocks are labeled; locate the blocks by the text
"Audit-correctness gate" and the block that mentions
user_query_ambiguity.critical_ambiguity[].sql_snippet /
knowledge_ambiguity[].sql_snippet and update their opening fences to ```text.

In @.claude/skills/audit-gold-sql/SKILL.md:
- Around line 81-87: The docs and write semantics currently describe
overwrite-by-instance_id which will delete alternate variants; change the
write-key / overwrite logic and documentation to use the composite key
(instance_id, variant_id) so each variant row is upserted independently. Update
any code paths or descriptions that perform deletes/overwrites keyed only by
instance_id to instead key by both instance_id and variant_id, and ensure the
primary boolean semantics (primary default true for single-variant) remain
unchanged when emitting multi-variant rows so exactly one variant per instance
can be marked primary.

In `@scripts/dev1515_cascade_summary.py`:
- Around line 152-155: The f-string prefix on the constant branch is unnecessary
and triggers Ruff F541; in the delta assignment (the variable delta computed
using prev_total and tot) replace f"  (+0)" with a plain string "  (+0)" so only
the branch that needs interpolation uses an f-string (i.e., keep f on f"  (+{tot
- prev_total})" but remove the leading f on the constant branch).

In `@scripts/dev1515_convert_livesqlbench_museum.py`:
- Line 69: The script currently hardcodes LIVESQL_ROOT = Path("/home/james/...")
(and other hardcoded dataset paths around lines 76-90 and 206-207); replace
those hardcoded absolute paths with calls to the repository path helpers in
bird_interact_agents.paths (e.g., use the appropriate paths.<dataset>_root()
function) so all data paths are resolved via the helper functions; update
references to LIVESQL_ROOT and any other hardcoded Path(...) variables to call
the proper paths.*_root() and then join subpaths with .joinpath(...) so the
script becomes portable and follows the repo path-resolution policy.
- Around line 99-109: _load_audit_rows currently maps rows by instance_id
overwriting previous variants; change it to collect a list of variants per
instance_id (rows should be dict[str, list]) by appending each parsed d to
rows[d["instance_id"]]; then adjust the call site(s) around grade_submission
(the block referenced at lines ~231–237) to pass the full list of audited
variants (rows[instance_id]) or iterate them according to the "any audited
variant" grading rule so grade_submission receives/handles all variants instead
of a single row. Use the existing symbols _load_audit_rows, rows, instance_id
and grade_submission to locate and update the code.

In `@scripts/dev1515_convert_runs.py`:
- Around line 269-277: audit_rows currently maps each instance_id to a single
dict and later variants overwrite prior ones; change audit_rows to collect all
variants per instance_id (e.g., initialize audit_rows as a defaultdict(list) or
use setdefault and append each d) so mini_interact_audited.jsonl entries are
preserved; also update the grading logic that consumes audit_rows (the code that
previously assumed a single dict per instance_id) to iterate over the list of
variants for that instance_id and compute cascade/ambiguous outcomes across all
variants instead of using only the last variant.

In `@scripts/dev1515_households_14_multivariant.py`:
- Around line 96-183: Detect if the instance is already multivariant and skip
creating duplicates: before creating kb_row/snippet_row, check existing rows for
the same d["instance_id"] (look for variant_id "kb_definition_reading" or
"snippet_reading" or any change with clause_kind "source_conflict") and if
found, just append d and continue; when constructing snippet_row ensure the
audited_sol_sql rewrite is idempotent by only replacing "lcs > 3" -> "lcs > 2"
when the string "lcs > 3" is present (and avoid double-replacing), and when
updating "changes" filter out any existing source_conflict entries (use the same
filter used earlier) before appending the new source_conflict so you never
create duplicate source_conflict entries for kb_row or snippet_row (references:
INSTANCE_ID, rows, kb_row, snippet_row, variant_id, changes, clause_kind
"source_conflict", audited_sol_sql).

In `@tests/test_livesqlbench_audited_gold.py`:
- Around line 349-372: The test test_no_duplicate_instance_id_variant_pairs only
records counts for instance_ids that have a primary row, so instance_ids with
zero primaries slip through; modify the loop over _iter_audit_rows to ensure
primaries_per_iid is initialized for every iid (e.g., set primaries_per_iid[iid]
= primaries_per_iid.get(iid, 0) before counting) or separately track a seen_iids
set and then compute counts for all seen iids, then build over_primaries from
primaries_per_iid.items() and assert n == 1 for every seen iid; keep the
existing logic for seen_pairs/dupes and defaulting variant_id via
row.get("variant_id", "primary").
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 51a7c81c-840a-4faa-8a1b-f3e72f227e27

📥 Commits

Reviewing files that changed from the base of the PR and between 83751ce and a91f861.

📒 Files selected for processing (14)
  • .claude/skills/annotate-task-submission/SKILL.md
  • .claude/skills/audit-gold-sql/SKILL.md
  • scripts/dev1515_cascade_summary.py
  • scripts/dev1515_convert_livesqlbench_museum.py
  • scripts/dev1515_convert_runs.py
  • scripts/dev1515_households_14_multivariant.py
  • scripts/dev1515_reclassify_sufficiency.py
  • scripts/dev1515_remap_failure_classes.py
  • src/bird_interact_agents/eval/annotate.py
  • src/bird_interact_agents/eval/annotation_schema.py
  • src/bird_interact_agents/eval/grade_in_place.py
  • src/bird_interact_agents/eval/regrade.py
  • tests/test_livesqlbench_audited_gold.py
  • tests/test_regrade_cli.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/bird_interact_agents/eval/regrade.py
  • src/bird_interact_agents/eval/annotate.py

Comment thread .claude/skills/annotate-task-submission/SKILL.md Outdated
Comment thread .claude/skills/audit-gold-sql/SKILL.md
Comment thread scripts/dev1515_cascade_summary.py Outdated
Comment thread scripts/dev1515_convert_livesqlbench.py Outdated
Comment thread scripts/dev1515_convert_livesqlbench.py Outdated
Comment thread scripts/dev1515_convert_runs.py Outdated
Comment thread scripts/dev1515_households_14_multivariant.py
Comment thread tests/test_livesqlbench_audited_gold.py Outdated
…16

Group 1 — Regrade CLI is operational + multi-statement SQL preserves
TEMP state (CodeRabbit r3330838210/r3330838211 + Codex):
* eval/tolerant_grader.py: _multi_sql_execute opens ONE shared sqlite
  connection when conn=None and len(sqls)>1, so CREATE TEMP setup
  statements share state with the final SELECT (closed in a finally so
  it doesn't leak across grade_submission calls). New regression test in
  tests/test_tolerant_grader_multi_sql_conn.py exercises the path with
  conn=None + a real two-statement gold list against a fresh sqlite DB.
* eval/regrade.py: stop passing db_path=Path("/dev/null"); resolve the
  real per-DB sqlite via paths.benchmark_data_root(args.benchmark)/<db>/
  <db>.sqlite using the attempt JSON's database key. Wrap the grader
  call in try/except so a sqlite/SQL error skips the instance rather
  than crashing the whole CLI.

Group 2 — Cascade verdict-label drift (Codex):
* eval/grade_in_place.py: extract verdict_label_from_cascade() as the
  single source of truth for cascade → SubmissionEvaluation.verdict.
  Both _build_submission_annotation (inline grader) and
  eval/annotate._eval_from_cascade (skeleton + regrade CLI) now go
  through it. Without this, N4/N5/N6/N7/N8 cascade-tier passes landed
  in annotate-side annotations with verdict="invalid" while the inline
  grader emitted "valid_interpretation". New
  tests/test_verdict_label_shared.py pins each cascade-tier mapping AND
  asserts the two persistence paths produce the same label.

Group 3 — Cloud eval.json gains the cascading_phase1 block (Codex):
* cloud/driver.py: after merge_submission_annotations, walk the
  downloaded run's <rows>/<inst>/submission_annotation.json files and
  call emit_cascading_eval_json to rewrite eval.json with the
  cascading_phase1 block + back-compat phase1_count/phase1_rate aliases.
  Extracted to _emit_cascading_phase1_on_fetch so it's unit-testable
  without round-tripping through GCS download + collation. Older runs
  pre-dating the DEV-1515 worker hook (no per-row annotations) skip
  cleanly. New tests/cloud/test_fetch_cascading_phase1.py pins the
  happy path + both no-op edges.

Group 4 — Markdown lint + dead code + fresh-checkout build (Codex +
CodeRabbit r3330838206/r3330838207/r3330838208 + summary-nitpick):
* analyses/raw/households_10.md + museum_2.md + museum_4.md: add
  language tags to fenced code blocks (text / yaml) to clear MD040.
* tests/cloud/test_collation.py: delete dead _read_dual_cols helper
  (queried phase1_passed_audited / phase1_passed_original columns that
  were removed in the DEV-1515 schema overhaul; would fail if called).
* paths.py: annotations_root() and audited_gold_root() now
  mkdir(parents=True, exist_ok=True) so a fresh checkout (where both
  dirs are gitignored) doesn't fail cloud/image.build_and_push's
  BuildKit --build-context resolve before the data is populated.

Full non-integration suite: 1811 passed, 95 skipped, 50 deselected
(was 1800; net +11 from the three new test files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bird_interact_agents/eval/regrade.py (1)

210-210: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use paths.results_root() for the regrade run_dir results path

run_dir is currently built as paths.main_checkout_root() / "results" / "cloud" / args.run_id, which bypasses the BIRD_RESULTS_ROOT override that paths.results_root() honors (and it already matches how cloud/driver.py builds local_results_root).

♻️ Proposed change
-    run_dir = paths.main_checkout_root() / "results" / "cloud" / args.run_id
+    run_dir = paths.results_root() / "cloud" / args.run_id
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bird_interact_agents/eval/regrade.py` at line 210, The run_dir is built
using paths.main_checkout_root() which ignores the BIRD_RESULTS_ROOT override;
change the construction in regrade.py so run_dir uses paths.results_root() and
then appends "cloud" and args.run_id (matching cloud/driver.py's
local_results_root pattern) so the BIRD_RESULTS_ROOT override is honored when
locating results for regrade.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/bird_interact_agents/eval/regrade.py`:
- Line 210: The run_dir is built using paths.main_checkout_root() which ignores
the BIRD_RESULTS_ROOT override; change the construction in regrade.py so run_dir
uses paths.results_root() and then appends "cloud" and args.run_id (matching
cloud/driver.py's local_results_root pattern) so the BIRD_RESULTS_ROOT override
is honored when locating results for regrade.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 54b49eec-8e9b-4652-a705-1affb2b35691

📥 Commits

Reviewing files that changed from the base of the PR and between a91f861 and 5f0d374.

📒 Files selected for processing (14)
  • analyses/raw/households_10.md
  • analyses/raw/museum_2.md
  • analyses/raw/museum_4.md
  • scripts/dev1515_convert_livesqlbench.py
  • src/bird_interact_agents/cloud/driver.py
  • src/bird_interact_agents/eval/annotate.py
  • src/bird_interact_agents/eval/grade_in_place.py
  • src/bird_interact_agents/eval/regrade.py
  • src/bird_interact_agents/eval/tolerant_grader.py
  • src/bird_interact_agents/paths.py
  • tests/cloud/test_collation.py
  • tests/cloud/test_fetch_cascading_phase1.py
  • tests/test_tolerant_grader_multi_sql_conn.py
  • tests/test_verdict_label_shared.py
💤 Files with no reviewable changes (2)
  • tests/cloud/test_collation.py
  • scripts/dev1515_convert_livesqlbench.py
✅ Files skipped from review due to trivial changes (3)
  • analyses/raw/museum_2.md
  • analyses/raw/museum_4.md
  • analyses/raw/households_10.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/bird_interact_agents/paths.py
  • src/bird_interact_agents/cloud/driver.py
  • src/bird_interact_agents/eval/tolerant_grader.py

ZmeiGorynych and others added 3 commits June 1, 2026 13:15
… contradiction + pre-submit mutation guard

Three independent fixes surfaced by the 53-task post-DEV-1478 run audit
(opus-agent / sonnet-sim; 15 households + 38 other tasks; 9 cases
tagged agent_miss + 1 alien_6 secondary).

1. N9 cascade tier — `correct_under_case_fold` (case_sensitivity)
   ----------------------------------------------------------------
   lca_17 returned the correct percentages but `'I/II/III/IV'`
   wage-level labels where the gold has `'i/ii/iii/iv'`
   (LOWER(TRIM(JSON_EXTRACT(...))) on the dim). The existing
   trailing-whitespace tier strips at the cell edge, not inside a
   string; column-order tier doesn't touch cell content. The
   `case_sensitivity` value was already reserved in `FailurePrimary`
   but had no comparator wired up.

   * eval/tolerant_grader.py: new `compare_case_fold` mirroring the
     `compare_trailing_whitespace` shape (per-cell `.lower()` on
     strings, non-strings unchanged); `n9_case_fold` appended to
     `_CASCADE_ORDER` and `CascadeVerdict`; `grade_submission` adds
     the N9 block iterating variants + `__original__` after N8.
   * eval/annotation_schema.py: `SubmissionEvaluation` adds
     `correct_under_case_fold: bool = False`.
   * eval/grade_in_place.py: `verdict_label_from_cascade` adds n9 to
     the `valid_interpretation` disjunction; `_auto_failure_class`
     finally wires `case_sensitivity` (was dead enum value).
   * eval/annotate.py: mirror n9 in `_eval_from_cascade`.
   * scripts/dev1515_convert_runs.py + dev1515_cascade_summary.py:
     serialize and label the new tier.
   * Tests: 4 new comparator tests, 1 orchestration "n9 lifts failing
     n8 to pass", 1 verdict-label parity test, 2 schema round-trip
     tests + extends the 2^n cascade-monotonicity loop to 2^9.

2. Multi-variant audit on source contradiction (mandatory)
   --------------------------------------------------------
   `planets_data_2` exposed the rule's enforcement gap: KB 31
   ("Retrograde Orbit") defines retrograde as strict
   `inclination > 90`; the `critical_ambiguity` sql_snippet for
   "spinning backwards" uses inclusive `inclination >= 90`. The
   original auditor noticed the conflict in `reasoning_summary` and
   collapsed to single primary `>= 90` anyway. The agent followed KB
   31 verbatim, returned 0 rows, and got tagged `agent_miss` for a
   contradiction the audit should have surfaced as multi-variant.

   * _shared/audit-gold-sql.contract.md: new top-level "Multi-variant
     audit on source contradiction (MANDATORY)" section with the
     contradiction-shapes table, the mechanics, and a synthetic
     BAD-vs-GOOD example. Picking one and noting the loser in
     reasoning_summary is explicitly forbidden — there is no "labeled
     wins" or "KB-anchored is primary" tie-breaker.
   * audit-gold-sql/SKILL.md: trim the inline multi-variant rule to
     point back at the shared contract; add Step 3.5 "Contradiction
     check (MUST precede any single-variant commit)" listing the
     mini-interact source set to scan.
   * audit-gold-sql-livesqlbench/SKILL.md: parallel Step 4 in the
     livesqlbench procedure, listing the livesqlbench source set
     (KB items, column meanings, schema FKs — no labeled-ambiguity
     blocks).

3. Pre-submit mutation check — prompt guard
   -----------------------------------------
   Across the 10 agent-miss cases, 5 had the same root cause: the
   agent applied a TRIM / LOWER / ROUND / CAST / CASE-canonicalize /
   output-shape choice on the final-assembly step that wasn't named in
   the question, in an `ask_user` reply, or in an encoded KB —
   silently corrupting otherwise-correct rowsets. Households_2,
   alien_5, alien_6 (defensive normalizations); credit_7,
   organ_transplant_4 (ignored a NAMED transformation in an
   `ask_user` reply).

   * claude_sdk_otf_ainteract/prompts.py: new step 6 PRE-SUBMIT
     MUTATION CHECK before SUBMIT. Each mutation MUST be (a) named in
     the user question, (b) named OR authorized in an `ask_user` reply
     this session, or (c) required by an encoded KB; otherwise drop
     it. Explicit clause for the inverse: when a reply DID name a
     transformation, the reply IS the authorization — apply it.
   * claude_sdk_otf/prompts.py: parallel step 5 for the one-shot
     framework (no `ask_user`, so only (a) and (c) are valid
     authorization sources).

Cascade summary post-changes (53-instance run): agent_miss 9 → 8,
no_fail 34 → 35 (planets_data_2 reclassified to no_fail since the
agent's submission now matches the new `kb_strict` variant via
n3_any_audited_variant). N9 row appears at the bottom with (+0)
delta — existing annotations were written before the tier existed; a
fresh re-grade is out of scope here.

Untouched but worth noting: the on-disk
`audited_gold/mini_interact_audited.jsonl` row for `planets_data_2`
was split into two variants (`labeled_snippet` primary `>= 90`,
`kb_strict` alternate `> 90`), and the corresponding
`annotations/mini-interact/planets_data/planets_data_2.task.json` +
`.submission.20260531t1343-claudes-slayer-b39bfc.json` were updated
with `internal_inconsistency.audit_resolution="multi_variant"` + two
`gold_variants` + `failure_classification.primary="no_fail"`. Both
trees are gitignored (per the existing policy), so these data edits
ride into the cloud image via the build-context, not via this commit.

Test suite: 1819 passed, 95 skipped (60s), no new failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new livesqlbench DBs taken from skeleton to fully-enriched
annotations (museum was the only previously-audited DB). 30 new audit
rows + 30 task annotations land in the gitignored audited_gold/ and
annotations/ trees per the cloud gold delivery split; this commit is
the supporting infrastructure.

- `scripts/dev1515_convert_livesqlbench.py`: renamed from the
  museum-specific script; now takes `--db` (required) and optional
  `--run-id`. Task-only mode when run-id is omitted. Adds a third
  rationale branch for management-category audits (verdict=sufficient
  with a deferral-aware rationale instead of the misleading "audit
  found nothing to change" text the old user-shortcut branch would
  emit for audited==original verbatim-copy rows).

- `tests/test_livesqlbench_audited_gold.py`: generalize beyond museum.
  `EXPECTED_INSTANCE_IDS_BY_DB` keys per-DB; coverage / db-tag /
  citation-resolvability tests dispatch by selected_database; the
  edited/unrecoverable test carves out the management-category
  deferral shape (audited==original is permitted when
  clause_kind="management_category", per the shared contract); the
  changes[].justified_by non-empty check is relaxed for management
  deferrals (no source to cite — the deferral is documented in
  why_unjustified).

- `.claude/skills/annotate-task-submission/SKILL.md`: restructured
  around the two-phase split. Task annotation is now explicitly
  self-contained (phase 1: KB + column meanings + schema + audited
  gold). Submission annotation is the optional phase 2 layered on top
  when a cloud run exists. Inputs section partitioned the same way;
  Workflow section split into phase-1 steps (always run) and phase-2
  steps (only with a run).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…16 (round 2)

Bundles 11 valid findings from CodeRabbit + Codex triage on PR #16:

Group 1 — cascade reporting integrity
* cascading_report: _per_row_cascade_bools now includes n9_case_fold;
  deltas loop extended to n9. Fixes silent counts.n9 = 0 on every
  published eval.json (n9 tier was added to _CASCADE_ORDER but the
  aggregator was hardcoded n1..n8).
* regrade: run_dir uses paths.results_root() so BIRD_RESULTS_ROOT is
  honored. Also picks up pre-existing in-flight _build_original_sql_index
  work so regrade can supply original_sol_sql for the N1 check.
* run.py: post-eval, auto-detect a per-row rows/ tree alongside eval.json
  and enrich it with cascading_phase1 when present (no-op for vanilla
  local runs; covers the symmetric local↔cloud convention).

Group 2 — multi-variant gold preservation in conversion scripts
* dev1515_convert_livesqlbench: drop hardcoded LIVESQL_ROOT, use
  paths.livesqlbench_root(); _load_audit_rows returns dict[str, list[dict]];
  _pick_primary helper; _process_one passes the full variant list to
  grade_submission so N3 (\"any audited variant\") sees every alternate.
* dev1515_convert_runs: same shape — dict-of-lists for mini-interact
  audit, _pick_primary, full list to grade_submission.
* dev1515_households_14_multivariant: idempotency guard refuses to
  re-run on already-multivariant rows; filters pre-existing
  source_conflict entries so reruns never duplicate change records.

Group 3 — annotation path mismatch + zero-primary contract test
* annotation_io: _canonical_benchmark() normalizes dash↔underscore for
  the benchmark segment of every annotation path. Cloud workers
  (_cloud_benchmark → mini_interact) and CLI callers (--benchmark
  mini-interact) now land in the same tree; on-disk
  annotations/mini-interact/ contents migrated to annotations/mini_interact/.
* annotate.py docstring updated to canonical form (both accepted).
* test_livesqlbench_audited_gold: extracted
  _check_unique_variant_pairs_and_primary_count helper; added
  zero-primary / two-primary regression tests so an iid with only
  non-primary rows can no longer slip through.

Group 4 — docs polish in skills
* annotate-task-submission/SKILL.md: text-tag the two unlabeled
  fenced blocks (MD040).
* audit-gold-sql/SKILL.md: rewrite the Outputs + Step 8 prose so the
  sidecar write key is (instance_id, variant_id) — overwriting on
  instance_id alone would silently drop alternates landed by the
  DEV-1515 multi-variant audits.

Test suite: 1823 passed, 95 skipped, 0 failed.

INVALID finding (no thread to reply on): CodeRabbit review-summary
nitpick claiming tests/cloud/test_collation.py:31-43 holds a stale
_read_dual_cols helper — that symbol does not exist in HEAD; lines
31-43 are the test_collate_picks_latest_attempt header + setup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/bird_interact_agents/eval/regrade.py (1)

150-154: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

--force-llm-judge is a silent no-op without --instance-ids.

clear_llm_judge_cache only runs when filter_set is truthy, so passing --force-llm-judge alone (regrade all instances) leaves the cache fully populated and the grader reuses every cached judge decision — the opposite of the flag's intent. If clearing the whole cache when unfiltered is undesirable, consider warning the user; otherwise clear all entries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bird_interact_agents/eval/regrade.py` around lines 150 - 154, The current
check only calls clear_llm_judge_cache when filter_set is truthy, making
--force-llm-judge a no-op without --instance-ids; change the logic so that when
force_llm_judge is true you always invoke clear_llm_judge_cache for the
llm_judge_cache.json: call clear_llm_judge_cache(cache_path=run_dir /
"llm_judge_cache.json", instance_ids=filter_set if filter_set else None) (or
pass an explicit empty/None to indicate clearing all), or alternatively emit a
user warning and skip clearing only if you prefer; update the branch around
force_llm_judge/filter_set to unconditionally call clear_llm_judge_cache with
instance_ids determined as described.
scripts/dev1515_cascade_summary.py (1)

27-27: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix DEV-1515 cascade summary to use the canonical annotations/ benchmark directory name (underscore).

scripts/dev1515_cascade_summary.py sets BENCHMARK = "mini-interact" and _gather_files() builds annroot = paths.annotations_root() / BENCHMARK, so it globs annotations/mini-interact/.... But src/bird_interact_agents/eval/annotation_io.py normalizes the benchmark token (_canonical_benchmark() replaces - with _) when building both submission_annotation_path and task_annotation_path, and tests/test_paths_annotations.py::test_annotation_paths_canonicalize_mini_interact asserts the on-disk directory is mini_interact (dash is not used on disk). Writers such as src/bird_interact_agents/eval/regrade.py and src/bird_interact_agents/cloud/post_run_merge.py write via submission_annotation_path(...), so the cascade summary will miss the written files and report 0 pairs.

Set BENCHMARK = "mini_interact" (and update the docstring path) or otherwise construct the directory using the same canonical benchmark token as annotation_io uses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/dev1515_cascade_summary.py` at line 27, The script sets BENCHMARK =
"mini-interact" causing _gather_files() to look under annotations/mini-interact
while on-disk writers (submission_annotation_path, task_annotation_path in
src/bird_interact_agents/eval/annotation_io.py which use _canonical_benchmark()
to replace '-' with '_') write to annotations/mini_interact; fix by changing
BENCHMARK to "mini_interact" (or build the annroot using the same
canonicalization as annotation_io) and update the docstring path to
annotations/mini_interact so _gather_files() and the rest of the pipeline
reference the same directory name.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/dev1515_cascade_summary.py`:
- Line 3: The docstring in scripts/dev1515_cascade_summary.py incorrectly
documents the directory as annotations/mini-interact/*/...; update that
docstring to use the canonical underscore form
annotations/mini_interact/*/<inst>.submission.<run>.json (matching the BENCHMARK
fix) and ensure any other docstring examples in the same file use the underscore
form so the documented layout matches the actual directory-resolution logic.

---

Outside diff comments:
In `@scripts/dev1515_cascade_summary.py`:
- Line 27: The script sets BENCHMARK = "mini-interact" causing _gather_files()
to look under annotations/mini-interact while on-disk writers
(submission_annotation_path, task_annotation_path in
src/bird_interact_agents/eval/annotation_io.py which use _canonical_benchmark()
to replace '-' with '_') write to annotations/mini_interact; fix by changing
BENCHMARK to "mini_interact" (or build the annroot using the same
canonicalization as annotation_io) and update the docstring path to
annotations/mini_interact so _gather_files() and the rest of the pipeline
reference the same directory name.

In `@src/bird_interact_agents/eval/regrade.py`:
- Around line 150-154: The current check only calls clear_llm_judge_cache when
filter_set is truthy, making --force-llm-judge a no-op without --instance-ids;
change the logic so that when force_llm_judge is true you always invoke
clear_llm_judge_cache for the llm_judge_cache.json: call
clear_llm_judge_cache(cache_path=run_dir / "llm_judge_cache.json",
instance_ids=filter_set if filter_set else None) (or pass an explicit empty/None
to indicate clearing all), or alternatively emit a user warning and skip
clearing only if you prefer; update the branch around force_llm_judge/filter_set
to unconditionally call clear_llm_judge_cache with instance_ids determined as
described.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4512e354-c71c-46fd-8f2f-233a199777cc

📥 Commits

Reviewing files that changed from the base of the PR and between 5f0d374 and 862e959.

📒 Files selected for processing (28)
  • .claude/skills/_shared/audit-gold-sql.contract.md
  • .claude/skills/annotate-task-submission/SKILL.md
  • .claude/skills/audit-gold-sql-livesqlbench/SKILL.md
  • .claude/skills/audit-gold-sql/SKILL.md
  • scripts/dev1515_cascade_summary.py
  • scripts/dev1515_convert_livesqlbench.py
  • scripts/dev1515_convert_runs.py
  • scripts/dev1515_households_14_multivariant.py
  • src/bird_interact_agents/agents/claude_sdk_otf/prompts.py
  • src/bird_interact_agents/agents/claude_sdk_otf_ainteract/prompts.py
  • src/bird_interact_agents/eval/annotate.py
  • src/bird_interact_agents/eval/annotation_io.py
  • src/bird_interact_agents/eval/annotation_schema.py
  • src/bird_interact_agents/eval/cascading_report.py
  • src/bird_interact_agents/eval/grade_in_place.py
  • src/bird_interact_agents/eval/regrade.py
  • src/bird_interact_agents/eval/tolerant_grader.py
  • src/bird_interact_agents/run.py
  • tests/cloud/test_fetch_annotation_merge.py
  • tests/test_cascading_report.py
  • tests/test_eval_annotate_cli.py
  • tests/test_eval_annotation_schema.py
  • tests/test_livesqlbench_audited_gold.py
  • tests/test_paths_annotations.py
  • tests/test_regrade_cli.py
  • tests/test_tolerant_grader_comparators.py
  • tests/test_tolerant_grader_orchestration.py
  • tests/test_verdict_label_shared.py
✅ Files skipped from review due to trivial changes (1)
  • .claude/skills/annotate-task-submission/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (12)
  • tests/test_cascading_report.py
  • tests/test_verdict_label_shared.py
  • tests/test_eval_annotate_cli.py
  • src/bird_interact_agents/eval/annotation_io.py
  • tests/cloud/test_fetch_annotation_merge.py
  • tests/test_eval_annotation_schema.py
  • src/bird_interact_agents/eval/annotation_schema.py
  • src/bird_interact_agents/eval/cascading_report.py
  • scripts/dev1515_convert_runs.py
  • scripts/dev1515_households_14_multivariant.py
  • src/bird_interact_agents/run.py
  • src/bird_interact_agents/eval/annotate.py

Comment thread scripts/dev1515_cascade_summary.py Outdated
The cascade adequately classifies the 42/53 instances that pass at some
tier — each tier (N3 strict, N6 numeric_epsilon, N9 case_fold, ...)
names the tolerance that saved them. For the 11 strict misses that
clear no cascade tier, the cascade only tells us "agent missed"; we
want structured signals at grading time so downstream tooling can
break down failure modes (wrong join path, missing predicate, wrong
projection, empty result, SQL error, ...) without re-running queries
or eyeballing trajectories.

Schema (annotation_schema.py):
- New MissDiagnostics Pydantic model captures rowset shape, column
  shape, sqlglot-derived SQL signals (tables_referenced,
  has_group_by, has_aggregate, join_count, where_conjunct_count,
  has_having, has_limit), execution status, and a multi-flag
  miss_patterns list with 14 independent flag categories. Comparison
  reference is the BEST-OVERLAP audited variant (multiset cardinality
  vs agent rows; tie-break primary > alphabetical variant_id) — not
  the primary by default, so multi-variant golds get diagnosed
  against the closest reading rather than misclassified as
  disjoint_rowset against an unrelated primary.
- Interactive-only signal (user_sim_n_asks) stays Optional[int] =
  None on one-shot benchmarks (livesqlbench); never_asked_user flag
  fires only when the benchmark is interactive AND the agent didn't
  ask the user-sim.
- SQL-derived fields are Optional[T] = None when sqlglot fails to
  parse, gated by *_sql_parse_ok booleans + truncated error excerpts.
  Silent False/0 defaults would produce spurious wrong_table_set /
  aggregation_shape_mismatch flags.

Grader (tolerant_grader.py):
- Wrapped the initial executor(submitted_sql, ...) call in try/except
  so a syntax/runtime error becomes pred_rows=[] + error excerpt
  + sql_execution_error flag, not a crash that aborts grading.
- Wrapped per-variant execution defensively too (variant-side SQL
  that fails just produces empty rowset; diagnostics catch the
  downstream sqlglot parse failure via sql_parse_error flag).
- New grade_submission kwarg user_sim_n_asks: Optional[int].
- _compute_miss_diagnostics with bag-aware row comparison
  (_bag_relation matches the grader's existing _set_equal multiset
  semantics) + sqlglot AST walkers that excludes CTE / derived-table
  aliases from base-table extraction.
- Defensive assert len(sol_sql) == 1 — confirmed empirically that
  0/915 SELECT-task gold rows are multi-statement; multi-statement is
  M-task territory (explicitly out of scope).

Flag rules are INDEPENDENT (every applicable rule appends to
miss_patterns; the list is sorted alphabetically before persist for
stable JSON diffs). An instance can carry multiple flags
simultaneously — e.g. wrong_table_set AND column_projection_mismatch
AND empty_agent_result.

Persistence (grade_in_place.py + annotate.py):
- _auto_failure_class strict-miss branch flipped "other" -> "agent_miss"
  (agent_at_fault=True, remediation_target="agent"). The "other"
  bucket disappears entirely; rich detail lives on
  ev.miss_diagnostics.
- _build_submission_annotation + _eval_from_cascade both plumb
  cascade.miss_diagnostics -> ev.miss_diagnostics so both the cloud
  worker path (grade_in_place) and the regrade / skeleton path
  (annotate) emit the same shape.
- FailureClassification.details is a free-form one-liner derived from
  miss_patterns; structured downstream consumers MUST go through
  miss_diagnostics.miss_patterns, not parse the prose.

Workflow:
- TDD-style spec with Codex pre-review of the design plan
  (11 findings folded), test-suite-first (39 new tests, all failed
  for the right "feature missing" reason before implementation),
  then Codex pre-review of the tests (11 more findings folded —
  most importantly: best-overlap test asserts exact == "alt"
  not in {primary, alt}; tie-break tests use non-zero overlap;
  multiset superset detection; mirror alias-exclusion on best-variant
  side; negative assertions on per-flag tests for mutual-exclusion
  contracts).
- Re-graded all 4 fetched runs (3 mini-interact + livesqlbench
  museum); the 12 cascade-fail instances all carry populated
  miss_diagnostics with >= 1 flag.

scripts/dev1515_strict_miss_diagnostics.py walks the latest
annotation per instance and prints per-instance table + per-flag
tally + instance x flag matrix. column_projection_mismatch fires on
every instance (slayer's namespaced agent column names vs gold's
bare names — a real signal); disjoint_rowset dominates (9/12)
suggesting wrong predicate / wrong join path; predicate_count_mismatch
fires on 7/12 suggesting missing or extra WHERE conjuncts.

Full non-integration suite: 1862 passed, 95 skipped, 50 deselected
(was 1811; +51 from the new test file).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bird_interact_agents/eval/tolerant_grader.py`:
- Around line 970-988: The current implementation uses expr.find(sg_expr.Where)
which can match a nested WHERE (e.g., in a subquery/CTE); update
_where_conjunct_count to first locate the outermost SELECT node (sg_expr.Select)
starting at the provided expr (do not search nested selects) and then read that
Select's WHERE child (the Select's where/args["where"] or equivalent) instead of
calling expr.find(sg_expr.Where); then count conjuncts from that outer Select's
Where as before.
- Around line 633-643: The current except block around _multi_sql_execute
coerces failures into empty rowsets (v_rows, v_cols = [], []), which can produce
false positives; instead, on exception capture the failure and record a distinct
unevaluable marker (e.g., v_rows = v_cols = None or append a tuple like (v,
None, None)) to variant_results so downstream comparison logic can skip or mark
unevaluable variants (update any logic that reads variant_results to treat None
as "skip/unevaluable" rather than an empty result). Ensure the exception still
gets logged (include the exception info) and do not convert failures into empty
lists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 17eeaa32-8241-4734-891e-ccb8d7800172

📥 Commits

Reviewing files that changed from the base of the PR and between 862e959 and 5a15380.

📒 Files selected for processing (6)
  • scripts/dev1515_strict_miss_diagnostics.py
  • src/bird_interact_agents/eval/annotate.py
  • src/bird_interact_agents/eval/annotation_schema.py
  • src/bird_interact_agents/eval/grade_in_place.py
  • src/bird_interact_agents/eval/tolerant_grader.py
  • tests/test_miss_diagnostics.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/bird_interact_agents/eval/grade_in_place.py
  • src/bird_interact_agents/eval/annotate.py

Comment thread src/bird_interact_agents/eval/tolerant_grader.py
Comment thread src/bird_interact_agents/eval/tolerant_grader.py
ZmeiGorynych and others added 11 commits June 2, 2026 11:01
…_user plumbing

Column-shape flag refinement (annotation_schema.py + tolerant_grader.py):
- Drop the noisy ``column_projection_mismatch`` flag — fired on every
  strict miss because slayer-namespaced column names
  (e.g. ``households.housenum``) consistently differ from gold's bare
  names (``housenum``). Neither the cascade's ``_set_equal`` nor
  BIRD-Interact's ``ex_base`` uses column names in equality (both
  compare tuples of values, not names), so a name-only divergence
  never causes cascade fail. The flag was muddying the failure-mode
  tally without identifying real cascade-fail causes.
- Add ``column_count_mismatch`` — fires when agent and gold project
  different column COUNTS. This IS load-bearing: cell tuples have
  different arity, so bag equality cannot hold. Matches the cascade
  semantics on both sides.
- Add ``column_order_mismatch`` — fires when counts match AND the
  column-name lists are equal as SETS after normalisation (lowercase
  + strip longest dot-prefix, so ``households.housenum -> housenum``)
  but differ as LISTS. Surfaces the "near miss" where the agent
  picked the right columns in wrong order and N8 column-order
  tolerance would have rescued the cascade if slayer's namespacing
  hadn't tripped its column-name set check.
- Pure name-only divergence with matching counts and non-matching
  normalised sets is intentionally unflagged — stylistic noise.
- The column-shape FIELDS (column_count_match,
  column_name_match_case_insensitive, column_order_match,
  agent_columns, best_variant_columns) stay populated as
  informational signals on MissDiagnostics; downstream tooling can
  inspect them without the flag muddying the tally.

never_asked_user plumbing (grade_in_place.py + regrade.py):
- grade_and_write resolves the benchmark interactivity once and
  forwards ``user_sim_n_asks`` to grade_submission so the
  ``never_asked_user`` diagnostic actually fires on interactive
  runs. Interactive: prefer ``user_sim_interaction.n_asks``, fall
  back to ``n_ask_user_calls``, then 0. One-shot: ``None`` so the
  flag stays out of miss_patterns (the signal doesn't apply).
- regrade.py::_grader mirrors the same logic, computing n_asks from
  the attempt's trajectory via the existing
  ``_user_sim_interaction_from_trajectory`` helper.
- Codex flagged this exact gap during the just-completed PR review
  (minor finding on grade_in_place.py:233-243) — the
  ``user_sim_n_asks`` parameter existed but no production call site
  forwarded it, so the diagnostic was systematically absent from
  every annotation.

Summary script (scripts/dev1515_strict_miss_diagnostics.py):
- Dedup key changed from ``instance_id`` to ``(benchmark,
  instance_id)``. Same-iid rows exist in BOTH benchmarks
  (``credit_4``, ``credit_7`` live in mini-interact AND livesqlbench);
  the previous latest-wins logic silently hid the mini-interact rows
  when livesqlbench had the same iid with a later run_id, undercounting
  the never_asked_user tally by 2.
- Same-iid rows from both benchmarks now disambiguated with
  ``iid@livesqlbench`` suffix in the per-instance table.

After re-grading all 5 runs (3 mini-interact + 2 livesqlbench, 20
cascade-fail instances total):

* Mini-interact: 11/11 strict misses fire ``never_asked_user`` —
  the agent never queried the user-sim on any cascade-failing
  instance, even on ambiguous-metadata tasks. Behavioural signal
  worth investigating in the prompt.
* Livesqlbench: 0/9 fire ``never_asked_user`` (one-shot, correctly
  excluded via the None sentinel).
* ``column_count_mismatch`` fires on the 4 arity-mismatch instances
  (alien_10, households_2, museum_5, credit_7).
* ``column_order_mismatch`` fires on 0 — no agent picked the right
  columns in wrong order. Useful negative finding.

Test suite: 41 miss_diagnostics tests pass (39 + 2 new for the
column_count / column_order split); full non-integration suite at
1861 passing (3 pre-existing failures in
tests/test_livesqlbench_audited_gold.py about museum_2/4/9 primary
flags — unrelated to this work).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/process-reviews triage (CodeRabbit + Codex carryover) — 7 valid items:

Group 1 (tolerant_grader.py correctness):
- Don't coerce broken audited variants to ([], []); skip with continue
  + logger.exception so the agent's empty rowset can't accidentally
  pass N2/N3 against an unevaluable gold (CR r3336709435).
- _where_conjunct_count walks to the OUTER Select and reads .args['where']
  directly — expr.find(Where) descends into subqueries / CTEs and picks
  whichever WHERE node sqlglot iterates first (CR r3336709443).
- Guard N1 with an explicit empty-original-sql check so _set_equal([], [])
  doesn't falsely mark N1 as a strict pass when no original gold exists.

Group 2 (regrade.py ops):
- --force-llm-judge with no --instance-ids now clears the WHOLE judge
  cache instead of being a silent no-op; the per-instance scope still
  applies when --instance-ids is set.
- regrade_rows scratch dir is reset before each pass (full wipe for an
  unfiltered run; per-instance subdirs for a filtered run) so stale
  rows can't leak into eval_regraded.json.

Group 3 (path canonicalization + CLI):
- scripts/dev1515_cascade_summary.py: BENCHMARK token 'mini-interact' →
  'mini_interact' so it reads the canonical underscore tree
  (CR r3335026971).
- annotation_io._annotations_root delegates to paths.annotations_root()
  when repo_root is None, honouring the BIRD_ANNOTATIONS_ROOT override.
- eval.annotate.main(): drop the unused --run-id / --submission-mode
  args; submission-skeleton writing lives in dev1515_convert_runs.py.

DEV-1519 (separate bug — not flagged by CodeRabbit/Codex):
- claude_sdk_otf_ainteract.agent.run_task wrote accum.model_dump() to
  usage but never copied the per-task asks_used counter. Grader then
  read n_ask_user_calls from usage as 0, falsely flagging
  never_asked_user on every interactive miss. Both error and success
  result rows now emit {"n_ask_user_calls": ctx_dict.get("asks_used", 0)}.

Tests:
- tests/test_miss_diagnostics.py: replace the broken-best-variant test
  with two — mixed-variants (broken + working) and all-variants-failing —
  pinning the new contract.
- tests/test_claude_sdk_otf_ainteract_agent.py: extend _make_fake_client
  + _stub_env with prefill_asks and add three regression tests
  (zero / nonzero / exception-path).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t override

Codex follow-up after the round-2 push:

Item 1 (tolerant_grader.py — major):
- When original_sol_sql is empty AND there are no audited variants,
  every cascade tier from N4 onward was falling back to comparing the
  agent rowset against the same empty orig_rows[]. compare_tie_order /
  compare_numeric_epsilon / compare_trailing_whitespace /
  compare_column_order / compare_case_fold all return True on ([], [])
  via set / bag equality — so a missing-gold + empty-agent pair would
  silently cascade-pass at N4 (and propagate as valid_interpretation
  even though nothing was actually compared).
- Gate the __original__ fallback on `original_sol_sql` truthiness for
  N4 and consolidate N6-N9's comparator targets into one list that
  applies the same gate, so the guard can't be missed at any tier.
- Regression test in test_tolerant_grader_orchestration.py pins:
  empty original + empty audited variants + empty agent rowset →
  every N-tier stays False.

Item 2 (ray_app.py — minor):
- _load_task_annotation_or_implicit was passing
  repo_root=paths.main_checkout_root() into task_annotation_path,
  which after round 2's annotation_io._annotations_root change
  deliberately bypasses BIRD_ANNOTATIONS_ROOT (the env override only
  fires when repo_root is None). Drop the explicit kwarg so the cloud
  worker honours the same override mounted by tests / forks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Codex caught that the local runner never invoked grade_and_write, so
the cascading_phase1 block was silently absent from local eval.json —
only cloud runs (via cloud.ray_app._grade_one_submission) populated it.
The aggregator at the bottom of run_evaluation was already in place,
but its precondition (per-instance submission_annotation.json files)
was never satisfied locally.

Changes
- Move the three per-task helpers (load_task_annotation_or_implicit,
  load_audited_gold_rows_for, grade_one_submission) from cloud.ray_app
  into eval.grade_in_place — the canonical home now matches the
  docstring's "shared inline grader" claim. ray_app keeps a thin alias
  on `_grade_one_submission` and re-imports the two loaders for the
  existing call sites (test_inline_grader, test_audited_gold_overlay_wiring,
  regrade.py — the last one updated to import from the canonical
  location).
- run.py: define `_grade_local_row` and call it after `_persist` in
  `_run_with_sem`. Best-effort — a grader raise on one instance logs
  and continues so the loop can't be killed by a single bad task.
  Loop sees `b.name` (canonical underscore form) and resolves the
  per-task sqlite via paths.benchmark_data_root, mirroring the cloud
  helper's argument shape.

Tests
- tests/test_run_local_inline_grader.py (new):
  - test_local_run_invokes_inline_grader_per_task — stub runner +
    stub grader; assert the grader is called once per task, the
    benchmark token is canonicalised, per-row submission_annotation.json
    files land, and eval.json carries cascading_phase1 with the
    expected counts/rates.
  - test_local_run_grader_failure_does_not_kill_loop — grader raises
    on alien_1; alien_2 still runs to completion and total_tasks==2.
- tests/cloud/test_inline_grader.py: patch the canonical
  grade_in_place.grade_and_write (in addition to the back-compat
  ray_app alias) so the existing wiring tests still pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nator

Two Codex findings on the round-4 grader plumbing:

Group 1 (Codex major — grade_in_place.py, regrade.py):
  Both call sites did ``list(value or [])`` to coerce the source
  row's ``sol_sql``/``original_sol_sql`` into a list. When the value
  comes through as a bare string (which ``run.py::_persist`` already
  handles via ``isinstance(sol, str)`` — so the shape is established
  upstream), ``list("SELECT 1")`` returns
  ``["S","E","L","E","C","T"," ","1"]``. The grader then runs each
  character through sqlite as a one-character statement, raises
  ``sqlite3.OperationalError`` per character, and the row's N1 silently
  drops to False under the broad-except.

  Fix: ``normalize_sol_sql(value)`` helper that returns ``[]`` for
  None/falsy, ``[value]`` for a string, and ``list(value)`` for a
  list — used at both call sites.

Group 2 (Codex major — run.py:1007-1012):
  ``_grade_local_row`` silently returned without writing any
  ``submission_annotation.json`` when ``submitted_sql`` was missing
  OR the inline grader raised. ``aggregate_cascading_phase1`` then
  walked ``rows_dir`` and only counted the per-task dirs that did
  have annotations — so ``n_dual_eval_tasks`` dropped below
  ``len(tasks)`` and ``cascading_phase1.rates`` were INFLATED by
  silently excluding the never-graded rows.

  Fix: ``write_failed_submission_annotation()`` helper writes a
  fail-everything ``SubmissionAnnotation`` (verdict=invalid,
  failure_classification.primary="other", all N-tiers 0) for both
  bypass paths. The aggregator then counts those rows in the
  denominator and honest rates fall out.

Tests:
- tests/test_normalize_sol_sql.py (new) — 7 tests pinning the shape
  contract: None/empty/string/list/tuple, plus a long-string
  regression that asserts no char-split semantics.
- tests/test_run_local_inline_grader.py:
  - test_local_run_grader_failure_writes_fail_everything_annotation
    (renamed) — extended to assert the fallback annotation lands,
    n_dual_eval_tasks==2, all-tiers count 0, alien_1's verdict shape.
  - test_local_run_no_submitted_sql_writes_fail_everything_annotation
    (new) — analog for the no-submit pre-submit-crash path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ocal)

Codex caught the symmetric cloud bug after round 5 landed the local
fix: ``_run_one_in_actor``'s inline-grader except branch only printed
a traceback and uploaded nothing. Two downstream consequences in
``driver._emit_cascading_phase1_on_fetch``:

  * All-tasks-failed run: ``has_per_row_anns`` is False, the whole
    ``cascading_phase1`` block is silently dropped from ``eval.json``.
  * Some-tasks-failed run: the aggregator's strict
    ``_per_row_cascade_bools`` raises ``FileNotFoundError`` on the
    missing row, which the driver surfaces as
    ``cascading_phase1_error`` and the block STILL doesn't land. So
    one broken task wipes the whole run's cascade metrics.

Fix: on grader bypass (unbound ``data_dir``, no ``submitted_sql``,
broken gold, ``_grade_one_submission`` exception) the except branch now
writes a fail-everything annotation via ``write_failed_submission_annotation``
into the same temp ``annotation_dir`` and uploads it via
``_gcs.write_submission_annotation`` — mirrors ``run._grade_local_row``
shape and the cloud driver-side aggregator now sees a per-row file for
EVERY task. Fallback-of-the-fallback prints a second traceback and
moves on (so a bug in the failed-annotation path can't crash the actor).

Tests:
- tests/cloud/test_inline_grader.py:
  - test_cloud_grader_failure_uploads_fail_everything_annotation —
    patches ``ray_app._grade_one_submission`` to raise, runs
    ``_LocalActor.run_one``, asserts the in-memory fake GCS bucket
    received a ``runs/<run_id>/rows/<iid>/submission_annotation.json``
    blob with verdict=invalid, primary=other, and the exception
    message in details.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…sql indexing

Two more Codex findings on the round-4/6 grader plumbing:

Item 1 (Codex major — cloud/ray_app.py:588-590):
  The round-6 cloud fallback only fires when the grader RAISES.
  When ``row.get("submitted_sql") is None`` the cloud worker still
  called ``_grade_one_submission(submitted_sql=str(None or "") == "")``,
  passing the empty SQL through to SQLite. Some sqlite3 versions
  return an empty rowset (no exception), which then matches an
  also-empty gold rowset via ``_set_equal([], []) == True`` and
  falsely passes N1/N2/N3. The local runner had the explicit guard
  in ``_grade_local_row``; the cloud worker did not.

  Fix: hoist the same short-circuit. Read ``row.get("submitted_sql")``
  + ``row.get("database") or task_data.get("selected_database")`` BEFORE
  the grader call; raise a ``RuntimeError`` if either is missing so
  the existing fail-everything except branch fires the fallback
  upload path. Cloud + local now agree on never-submitted rows.

Item 2 (Codex major — eval/regrade.py:103-106, :134-137):
  ``_build_original_sql_index`` filtered on
  ``isinstance(sol, list) and sol`` at BOTH branches (mini_interact
  data file + livesqlbench gated sidecar). That silently dropped any
  source row whose ``sol_sql`` was the bare-string shape that round-5's
  ``normalize_sol_sql`` was explicitly designed to support. After the
  index returns empty for such rows, the per-task lookup falls through
  to ``original_sql_by_inst.get(iid) → None → []`` and N1 can never
  pass, undercounting ``eval_regraded.json``.

  Fix: pass ``r.get("sol_sql")`` through ``normalize_sol_sql`` at both
  branches; skip only when the normalised value is empty.

Tests:
- tests/test_regrade_cli.py:
  - test_build_original_sql_index_accepts_string_sol_sql — mixed
    string/list/missing rows; assert string wraps to ``[s]``, list
    passes through, missing-sol_sql is skipped.
  - test_build_original_sql_index_does_not_char_split_string —
    belt-and-braces against the round-5 char-split regression.
- tests/cloud/test_inline_grader.py:
  - test_cloud_no_submitted_sql_short_circuits_before_real_grader —
    stubs ``run_one_task`` to return ``submitted_sql=None``, spies
    on ``_grade_one_submission`` and asserts it is never called,
    and asserts the fail-everything annotation still lands in GCS
    with primary=``other`` + the short-circuit details.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three Codex findings, all on the round 4-7 grader plumbing:

Item 1 (Codex major — run.py:1046-1058):
  Local ``_grade_local_row`` was computing ``per_task_db`` from
  ``paths.benchmark_data_root(_benchmark_canonical)`` — the GLOBAL
  benchmark root — instead of the caller-provided ``data_dir`` the
  agent actually executed against. An alternate checkout, a temp
  fixture, or a ``BIRD_DB_PATH`` override would silently route the
  grader at a different sqlite than the agent's, so a correct
  submission could be marked failing for purely path-routing reasons.

  Fix: root ``per_task_db`` at ``Path(data_dir)`` (already in scope on
  the closure). Mirrors the cloud worker's ``cfg["data_dir"]`` pattern.
  Regression test extended to assert ``db_path`` is rooted under the
  test's ``data_dir`` arg.

Item 2 (Codex major — cloud/ray_app.py:570-631):
  ``_run_one_in_actor`` uploaded the attempt row BEFORE the inline
  grader + annotation upload. ``driver.wait_until_done`` returns
  ``done`` when ``len(attempts) >= total`` (attempt rows in GCS).
  Non-detached ``submit`` immediately calls ``fetch``; if the row
  landed first, the in-flight annotation upload could race the
  download, and the cascade aggregator would either drop the
  ``cascading_phase1`` block (none present) or surface
  ``cascading_phase1_error`` (some present).

  Fix: swap the order. Inline-grade + annotation upload runs FIRST,
  then ``_gcs.write_row``. The row blob becomes the canonical
  "task fully done, including annotation" marker. New ordering test
  records the call sequence and asserts annotation < row.

Item 3 (Codex major — cloud/post_run_merge.py:487-497):
  The merge dest is keyed only by ``(benchmark, db, instance_id,
  run_id)`` and skipped when already present. Resubmit reuses the
  same ``run_id`` and bumps ``attempt``; a partial earlier fetch
  could pin attempt-1's annotation forever while ``eval.json`` /
  ``results.db`` reflect attempt-2.

  Fix: parse ``submission.trajectory_path`` (``rows/<iid>/attempt-N.json``)
  on src and dest; overwrite ONLY when the new attempt number is
  strictly greater. Unknown / equal / older → keep existing (safety
  floor preserved). New report counters
  ``overwritten_newer_attempt`` + ``overwritten_paths``.
  Prerequisite: ``grade_one_submission`` now takes an ``attempt``
  kwarg (default 1) and uses it in ``trajectory_path``; cloud worker
  threads the real attempt through. Pre-fix the path was hardcoded
  to ``attempt-1.json`` and the attempt comparison would always
  short-circuit to ``equal``. 3 new merge tests + 1 helper test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ostics

Two Codex findings on the round-8 grader plumbing:

Item 1 (Codex major — run.py:984-985 + results_db.py):
  ``run.py::_persist`` had been passing
  ``phase1_observation_audited`` / ``phase1_observation_original`` into
  ``TaskResultRow`` for every per-task insert, but the model + DDL
  silently lost those two fields somewhere along the DEV-1515 dual-eval
  cleanup. Pydantic's default ``extra="ignore"`` ate them without
  warning, so ``results.db`` quietly stopped storing the observation
  strings even though ~6 agent flavors (claude_sdk, pydantic_ai,
  pydantic_ai_otf_encode, claude_sdk_otf_ainteract, plus
  ``agents/_submit.py``) continued to emit them on every row.

  Fix:
  * Add ``phase1_observation_audited: str | None = None`` +
    ``phase1_observation_original: str | None = None`` to
    ``TaskResultRow``.
  * Append both to ``_DIAGNOSTIC_COLUMNS`` so ``open_db`` ALTERs
    pre-existing on-disk DBs to add the columns (mirroring how
    ``phase1_observation`` etc. are upgraded).
  * Append both to the fresh DDL string for documentation parity.
  * Extend the ``INSERT OR REPLACE`` statement in
    ``insert_task_result`` to bind both fields.
  * Two regression tests pin (a) a model-INSERT-SELECT round trip in
    a fresh DB and (b) the column-upgrade path on a pre-existing DB
    that lacks both columns at open time.

Item 2 (Codex minor — tolerant_grader.py:1081-1089):
  ``_compute_miss_diagnostics`` asserted ``len(sqls) <= 1`` on every
  variant's ``audited_sol_sql`` and on ``original_sol_sql``. But the
  grader's executor (``_multi_sql_execute``) explicitly supports
  multi-statement gold (DDL prelude + final SELECT). When a
  multi-statement gold produced a strict miss, the assert raised,
  the round-5 outer except clause caught it, and the row fell back
  to the round-6/7 fail-everything fallback — losing all structured
  miss_patterns + Tier 2 informational detail for that row.

  Fix: drop the asserts and use the LAST statement of the gold's
  ``audited_sol_sql`` for sqlglot parsing. The setup statements
  don't constrain miss patterns; the SELECT under diagnosis is
  what determines tables / aggregation shape / predicate count.
  Replaced the two pre-fix "must raise" tests with two new ones
  pinning the post-fix contract:
    * test_multi_statement_audited_gold_uses_last_for_sql_signals
      — best_variant_tables_referenced parses from the SELECT, not
      the DDL.
    * test_multi_statement_original_gold_does_not_crash_diagnostics
      — original-gold side handles multi-stmt gracefully too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ustness, cloud parity

Four Codex findings on the round 4-9 plumbing:

Group 1 (Codex major — run.py:1001-1002):
  ``run_evaluation`` reused ``output_dir/rows`` with
  ``mkdir(exist_ok=True)`` — never wiped. The aggregator at
  ``cascading_report.aggregate_cascading_phase1`` walks every
  subdirectory under ``rows_dir``, so rerunning with the same
  output path but a different ``--limit`` / ``--instance-id`` subset
  carried forward stale annotations from prior passes, inflating
  ``cascading_phase1.n_dual_eval_tasks`` and rewriting
  ``phase1_count`` / ``phase1_rate`` from the union of old + new.

  Fix: wipe before the run. ``filter_ids is None`` → ``shutil.rmtree``
  the whole rows dir; filtered run → reset only the per-instance
  subdirs in the current task set so unrelated prior-run annotations
  survive and still contribute. Two new tests: stale-iid is wiped on
  full reruns; filtered rerun preserves unrelated rows.

Group 2 (Codex major — harness.py:472-493 + cloud/_audited_gold_check.py:77-94):
  Both single-file audited gold indexes did ``out[iid] = row`` with
  latest-wins semantics. DEV-1515 multi-variant audits ship N rows
  per ``instance_id`` (one ``primary=True`` + non-primary alternates);
  on a file where an alternate appears AFTER the primary, the
  alternate's ``audited_sol_sql`` / ``audit_status`` would overwrite
  the primary at index time. Interaction-time overlay then applied
  the alt's reading; cloud audit-gold guard checked against the alt's
  status.

  Fix: prefer ``primary=True`` over alternates. Once a primary lands
  in the index, never overwrite. A non-primary recorded first gets
  upgraded by the primary later. Three new tests pinning the
  primary-first contract on both index helpers in both file orders.

Group 3 (Codex major — tolerant_grader.py:624-626):
  Original gold execution was unguarded. The agent SQL and audited
  variant SQL got try/except wrappers in round 3 / 1.1, but
  ``_multi_sql_execute(original_sol_sql, ...)`` was bare. An invalid
  original gold (broken upstream SQL, schema drift, …) raised the
  whole grader; the local/cloud fail-everything fallback then wrote
  a generic "grader raised" annotation, losing any valid audited-
  variant passes (N2/N3) the row would have earned.

  Fix: wrap the original-exec call, log on raise, set
  ``orig_rows = []`` + ``original_sql_executed_ok = False``. Then
  extend the round-3 missing-gold guards on N1 (line 682) and the
  ``__original__`` fallback gates for N4 (line 724) + N6-N9 (line
  790) to AND-in ``original_sql_executed_ok`` so the false-pass
  shape ``_set_equal([], [])`` / ``compare_tie_order([], [])`` is
  also blocked on exec-failure. Two new tests: valid audited
  variant still passes through a broken original; empty agent +
  broken original + no variants stays False at every tier.

Group 4 (Codex minor — cloud/collation.py:69-94):
  Round 9 restored ``phase1_observation_audited`` /
  ``phase1_observation_original`` on ``TaskResultRow`` + DDL; the
  local ``run.py::_persist`` path was already plumbing them. But
  cloud collation's ``_row_to_task_result_row`` never received the
  update, so cloud-fetched ``results.db`` files quietly lost the
  observation columns while local runs retained them.

  Fix: append both kwargs to the ``TaskResultRow(...)`` build in
  collation.py to mirror local. New test: synthesize a per-task
  row with both observation fields set, run collate, assert both
  come back populated when reading ``results.db``.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…y shape

Two Codex findings on the round-8 / round-10 plumbing:

Item 1 (Codex major — regrade.py:193):
  The CLI hardcoded ``attempt = sub / "attempt-1.json"``. But cloud
  collation has long treated the highest ``attempt-N.json`` as
  canonical and round-8's post-fetch merge now compares attempt
  numbers — so the regrade CLI was the odd one out: after a resubmit
  it would either silently SKIP instances that had only attempt-2
  (because attempt-1 didn't exist) or, when both attempts coexisted,
  OVERWRITE the submission annotation + ``eval_regraded.json`` with
  results computed against STALE attempt-1 SQL.

  Fix: ``_latest_attempt_file(sub)`` scans for ``attempt-N.json``,
  parses the digit, and returns the max. Mirrors the regex used in
  ``cloud/post_run_merge._attempt_from_trajectory_path``. Two new
  tests: prefer attempt-3 over attempt-1 when both exist; pick up
  attempt-2 as the sole file.

Item 2 (Codex major — annotate.py:120 + regrade.py:281 + annotate.py:241):
  ``_user_sim_interaction_from_trajectory`` iterated ``traj`` and
  called ``item.get("role")``. Three call sites wrapped the source
  trajectory with ``list(attempt_data.get("trajectory") or [])`` —
  fine when ``trajectory`` is a list of turn-step dicts, but
  ``pydantic_ai_otf_encode/agent.py:1537`` (and the recursive flavor's
  ``agent.py:892``) emit ``trajectory = {"final_output_excerpt":
  ..., "agents": [...]}`` (a DICT). ``list(dict)`` returns the dict's
  KEYS as strings, then ``str.get("role")`` raises AttributeError —
  the grader-fallback / skeleton-build paths crashed after the cascade
  computed cleanly.

  Fix:
  * Drop the function's strict ``traj: list[dict]`` annotation and
    short-circuit to ``UserSimInteraction()`` when the value isn't
    a list. Skip individual non-dict items in the iteration too
    (handles ``list(dict)`` -> list of strings as the safety floor).
    Guard the ``traj[i-1]`` previous-step lookup against non-dicts
    on the same principle.
  * At the three call sites, drop the ``list(...)`` wrap and pass
    the raw value (or ``[]`` when None); the helper now handles
    every shape defensively.

  New tests file ``tests/test_user_sim_interaction_trajectory.py`` —
  7 tests pinning: canonical list-of-dicts (load-bearing happy path),
  dict-shaped traj (Codex's bug fixture), list-of-strings (the
  ``list(dict)`` coercion case), None, empty list, mixed list with
  stray non-dict entries, and a non-dict ``traj[i-1]`` prev-step.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ZmeiGorynych and others added 4 commits June 2, 2026 21:25
Codex r11 follow-up on round 10's Group 1 design. Round 10's filtered
rerun preserved unrelated prior ``rows/<iid>/submission_annotation.json``
directories on disk so previously-graded instances would still be
visible for human inspection. The aggregator at the bottom of
``run_evaluation`` then walked EVERY subdirectory under
``output_dir/rows`` to build ``cascading_phase1``, while the rest of
the metrics dict (``total_tasks``, ``results``, durations, usage,
phase counts) was built from only the filtered ``tasks`` list. The
final ``emit_cascading_eval_json`` rewrote ``phase1_count`` /
``phase1_rate`` from the cascade's N1 count — over the union — while
``total_tasks`` stayed at the filtered count. Observable on the
published ``eval.json``:

  * ``total_tasks`` = filtered count (e.g. 1)
  * ``phase1_count`` = union count (e.g. 3 — 2 prior + 1 fresh)
  * ``cascading_phase1.n_dual_eval_tasks`` = union count

  ``phase1_count > total_tasks`` and rates uninterpretable.

Fix: add ``instance_filter: set[str] | None`` to
``aggregate_cascading_phase1`` and pipe it through
``emit_cascading_eval_json``. When set, only subdirectories whose name
is in the filter are counted (preserved prior annotations stay on
disk but DON'T pollute the published metrics). At the ``run.py`` call
site, build the set from ``{td["instance_id"] for td in tasks}`` so the
cascade describes the same row set as ``total_tasks``. Cloud collation
and full local runs pass ``None`` to keep back-compat.

Tests:
- tests/test_cascading_report.py:
  test_aggregator_instance_filter_scopes_to_current_run — pin the
  helper's filter contract directly: 3 dirs on disk, filter to 2,
  assert the third dir is preserved on disk but excluded from the
  count.
- tests/test_run_local_inline_grader.py:
  test_local_run_filter_ids_preserves_unrelated_rows — extended with
  the metrics-consistency invariant ``total_tasks ==
  cascading_phase1.n_dual_eval_tasks`` after a filtered rerun with a
  stale unrelated annotation on disk.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two Codex findings on the cascade comparators:

Item 1 (Codex major — tolerant_grader.py:150-156):
  ``compare_tie_order`` only checks row-count cardinality before
  indexing every row by ``orderby_indices``. A wrong-projection-width
  agent submission (fewer columns than the gold ORDER BY references)
  raised ``IndexError`` inside the ``_key`` builder; the exception
  bubbled out of the grader and the cloud / local fail-everything
  fallback wrote a generic "grader raised" annotation instead of the
  structured cascade verdict with miss_diagnostics — losing the very
  signal that would tell the analyst the agent's projection was too
  narrow.

  Fix: bounds-check ``max(orderby_indices)`` against the NARROWEST
  row on either side up front. Out-of-range index → return False
  cleanly (same disposition as the row-count check immediately above).
  Two new tests: pred too narrow, gold too narrow.

Item 2 (Codex major — tolerant_grader.py:281-285):
  ``compare_column_order`` did ``set(pred_l) != set(gold_l)`` and then
  ``perm = [pred_l.index(c) for c in gold_l]``. With duplicate column
  names (e.g. ``a, b, a`` after a join + alias collapse), ``set(...)``
  collapsed the duplicates and ``.index()`` returned the FIRST
  matching position for every later occurrence — so both gold "a"
  positions mapped to pred's first "a" column, and the second pred
  "a" column's actual value was silently ignored. A submission with
  wrong values in the second duplicate column could falsely pass N8.

  Fix: reject duplicate column names at the boundary. Duplicates
  make "column order tolerance" ill-defined — the tier was designed
  for distinct projections. Three new tests: duplicates on pred,
  duplicates on gold, plus a belt-and-braces regression that the
  canonical "same names, different order" pass still works.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ation

Three Codex findings, all addressed:

Item 1 (Codex major — regrade.py + tolerant_grader.py):
  Regrade never threaded an ``llm_judge`` into ``grade_submission``,
  so N5 was permanently dormant for insufficient-task rows. Now the
  judge fires automatically as part of the regular grader flow:
  * New ``LiteLLMJudge`` concrete class in ``tolerant_grader.py`` —
    wraps ``litellm.completion`` with a fixed ACCEPT/REJECT prompt
    contract and parses the last line of the model's reply. Network
    / shape errors return ``None`` per ``LLMJudgeProtocol`` so the
    cascade falls through cleanly.
  * Regrade CLI reads ``agent_model`` from
    ``<run_dir>/manifest.json`` and builds
    ``CachedLLMJudge(inner=LiteLLMJudge(model=agent_model),
    cache_path=<run_dir>/llm_judge_cache.json)`` ONCE per regrade —
    no new flags, no extra scaffolding.
  * Dropped ``--force-llm-judge`` and the matching
    ``clear_llm_judge_cache`` helper (and the obsolete tests). The
    cache key already includes ``model_name`` + content hashes, so
    changing the agent's model on a resubmit naturally invalidates
    entries without a separate cache-clear flag.

  5 new tests in test_tolerant_grader_orchestration.py: ACCEPT /
  REJECT / inconclusive parsing, litellm exceptions degrade to None,
  and a mocked-response happy path that validates the OpenAI-shaped
  reply path.

Item 2 (Codex major — annotation_schema.py):
  ``TaskAnnotation.external_knowledge`` was ``List[int]`` only. All
  observed mini-interact / livesqlbench data currently ships int-only,
  but some livesqlbench fixtures + forward-looking benchmark variants
  carry the KB body inline as a dict (``{"id": 31, "label": "TETL",
  "definition": "..."}``); pydantic would hard-reject those.

  Fix: widen to ``List[Union[int, dict]]`` + add a regression test
  that round-trips a mixed list through model_dump / model_validate.

Item 3 (Codex minor — scripts/dev1515_convert_runs.py):
  Same char-splitting bug round 5 fixed elsewhere — the script did
  ``list(task_row.get("sol_sql") or [])``, which on a bare-string
  ``sol_sql`` (mini-interact does both shapes) would produce
  ``["S","E","L","E","C","T",...]`` and the grader would mis-execute.
  Replaced with ``normalize_sol_sql(...)`` from grade_in_place.
  (No new test — the helper is already tested in
  tests/test_normalize_sol_sql.py round 5.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ariant-gold-annotations-tolerant-grader-post-dev-1478

# Conflicts:
#	tests/test_livesqlbench_audited_gold.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant