fix(adjudicate): a judgement that keeps nothing gets an artifact

Measured on the K3 corpus: 4 of 12 judgements produced no artifact, because
the verdict was "none of these segments should be persisted" and the plan
grammar refuses zero entries. That refusal is correct for the run path -- an
empty plan replayed would silently persist nothing for a document that was
dropped -- so the grammar is untouched and the recording tool is taught to
record a rejection instead.

Refusing to materialize and refusing to record are different acts. The
rejection artifact is deliberately NOT replayable: parse_segmentation_plan
still refuses it, and the suite asserts that rather than assuming it. The dwell
time rides at the top level because there is no entry to carry it, and a
ratified rejection with no time on it is as unfalsifiable as a ratified
acceptance with none.

Only the empty LIST takes the branch. A missing entries key, or one that is not
a list, stays the grammar's to refuse: "the adjudicator kept nothing" and
"this file is not a plan" must not collapse.

K4a re-run after the change: propose, adjudicate, run the path twice into two
bundles under SEGMENTED_OKF_V0_2, diff -r exit 0 with no output. 1034 -> 1041
tests.
This commit is contained in:
Kjell Tore Guttormsen 2026-09-02 16:15:28 +02:00
commit 62b61927a4
2 changed files with 200 additions and 9 deletions

View file

@ -39,7 +39,7 @@ from typing import Any
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from llm_ingestion_okf.segmentation import parse_segmentation_plan # noqa: E402
from llm_ingestion_okf.segmentation import PLAN_FIELDS, parse_segmentation_plan # noqa: E402
#: This tool's identity, written into the artifact so an operator reading a
#: verdict six months later can tell what produced it.
@ -95,6 +95,56 @@ def run_model(model: str, prompt: str, *, timeout: int = 300) -> str:
return proc.stdout.strip()
def is_rejection(payload: dict[str, Any]) -> bool:
"""A plan whose entry list is present and EMPTY.
Only the empty list. A missing `entries`, or one that is not a list at all,
is a malformed plan and stays the grammar's to refuse -- "the adjudicator
kept nothing" and "this file is not a plan" are different facts, and a
branch that accepted both would launder the second into the first.
"""
entries = payload.get("entries")
return isinstance(entries, list) and not entries
def build_rejection(
payload: dict[str, Any],
*,
adjudicator: str,
adjudicated_at: str,
dwell_s: int,
) -> dict[str, Any]:
"""The verdict for a plan the adjudicator kept nothing from.
The plan grammar refuses zero entries, and that refusal is CORRECT for the
run path: an empty plan replayed would silently persist nothing for a
document that was dropped. But refusing to MATERIALIZE and refusing to
RECORD are different acts. Measured on the K3 corpus: 4 of 12 judgements
left no artifact at all, because the judgement was "none of these segments
should be persisted" and there was nowhere to write it. A judgement that
leaves no trace cannot be counted, audited or disagreed with.
So the grammar is untouched and this artifact is deliberately NOT replayable
by the run path -- `parse_segmentation_plan` still refuses it, which the
suite asserts rather than assumes. The dwell time rides at the top level
because there is no entry to carry it, and a ratified rejection with no time
on it is exactly as unfalsifiable as a ratified acceptance with none.
"""
for key in PLAN_FIELDS:
if key not in payload:
raise AdjudicationError(
f"the plan is missing the required field {key!r} -- an empty entry "
"list is a judgement, but a plan is still a plan"
)
verdict = dict(payload)
verdict["entries"] = []
verdict["adjudicated"] = True
verdict["adjudicated_at"] = adjudicated_at
verdict["adjudicated_by"] = adjudicator
verdict["adjudication_dwell_s"] = dwell_s
return verdict
def build_verdict(
payload: dict[str, Any],
*,
@ -140,10 +190,12 @@ def run(
payload = json.loads(plan_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise AdjudicationError(f"{plan_path} is not readable JSON: {exc}") from exc
# Parsed before anything is written: a proposal this library cannot read
# back is one no verdict can be recorded against, and finding that out
# after writing would leave a verdict pointing at nothing.
parse_segmentation_plan(payload)
rejection = is_rejection(payload)
if not rejection:
# Parsed before anything is written: a proposal this library cannot read
# back is one no verdict can be recorded against, and finding that out
# after writing would leave a verdict pointing at nothing.
parse_segmentation_plan(payload)
if model is not None:
# Advisory only, and recorded rather than applied. The judgement stays
@ -152,10 +204,15 @@ def run(
# very number this command exists to produce.
run_model(model, "Summarise the proposed segmentation for review.")
verdict = build_verdict(
payload, adjudicator=adjudicator, adjudicated_at=adjudicated_at, dwell_s=dwell_s
)
parse_segmentation_plan(verdict)
if rejection:
verdict = build_rejection(
payload, adjudicator=adjudicator, adjudicated_at=adjudicated_at, dwell_s=dwell_s
)
else:
verdict = build_verdict(
payload, adjudicator=adjudicator, adjudicated_at=adjudicated_at, dwell_s=dwell_s
)
parse_segmentation_plan(verdict)
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=""