feat(toolbox): the judgement through the same door -- validate-proposal, verdict-key, capture-verdict

B's premise applied to the three steps that DECIDE a proposal: a proposal authored outside po --
by a human, or by an agent that is not po -- now meets the blocking deterministic gate, mints the
learning key, and is captured as a Verdict, all without a chat client on the way.

Three thin adapters, no second implementation. The reason is the one the first four doors were
built on, but it bites harder here: the refusal SENTENCE is fed back verbatim into the next
attempt by step 5, so a door that reworded it would break the repair loop while still looking
correct. The probes assert the sentence, not a substring two stages share.

One measurement decided a design detail. The IR writes whole magnitudes as JSON integers
(30000), the run path carries the pydantic float, and verdicts._mint_id hashes the raw value --
so minting from the undeclared JSON would hand out a DIFFERENT verdict id than the debate does
for the same proposal. The door therefore reads the proposal through SavingsProposal and feeds
model_dump() to the public features_from_ir; the probe pins both forms and asserts they differ,
so the shortcut cannot come back silently.

A blocked proposal exits 3, carrying the verdict rather than an exception envelope. "You asked
right and the answer is no" is the same fact whether a file was missing or a claim was
infeasible, and a caller that only reads the exit code must not see a blocked proposal as a
cleared one.

Fasit outside the door in every arm: the base's own checked-in golden suite (written before the
toolbox existed, so it cannot have been fitted to it), a cost baseline authored in the test, a
method cap computed by hand from the fixture, and the public minting rule. Each refusal arm has
an rc-0 control on an argv that would otherwise be accepted.

STATED LIMIT: the input-grounding stage (P7, stage 0b) has no flag here. It falsifies a proposal
against the rendered prompt the model received, and an outside caller has no such prompt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-20 10:08:37 +02:00
commit 368367e1c5
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
4 changed files with 408 additions and 14 deletions

View file

