From 740119ecf8b28db11364cb5cf341759d9cd16451 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Sat, 14 Nov 2026 19:52:14 +0000 Subject: [PATCH] fix(novelty): routing partial-file guard reads status "partial" alone (#700) `_is_partial_file` in `novelty_routing_report.py` tested only the `partial` key, so a routing file holding `{"status": "partial"}` was taken for a finished result and a capped run wrote its partial result to a sidecar instead of replacing it. It now tests `"partial" in data or data.get("status") == "partial"`, the marker the atoms reader and `artifacts.py` use. `_refuse_partial` in `novelty/items.py` already refuses a probe file marked by status alone (landed with #633, pinned by `test_novelty_items_refuses_a_probe_file_marked_partial_by_status_alone`), so only the routing guard changes here. Signed-off-by: Anton Volkov --- .../20261114_130000_partial-status-guard.md | 3 +++ dokimi/dokimi/commands/novelty_routing_report.py | 2 +- dokimi/tests/test_novelty_routing_spend_cap_partial.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 dokimi/changelog.d/20261114_130000_partial-status-guard.md diff --git a/dokimi/changelog.d/20261114_130000_partial-status-guard.md b/dokimi/changelog.d/20261114_130000_partial-status-guard.md new file mode 100644 index 000000000..a5222f3c5 --- /dev/null +++ b/dokimi/changelog.d/20261114_130000_partial-status-guard.md @@ -0,0 +1,3 @@ +### Fixed + +- **A routing result marked `"status": "partial"` without a `partial` block is a partial result to the overwrite guard** — a capped `novelty routing` run may replace it, and a finished result is kept as before. `novelty items` already refused such a probe file. diff --git a/dokimi/dokimi/commands/novelty_routing_report.py b/dokimi/dokimi/commands/novelty_routing_report.py index 7dc1dfe8c..63cceb000 100644 --- a/dokimi/dokimi/commands/novelty_routing_report.py +++ b/dokimi/dokimi/commands/novelty_routing_report.py @@ -531,7 +531,7 @@ def _is_partial_file(path: Path) -> bool: earlier = json.loads(path.read_text(encoding="utf-8")) except (OSError, ValueError): return False - return isinstance(earlier, dict) and "partial" in earlier + return isinstance(earlier, dict) and ("partial" in earlier or earlier.get("status") == "partial") def _takes_partial(candidate: Path) -> bool: diff --git a/dokimi/tests/test_novelty_routing_spend_cap_partial.py b/dokimi/tests/test_novelty_routing_spend_cap_partial.py index 2ec4d6f0f..f27f0409f 100644 --- a/dokimi/tests/test_novelty_routing_spend_cap_partial.py +++ b/dokimi/tests/test_novelty_routing_spend_cap_partial.py @@ -815,3 +815,13 @@ def test_a_partial_single_run_rates_its_selections_over_the_calls_it_made(tmp_pa hits = sum(1 for row in model["per_query"] if row.get("correct")) assert model["n_calls"] == 2 assert model["selection_rate"] == round(hits / 2, 4) + + +def test_a_file_marked_partial_by_status_alone_is_a_partial_result_to_write_over(tmp_path): + from dokimi.commands.novelty_routing_report import _is_partial_file, save_result + + marked = tmp_path / "routing.json" + marked.write_text('{"status": "partial"}', encoding="utf-8") + assert _is_partial_file(marked) + assert save_result({}, marked, {"partial": PARTIAL}) == marked + assert json.loads(marked.read_text(encoding="utf-8"))["partial"] == PARTIAL