FIX: stop scorer-metrics registry rewrites from deleting entries they could not read - #2797
fei (feiiiiii5) wants to merge 8 commits into
Conversation
replace_evaluation_results() rebuilt the JSONL registry from _load_jsonl(), which is a lookup helper that drops lines it cannot parse and returns a short list when the read fails. Every rewrite therefore deleted those entries for good, and a decode error could reduce a registry of pre-computed metrics to the single new entry while the run reported success. Read the file as raw lines for the rewrite instead: unparseable lines are preserved verbatim, read errors propagate before the file is touched, and the new contents move into place with os.replace() as the docstring already claimed.
|
Good catch, and thanks for the three concrete checks. I addressed them in
Test: |
|
Thanks for the two additional checks. I addressed them in
Added regression tests for new-file umask permissions and for preserving the original replace error while removing a read-only staging file. Validation: |
Description
Fixes #2796
replace_evaluation_results()rebuilt the JSONL metrics registry from_load_jsonl(), which is a lookup helper: it drops lines it cannot parse, and when the read itself fails it returns the partial list it managed to read. Used as the source of truth for a truncating rewrite, both behaviours delete data — a torn line disappeared permanently, and one undecodable byte left a registry file holding only the new entry while the call returned normally. Those files ship in the package (pyrit/datasets/scorer_evals/) and hold scores that cost hours of model calls; #2796 has the measured output.The rewrite now reads through a private
_read_registry_lines(), which returns(raw line, parsed)pairs: unparseable lines are preserved verbatim, and a read error propagates before the file is touched. The new contents move into place with a temp file plusos.replace(), which is what the docstring's "atomic operation" claim needed anyway._load_jsonl()is untouched, so lookups stay as lenient as they are today (test_load_jsonl_skips_invalid_jsonstill pins that behaviour).Two consequences to weigh, since neither is invisible:
replace_evaluation_results()instead of rewriting from a partial read._write_metrics_to_registry()already wraps this call inexcept Exceptionwith a warning (pyrit/score/scorer_evaluation/scorer_evaluator.py:619), so an evaluation run still completes; it just keeps the old registry instead of truncating it.json.dumps-ed). Entries keep their exact bytes, including unknown fields and float formatting. If the registry is meant to stay in canonical form instead, keeping only the failed lines verbatim and re-serialising the rest is a small change to the same function — say which you prefer.Tests and Documentation
tests/unit/score/test_scorer_metrics_io.py:test_replace_evaluation_results_keeps_unparseable_linesandtest_replace_evaluation_results_leaves_registry_intact_after_a_failed_read. Onfc692226both fail — the first withthe rewrite deleted a line it could not read, the second withDID NOT RAISE UnicodeDecodeError— and that file then reads 2 failed, 29 passed. On this branch: 31 passed.pytest -q tests/unit/score/(2330 tests, offline, no API keys): passed on this branch, with the two new tests included.ruff checkandruff format --checkon both changed files: clean.tytype checking (not available in this environment) and JupyText (no documentation files changed; the only docstrings added are on the two new private helpers).