Skip to content

fix(evals): take the midpoint of the two middle ratings for Critical Thinking Overall - #420

Merged
czi-fsisenda merged 3 commits into
mainfrom
fsisenda/ct-midpoint-overall
Nov 4, 2026
Merged

czi-fsisenda merged 3 commits into
mainfrom
fsisenda/ct-midpoint-overall

Conversation

@czi-fsisenda

Copy link
Copy Markdown
Contributor

Refs DSCR-2633.

Why

With one source passage, indicator 2.1 (synthesizing_sources) is omitted, so Overall is the median of four ratings. Decision rule 6 rounded that median up to the higher of the two middle levels. A strong essay whose citations don't appear in its only source could be rated Exploring, Exploring, Extending, Extending and get Overall = Extending.

What changes

Overall is now the level exactly halfway between the two middle ratings, rounding up only when no level sits exactly halfway (ceil((a + b) / 2) on the scale). Odd counts are unchanged.

Two middle ratings Before After
Exploring & Analyzing (neighbors) Analyzing Analyzing
Exploring & Integrating Integrating Analyzing
Exploring & Extending Extending Integrating
Not Evident & Extending Extending Analyzing
  • system.txt: rule 6 rewritten with these examples; config.json hash updated; Python contracts regenerated.
  • fixtures.json: adds fabricated_citations_single_source, expecting integrating.
  • config.json: allow_adjacent_levels is now false for Critical Thinking. A neighboring level would accept a slide back to Extending on the new case. Both live suites now read this setting for Critical Thinking (TS already claimed to; Python hard-coded adjacency). The two existing fixtures came out exactly as expected in every validation run, so exact match doesn't make them flaky.
  • Test helpers (median_rating / medianRating) follow the new rule, with unit cases for each row above. The live suites use them to check that every run's Overall matches its own indicator ratings.

Validation

Python SDK, built from main (current prompt) and from this branch (new prompt). Claude Opus 5 has no temperature control, so results are over repeated runs.

Fabricated-citation case, 10 runs per prompt

Current New
Overall Extending 8, Integrating 2 Integrating 10
Overall matches the rule on that run's ratings 10/10 10/10

The new rule changed the result on all 10 new-prompt runs; the old rule would have given Extending on each.

Regression: 15 expert-annotated essays (84 essay × dimension cells)

Two runs per essay per prompt, a third where they disagreed, then the round-up median of the runs per cell.

Current New
Exact, all cells 66/84 (78.6%) 63/84 (75.0%)
Exact, Overall 12/15 12/15
Within one level 84/84 84/84
Single-run exact 77.6% 74.1%

No run on these 15 essays had middle ratings more than one level apart, so the new rule never fired on them, and Overall is unchanged. The three cells that differ are indicator ratings, which rule 6 doesn't touch; about 11 of 84 cells vary between runs of the same prompt.

Repo checks, make verify and the TS lint, typecheck and unit suites pass. Live integration tests were not run in CI (no key).

Moving the Overall computation into code is a separate follow-up (DSCR-2634).

🤖 Generated with Claude Code

…Thinking Overall

With one source passage, indicator 2.1 is omitted and Overall is the median
of four ratings. Rounding that median up to the higher middle level let a
strong essay with fabricated citations (Exploring, Exploring, Extending,
Extending) land on Extending. Overall now takes the level halfway between
the two middle ratings, rounding up only when no level sits exactly halfway.
Neighboring ratings resolve as before.

- system.txt: rewrite decision rule 6 with worked examples; update its hash.
- fixtures.json: add a single-source fabricated-citation case expecting
  integrating.
- config.json: set allow_adjacent_levels to false so that case needs an
  exact match; a neighboring level would accept the slide back to Extending.
- SDK tests: the median helpers follow the new rule, and both live suites
  read the contract's tolerance for Critical Thinking.

Refs DSCR-2633

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 3, 2026 02:14
@czi-fsisenda
czi-fsisenda requested a review from a team as a code owner November 3, 2026 02:14

Copilot AI 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.

🟢 Approval recommended

No active review issues remain for the midpoint-scoring update.

No open issues 🎉

What changed in this PR

Updates Critical Thinking’s Overall rating to use the midpoint of the two middle indicator levels for even counts, rounding upward only when the midpoint falls between levels. Odd-count behavior remains unchanged.

Changes:

  • Revises the evaluator prompt and synchronizes the generated Python contract and prompt hashes.
  • Adds a single-source fabricated-citation regression fixture and requires exact fixture matches.
  • Updates Python and TypeScript scoring helpers, midpoint unit cases, and integration-test tolerance handling.