@ -25,6 +25,12 @@ import sys
from pathlib import Path
from portfolio_optimiser import datasource
from portfolio_optimiser.verdicts import (
ProposalFeatures,
capture_verdict,
verdict_key,
verdict_to_dict,
)
_REPO = Path(__file__).resolve().parents[1]
_MIKRO = _REPO / "shared" / "examples" / "bygg-energi-mikro"
@ -87,9 +93,7 @@ def test_navigate_bundle_refuses_a_directory_that_is_no_bundle_with_a_named_code
def test_cost_baseline_from_outside_derives_exactly_what_the_priced_table_says() -> None:
proc = _toolbox(
"cost-baseline", "--bundle-dir", str(_PRISSKJEMA), "--project-id", "K2"
)
proc = _toolbox("cost-baseline", "--bundle-dir", str(_PRISSKJEMA), "--project-id", "K2")
assert proc.returncode == 0, proc.stderr
payload = json.loads(proc.stdout)
assert payload["project_id"] == "K2"
@ -124,9 +128,7 @@ def test_retrieve_chunks_from_outside_matches_the_in_process_seam_exactly() -> N
def test_retrieve_chunks_refuses_a_top_k_that_asks_for_nothing() -> None:
proc = _toolbox(
"retrieve-chunks", "--query", "x", "--docs-dir", str(_MIKRO), "--top-k", "0"
)
proc = _toolbox("retrieve-chunks", "--query", "x", "--docs-dir", str(_MIKRO), "--top-k", "0")
assert proc.returncode == 3, proc.stdout
assert "top_k" in json.loads(proc.stdout)["error"]["message"]
@ -212,3 +214,204 @@ def test_wrong_usage_is_exit_two_and_never_a_silent_zero() -> None:
assert _toolbox().returncode == 2
assert _toolbox("ingen-slik-kommando").returncode == 2
assert _toolbox("navigate-bundle").returncode == 2
# --- dommen: validate-proposal / verdict-key / capture-verdict ----------------------------------
#
# Steg 3 i B-gatens kø: de tre stegene som avgjør et forslag. Samme dør, samme regel om fasit —
# den hentes UTENFOR døren i hver arm: basens egen innsjekkede golden-suite (validate-proposal),
# den publike myntingsregelen `verdicts.verdict_key` (verdict-key) og `verdicts.verdict_to_dict`
# over en dom bygget i testen (capture-verdict).
_IR = _MIKRO / "validator-input.json"
_GOLDEN = json.loads((_MIKRO / "golden.json").read_text(encoding="utf-8"))["validator"]
#: Mikro-basens IR, transkribert fra `validator-input.json` — ikke lest ut av den. En arm som
#: leste fila ville flyttet seg med den, og da måler den ikke lenger at døren gir DISSE tallene.
_IR_CODE = "ENERGI-TOTAL-EL"
_IR_PROJECT = "BYGG-KONTOR-NORD"
_IR_QUANTITY = 300000.0
_IR_UNIT_COST = 1.0
_IR_CLAIMED = 30000.0
def _measure() -> str:
"""Tiltakets tekst, lest fra basen: den ER lang og står ordrett i domsnøkkelens kanoniske form,
en kopi her ville vært en transkribering som kunne drive fra basen uten at noe ble rødt."""
return str(json.loads(_IR.read_text(encoding="utf-8"))["measure"])
def _baseline_file(path: Path, codes: dict[str, tuple[float, float]]) -> str:
"""En kostnadsgrunnlags-JSON på PROSJEKTETS form (`okf.load_cost_baseline_file`)."""
path.write_text(
json.dumps(
{
"project_id": _IR_PROJECT,
"items": {c: {"quantity": q, "unit_cost": u} for c, (q, u) in codes.items()},
}
),
encoding="utf-8",
)
return str(path)
def test_validate_proposal_from_outside_reproduces_the_bases_own_golden() -> None:
"""Fasiten er basens INNSJEKKEDE golden-suite — produsentens frosne utfall av
`validate_proposal` nøyaktig denne IR-en, med seedet Monte Carlo. Den ble skrevet før
verktøykassen fantes, den kan ikke ha blitt tilpasset døren."""
proc = _toolbox("validate-proposal", "--proposal", str(_IR))
assert proc.returncode == 0, proc.stderr
payload = json.loads(proc.stdout)
assert payload["decision"] == "validated"
assert payload["nominal_feasible"] == _GOLDEN["nominal_feasible"]
assert payload["p10"] == _GOLDEN["p10"]
assert payload["p50"] == _GOLDEN["p50"]
assert payload["p90"] == _GOLDEN["p90"]
assert payload["claimed_saving_nok"] == _GOLDEN["claimed_saving_nok"]
# Uten grunnlag kjørte stadium 0 ALDRI, og døren sier det framfor å la kalleren tro at
# tallene ble holdt mot prosjektets egne kostnadslinjer.
assert payload["cost_baseline_anchored"] is False
def test_validate_proposal_refuses_a_code_the_cost_baseline_never_priced(tmp_path: Path) -> None:
"""Stadium 0 gjennom døren, med den ORDRETTE setningen løpet ellers skriver — det er den
Steg 5 mater tilbake til neste forsøk, en dør som omskrev den ville brutt sløyfa."""
ukjent = _baseline_file(tmp_path / "annen.json", {"VARME-TOTAL": (1.0, 2.0)})
proc = _toolbox("validate-proposal", "--proposal", str(_IR), "--cost-baseline", ukjent)
assert proc.returncode == 3, proc.stdout
payload = json.loads(proc.stdout)
assert payload["decision"] == "rejected"
assert payload["reason"] == (
f"unknown cost code '{_IR_CODE}': not in project {_IR_PROJECT}'s cost baseline "
f"(1 known codes: 'VARME-TOTAL')"
)
assert payload["stage"] == "stage0-baseline"
assert payload["cost_baseline_anchored"] is True
# rc-0-kontroll: SAMME forslag mot et grunnlag som priser koden. Uten den måler armen over
# ingenting — den ville vært grønn mot en dør som avviste alt.
priset = _baseline_file(tmp_path / "priset.json", {_IR_CODE: (_IR_QUANTITY, _IR_UNIT_COST)})
ok = _toolbox("validate-proposal", "--proposal", str(_IR), "--cost-baseline", priset)
assert ok.returncode == 0, ok.stdout
assert json.loads(ok.stdout)["decision"] == "validated"
assert json.loads(ok.stdout)["cost_baseline_anchored"] is True
def test_validate_proposal_applies_the_method_cap_through_the_same_door(tmp_path: Path) -> None:
"""Metodetaket er DATA (F8), og døren rekker det: et strengere tak enn den generiske P90
avviser et forslag P90-stadiet slapp gjennom samme dør, samme ordrette setning.
Taket regnes UAVHENGIG her: 0,05 × (300 000 × 1,0) = 15 000, transkribert fra basen."""
tak = tmp_path / "tak.json"
tak.write_text(json.dumps({_measure(): 0.05}), encoding="utf-8")
ventet_tak = 0.05 * _IR_QUANTITY * _IR_UNIT_COST
assert ventet_tak == 15000.0
proc = _toolbox("validate-proposal", "--proposal", str(_IR), "--method-caps", str(tak))
assert proc.returncode == 3, proc.stdout
payload = json.loads(proc.stdout)
assert payload["reason"] == (
f"claimed {_IR_CLAIMED:.0f} exceeds the {_measure()} method cap {ventet_tak:.0f} "
f"(stricter than the generic P90)"
)
assert payload["stage"] == "stage5-method-cap"
# rc-0-kontroll: et tak som IKKE er strengere enn kravet slipper samme forslag gjennom.
romslig = tmp_path / "romslig.json"
romslig.write_text(json.dumps({_measure(): 0.30}), encoding="utf-8")
ok = _toolbox("validate-proposal", "--proposal", str(_IR), "--method-caps", str(romslig))
assert ok.returncode == 0, ok.stdout
assert json.loads(ok.stdout)["decision"] == "validated"
def test_verdict_key_from_outside_is_the_key_the_learning_loop_mints(tmp_path: Path) -> None:
"""A5: én publik domsnøkkel. Fasiten er `verdicts.verdict_key` kalt på trekk bygget HER —
og armen skiller de to formene som ellers ser like ut: IR-en skriver `30000`, kjørestien
sender en pydantic-FLOAT videre, og `_mint_id` hasher den verdien. En dør som mintet fra
JSON-en slik den står ville gitt en ANNEN nøkkel enn løpet, samme forslag."""
trekk = ProposalFeatures(
affected_codes=frozenset({_IR_CODE}),
measure_type=_measure(),
claimed_saving_nok=_IR_CLAIMED,
description=_measure(),
)
som_heltall = ProposalFeatures(
affected_codes=frozenset({_IR_CODE}),
measure_type=_measure(),
claimed_saving_nok=30000,
description=_measure(),
)
assert verdict_key(trekk) != verdict_key(som_heltall), "de to formene er ikke lenger ulike"
proc = _toolbox("verdict-key", "--proposal", str(_IR))
assert proc.returncode == 0, proc.stderr
payload = json.loads(proc.stdout)
assert payload["verdict_id"] == verdict_key(trekk)
assert payload["verdict_id"] != verdict_key(som_heltall)
assert payload["affected_codes"] == [_IR_CODE]
assert payload["claimed_saving_nok"] == _IR_CLAIMED
# Nøkkelen er ingen konstant: et forslag med et annet beløp nøkles annerledes.
annet = json.loads(_IR.read_text(encoding="utf-8"))
annet["claimed_saving_nok"] = 12345
sti = tmp_path / "annet.json"
sti.write_text(json.dumps(annet), encoding="utf-8")
andre = _toolbox("verdict-key", "--proposal", str(sti))
assert andre.returncode == 0, andre.stderr
assert json.loads(andre.stdout)["verdict_id"] != payload["verdict_id"]
def test_capture_verdict_from_outside_is_the_verdict_the_store_would_hold() -> None:
"""Dommen fanget utenfra skal være NØYAKTIG den `capture_verdict` bygger i løpet — samme
felter, samme id, samme serialisering (`verdict_to_dict`, som er formen `write_verdict`
legger disk)."""
grunn = "realiseringsgapet er dokumentert i basen; tallene holder"
ventet = verdict_to_dict(
capture_verdict(
ProposalFeatures(
affected_codes=frozenset({_IR_CODE}),
measure_type=_measure(),
claimed_saving_nok=_IR_CLAIMED,
description=_measure(),
),
"approved",
grunn,
)
)
proc = _toolbox(
"capture-verdict",
"--proposal",
str(_IR),
"--decision",
"approved",
"--rationale",
grunn,
)
assert proc.returncode == 0, proc.stderr
assert json.loads(proc.stdout) == ventet
def test_capture_verdict_refuses_a_decision_the_feedback_contract_does_not_know() -> None:
"""Vokabularet er kontraktens (`FeedbackContract`), ikke dørens egen kopi: en halvgitt eller
oppfunnet dom avvises med FELTNAVNET, aldri fullført fagpersonens vegne."""
proc = _toolbox(
"capture-verdict",
"--proposal",
str(_IR),
"--decision",
"kanskje",
"--rationale",
"x",
)
assert proc.returncode == 3, proc.stdout
melding = json.loads(proc.stdout)["error"]["message"]
assert "decision" in melding and "kanskje" in melding
# rc-0-kontroll: samme kall med en dom kontrakten KJENNER.
ok = _toolbox(
"capture-verdict",
"--proposal",
str(_IR),
"--decision",
"rejected",
"--rationale",
"x",
)
assert ok.returncode == 0, ok.stdout
assert json.loads(ok.stdout)["decision"] == "rejected"