fix(rank): report malformed scoring results in apply instead of a traceback (#635)

* fix(rank): report malformed scoring results in apply instead of a traceback

A result that is not an object, a non-string key or a non-object scores value
raised AttributeError/TypeError and discarded the batch. They now become
errors rows and valid results in the same file still apply.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* changelog: place the #635 entry under [Unreleased]

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Mads Lorentzen <madslorentzen_17@hotmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-19 19:31:34 +02:00
1 parent 3c97e7bc93
commit 9b24a8a123
3 files changed
+40 -1

No files matched your search

+6
View File
@@ -77,6 +77,12 @@ per-file diff commands.
`AttributeError` traceback on `.rstrip`, although the audit already reports other malformed state `AttributeError` traceback on `.rstrip`, although the audit already reports other malformed state
with a one-line error and exit 1. It now names the offending key the same way. with a one-line error and exit 1. It now names the offending key the same way.
- **`rank_state.py apply` reports a malformed scoring result instead of crashing** (#635, `tools/rank_state.py`,
`tests/test_rank_state.py`) - a result that is not an object, a `key` that is not a string, or a
`scores` value that is not an object raised an `AttributeError`/`TypeError` traceback and discarded the
whole batch, although `apply` already collects per-result problems in `errors`. Each now becomes an
`errors` row (exit 1) and the valid results in the same file are still written.
## [1.7.3] - 2026-10-19 ## [1.7.3] - 2026-10-19
### Added ### Added
+28
View File
@@ -805,6 +805,34 @@ class Unreachable(RankStateCase):
self.assertIn("unknown status 'unreachble'", out["errors"][0]["error"]) self.assertIn("unknown status 'unreachble'", out["errors"][0]["error"])
self.assertEqual(self.read_state()["a"]["status"], "new", "a rejected result never half-writes an entry") self.assertEqual(self.read_state()["a"]["status"], "new", "a rejected result never half-writes an entry")
def test_malformed_results_are_errors_not_a_traceback(self):
self.write_state({"a": entry(), "b": entry(title="Other")})
out = self.run_tool(
"apply",
"--results",
self.results(
[
7,
"a",
{"key": ["a"], "scores": {}},
{"key": "a", "scores": [90, 80]},
{"key": "b", "scores": {"technical": 80, "experience": 80, "behavioral": 80, "career": 80}},
]
),
expect=1,
)
self.assertEqual(
[e["error"] for e in out["errors"]],
[
"result is not an object",
"result is not an object",
"no such key in seen_jobs.json",
"scores must be an object",
],
)
self.assertEqual([r["key"] for r in out["ranked"]], ["b"], "valid results in the same batch still apply")
self.assertEqual(self.read_state()["a"]["status"], "new")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+6 -1
View File
@@ -273,6 +273,8 @@ def cmd_sweep(args) -> int:
def overall_score(scores: dict) -> int: def overall_score(scores: dict) -> int:
if not isinstance(scores, dict):
raise ValueError("scores must be an object")
total = 0.0 total = 0.0
for dim, weight in WEIGHTS.items(): for dim, weight in WEIGHTS.items():
value = scores.get(dim) value = scores.get(dim)
@@ -306,8 +308,11 @@ def cmd_apply(args) -> int:
rows, expired, unreachable, gave_up, errors = [], [], [], [], [] rows, expired, unreachable, gave_up, errors = [], [], [], [], []
for result in results: for result in results:
if not isinstance(result, dict):
errors.append({"key": None, "error": "result is not an object"})
continue
key = result.get("key") key = result.get("key")
entry = seen.get(key) entry = seen.get(key) if isinstance(key, str) else None
if entry is None: if entry is None:
errors.append({"key": key, "error": "no such key in seen_jobs.json"}) errors.append({"key": key, "error": "no such key in seen_jobs.json"})
continue continue