File Description
sdks/​typescript/​tests/​unit/​evaluators/​critical-thinking-score.test.ts Implements midpoint scoring in the helper and adds unit cases.
sdks/​typescript/​tests/​integration/​critical-thinking.integration.test.ts Reads adjacency tolerance from the contract.
sdks/​python/​tests/​unit/​evaluators/​durable_skills/​ela_writing/​test_critical_thinking_score.py Implements midpoint scoring in the helper and adds unit cases.
sdks/​python/​tests/​integration/​test_pilot_evaluators.py Honors Critical Thinking’s configured adjacency tolerance.
sdks/​python/​src/​learning_commons_evaluators/​contracts/​_generated/​durable_skills/​ela_writing/​critical_thinking/​system.txt Synchronizes the generated prompt’s midpoint rule.
sdks/​python/​src/​learning_commons_evaluators/​contracts/​_generated/​durable_skills/​ela_writing/​critical_thinking/​config.json Synchronizes the prompt hash and exact-match tolerance.
evals/​durable-skills/​ela-writing/​critical-thinking/​system.txt Defines the midpoint rule with examples.
evals/​durable-skills/​ela-writing/​critical-thinking/​fixtures.json Adds the fabricated-citation case expecting integrating.
evals/​durable-skills/​ela-writing/​critical-thinking/​config.json Updates the prompt hash and disables adjacent-level matches.

Review effort: Balanced

@gary-mu gary-mu 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.

Reviewed against DSCR-2633. The PR covers every item in the ticket's scope and acceptance criteria, CI is green, and the validation is upfront about its limits. I'm fine with this shipping Wednesday. A couple of things I'd recommend:

Suggested change (non-blocking):

Rule 5 says "can live", rule 6 is now prescriptive, and both live suites assert equality. Rule 5 reads "Overall Critical Thinking can live at the median", while the SDK tests now assert the model's Overall equals the helper's median on every run. The permissive wording invites the model to deviate, and the suite fails when it does. Since rule 6 is being rewritten anyway, tighten rule 5 to "is the median". (Prompt change, so the hash moves again; a few more runs on the fabricated case would be good.)

Not sure, want to confirm:

The new Python integration branch confirmation The exact-match path in test_pilot_evaluators.py only runs when all three attempts miss, and live tests weren't run in CI. The accessor is safe (Fixtures.tolerance is a plain dict), but please confirm the Python live suite was run locally for Critical Thinking, since validation used standalone scripts. Also, when exact match is required the failure message should say so; "expected integrating, got ['extending', ...]" otherwise reads like a flake.

Questions on the validation

  • How many of the 15 Round 3 essays were single-source? The rule never fired on them, so the regression shows the prompt edit didn't perturb indicator ratings (useful), but says nothing about how often the midpoint rule fires on the population it targets.
  • Across the 10 new-prompt runs on Mike's essay, what was the indicator split each run? "10/10 Integrating, self-consistent" is compatible with Exploring/Exploring/Extending/Extending every time, and also with 3.1 or 4.1 drifting down. The first means the new fixture is fragile under exact match (one indicator moving one level flips Overall to Analyzing); the second means it's more robust than it looks.

Design notes, non-blocking

  • The fixture's expected label is the rule's output, not an expert label. The other two fixtures carry Round 3 consensus; this one's integrating is what the split resolves to under the new rule. Legitimate as a regression guard, but a different kind of fixture, and nobody has asked an expert whether an essay that fabricates every citation and its opposing view should be Integrating. Flag it in the description as a rule guard, or get one expert rating. The one-line source passage and the obviously non-student prose are fine for an adversarial case but not representative.
  • Exact match is now evaluator-wide to protect one fixture. Keep the semantics sentence in notes alongside the rationale. Longer term a per-fixture tolerance override would let the expert-labeled fixtures keep adjacency.
  • This treats the symptom. The split happens because rules 10 and 15 penalize fabrication only in 2.2 and 3.2, rule 18 exempts 4.1, and nothing gates 3.1 (the essay invents "Coach Reyes" and still earns Extending there). Smallest safe change for Wednesday, agreed. But worth recording on the ticket that the midpoint rule doesn't touch multi-source prompts (five ratings, odd count), and that the durable fix is either 3.1 sourcing rules or DSCR-2634.

Nits

  • test_four_ratings_round_up / its TS twin now test the neighbors case under a name describing the old rule.
  • The Python special-casing by evaluator id could read allow_adjacent_levels from every contract, defaulting to true when absent, with no behavior change for the text-complexity and graphics contracts (all true).
  • Rule 6 wording and the four worked examples (gaps 1 to 4) are clear. Generated Python copies match the source hash.

