feat(hitl): ekspertdommen kan ikke oppstaa av stillhet (F2, ORDRE 20260825T214801Z)

run_project KREVDE verdict_input og kjorte capture_verdict ubetinget; CLI-en
defaultet det til {"approved", "reviewed by expert"} og hosting listet det som
PAAKREVD. Netto: hver flaggloes kjoering myntet en ekspertgodkjenning ingen ga,
den gikk inn i den delte storen, og run_portfolio bar den inn i neste prosjekts
hypotese-prompt som en prior expert verdict -- paa flaten som ble overlevert
14.08. Non-goal 3, brutt i en soem.

RunResult.verdict er naa Verdict | None, og None er hva stillhet produserer:
ingenting myntes, ingenting lagres, ingenting varsles. Prinsippet sto allerede i
repoet -- RunFailure sin docstring: aa fylle et felt med en dummy legger
FABRIKKERT proveniens inn i aggregatet.

Traceability koster ingenting: RunResult.verdict_key (property, derivert fra
kandidaten) er verdicts.verdict_key sitt alt dokumenterte formaal -- identisk
med verdict.id naar en dom BLE gitt, og fortsatt meningsfull naar ingen ble det.
Det er den outboxen og den hostede responsen stempler.

Halv dom NEKTES paa begge doerer (FeedbackContract er eneste sted formen
valideres; CLI-en nekter ved navn FOER enhver mode-dispatch). Validering, aldri
reparasjon. De to mode-partisjonene fikk --decision/--rationale inn: kommentarene
sa ordrett at en aerlig nekt var uimplementerbar fordi de non-None
argparse-defaultene gjorde en eksplisitt verdi uskillbar fra defaulten -- med
defaultene borte er den implementerbar.

Hosting er WIDENING, ikke bryting: verdict_input flyttet fra _REQUIRED_FIELDS
til _OPTIONAL_FIELDS. Ingen ekstern kaller brekker.

AERLIGHETS-GRENSE: referanse-fixturens SYNTETISKE verdict_input-rader staar
uroert -- de er merket SYNTETISK paa fire steder og er reviewens F5 (maaling av
misjonspaastanden), ikke F2. Project.verdict_input er naa valgfri.

Load-bearing MAALT (tests/test_ungiven_verdict_loadbearing.py, 15 armer), aatte
mutasjoner alle roede mot HELE suiten + gronn kontroll 1080/5 og golden
demo-transcript.stdout BYTE-UENDRET (ea8c534773acdbe41ae68f2c55724d69aaf8be4f).
En mutasjon falsifiserte testen foerst (vakuoes-gate-klassen, ellevte gang):
--report-armen brukte et bart --report, som nekter rc 1 uansett fordi --ledger
mangler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-08-27 01:22:07 +02:00
commit 56f4f6d084
12 changed files with 685 additions and 49 deletions

View file

@ -154,11 +154,28 @@ project's own numbers cannot support, no matter how confidently the proposer ass
the part of the method that carries the weight — the agents propose, and something that cannot be
argued with decides.
Read that summary line carefully: `Rejection (verdict id=…, decision=approved)` is not a
contradiction. `Rejection` is the **validator's** outcome, while `decision=` echoes the
**human's** recorded verdict — here the `--decision` default, since nobody reviewed this run.
The two are deliberately separate: a machine gate that blocks, and a human judgement that
approves, are different questions and are never collapsed into one field.
Read that summary line carefully. Nobody reviewed this run, so it says exactly that:
`Rejection (no expert verdict given; verdict key=…)`. `Rejection` is the **validator's** outcome;
the second half is about the **human**, and there was no human here. The key it quotes is the id
under which an expert verdict on this candidate will arrive later — your join back into the
[expert-answer channel](docs/ekspert-svar.md).
Record one and the line changes:
```bash
uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD \
--docs-dir shared/examples/bygg-energi-mikro \
--bundle-dir shared/examples/bygg-energi-mikro \
--scripted-replies replies.json \
--decision approved --rationale "the retrofit is within scope"
```
Now it reads `Rejection (verdict id=…, decision=approved)`, which is not a contradiction: a machine
gate that blocks and a human judgement that approves are different questions and are never
collapsed into one field. The two flags go together or not at all — half a verdict is refused by
name, because the missing half is the expert's to write and never ours to default. Until 1.1.0
`--decision` defaulted to `approved`, so every flagless run recorded an approval nobody gave and
carried it into the next project's hypothesis; that default is gone.
**5 — See what it would cost with a real model**, before spending anything:
@ -570,7 +587,10 @@ when the seam is detached, so the loop cannot silently degrade into theater.
The **prior-verdict fold — the learning step — happens only on the `--bundle-dir` path**; a
plain `--docs-dir`-only run is single-shot (no fold). `--decision`/`--rationale` apply to the
single-project path only and are inert in portfolio mode. **`--outbox-dir` must differ from
single-project path only and are **refused** in portfolio and report mode rather than ignored —
a portfolio pass takes each project's verdict from its own row, so a run-level verdict flag has
nowhere to go, and silently dropping a judgement an expert actually typed is the failure this
partition exists to prevent. **`--outbox-dir` must differ from
`--verdict-dir`**: writing the raw outbox into a folder later read as an inbox would re-ingest
raw agent output past the promotion gate (self-contamination) — documented here, deliberately
not CLI-enforced. Stop criteria and budget caps are required at startup. Try the offline
@ -632,9 +652,11 @@ command) was **deleted with them** rather than weakened into a check that could
start command now has exactly one copy, in [`DEPLOY.md`](DEPLOY.md), and
`tests/test_handover_package_loadbearing.py` is what keeps it there.
An invocation is a JSON object whitelisted onto `run_project`'s signature — `project_id`,
`docs_dir` and `verdict_input` required; `bundle_dir`, `profile`, `max_rounds`, `max_tokens`
and `top_k` optional. Unknown fields are refused by name (400), never silently dropped.
An invocation is a JSON object whitelisted onto `run_project`'s signature — `project_id` and
`docs_dir` required; `verdict_input`, `bundle_dir`, `profile`, `max_rounds`, `max_tokens`
and `top_k` optional. `verdict_input` was required until 1.1.0, which forced an external caller to
invent an expert verdict just to get a run at all; omitting it now means nobody reviewed the run,
and the response's `verdict_id` is the key one would arrive under. Unknown fields are refused by name (400), never silently dropped.
`profile` defaults to `azure` on this surface: the AZURE profile reads its endpoint and
credential from the environment at call time, so the same process runs hosted (managed identity)
and locally (`az login`) without rewiring.