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

@ -100,10 +100,24 @@ uv run portfolio-optimiser-toolbox cost-baseline --bundle-dir <base> --project-i
# Admit (or refuse, by name) a declared pre-pass cut before it may shape a run
uv run portfolio-optimiser-toolbox prepass-admit --payload <cut.json> --bundle-dir <base>
# Run the blocking deterministic gate on a proposal written OUTSIDE the framework.
# Exit 3 carries the verdict: the verbatim refusal sentence and the stage that wrote it.
uv run portfolio-optimiser-toolbox validate-proposal \
--proposal shared/examples/bygg-energi-mikro/validator-input.json \
--cost-baseline <project-prices.json>
# The learning key a verdict on that proposal will arrive under — without deciding anything
uv run portfolio-optimiser-toolbox verdict-key --proposal <proposal.json>
# Mint an expert's judgement into the Verdict the store holds (stdout is the on-disk form)
uv run portfolio-optimiser-toolbox capture-verdict --proposal <proposal.json> \
--decision approved --rationale "why"
```
Each subcommand calls the same function the run path calls — not a copy of it. That is what makes
the answers you get here the answers the debate gets.
the answers you get here the answers the debate gets: a proposal a human wrote meets the same
stages, in the same order, with the same sentence, as one an agent produced.
Verify the install by running the whole suite from the clean clone: