Skip to content

Add the threshold-shift drift gate and a paired head-to-head comparison - #6

Merged
rlaope merged 5 commits into
mainfrom
feat/threshold-shift-gate
Sep 26, 2026
Merged

rlaope merged 5 commits into
mainfrom
feat/threshold-shift-gate

Conversation

@rlaope

@rlaope rlaope commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • --fail-on threshold-shift=X: fails a drift run when the cost-optimal threshold moves by more than X, in either direction. Without a cost matrix the check is refused with exit 1 instead of passing. The CI block now names each failed check and its numbers under the table.
  • jeval drift --paired: compares two model versions on the same requests, matched by (source_key, question_key).
    • Reports accuracy, ECE and Brier differences with paired-bootstrap intervals, plus an exact McNemar p-value.
    • Gives a plain-words verdict that never oversells.
    • Counts every pair it excludes and refuses questions with fewer than 30 pairs.
    • Reports only; --fail-on alone decides the exit code.
  • The statistics live in jeval/calibration.py. jeval/synth.py gains generate_paired for shadow-traffic logs. Existing generator output is byte-identical.
  • Docs: README, CHANGELOG, llms.txt, examples/ci/drift.yml, the drift-gate skill (re-exported), and examples/make-paired-log.py.

Test plan

  • Synthetic tests written first:
    • identical versions: the intervals cover 0 and McNemar is not significant
    • inflated current version: the ECE difference interval excludes 0
    • less accurate current version: McNemar p < 0.001
  • McNemar checked against known values.
  • Threshold-shift fails past its limit, passes under it, and is refused without costs (unit test and CLI test).
  • The README's paired block is reproduced character for character by a test.
  • uv run pytest, ruff and mypy are clean.

--fail-on threshold-shift=X fails when the cost-optimal line moves by more than X in either
direction between the compared versions. Without a cost matrix there is no line on either side, so
the check is refused with exit 1 instead of passing. The CI block now names each failed check and
its numbers under the table, whose FAIL only said a question failed something.

Signed-off-by: rlaope <[email protected]>
… and the drift skill

The captured drift run is re-captured with the new failed-check line; the CI example adds the check
only when costs.yaml exists; the skill pack is re-exported with tools/export_skills.py.

Signed-off-by: rlaope <[email protected]>
Shadow traffic logs both versions on the same requests under one
source_key. Pairing them by (source_key, question) removes request
difficulty from the difference. calibration gains mcnemar_exact and
paired_bootstrap (accuracy, ECE on fixed edges, Brier; resampled over
pairs). drift.compare_paired pairs gold records, refuses duplicated
keys, excludes pairs without gold on both sides or with conflicting
labels, counts each exclusion, and refuses questions below 30 pairs.
synth.generate_paired shares request keys, true answers and difficulty
while leaving generate() byte-identical.

Signed-off-by: rlaope <[email protected]>
The README quotes a captured run on examples/make-paired-log.py, where
the ECE gate passes and the paired check finds the lower accuracy.
test_readme_example reruns that block literally, and the documented
features guard pins --paired in the README and llms.txt.

Signed-off-by: rlaope <[email protected]>
Signed-off-by: rlaope <[email protected]>

# Conflicts:
#	jeval/cli.py
#	jeval/drift.py
@rlaope
rlaope merged commit bf90f5c into main Sep 26, 2026
5 checks passed
@rlaope
rlaope deleted the feat/threshold-shift-gate branch September 26, 2026 01:08
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