fix(tools): do not blame the plan for a verdict okf_adjudicate built

run() parses the plan before building anything and parses its own
verdict after. Only the first failure is the operator's file.

The second is reachable: an empty --adjudicator produces a verdict the
grammar refuses ("adjudication field 'adjudicated_by' must be a
non-empty string"). The plan parsed fine; the fault is in what this
command stamped onto it.

Catching SegmentationError at the top of main() -- the previous commit
-- caught both raise sites and printed "malformed plan" for each. On
this path that is a clean, confident, WRONG diagnosis: it sends the
operator to fix the one artifact that was fine. Worse than the traceback
it replaced, because a traceback at least does not claim to know.

The verdict parse now raises AdjudicationError, which is what "this
command failed" already means in this file and already returns 2. Exit
code unchanged either way, nothing written either way; only the message
changes.

Suite 1073 -> 1074 passed (pytest exit 0, measured without a pipe);
ruff and mypy --strict clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-05 07:35:14 +02:00
commit 798f64ad6b
3 changed files with 55 additions and 1 deletions

View file

@ -275,6 +275,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
belonged to. Pre-existing since the empty-verdict branch landed, and pinned
until now by a test that recorded it as a finding rather than fixing it.
- **A verdict `okf_adjudicate` cannot read back is no longer reported as a
malformed plan.** The command parses the plan before building anything and
parses its own verdict after, so only the first failure is the operator's
file. The second is reachable -- an empty `--adjudicator` produces a verdict
the grammar refuses -- and before the change above it surfaced as a
traceback. Catching the grammar error at the top would have turned it into a
clean, confident, wrong diagnosis naming the plan file, which is worse than
the traceback: it sends the operator to fix the one artifact that was fine.
The verdict parse now raises `AdjudicationError`, so the message names what
actually failed. Exit 2 either way; nothing is written either way.
## [0.5.0a2] — 2026-07-31
**This is the pre-release the pilots pin. `v0.5.0a1` was tagged and abandoned

View file

@ -347,6 +347,40 @@ def test_an_entries_value_that_is_not_a_list_is_still_refused(
assert not (tmp_path / "verdict.json").exists()
def test_a_verdict_this_tool_cannot_read_back_is_not_blamed_on_the_plan(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""The plan is parsed BEFORE the verdict is built, and the verdict is parsed
after. Only the first failure is the plan's.
A verdict that will not parse back is THIS command failing on what it was
told to stamp -- here an empty `--adjudicator`. Reporting that as a
malformed plan sends the operator to fix the one artifact that was fine,
which is worse than the traceback it replaced: a clean, confident, wrong
diagnosis."""
plan_path = proposal(tmp_path)
capsys.readouterr() # the proposer's own report is not what is under test
code = okf_adjudicate.main(
[
"--plan",
str(plan_path),
"--out",
str(tmp_path / "verdict.json"),
"--adjudicator",
"",
"--adjudicated-at",
AT,
]
)
assert code == 2
stderr = capsys.readouterr().err
assert "malformed plan" not in stderr
assert "verdict" in stderr
assert not (tmp_path / "verdict.json").exists()
def test_a_malformed_non_empty_plan_exits_two_with_one_stderr_line(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:

View file

@ -213,7 +213,16 @@ def run(
verdict = build_verdict(
payload, adjudicator=adjudicator, adjudicated_at=adjudicated_at, dwell_s=dwell_s
)
parse_segmentation_plan(verdict)
# The plan already parsed above, so a failure HERE is this command's
# own output, not the operator's file. Raised as an AdjudicationError
# so it is not reported as a malformed plan: that would send the
# operator to fix the one artifact that was fine.
try:
parse_segmentation_plan(verdict)
except SegmentationError as exc:
raise AdjudicationError(
f"the verdict this command built does not parse back [{exc.code}]: {exc}"
) from exc
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(
json.dumps(verdict, indent=2, ensure_ascii=False) + "\n", encoding="utf-8", newline=""