@gary-mu gary-mu 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.

approved with comments

… rule-guard fixture

- config.json: the tolerance note states what the flag means again, then why
  Critical Thinking turns it off.
- fixtures.json: the fabricated-citation case says its expected label comes
  from the midpoint rule, not from expert raters.
- Python live suite: every contract's allow_adjacent_levels decides whether a
  neighboring level passes (absent means it does, so other evaluators are
  unchanged), and the failure message says when an exact match was required.
- Rename the neighboring-middle-ratings unit test in both SDKs.

Refs DSCR-2633

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@czi-fsisenda

Copy link
Copy Markdown
Contributor Author

Answers to the validation questions in the review summary:

  • Single-source Round 3 essays: 6 of 15 (3 Driverless cars, 3 Face on Mars). No run on any of them had middle ratings more than one level apart, so the regression shows the prompt edit didn't move indicator ratings, but not how often the rule fires on single-source essays in general.
  • Per-run splits on the fabricated case: answered in the fixtures.json thread. 3.1 and 4.1 were Extending in all 10 new-prompt runs.
  • Addressed in 5ec861d: the tolerance note, the rule-guard label on the fixture, the Python failure message and the contract-wide tolerance read, and the test rename.
  • Still open: the rule 5 wording ("can live" → "is the median") and a local run of the Python live suite. Both need paid model calls and a decision from the PR author, so they're not in this push.

🤖 Addressed by Claude Code

Rule 5 said Overall "can live at" the median, which read as optional next to
rule 6 and the tests, which require it. It now says Overall is the median.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@czi-fsisenda

Copy link
Copy Markdown
Contributor Author

More runs on the final prompt (2a82580) to put numbers on the remaining review questions. Every run below is a single attempt with no retry.

How often each fixture matches exactly on one attempt (the notebook case, and how fragile exact match is)

Fixture Exact on one attempt Notes
fabricated_citations_single_source (integrating) 21 / 21 3.1 and 4.1 Extending in every run on the new prompt (30 / 30 across both wordings); 3.2 splits Analyzing / Exploring, which does not move Overall
5076429_cleaned (analyzing, expert) 11 / 11 Five ratings. Counterarguments drifted to Exploring 3 / 10, which leaves the median at Analyzing
AAAOPP13416000111182 (exploring, expert) 11 / 11 All four indicators Exploring in every run

Each count is 10 new runs plus the notebook run on this commit, and the fabricated case also includes the 10 rule 5 validation runs. Overall equalled the midpoint of that run's own indicators in all 30 new runs. With zero misses in 11 runs the per-attempt miss rate could still be as high as about 25% (95% bound), so for the suites, which retry 3 times, the worst case is about a 2% chance per fixture of missing on all three. For the notebook, which tries once and is advisory, a miss would show up as one fail row. If one does start flaking, giving the expert fixtures their neighbor tolerance back per fixture is the fix.

How often the midpoint rule applies to single-source essays

30 single-source PERSUADE 2.0 essays not used before (15 Driverless cars, 15 Face on Mars), grades 8 to 10, two per holistic score 1 to 6 plus extras at 1 to 3, one run each:

  • All 30 got four indicator ratings, an even count.
  • The two middle ratings were equal in 22 runs and one level apart in 8. Both rules give the same Overall in those cases.
  • They were never two or more levels apart, so the rule applied in 0 / 30. Overall still matched the rule in all 30.

So on ordinary student essays the rule seems to apply rarely: a ratings split that wide looks specific to essays like the fabricated-citation one, which is strong on some skills and weak on others. That also makes it unlikely the change moves Overall scores on typical single-source essays.

Still open, and runs can't answer it: the exact-match failure branch in test_pilot_evaluators.py hasn't executed, because every attempt matched. Whether Integrating is the right label for an essay that fabricates every citation is a question for an expert rater. It stays labelled as a rule guard in the fixture.

Cost of these 60 calls: about $8.65.

🤖 Addressed by Claude Code

@adnanrhussain adnanrhussain left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Prompt changes were already reviewed by DS.

@michael-berger-czi michael-berger-czi 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.

Nice, LGTM

@czi-fsisenda
czi-fsisenda merged commit 8362a00 into main Nov 4, 2026
23 checks passed
@czi-fsisenda
czi-fsisenda deleted the fsisenda/ct-midpoint-overall branch November 4, 2026 21:30
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.

5 participants